This commit is contained in:
cyborvirtue 2025-05-20 17:08:18 +08:00
parent fcb02de2cb
commit cf0990d22d
3 changed files with 0 additions and 402 deletions

View File

@ -1,402 +0,0 @@
"""
图层合成模块 - 将生成的各个图层组合成完整海报 (修改版)
提供增强的图层信息导出便于PSD模块使用
"""
import os
import json
import logging
from typing import List, Dict, Any, Optional, Tuple
import numpy as np
from PIL import Image, ImageFont, ImageDraw
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class PosterComposer:
"""海报合成类,负责将各个图层组合成完整海报"""
def __init__(self, images_dir: str = "./images", output_dir: str = "./outputs"):
"""
初始化海报合成器
参数:
images_dir: 源图像目录
output_dir: 合成图像输出目录
"""
self.images_dir = images_dir
self.output_dir = output_dir
os.makedirs(output_dir, exist_ok=True)
logger.info(f"海报合成器初始化完成,图像目录: {images_dir}, 输出目录: {output_dir}")
def apply_golden_ratio_positioning(self,
canvas_size: Tuple[int, int],
element_size: Tuple[int, int],
position: str = "right") -> Tuple[int, int]:
"""
基于黄金分割比例(约1.618)计算位置
参数:
canvas_size: 画布宽度和高度
element_size: 要放置元素的宽度和高度
position: 黄金分割位置 ("left", "right", "top", "bottom")
返回:
元素的(x, y)坐标
"""
width, height = canvas_size
elem_width, elem_height = element_size
# 黄金比例约为1.618
golden_ratio = 1.618
if position == "right":
# 水平方向右侧黄金分割点
x = int((width / golden_ratio) - (elem_width / 2))
y = int((height - elem_height) / 2) # 垂直居中
elif position == "left":
# 水平方向左侧黄金分割点
x = int(width - (width / golden_ratio) - (elem_width / 2))
y = int((height - elem_height) / 2) # 垂直居中
elif position == "top":
# 垂直方向上侧黄金分割点
x = int((width - elem_width) / 2) # 水平居中
y = int((height / golden_ratio) - (elem_height / 2))
elif position == "bottom":
# 垂直方向下侧黄金分割点
x = int((width - elem_width) / 2) # 水平居中
y = int(height - (height / golden_ratio) - (elem_height / 2))
else: # 默认居中
x = int((width - elem_width) / 2)
y = int((height - elem_height) / 2)
return (x, y)
def calculate_position(self,
position_type: str,
canvas_size: Tuple[int, int],
element_size: Tuple[int, int],
offset: Tuple[int, int] = (0, 0),
reference: str = None) -> Tuple[int, int]:
"""
根据指定的位置类型计算位置
参数:
position_type: 位置类型 ("center", "golden_ratio", "corner", "custom")
canvas_size: 画布宽度和高度
element_size: 要放置元素的宽度和高度
offset: 从计算位置的(x, y)偏移量
reference: 额外位置参考 ( "top_left", "golden_right")
返回:
元素的(x, y)坐标
"""
width, height = canvas_size
elem_width, elem_height = element_size
offset_x, offset_y = offset
if position_type == "center":
# 居中定位
x = int((width - elem_width) / 2) + offset_x
y = int((height - elem_height) / 2) + offset_y
elif position_type == "golden_ratio":
# 黄金分割定位
golden_pos = reference.split("_")[1] if reference else "right"
x, y = self.apply_golden_ratio_positioning(canvas_size, element_size, golden_pos)
x += offset_x
y += offset_y
elif position_type == "corner":
# 角落定位
corner = reference if reference else "top_left"
if corner == "top_left":
x, y = 0, 0
elif corner == "top_right":
x, y = width - elem_width, 0
elif corner == "bottom_left":
x, y = 0, height - elem_height
elif corner == "bottom_right":
x, y = width - elem_width, height - elem_height
x += offset_x
y += offset_y
elif position_type == "custom":
# 自定义位置,偏移量为绝对位置
x, y = offset_x, offset_y
else: # 默认居中
x = int((width - elem_width) / 2) + offset_x
y = int((height - elem_height) / 2) + offset_y
return (x, y)
def fit_image_to_canvas(self,
image: Image.Image,
canvas_size: Tuple[int, int],
fit_method: str = "contain") -> Image.Image:
"""
调整图像大小以适应画布尺寸
参数:
image: 要调整大小的PIL图像
canvas_size: 目标画布尺寸
fit_method: 适应方法 ("contain", "cover", "stretch")
返回:
调整大小后的PIL图像
"""
canvas_width, canvas_height = canvas_size
img_width, img_height = image.size
if fit_method == "contain":
# 缩放图像以适应画布同时保持纵横比
ratio = min(canvas_width / img_width, canvas_height / img_height)
new_width = int(img_width * ratio)
new_height = int(img_height * ratio)
elif fit_method == "cover":
# 缩放图像以覆盖画布同时保持纵横比
ratio = max(canvas_width / img_width, canvas_height / img_height)
new_width = int(img_width * ratio)
new_height = int(img_height * ratio)
elif fit_method == "stretch":
# 拉伸图像以匹配画布尺寸
new_width = canvas_width
new_height = canvas_height
else: # 默认为"contain"
ratio = min(canvas_width / img_width, canvas_height / img_height)
new_width = int(img_width * ratio)
new_height = int(img_height * ratio)
# 调整图像大小
return image.resize((new_width, new_height), Image.LANCZOS)
def apply_overlay(self,
base_image: Image.Image,
overlay_image: Image.Image,
position: Tuple[int, int] = (0, 0),
opacity: float = 1.0) -> Image.Image:
"""
将叠加图像应用到基础图像上
参数:
base_image: 基础PIL图像
overlay_image: 叠加PIL图像
position: 放置叠加图的位置(x, y)
opacity: 叠加图像的不透明度(0.0到1.0)
返回:
合并后的PIL图像
"""
# 创建基础图像的副本
result = base_image.copy()
# 确保叠加图像有alpha通道
if overlay_image.mode != 'RGBA':
overlay_image = overlay_image.convert('RGBA')
# 如果不透明度小于1调整alpha通道
if opacity < 1.0:
overlay_data = list(overlay_image.getdata())
new_data = []
for item in overlay_data:
# 通过不透明度调整alpha通道
new_data.append((item[0], item[1], item[2], int(item[3] * opacity)))
overlay_image.putdata(new_data)
# 将叠加图粘贴到结果上
result.paste(overlay_image, position, overlay_image)
return result
def load_image(self, path: str) -> Optional[Image.Image]:
"""
从文件路径加载图像
参数:
path: 图像文件的路径
返回:
PIL图像对象加载失败则返回None
"""
try:
if os.path.exists(path):
return Image.open(path).convert('RGBA')
else:
logger.warning(f"未找到图像文件: {path}")
return None
except Exception as e:
logger.error(f"{path}加载图像时出错: {e}")
return None
def compose_from_layout(self,
layout_config: Dict[str, Any],
output_filename: str = "composed_poster.png",
canvas_size: Tuple[int, int] = (1200, 1600)) -> Dict[str, Any]:
"""
根据布局配置合成海报
参数:
layout_config: 指定图层及其属性的配置
output_filename: 输出图像文件的名称
canvas_size: 输出画布的大小(宽度高度)
返回:
包含合成海报信息的字典
"""
# 创建带alpha通道的空白画布
canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0))
# 获取从底到顶的图层顺序
layer_order = layout_config.get("layer_order", [
"background", "main_subject", "secondary_subject",
"decorative_elements", "logo", "title", "subtitle"
])
# 跟踪已处理的图层以供输出信息
processed_layers = {}
# 根据布局配置处理每个图层
for layer_name in layer_order:
layer_config = layout_config.get(layer_name, {})
if not layer_config:
logger.warning(f"未找到图层'{layer_name}'的配置")
continue
# 获取图层图像路径
image_path = layer_config.get("image_path", "")
full_path = os.path.join(self.images_dir, image_path) if image_path else None
if not full_path or not os.path.exists(full_path):
logger.warning(f"未找到图层'{layer_name}'的图像文件: {full_path}")
continue
# 加载图层图像
layer_img = self.load_image(full_path)
if layer_img is None:
continue
# 获取拟合方法
fit_method = layer_config.get("fit_method", "contain")
# 如果指定,调整图层大小以适应画布
if fit_method != "none":
layer_img = self.fit_image_to_canvas(layer_img, canvas_size, fit_method)
# 获取位置信息
position_type = layer_config.get("position_type", "center")
position_reference = layer_config.get("position_reference", None)
position_offset = layer_config.get("position_offset", (0, 0))
# 计算位置
position = self.calculate_position(
position_type,
canvas_size,
layer_img.size,
position_offset,
position_reference
)
# 获取不透明度
opacity = layer_config.get("opacity", 1.0)
# 将图层应用到画布
canvas = self.apply_overlay(canvas, layer_img, position, opacity)
# 记录处理信息
processed_layers[layer_name] = {
"image_path": full_path,
"size": layer_img.size,
"position": position,
"opacity": opacity
}
logger.info(f"已添加图层'{layer_name}',位置{position},大小{layer_img.size}")
# 保存合成图像
output_path = os.path.join(self.output_dir, output_filename)
canvas.save(output_path, format="PNG")
logger.info(f"已保存合成图像到{output_path}")
# 保存图层顺序用于PSD导出
composition_result = {
"output_path": output_path,
"layers": processed_layers,
"canvas_size": canvas_size,
"layer_order": [layer for layer in layer_order if layer in processed_layers] # 只包含实际处理的图层
}
return composition_result
def generate_layer_info_for_psd(self, composition_result: Dict[str, Any]) -> Dict[str, Any]:
"""
生成PSD导出所需的完整图层信息
参数:
composition_result: compose_from_layout的结果
返回:
包含图层位置路径透明度等信息的字典供PSD导出使用
"""
# 这个方法直接返回composition_result中已经包含的完整图层信息
# 为了保持与原代码的API兼容性我们保留这个方法
psd_info = {
"layer_order": composition_result.get("layer_order", []),
"layers": composition_result.get("layers", {}),
"canvas_size": composition_result.get("canvas_size", (1200, 1600))
}
logger.info(f"已生成PSD导出信息包含{len(psd_info['layer_order'])}个图层")
return psd_info
def load_layout_config(config_path: str) -> Dict[str, Any]:
"""
从JSON文件加载布局配置
参数:
config_path: 布局配置JSON文件的路径
返回:
包含布局配置的字典
"""
try:
with open(config_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.error(f"{config_path}加载布局配置时出错: {e}")
return {}
if __name__ == "__main__":
# 示例用法
layout_config_path = "../configs/layout_config.json"
# 加载布局配置
layout_config = load_layout_config(layout_config_path)
# 创建合成器并合成海报
composer = PosterComposer()
composition_result = composer.compose_from_layout(layout_config)
print(f"海报成功合成到: {composition_result['output_path']}")
# 为PSD导出生成图层信息
psd_info = composer.generate_layer_info_for_psd(composition_result)
print(f"PSD导出信息准备完成包含{len(psd_info['layer_order'])}个图层")