2025-05-12 02:39:56 +08:00

524 lines
25 KiB
Python
Raw Permalink 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.

# pattern_to_tshirt.py
import sys
import os
import json
import io
import cv2
import numpy as np
from PIL import Image, ImageFilter, ImageDraw, ImageChops
import uuid
import urllib.request
import urllib3
import requests
from pydantic import BaseModel
from typing import Optional
import base64
import asyncio
import warnings
import tempfile
from urllib.parse import urlparse
import torch
import time
import gc
# 关闭不必要的警告
warnings.filterwarnings("ignore", category=UserWarning)
warnings.filterwarnings("ignore", category=FutureWarning)
class PtnToTshirtService:
# 默认配置
DEFAULT_CONFIG = {
'background_removed_marker': "_rmbg",
'ptt_exclude_markers': ["_upscaled", "_vector", "_processed", "_tshirt", "_tryon"],
'tshirt_marker': "_tshirt",
'processed_pattern_marker': "_processed",
'tshirt_image_path': 'home/tshirt',
'tshirt_urls': [], # 新增: T恤图片URL列表
'alpha': 1, # 透明度0表示全透明1表示全不透明
'ptt_design_size_ratio': 0.4, # 设计图像占T恤图像的比例
'ptt_design_offset': [0.5, 0.45], # 设计图像在T恤图像中的相对位置 [x, y]
'ptt_design_rotation': 0, # 设计图案旋转角度
'enable_gradient_effect': True, # 是否启用渐变效果
'gradient_width': 512, # 渐变宽度
'gradient_direction': 'outward', # 渐变方向: 'outward', 'inward'
'gradient_type': 'linear', # 渐变类型: 'linear', 'radial'
'gradient_max_alpha': 150, # 渐变的最大透明度值0-255
'gradient_start_alpha': 0, # 渐变起始处的透明度0-255
'gradient_color': [255, 255, 255, 255], # 渐变颜色
'gradient_blur_intensity': 10, # 渐变模糊强度
'gradient_center': [0.5, 0.5], # 渐变中心位置,相对于设计图案的 [x, y]
'gradient_repeat_count': 1, # 渐变重复次数
'ptt_enable_texture_effect': False, # 是否启用纹理效果
'ptt_texture_type': 'lines', # 纹理类型: 'noise', 'lines'
'ptt_texture_blend_mode': 'multiply', # 纹理混合模式
'enable_save_processed_design': True, # 是否单独保存处理后的设计图案
'ptt_design_output_format': 'png', # 设计图案保存格式: 'png' 或 'tiff'
'ptt_enable_color_matching': True, # 是否启用颜色匹配
'ptt_enable_lighting_effect': False, # 是否启用光效
'ptt_enable_monochrome': False,
'ptt_light_intensity': 0.5, # 光照强度
'ptt_light_position': [0.5, 0.3], # 光源位置 [相对位置 x, y]
'ptt_light_radius_ratio': [0.4, 0.25], # 光源半径相对比例 [宽, 高]
'ptt_light_angle': 45, # 光源角度(度)
'ptt_light_blur': 91, # 光源模糊程度
'ptt_light_shape': 'ellipse', # 光源形状: 'ellipse', 'circle', 'rect'
}
def __init__(self):
"""初始化图案到T恤服务"""
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {self.device}")
if torch.cuda.is_available():
print(f"GPU: {torch.cuda.get_device_name(0)}")
print(f"显存总量: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f}GB")
def overlay_image_alpha(self, img, img_overlay, pos, alpha_mask):
"""在图像上叠加另一个具有透明度的图像"""
x, y = pos
y1, y2 = max(0, y), min(img.shape[0], y + img_overlay.shape[0])
x1, x2 = max(0, x), min(img.shape[1], x + img_overlay.shape[1])
y1o, y2o = max(0, -y), min(img_overlay.shape[0], img.shape[0] - y)
x1o, x2o = max(0, -x), min(img_overlay.shape[1], img.shape[1] - x)
if y1 >= y2 or x1 >= x2 or y1o >= y2o or x1o >= x2o:
return
img_crop = img[y1:y2, x1:x2]
img_overlay_crop = img_overlay[y1o:y2o, x1o:x2o]
alpha = alpha_mask[y1o:y2o, x1o:x2o, np.newaxis]
img_crop[:] = alpha * img_overlay_crop + (1 - alpha) * img_crop
def color_transfer(self, source, target):
"""颜色匹配:将源图像的颜色转换为目标图像的颜色风格"""
source = cv2.cvtColor(source, cv2.COLOR_BGR2LAB)
target = cv2.cvtColor(target, cv2.COLOR_BGR2LAB)
src_mean, src_std = cv2.meanStdDev(source)
tgt_mean, tgt_std = cv2.meanStdDev(target)
src_mean = src_mean.reshape(1, 1, 3)
src_std = src_std.reshape(1, 1, 3)
tgt_mean = tgt_mean.reshape(1, 1, 3)
tgt_std = tgt_std.reshape(1, 1, 3)
result = (source - src_mean) * (tgt_std / src_std) + tgt_mean
result = np.clip(result, 0, 255)
result = result.astype(np.uint8)
return cv2.cvtColor(result, cv2.COLOR_LAB2BGR)
def apply_lighting_effect(self, image, light_intensity=0.5, light_position=[0.5, 0.3],
light_radius_ratio=[0.4, 0.25], light_angle=45, light_blur=91, light_shape='ellipse'):
"""应用光照效果到图像"""
height, width = image.shape[:2]
light_position = (int(light_position[0] * width), int(light_position[1] * height))
light_radius = (int(light_radius_ratio[0] * width), int(light_radius_ratio[1] * height))
mask = np.zeros((height, width), dtype=np.uint8)
if light_shape == 'ellipse':
cv2.ellipse(mask, light_position, light_radius, light_angle, 0, 360, 255, -1)
elif light_shape == 'circle':
cv2.circle(mask, light_position, min(light_radius), 255, -1)
elif light_shape == 'rect':
rect_top_left = (light_position[0] - light_radius[0] // 2, light_position[1] - light_radius[1] // 2)
rect_bottom_right = (light_position[0] + light_radius[0] // 2, light_position[1] + light_radius[1] // 2)
cv2.rectangle(mask, rect_top_left, rect_bottom_right, 255, -1)
mask = cv2.GaussianBlur(mask, (light_blur, light_blur), 0)
mask = mask.astype(np.float32) / 255
result = image.astype(np.float32)
for i in range(3):
result[:, :, i] = result[:, :, i] * (1 - light_intensity + mask * light_intensity)
return result.astype(np.uint8)
def apply_monochrome(self, image):
"""将图像转换为单色"""
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
monochrome_image = cv2.cvtColor(gray_image, cv2.COLOR_GRAY2BGR)
return monochrome_image
def enhance_design(self, design_image, tshirt_image, config):
"""增强设计图像,应用各种效果"""
if config.get('ptt_enable_color_matching', self.DEFAULT_CONFIG['ptt_enable_color_matching']):
design_image = self.color_transfer(design_image, tshirt_image)
if config.get('ptt_enable_lighting_effect', self.DEFAULT_CONFIG['ptt_enable_lighting_effect']):
design_image = self.apply_lighting_effect(
design_image,
config.get('ptt_light_intensity', self.DEFAULT_CONFIG['ptt_light_intensity']),
config.get('ptt_light_position', self.DEFAULT_CONFIG['ptt_light_position']),
config.get('ptt_light_radius_ratio', self.DEFAULT_CONFIG['ptt_light_radius_ratio']),
config.get('ptt_light_angle', self.DEFAULT_CONFIG['ptt_light_angle']),
config.get('ptt_light_blur', self.DEFAULT_CONFIG['ptt_light_blur']),
config.get('ptt_light_shape', self.DEFAULT_CONFIG['ptt_light_shape'])
)
if config.get('ptt_enable_monochrome', self.DEFAULT_CONFIG['ptt_enable_monochrome']):
design_image = self.apply_monochrome(design_image)
return design_image
def add_edge_gradient(self, image, gradient_width, gradient_direction, gradient_type,
gradient_max_alpha, gradient_start_alpha, gradient_color,
gradient_blur_intensity, gradient_center):
"""添加边缘渐变效果"""
alpha = image.getchannel('A')
width, height = alpha.size
mask = Image.new('L', (width, height), 0)
draw = ImageDraw.Draw(mask)
# 确保渐变宽度不超过图像尺寸的一半
gradient_width = min(gradient_width, width // 2, height // 2)
if gradient_type == 'linear':
if gradient_direction == 'outward':
for i in range(gradient_width):
if i >= width // 2 or i >= height // 2:
break # 避免无效的矩形坐标
fill_value = int(gradient_start_alpha + (gradient_max_alpha - gradient_start_alpha) * (i / gradient_width))
draw.rectangle([i, i, width - i - 1, height - i - 1], fill=fill_value)
elif gradient_direction == 'inward':
for i in range(gradient_width):
if i >= width // 2 or i >= height // 2:
break # 避免无效的矩形坐标
fill_value = int(gradient_start_alpha + (gradient_max_alpha - gradient_start_alpha) * ((gradient_width - i) / gradient_width))
draw.rectangle([i, i, width - i - 1, height - i - 1], fill=fill_value)
elif gradient_type == 'radial':
center_x = int(width * gradient_center[0])
center_y = int(height * gradient_center[1])
max_radius = min(center_x, center_y, width - center_x, height - center_y)
for i in range(gradient_width):
radius = max_radius * (i / gradient_width)
if radius <= 0:
continue
fill_value = int(gradient_start_alpha + (gradient_max_alpha - gradient_start_alpha) * (i / gradient_width))
draw.ellipse([center_x - radius, center_y - radius, center_x + radius, center_y + radius], fill=fill_value)
if gradient_blur_intensity < 1:
gradient_blur_intensity = 1
mask = mask.filter(ImageFilter.GaussianBlur(gradient_blur_intensity))
alpha = ImageChops.multiply(alpha, mask)
image.putalpha(alpha)
if gradient_color != [255, 255, 255, 255]:
colored_mask = Image.new('RGBA', image.size, tuple(gradient_color))
colored_mask.putalpha(mask)
image = Image.alpha_composite(image, colored_mask)
return image
def add_gradient_repeat(self, image, gradient_repeat_count, *args, **kwargs):
"""重复应用渐变效果"""
for _ in range(max(gradient_repeat_count, 1)): # 确保至少执行一次
image = self.add_edge_gradient(image, *args, **kwargs)
return image
def generate_noise_texture(self, size, intensity=64):
"""生成噪点纹理"""
noise = np.random.randint(0, intensity, (size, size, 4), dtype=np.uint8)
noise[..., 3] = 255 # 设置 alpha 通道为不透明
return Image.fromarray(noise)
def generate_line_texture(self, size, line_width=4, spacing=20, color=(0, 0, 0, 255)):
"""生成线条纹理"""
texture = Image.new('RGBA', (size, size), (255, 255, 255, 0))
draw = ImageDraw.Draw(texture)
for y in range(0, size, spacing):
draw.line([(0, y), (size, y)], fill=color, width=line_width)
for x in range(0, size, spacing):
draw.line([(x, 0), (x, size)], fill=color, width=line_width)
return texture
def add_texture(self, image, texture_type, texture_blend_mode):
"""添加纹理效果到图像"""
if texture_type == 'noise':
texture = self.generate_noise_texture(image.size[0])
elif texture_type == 'lines':
texture = self.generate_line_texture(image.size[0])
else:
return image
if texture_blend_mode == 'multiply':
return ImageChops.multiply(image, texture)
elif texture_blend_mode == 'overlay':
return ImageChops.overlay(image, texture)
else:
return image
def rotate_image_with_transparency(self, image, angle):
"""旋转带有透明度的图像"""
rotated_image = image.rotate(angle, expand=True)
return rotated_image
def save_processed_design_image(self, design_image, output_format='png'):
"""保存处理后的设计图像"""
try:
img_bytes = io.BytesIO()
# 确保使用包含透明背景的BGRA格式
design_image_pil = Image.fromarray(cv2.cvtColor(design_image, cv2.COLOR_BGRA2RGBA)).convert('RGBA')
if output_format == 'tiff':
design_image_pil.save(img_bytes, format='TIFF', save_all=True, compression='tiff_deflate')
else:
design_image_pil.save(img_bytes, format='PNG')
img_bytes.seek(0)
return img_bytes
except Exception as e:
print(f"保存处理后的设计图像时发生错误: {e}")
return None
def generate_tshirt_image(self, design_image, tshirt_image, config):
"""将花型图案合成到T恤图像上"""
# 合并默认配置和用户配置
config = {**self.DEFAULT_CONFIG, **config}
# 将设计图像从RGBA转换为BGRA如果需要
if isinstance(design_image, np.ndarray) and design_image.shape[2] == 4:
if design_image.dtype != np.uint8:
design_image = design_image.astype(np.uint8)
else:
# 如果输入是PIL图像转换为OpenCV格式
if isinstance(design_image, Image.Image):
design_image = cv2.cvtColor(np.array(design_image), cv2.COLOR_RGBA2BGRA)
else:
raise ValueError("设计图像必须是PIL Image或带Alpha通道的NumPy数组")
# 对设计图像应用渐变效果
if config.get('enable_gradient_effect', self.DEFAULT_CONFIG['enable_gradient_effect']):
design_image_pil = Image.fromarray(cv2.cvtColor(design_image, cv2.COLOR_BGRA2RGBA)).convert("RGBA")
design_image_pil = self.add_gradient_repeat(
design_image_pil,
config.get('gradient_repeat_count', self.DEFAULT_CONFIG['gradient_repeat_count']),
config.get('gradient_width', self.DEFAULT_CONFIG['gradient_width']),
config.get('gradient_direction', self.DEFAULT_CONFIG['gradient_direction']),
config.get('gradient_type', self.DEFAULT_CONFIG['gradient_type']),
config.get('gradient_max_alpha', self.DEFAULT_CONFIG['gradient_max_alpha']),
config.get('gradient_start_alpha', self.DEFAULT_CONFIG['gradient_start_alpha']),
config.get('gradient_color', self.DEFAULT_CONFIG['gradient_color']),
config.get('gradient_blur_intensity', self.DEFAULT_CONFIG['gradient_blur_intensity']),
config.get('gradient_center', self.DEFAULT_CONFIG['gradient_center'])
)
design_image = cv2.cvtColor(np.array(design_image_pil), cv2.COLOR_RGBA2BGRA)
# 应用纹理效果到设计图案
if config.get('ptt_enable_texture_effect', self.DEFAULT_CONFIG['ptt_enable_texture_effect']):
design_image_pil = Image.fromarray(cv2.cvtColor(design_image, cv2.COLOR_BGRA2RGBA)).convert("RGBA")
design_image_pil = self.add_texture(
design_image_pil,
config.get('ptt_texture_type', self.DEFAULT_CONFIG['ptt_texture_type']),
config.get('ptt_texture_blend_mode', self.DEFAULT_CONFIG['ptt_texture_blend_mode'])
)
design_image = cv2.cvtColor(np.array(design_image_pil), cv2.COLOR_RGBA2BGRA)
# 进行设计图像增强处理
design_image_enhanced = self.enhance_design(design_image[:, :, :3], tshirt_image, config)
# 应用旋转效果到设计图案
ptt_design_rotation = config.get('ptt_design_rotation', self.DEFAULT_CONFIG['ptt_design_rotation'])
if ptt_design_rotation != 0:
design_image_pil = Image.fromarray(cv2.cvtColor(design_image, cv2.COLOR_BGRA2RGBA)).convert("RGBA")
design_image_pil = self.rotate_image_with_transparency(design_image_pil, ptt_design_rotation)
processed_design_image_with_alpha = cv2.cvtColor(np.array(design_image_pil), cv2.COLOR_RGBA2BGRA)
else:
processed_design_image_with_alpha = cv2.merge((design_image_enhanced, design_image[:, :, 3]))
# 保存处理后的设计图像
processed_design_io = None
if config.get('enable_save_processed_design', self.DEFAULT_CONFIG['enable_save_processed_design']):
processed_design_io = self.save_processed_design_image(
processed_design_image_with_alpha,
config.get('ptt_design_output_format', self.DEFAULT_CONFIG['ptt_design_output_format'])
)
# 调整设计图像大小
tshirt_height, tshirt_width = tshirt_image.shape[:2]
design_width = int(tshirt_width * config.get('ptt_design_size_ratio', self.DEFAULT_CONFIG['ptt_design_size_ratio']))
aspect_ratio = processed_design_image_with_alpha.shape[0] / processed_design_image_with_alpha.shape[1]
design_height = int(design_width * aspect_ratio)
design_image_resized = cv2.resize(processed_design_image_with_alpha, (design_width, design_height))
# 提取Alpha通道
alpha_channel = design_image_resized[:, :, 3] / 255.0
# 计算设计图像在T恤上的位置
ptt_design_offset = config.get('ptt_design_offset', self.DEFAULT_CONFIG['ptt_design_offset'])
design_position = (
int((tshirt_width - design_width) * ptt_design_offset[0]),
int((tshirt_height - design_height) * ptt_design_offset[1])
)
# 将设计图像叠加到T恤图像上
result_image = tshirt_image.copy()
self.overlay_image_alpha(result_image, design_image_resized[:, :, :3], design_position, alpha_channel)
# 返回结果图像和处理后的设计图像
return result_image, processed_design_io
def image_to_base64(self, image, format='png'):
"""将图像转换为base64字符串"""
try:
if isinstance(image, np.ndarray):
# 如果是OpenCV图像NumPy数组转换为PIL图像
if image.shape[2] == 3:
# BGR转RGB
image_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
else:
# BGRA转RGBA
image_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGRA2RGBA))
else:
# 已经是PIL图像
image_pil = image
# 保存为BytesIO对象
buffered = io.BytesIO()
image_pil.save(buffered, format=format.upper())
img_str = base64.b64encode(buffered.getvalue()).decode()
return img_str
except Exception as e:
print(f"将图像转换为base64时发生错误: {e}")
return None
def download_tshirt_images(self, config):
"""下载T恤图像列表"""
try:
# 首先检查是否提供了T恤图片URL列表
tshirt_urls = config.get('tshirt_urls', self.DEFAULT_CONFIG['tshirt_urls'])
if tshirt_urls and isinstance(tshirt_urls, list) and len(tshirt_urls) > 0:
tshirt_images = []
for url in tshirt_urls:
if self.is_valid_url(url):
tshirt_io = self.download_image(url)
if tshirt_io:
tshirt_image = cv2.imdecode(np.frombuffer(tshirt_io.getvalue(), np.uint8), cv2.IMREAD_COLOR)
if tshirt_image is not None:
tshirt_images.append(tshirt_image)
if tshirt_images:
return tshirt_images
# 如果没有提供URL或URL下载失败则尝试使用本地模板
sample_tshirt_path = os.path.join(config.get('tshirt_image_path', self.DEFAULT_CONFIG['tshirt_image_path']), 'sample_tshirt.jpg')
if os.path.exists(sample_tshirt_path):
tshirt_image = cv2.imread(sample_tshirt_path)
return [tshirt_image]
else:
# 创建一个纯白色的示例T恤图像作为最后的备选
tshirt_image = np.ones((800, 600, 3), dtype=np.uint8) * 255
return [tshirt_image]
except Exception as e:
print(f"下载T恤图像时发生错误: {e}")
# 创建一个纯白色的示例T恤图像作为最后的备选
tshirt_image = np.ones((800, 600, 3), dtype=np.uint8) * 255
return [tshirt_image]
def is_valid_url(self, url):
"""检查URL是否有效"""
try:
result = urlparse(url)
return all([result.scheme, result.netloc])
except:
return False
def download_image(self, url):
"""下载图像"""
try:
if self.is_valid_url(url):
response = requests.get(url, verify=False, timeout=10)
if response.status_code == 200:
return io.BytesIO(response.content)
return None
except Exception as e:
print(f"下载图像失败: {str(e)}")
return None
async def pattern_to_tshirt(self, image_url, config=None):
"""将花型图案添加到T恤上URL输入"""
if not config:
config = {}
try:
# 下载花型图案
design_io = self.download_image(image_url)
if not design_io:
return {"status": "error", "message": "无法下载图像"}
return await self.pattern_to_tshirt_from_file(design_io.getvalue(), config)
except Exception as e:
import traceback
error_trace = traceback.format_exc()
print(f"处理图像时发生错误: {str(e)}\n{error_trace}")
return {"status": "error", "message": f"处理图像失败: {str(e)}"}
async def pattern_to_tshirt_from_file(self, file_content, config=None):
"""将花型图案添加到T恤上文件输入"""
if not config:
config = {}
try:
# 加载花型图案
design_io = io.BytesIO(file_content)
design_image = Image.open(design_io).convert("RGBA")
# 获取T恤图像列表
tshirt_images = self.download_tshirt_images(config)
if not tshirt_images:
return {"status": "error", "message": "无法获取T恤图像模板"}
results = []
processed_design_base64 = None
# 处理每个T恤图像
for tshirt_image in tshirt_images:
try:
# 生成合成图像
result_image, processed_design_io = self.generate_tshirt_image(design_image, tshirt_image, config)
# 转换为base64
result_base64 = self.image_to_base64(result_image)
# 如果有处理后的设计图像也转换为base64
if processed_design_io and processed_design_base64 is None:
processed_design_image = Image.open(processed_design_io)
processed_design_base64 = self.image_to_base64(processed_design_image)
results.append({
"tshirt_image": result_base64
})
except Exception as e:
import traceback
error_trace = traceback.format_exc()
print(f"处理单个T恤图像时发生错误: {str(e)}\n{error_trace}")
# 继续处理下一个T恤图像
if not results:
return {"status": "error", "message": "所有T恤图像处理均失败"}
response = {
"status": "success",
"results": results
}
# 如果有处理后的设计图像,添加到响应中
if processed_design_base64:
response["processed_design"] = processed_design_base64
return response
except Exception as e:
import traceback
error_trace = traceback.format_exc()
print(f"处理图像时发生错误: {str(e)}\n{error_trace}")
return {"status": "error", "message": f"处理图像失败: {str(e)}"}
def cleanup(self):
"""清理资源"""
if torch.cuda.is_available():
torch.cuda.empty_cache()
gc.collect()