TravelContentCreator/core/poster_gen.py

861 lines
38 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import cv2
import os
import numpy as np
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
import json
import random
import traceback
class PosterInfo:
def __init__(self, index, main_title, texts):
self.index = index
self.main_title = main_title
self.texts = texts
class PosterConfig:
def __init__(self, config_path):
self.config_path = config_path
try:
if isinstance(config_path, str) and os.path.exists(config_path):
# 如果是文件路径,从文件读取
print(f"从文件加载配置: {config_path}")
self.config = json.load(open(config_path, "r", encoding="utf-8"))
else:
# 如果是字符串但不是文件路径,尝试直接解析
print("尝试直接解析配置字符串")
self.config = json.loads(config_path)
self.img_list = []
for item in self.config:
print(item)
self.img_list.append([item['index'], item["main_title"], item["texts"]])
# self.img_list.append(PosterInfo(item['img_url'], item['main_title'], item['texts']))
except json.JSONDecodeError as e:
print(f"JSON解析错误: {e}")
print(f"尝试解析的内容: {config_path[:100]}...") # 只打印前100个字符
# 创建一个默认配置
self.config = [{"index": 0, "main_title": "景点风光", "texts": ["自然美景", "人文体验"]}]
self.img_list = [[0, "景点风光", ["自然美景", "人文体验"]]]
print("使用默认配置")
except Exception as e:
print(f"加载配置时出错: {e}")
# 创建一个默认配置
self.config = [{"index": 0, "main_title": "景点风光", "texts": ["自然美景", "人文体验"]}]
self.img_list = [[0, "景点风光", ["自然美景", "人文体验"]]]
print("使用默认配置")
def get_config(self):
return self.config
def get_config_by_index(self, index):
if index >= len(self.config):
print(f"警告: 索引 {index} 超出配置范围,使用默认配置")
return self.config[0]
return self.config[index]
class PosterGenerator:
def __init__(self, base_dir, output_dir=None):
# 基础路径设置
if not base_dir or not os.path.isdir(base_dir):
# Handle error: Base directory is essential
raise ValueError(f"Poster assets base directory '{base_dir}' is invalid or not provided.")
self.base_dir = base_dir
print(f"Initializing PosterGenerator with asset base: {self.base_dir}") # Log the used base_dir
self.font_dir = os.path.join(self.base_dir, "font")
self.frame_dir = os.path.join(self.base_dir, "frames") # 边框素材目录
self.sticker_dir = os.path.join(self.base_dir, "stickers") # 贴纸素材目录
self.text_bg_dir = os.path.join(self.base_dir, "text_backgrounds") # 文本框底图目录
self.img_frame_posbility = 0.7
self.text_bg_posbility = 0
# 设置输出目录
if output_dir:
self.output_dir = output_dir
else:
self.output_dir = os.path.join(self.base_dir, "output")
# 初始化资源
self.fonts = self._initialize_fonts()
self.frames = self._initialize_frames()
self.stickers = self._initialize_stickers()
self.text_bgs = self._initialize_text_backgrounds()
# 创建输出目录
os.makedirs(self.output_dir, exist_ok=True)
# 固定使用白色字体蓝色描边效果
self.selected_effect = "白色字体蓝色描边"
# 海报计数器,用于控制添加边框的频率
self.poster_count = 0
self.default_font_path = os.path.join(self.font_dir, "华康海报体简.ttc")
def _initialize_text_backgrounds(self):
"""初始化文本框底图"""
try:
text_bg_files = [f for f in os.listdir(self.text_bg_dir) if f.endswith(('.png'))]
print(f"找到文本框底图: {text_bg_files}")
return text_bg_files
except Exception as e:
print(f"初始化文本框底图失败: {e}")
return []
def _initialize_fonts(self):
"""初始化字体"""
fonts = []
try:
font_files = [f for f in os.listdir(self.font_dir) if f.endswith(('.ttf', '.otf'))]
print(f"找到字体文件: {font_files}")
return font_files
except Exception as e:
print(f"初始化字体失败: {e}")
return []
def _initialize_frames(self):
"""初始化边框素材"""
frames = []
try:
frame_files = [f for f in os.listdir(self.frame_dir) if f.endswith(('.png', '.jpg'))]
print(f"找到边框素材: {len(frame_files)}")
return frame_files
except Exception as e:
print(f"初始化边框失败: {e}")
return []
def _initialize_stickers(self):
"""初始化贴纸素材"""
stickers = []
try:
sticker_files = [f for f in os.listdir(self.sticker_dir) if f.endswith(('.png'))]
print(f"找到贴纸素材: {sticker_files}")
return sticker_files
except Exception as e:
print(f"初始化贴纸失败: {e}")
return []
def get_random_font(self):
"""获取随机字体"""
try:
# 使用正确的字体目录
font_dir = self.font_dir
# 获取所有支持的字体文件
font_files = [f for f in os.listdir(font_dir)
if f.lower().endswith(('.ttf', '.otf', '.ttc', '.TTC', '.TTF', '.OTF'))]
if not font_files:
print("警告: 字体目录为空,使用默认字体")
return os.path.join(font_dir, "华康海报体简.ttc")
# 随机选择一个字体文件
font_file = random.choice(font_files)
font_path = os.path.join(font_dir, font_file)
# 验证字体文件是否存在且可读
if not os.path.isfile(font_path):
print(f"警告: 字体文件 {font_file} 不存在,使用默认字体")
return os.path.join(font_dir, "华康海报体简.ttc")
print(f"使用字体: {font_file}")
return font_path
except Exception as e:
print(f"获取字体时出错: {str(e)}")
return os.path.join(font_dir, "华康海报体简.ttc")
def create_base_layer(self, image_path, target_size):
"""创建底层(图片层)
Args:
image_path: 可以是图片文件路径字符串,也可以是已加载的 PIL Image 对象
target_size: 目标图片尺寸 (width, height)
Returns:
调整大小后的 PIL Image 对象
"""
try:
# 检查输入类型
if isinstance(image_path, Image.Image):
# 如果已经是 PIL Image 对象
print("输入已是 PIL Image 对象,无需加载")
base_image = image_path.convert('RGBA')
print(f"图像原始尺寸: {base_image.size}")
else:
# 否则,作为路径处理
# 先验证图片路径
if not image_path:
raise ValueError("图片路径为空")
if not os.path.exists(image_path):
raise FileNotFoundError(f"图片文件不存在: {image_path}")
print(f"尝试加载底图: {image_path}")
base_image = Image.open(image_path).convert('RGBA')
print(f"底图加载成功,原始尺寸: {base_image.size}")
# 调整尺寸
base_image = base_image.resize(target_size, Image.Resampling.LANCZOS)
print(f"底图调整尺寸完成: {target_size}")
return base_image
except FileNotFoundError as e:
print(f"创建底层失败: {e}")
print(f"当前工作目录: {os.getcwd()}")
return Image.new('RGBA', target_size, (255, 255, 255, 255))
except Exception as e:
print(f"创建底层失败: {e}")
traceback.print_exc()
return Image.new('RGBA', target_size, (255, 255, 255, 255))
def add_frame(self, image, target_size):
"""添加边框"""
if not self.frames:
print("没有可用的边框素材")
return image
try:
# 随机选择一个边框
frame_file = random.choice(self.frames)
frame_path = os.path.join(self.frame_dir, frame_file)
# 加载边框图像
frame_image = Image.open(frame_path).convert('RGBA')
# 调整边框大小以匹配目标尺寸
frame_image = frame_image.resize(target_size, Image.Resampling.LANCZOS)
# 创建一个新图层用于合成
framed_image = Image.new('RGBA', target_size, (0, 0, 0, 0))
# 先放置基础图像
framed_image.paste(image, (0, 0))
# 再合成边框图像
framed_image.alpha_composite(frame_image)
print(f"添加边框: {frame_file}")
return framed_image
except Exception as e:
print(f"添加边框失败: {e}")
return image
def create_middle_layer(self, target_size):
"""创建中间层(文本框底图)"""
try:
middle_layer = Image.new('RGBA', target_size, (0, 0, 0, 0))
# 1. 首先确定固定的文本区域
width, height = target_size
# 定义文本区域大小
header_height = int(height * 0.2) # 占顶部20%
header_width = int(width * 0.9) # 占宽度的90%
# 标题区域的位置(居中)
title_x = (width - header_width) // 2
title_y = int(height * 0.16) # 从原来的0.05(5%)改为0.12(12%),向下移动
# 保存标题区域信息
self.title_area = {
'x': title_x,
'y': title_y,
'width': header_width,
'height': header_height
}
# 计算额外文本的区域(位于底部区域)
additional_text_y = int(height * 0.65) # 位于底部35%的位置
additional_text_height = int(height * 0.1) # 占高度的10%
# 保存额外文本区域信息
self.additional_text_area = {
'x': int(width * 0.1), # 左边距10%
'y': additional_text_y,
'width': int(width * 0.8), # 占宽度的80%
'height': additional_text_height
}
# 输出文本区域信息
print("文本区域信息:")
print(f"主标题区域: x={self.title_area['x']}, y={self.title_area['y']}, "
f"宽={self.title_area['width']}, 高={self.title_area['height']}")
# 2. 检查是否使用文本框底图
use_text_bg = random.random() < self.text_bg_posbility
print(f"环境变量USE_TEXT_BG设置为: {os.environ.get('USE_TEXT_BG', 'True')}, 是否使用文本框底图: {use_text_bg}")
if use_text_bg and self.text_bgs:
print("根据环境变量设置,使用文本框底图")
# 随机选择文本背景
text_bg_path = os.path.join(self.text_bg_dir, random.choice(self.text_bgs))
text_bg = Image.open(text_bg_path).convert('RGBA')
# 计算文本框底图的尺寸,适度放大
bg_width = int(width * 1.9) # 宽度为画布宽度的130%
bg_height = int(height * 0.85) # 高度为画布高度的50%
# 计算主标题的中心点
title_center_x = self.title_area['x'] + self.title_area['width'] // 2
title_center_y = self.title_area['y'] + self.title_area['height'] // 2
# 根据主标题中心点计算底图位置,确保完全对齐
bg_x = title_center_x - (bg_width // 2) # 从标题中心点向两侧扩展
bg_y = title_center_y - (bg_height // 2) # 从标题中心点向上下扩展
# 调整文本背景大小
text_bg = text_bg.resize((bg_width, bg_height), Image.Resampling.LANCZOS)
# 创建临时图层并合并
temp = Image.new('RGBA', target_size, (0, 0, 0, 0))
temp.paste(text_bg, (bg_x, bg_y), text_bg)
middle_layer.alpha_composite(temp)
print(f"文本背景: 宽={bg_width}px (画布宽度的{bg_width/width:.1f}倍), "
f"高={bg_height}px (画布高度的{bg_height/height:.1f}倍)")
else:
print("根据环境变量设置,不使用文本框底图,仅使用固定文本区域")
return middle_layer
except Exception as e:
print(f"创建中间层时出错: {str(e)}")
traceback.print_exc() # 打印详细错误信息
return Image.new('RGBA', target_size, (0, 0, 0, 0))
def create_text_layer(self, target_size, text_data=None):
"""创建文字层"""
try:
text_layer = Image.new('RGBA', target_size, (0, 0, 0, 0))
draw = ImageDraw.Draw(text_layer)
# 使用固定效果:字体蓝色立体效果
self.selected_effect = "文字蓝色立体效果"
print(f"使用文字效果: {self.selected_effect}")
# 检查文字数据
if text_data is None:
print("警告: 未提供文本数据,使用默认文本")
text_data = {'title': '旅游景点', 'subtitle': '', 'additional_texts': []}
print(f"处理文本数据: {text_data}")
# 1. 处理主标题
if hasattr(self, 'title_area') and 'title' in text_data and text_data['title']:
font_path = self._get_font_path()
title = text_data['title']
# 使用柠檬黄色作为主标题颜色
lemon_yellow = (255, 250, 55, 255) # 柠檬黄色
# 使用统一定义的标题区域
font, text_width, text_height, x, y = self._calculate_text_layout(
draw, font_path, title, self.title_area, size_factor=0.75)
# 绘制主标题文字
self._draw_text_with_effects(draw, title, font, x, y,
shadow_offset=8,
color=lemon_yellow)
# 打印调试信息
self._print_text_debug_info("主标题", font, text_width, x, y, font_path)
print(f"- 主标题颜色: 柠檬黄色 RGB(255, 250, 55)")
else:
print("警告: 无法处理主标题可能缺少标题数据或title_area未定义")
# 2. 处理副标题(如果有)
if hasattr(self, 'title_area') and 'subtitle' in text_data and text_data['subtitle']:
subtitle = text_data['subtitle']
# 计算副标题区域(在主标题下方)
subtitle_area = {
'x': self.title_area['x'],
'y': self.title_area['y'] + self.title_area['height'],
'width': self.title_area['width'],
'height': int(self.title_area['height'] * 0.5) # 副标题高度为主标题的一半
}
# 获取副标题字体和布局
font, text_width, text_height, x, y = self._calculate_text_layout(
draw, font_path, subtitle, subtitle_area, size_factor=0.6)
# 副标题保持白色
white_color = (255, 255, 255, 255)
# 绘制副标题
self._draw_text_with_effects(draw, subtitle, font, x, y,
shadow_offset=8,
color=white_color)
# 打印调试信息
self._print_text_debug_info("副标题", font, text_width, x, y, font_path)
# 3. 处理额外文本(如果有)
if 'additional_texts' in text_data and text_data['additional_texts']:
# 打印接收到的额外文本
print(f"接收到额外文本数据: {text_data['additional_texts']}")
# 过滤掉空文本项
valid_additional_texts = []
for item in text_data['additional_texts']:
if isinstance(item, dict) and 'text' in item and item['text']:
valid_additional_texts.append(item)
elif isinstance(item, str) and item:
# 如果是字符串,转换为字典格式
valid_additional_texts.append({"text": item, "position": "middle", "size_factor": 0.5})
print(f"有效额外文本项: {len(valid_additional_texts)}")
if valid_additional_texts and hasattr(self, 'title_area'):
# 获取主标题的字体大小
main_title_font_size = font.size if 'font' in locals() else 48 # 默认字体大小
# 使用固定字体
specific_font_path = self.default_font_path
# specific_font_path = os.path.join(self.font_dir, "华康海报体简.ttc")
if not os.path.isfile(specific_font_path):
specific_font_path = font_path if 'font_path' in locals() else self._get_font_path()
# 计算额外文本在屏幕上的位置
height = target_size[1]
width = target_size[0]
# 将垂直位置调整到主标题下方
title_bottom = self.title_area['y'] + self.title_area['height']
extra_text_y_start = title_bottom + int(height * 0.01) # 从原来的0.05(5%)减小到0.02(2%)
extra_text_height = int(height * 0.2)
# 安全边距
safe_margin_x = int(width * 0.05)
max_text_width = width - (safe_margin_x * 2)
# 总文本行数
total_lines = len(valid_additional_texts)
line_height = extra_text_height // total_lines if total_lines > 0 else 0
print(f"额外文本区域: y={extra_text_y_start}, 高度={extra_text_height}, 每行高度={line_height}")
print(f"文本安全宽度: {max_text_width}px (留出两侧各{safe_margin_x}px安全边距)")
print(f"文本颜色: 统一白色")
# 渲染每一行文本
for i, text_item in enumerate(valid_additional_texts):
item_text = text_item['text']
# 检查文本内容
if not item_text:
print(f"警告: 额外文本项 {i+1} 文本为空,跳过")
continue
# 设置字体大小为主标题的0.8倍或使用指定的size_factor
size_factor = text_item.get('size_factor', 0.8)
font_size = int(main_title_font_size * size_factor)
text_font = ImageFont.truetype(specific_font_path, font_size)
# 测量文本宽度并调整
text_width = draw.textlength(item_text, font=text_font)
# 保护措施:如果文本宽度超出安全区域,逐步缩小字体直到适合
while text_width > max_text_width and font_size > int(main_title_font_size * 0.4):
size_factor *= 0.95
font_size = int(main_title_font_size * size_factor)
text_font = ImageFont.truetype(specific_font_path, font_size)
text_width = draw.textlength(item_text, font=text_font)
# 再次测量调整后的文本尺寸
text_bbox = draw.textbbox((0, 0), item_text, font=text_font)
text_height = text_bbox[3] - text_bbox[1]
# 获取位置参数
position = text_item.get('position', 'bottom')
# 根据位置设置垂直位置
if position == 'top':
line_y = int(height * 0.05) + (i * line_height)
elif position == 'middle':
line_y = int(height * 0.4) + (i * line_height)
else: # position == 'bottom' 或其他
# 在底部区域使用更大的垂直间距比如整个海报高度的65%到85%
bottom_start = int(height * 0.65)
bottom_height = int(height * 0.2)
bottom_line_height = bottom_height // total_lines if total_lines > 0 else 0
line_y = bottom_start + (i * bottom_line_height)
# 水平居中位置
line_x = (width - text_width) // 2
# 统一使用白色文本
text_color = (255, 255, 255, 255) # 白色
# 绘制文本
self._draw_text_with_effects(draw, item_text, text_font, line_x, line_y,
shadow_offset=3,
color=text_color)
# 打印调试信息
print(f"额外文本{i+1}: '{item_text}'")
print(f"- 文本颜色: 白色")
print(f"- 字体大小: {font_size}px (主标题的{size_factor:.2f}倍)")
print(f"- 位置: x={line_x}, y={line_y}")
else:
print("无法处理额外文本没有有效的额外文本项或title_area未定义")
return text_layer
except Exception as e:
print(f"创建文字层时出错: {str(e)}")
traceback.print_exc()
return Image.new('RGBA', target_size, (0, 0, 0, 0))
def _get_font_path(self):
"""获取并验证字体路径"""
# 获取随机字体
font_path = self.get_random_font()
try:
# 尝试加载字体
ImageFont.truetype(font_path, size=1) # 先用小尺寸测试字体是否可用
except Exception as e:
print(f"加载字体失败: {str(e)},使用默认字体")
font_path = self.default_font_path
return font_path
def _get_text_area(self, text_item, target_size, index):
"""获取文本区域"""
width, height = target_size
# 根据指定位置创建文本区域
position = text_item.get('position', 'bottom')
if position == 'custom' and 'x' in text_item and 'y' in text_item:
# 自定义位置
text_area = {
'x': text_item['x'],
'y': text_item['y'],
'width': text_item.get('width', width // 2),
'height': text_item.get('height', height // 10)
}
elif position == 'top':
# 顶部区域
text_area = {
'x': int(width * 0.1),
'y': int(height * 0.05),
'width': int(width * 0.8),
'height': int(height * 0.1)
}
elif position == 'middle':
# 中部区域
text_area = {
'x': int(width * 0.1),
'y': int(height * 0.45),
'width': int(width * 0.8),
'height': int(height * 0.1)
}
else: # 默认为底部
# 底部区域,增加随机性
# 从图像的一半高开始height * 0.5到底部的0.9位置之间随机选择
random_y_position = random.uniform(0.5, 0.85)
# 水平位置也增加随机性在10%到30%之间
random_x_position = random.uniform(0.05, 0.1)
# 多个底部文本时的偏移
offset = index * 0.08
text_area = {
'x': int(width * random_x_position),
'y': int(height * (random_y_position + offset)),
'width': int(width * 0.8),
'height': int(height * 0.1)
}
return text_area
def _calculate_text_layout(self, draw, font_path, text, text_area, size_factor=0.85, align_left=False):
"""计算文本布局、字体大小和位置
Args:
draw: ImageDraw对象
font_path: 字体路径
text: 文本内容
text_area: 文本区域字典
size_factor: 字体大小因子(相对于区域高度)
align_left: 是否左对齐
"""
# 设置初始字体大小
initial_font_size = int(text_area['height'] * size_factor)
font = ImageFont.truetype(font_path, initial_font_size)
# 调整字体大小以适应宽度目标是占据95%的宽度)
target_width = text_area['width'] * 0.98 # 增加宽度利用率到98%
text_width = draw.textlength(text, font=font)
# 字体大小调整逻辑优化:先尝试增大字体以更好地填充宽度
if text_width < target_width * 0.9 and initial_font_size < int(text_area['height'] * 0.98):
# 如果文本宽度小于目标宽度的90%,尝试增大字体
while text_width < target_width * 0.9 and initial_font_size < int(text_area['height'] * 0.98):
initial_font_size += 2
font = ImageFont.truetype(font_path, initial_font_size)
text_width = draw.textlength(text, font=font)
# 如果字体太大,再缩小字体以适应宽度
while text_width > target_width and initial_font_size > 10:
initial_font_size -= 2
font = ImageFont.truetype(font_path, initial_font_size)
text_width = draw.textlength(text, font=font)
# 获取文字高度
text_bbox = draw.textbbox((0, 0), text, font=font)
text_height = text_bbox[3] - text_bbox[1]
# 计算位置
if align_left:
# 左对齐
x = text_area['x'] + int(text_area['width'] * 0.05) # 5% 的左边距
y = text_area['y'] + (text_area['height'] - text_height) // 2
else:
# 居中对齐
x = text_area['x'] + (text_area['width'] - text_width) // 2
y = text_area['y'] + (text_area['height'] - text_height) // 2
return font, text_width, text_height, x, y
def _draw_text_with_effects(self, draw, text, font, x, y, shadow_offset=8, color=(255, 255, 255, 255)):
"""绘制带立体效果的文字
Args:
draw: ImageDraw对象
text: 文本内容
font: 字体对象
x, y: 文本坐标
shadow_offset: 立体深度
color: 文本表面颜色
"""
# 文字颜色使用传入的color参数
text_color = color
# 蓝色立体效果 - 只用一种深蓝色
blue_color = (29, 60, 171, 255) # 深蓝色
# 立体深度 - 使用更大的深度
depth = 8 # 8像素的立体深度
# 1. 首先绘制深蓝色立体部分
for i in range(1, depth + 1):
# 下方
draw.text((x, y + i), text, font=font, fill=blue_color)
# 右侧
draw.text((x + i, y), text, font=font, fill=blue_color)
# 右下角
draw.text((x + i, y + i), text, font=font, fill=blue_color)
# 2. 绘制蓝色描边 - 增强文字的边缘清晰度
stroke_thickness = 2
for dx in range(-stroke_thickness, stroke_thickness+1):
for dy in range(-stroke_thickness, stroke_thickness+1):
if dx == 0 and dy == 0:
continue
# 计算当前点到中心的距离
distance = (dx**2 + dy**2)**0.5
# 如果距离在描边范围内,绘制蓝色描边
if distance <= stroke_thickness:
draw.text((x + dx, y + dy), text, font=font, fill=blue_color)
# 3. 最后绘制主文字在最上层,使用传入的颜色
draw.text((x, y), text, font=font, fill=text_color)
def _print_text_debug_info(self, text_type, font, text_width, x, y, font_path):
"""打印文本调试信息"""
print(f"- {text_type} 字体大小: {font.size}px")
print(f"- {text_type} 文本宽度: {text_width}px")
print(f"- {text_type} 位置: x={x}, y={y}")
print(f"- {text_type} 使用字体: {os.path.basename(font_path)}")
def add_stickers(self, poster_image):
"""
在海报上添加装饰性贴纸
Args:
poster_image: 要添加贴纸的海报图像对象
Returns:
添加了贴纸的海报图像对象
"""
if not hasattr(self, 'sticker_files') or not self.sticker_files:
print("没有可用的贴纸素材,跳过贴纸添加")
return poster_image
try:
# 获取海报尺寸
width, height = poster_image.size
# 决定是否添加贴纸50%概率)
if random.random() < 0.5:
print("随机决定不添加贴纸")
return poster_image
# 随机决定添加1-3个贴纸
sticker_count = random.randint(1, 3)
print(f"准备添加 {sticker_count} 个贴纸")
# 创建一个新图层用于合成
result_image = poster_image.copy()
for i in range(sticker_count):
# 随机选择一个贴纸
sticker_file = random.choice(self.sticker_files)
sticker_path = os.path.join(self.sticker_dir, sticker_file)
# 加载贴纸图像
sticker = Image.open(sticker_path).convert('RGBA')
# 调整贴纸大小原始尺寸的10%-30%
sticker_size_factor = random.uniform(0.1, 0.3)
new_width = int(width * sticker_size_factor)
new_height = int(new_width * sticker.height / sticker.width) # 保持纵横比
sticker = sticker.resize((new_width, new_height), Image.Resampling.LANCZOS)
# 随机选择贴纸位置(避开中央区域)
margin = int(width * 0.1) # 边缘区域的10%
# 生成随机位置(避开中间区域)
if random.random() < 0.5: # 左/右边缘
x = random.randint(margin, int(width * 0.25)) if random.random() < 0.5 else random.randint(int(width * 0.75), width - new_width - margin)
y = random.randint(margin, height - new_height - margin)
else: # 上/下边缘
x = random.randint(margin, width - new_width - margin)
y = random.randint(margin, int(height * 0.25)) if random.random() < 0.5 else random.randint(int(height * 0.75), height - new_height - margin)
# 将贴纸粘贴到结果图像上
result_image.paste(sticker, (x, y), sticker)
print(f"添加贴纸 {i+1}: {sticker_file}, 大小: {new_width}x{new_height}, 位置: ({x}, {y})")
return result_image
except Exception as e:
print(f"添加贴纸失败: {e}")
traceback.print_exc()
return poster_image
def create_poster(self, image_input, text_data):
"""
Creates a poster by combining the base image, frame (optional), stickers (optional),
and text layers.
Args:
image_input: 底图输入,可以是图片路径字符串或 PIL Image 对象
text_data: Dictionary containing text information (
{
'title': 'Main Title Text',
'subtitle': 'Subtitle Text (optional)',
'additional_texts': [{'text': '...', 'position': 'top/bottom/center', 'size_factor': 0.8}, ...]
}
).
Returns:
A PIL Image object representing the final poster, or None if creation failed.
"""
target_size = (900, 1200) # TODO: Make target_size a parameter?
print(f"\n--- Creating Poster --- ")
if isinstance(image_input, Image.Image):
print(f"Input: PIL Image 对象,尺寸 {image_input.size}")
else:
print(f"Input Image: {image_input}")
print(f"Text Data: {text_data}")
try:
# 1. 创建底层(图片)
base_layer = self.create_base_layer(image_input, target_size)
if not base_layer:
raise ValueError("Failed to create base layer.")
print("Base layer created.")
# 2. (可选) 添加边框 - 根据计数器决定
# if self.poster_count % 5 == 0: # 每5张海报添加一次边框
if random.random() < self.img_frame_posbility:
print("Attempting to add frame...")
base_layer = self.add_frame(base_layer, target_size)
else:
print("Skipping frame addition.")
self.poster_count += 1 # 增加计数器
# 3. 创建中间层(文本框底图)
# middle_layer 包含文本区域定义 (self.title_area, self.additional_text_area)
middle_layer = self.create_middle_layer(target_size)
print("Middle layer (text areas defined) created.")
# 4. 创建文本层
text_layer = self.create_text_layer(target_size, text_data)
print("Text layer created.")
# 5. 合成图层
# Start with the base layer (which might already have a frame)
final_poster = base_layer
# Add middle layer (text backgrounds)
final_poster.alpha_composite(middle_layer)
# Add text layer
final_poster.alpha_composite(text_layer)
# (可选) 添加贴纸
final_poster = self.add_stickers(final_poster)
print("Layers composed.")
# 转换回 RGB 以保存为 JPG/PNG (移除 alpha通道)
final_poster_rgb = final_poster.convert("RGB")
# 移除保存逻辑,直接返回 Image 对象
# final_save_path = os.path.join(self.output_dir, output_name)
# os.makedirs(os.path.dirname(final_save_path), exist_ok=True)
# final_poster_rgb.save(final_save_path)
# print(f"Final poster saved to: {final_save_path}")
# return final_save_path
return final_poster_rgb
except Exception as e:
print(f"Error creating poster: {e}")
traceback.print_exc()
return None
def set_img_frame_possibility(self, img_frame_possibility):
self.img_frame_posbility = img_frame_possibility
def set_text_bg_possibility(self, text_bg_possibility):
self.text_bg_posbility = text_bg_possibility
def main():
# 设置是否使用文本框底图True为使用False为不使用
os.environ['USE_TEXT_BG'] = 'False' # 或 'True'
print(f"设置环境变量USE_TEXT_BG为: {os.environ['USE_TEXT_BG']}")
# 创建生成器实例
generator = PosterGenerator("/root/autodl-tmp/poster_baseboard_0403")
poster_config_path = "/root/autodl-tmp/poster_generate_result/2025-04-16_20-49-32.json"
poster_config = PosterConfig(poster_config_path)
for item in poster_config.get_config():
text_data = {
"title": f"{item['main_title']}",
"subtitle": "",
"additional_texts": [
{"text": f"{item['texts'][0]}", "position": "bottom", "size_factor": 0.5},
{"text": f"{item['texts'][1]}", "position": "bottom", "size_factor": 0.5}
]
}
# 处理目录中的所有图片
img_path = "/root/autodl-tmp/poster_baseboard_0403/output_collage/random_collage_1_collage.png"
# 先加载图片,然后传递 PIL Image 对象
try:
# 先加载图片
collage_img = Image.open(img_path).convert('RGBA')
print(f"已加载拼贴图: {img_path}, 尺寸: {collage_img.size}")
# 传递图片对象而不是路径
generator.create_poster(collage_img, text_data)
except Exception as e:
print(f"加载或处理图片时出错: {e}")
traceback.print_exc()
# 失败时回退到使用路径
generator.create_poster(img_path, text_data)
if __name__ == "__main__":
main()