PSD init
This commit is contained in:
parent
fcb02de2cb
commit
cf0990d22d
@ -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'])}个图层")
|
|
Loading…
Reference in New Issue
Block a user