diff --git a/app/core/utils_litserve.py b/app/core/utils_litserve.py index fcaa4a4..7cf0e07 100644 --- a/app/core/utils_litserve.py +++ b/app/core/utils_litserve.py @@ -44,6 +44,9 @@ def merge_images_to_square(outfit_items: List[Dict[str, str]], max_len=9, add_te # Define the final canvas size CANVAS_SIZE = 1024 + # 定义每个 item 的外边距 + MARGIN = 5 # 5像素外边距 + # 1. Create the final white canvas # Using 'RGB' mode for JPG output canvas = Image.new('RGB', (CANVAS_SIZE, CANVAS_SIZE), 'white') @@ -88,6 +91,9 @@ def merge_images_to_square(outfit_items: List[Dict[str, str]], max_len=9, add_te # Get the correct list of target areas based on the number of valid images target_areas = quadrants.get(num_images, []) + if not target_areas: + raise ValueError(f"No layout defined for {num_images} images.") + # 4. Resize and Paste for i, (img, item) in enumerate(zip(valid_images, outfit_items)): item_id = item['item_id'] @@ -96,13 +102,24 @@ def merge_images_to_square(outfit_items: List[Dict[str, str]], max_len=9, add_te # This should not happen if num_images <= 4 break - # Target area dimensions (x_start, y_start, width, height) - x_start, y_start, target_w, target_h = target_areas[i] + # 原始目标区域 (x_start, y_start, width, height) + orig_x_start, orig_y_start, orig_w, orig_h = target_areas[i] + + # 📢 应用边距:实际用于图像和文本的区域 + # 新的起始点:向内移动 MARGIN + x_start = orig_x_start + MARGIN + y_start = orig_y_start + MARGIN + + # 新的宽高:减去两倍的 MARGIN (左右/上下) + target_w = orig_w - 2 * MARGIN + target_h = orig_h - 2 * MARGIN + + # --- 图像缩放与居中 --- # Calculate new size while maintaining aspect ratio original_w, original_h = img.size - # Calculate the ratio needed to fit within the target area + # Calculate the ratio needed to fit within the *带边距的* 目标区域 ratio_w = target_w / original_w ratio_h = target_h / original_h @@ -113,45 +130,46 @@ def merge_images_to_square(outfit_items: List[Dict[str, str]], max_len=9, add_te new_w = int(original_w * resize_ratio) new_h = int(original_h * resize_ratio) - # Resize the image. Image.Resampling.LANCZOS provides high-quality scaling. - # Pillow documentation recommends ANTIALIAS or BICUBIC for downscaling, - # but LANCZOS is a good general high-quality filter. - # Note: In Pillow versions > 9.0.0, Image.LANCZOS is now Image.Resampling.LANCZOS + # Resize the image. resized_img = img.resize((new_w, new_h), Image.Resampling.LANCZOS) # Calculate the paste position to center the resized image within its target area - # Center X: (Target Width - New Width) / 2 + X Start + # Center X: (Target Width - New Width) / 2 + X Start (带边距的 X_start) paste_x = (target_w - new_w) // 2 + x_start - # Center Y: (Target Height - New Height) / 2 + Y Start - # paste_y = (target_h - new_h) // 2 + y_start + # 预留文本高度 ( TEXT_RESERVE_HEIGHT ) TEXT_RESERVE_HEIGHT = 30 + + # Center Y: (Target Height - New Height - 预留文本高度) / 2 + Y Start (带边距的 Y_start) paste_y = (target_h - new_h - TEXT_RESERVE_HEIGHT) // 2 + y_start + # 确保图片顶部不超出目标区域的 Y_start paste_y = max(paste_y, y_start) # Paste the resized image onto the canvas canvas.paste(resized_img, (paste_x, paste_y)) + # --- 文本居中与定位 --- + full_text = f"ID: {item_id}, Category: {category}" - try: - # 推荐使用:计算文本的实际尺寸 (width, height) - bbox = draw.textbbox((0, 0), full_text, font=font) - text_w = bbox[2] - bbox[0] - text_h = bbox[3] - bbox[1] - except AttributeError: - # 兼容旧版本 Pillow - text_w, text_h = draw.textsize(full_text, font=font) - - # 计算 X 轴起始位置:使其在目标区域 (target_w) 中居中 - text_x_center = x_start + target_w // 2 - text_x_start = text_x_center - text_w // 2 - - # 计算 Y 轴起始位置:将其放在目标区域的底部 - # (目标区域的起始Y + 目标区域的高度 - 文本行的高度) - text_y_start = y_start + target_h - text_h - 5 # 减去 5 像素作为边距 - - # 3. 绘制合并后的文本 if add_text: + try: + # 推荐使用:计算文本的实际尺寸 (width, height) + bbox = draw.textbbox((0, 0), full_text, font=font) + text_w = bbox[2] - bbox[0] + text_h = bbox[3] - bbox[1] + except AttributeError: + # 兼容旧版本 Pillow + text_w, text_h = draw.textsize(full_text, font=font) + + # 计算 X 轴起始位置:使其在 *带边距的目标区域* (target_w) 中居中 + text_x_center = x_start + target_w // 2 + text_x_start = text_x_center - text_w // 2 + + # 计算 Y 轴起始位置:将其放在 *带边距的目标区域* 的底部 + # (带边距的起始Y + 带边距的高度 - 文本行的高度) + # 📢 在带边距的目标区域底部再减去 5 像素作为与底部的边距 + text_y_start = y_start + target_h - text_h + draw.text((text_x_start, text_y_start), full_text, fill='black', @@ -161,3 +179,142 @@ def merge_images_to_square(outfit_items: List[Dict[str, str]], max_len=9, add_te # canvas.save(output_path, 'JPEG', quality=90) return canvas + +# def merge_images_to_square(outfit_items: List[Dict[str, str]], max_len=9, add_text=True): +# """ +# Loads up to 4 images from the given paths, resizes them while maintaining +# aspect ratio, and merges them onto a 1024x1024 white background JPG. +# +# The layout depends on the number of images: +# 1: Center the single image on the 1024x1024 canvas. +# 2: Place side-by-side, each scaled to fit a 512x1024 half. +# 3: Place in top-left (512x512), top-right (512x512), and bottom-left (512x512). +# 4: Place in all four 512x512 quadrants. +# +# Args: +# outfit_items: A list of item metadata (max length 9). +# +# Returns: +# The file path of the temporary merged JPG image. +# """ +# +# # Define the final canvas size +# CANVAS_SIZE = 1024 +# +# # 1. Create the final white canvas +# # Using 'RGB' mode for JPG output +# canvas = Image.new('RGB', (CANVAS_SIZE, CANVAS_SIZE), 'white') +# draw = ImageDraw.Draw(canvas) +# font = ImageFont.load_default() +# +# # 2. Define the quadrants/target areas (x, y, w, h) +# # The positions are based on a 512x512 quadrant size +# quadrants = { +# 1: [(0, 0, CANVAS_SIZE, CANVAS_SIZE)], # Single full-size placement +# 2: [(0, 0, 512, CANVAS_SIZE), (512, 0, 512, CANVAS_SIZE)], # Left, Right +# 3: [(0, 0, 512, 512), (512, 0, 512, 512), (0, 512, 512, 512)], # Top-Left, Top-Right, Bottom-Left +# 4: [(0, 0, 512, 512), (512, 0, 512, 512), (0, 512, 512, 512), (512, 512, 512, 512)], # All Four +# 5: ALL_9_CELLS[:5], # 布局前5个单元格 (1-5) +# 6: ALL_9_CELLS[:6], # 布局前6个单元格 (1-6) +# 7: ALL_9_CELLS[:7], # 布局前7个单元格 (1-7) +# 8: ALL_9_CELLS[:8], # 布局前8个单元格 (1-8) +# 9: ALL_9_CELLS[:9] # 布局全部9个单元格 (1-9) +# } +# +# # 3. Load and Filter Images +# valid_images = [] +# image_paths = [item['image_path'] for item in outfit_items] +# for path in image_paths: +# try: +# # We use Image.open() and convert to 'RGB' to handle potential transparency (RGBA) +# # and ensure compatibility with the final 'RGB' canvas and JPG output. +# img = oss_get_image(oss_client=minio_client, path=f"{MINIO_LC_DATA_PATH}/{path}", data_type="PIL").convert('RGB') +# # img = Image.open(path).convert('RGB') +# valid_images.append(img) +# except Exception as e: +# logger.error(f"Error loading image {path}. Skipping: {e}") +# +# num_images = len(valid_images) +# +# if num_images == 0: +# raise ValueError("No valid images were loaded.") +# +# if num_images > max_len: +# raise ValueError(f"Valid item number {num_images} exceed max limit {max_len}") +# +# # Get the correct list of target areas based on the number of valid images +# target_areas = quadrants.get(num_images, []) +# +# # 4. Resize and Paste +# for i, (img, item) in enumerate(zip(valid_images, outfit_items)): +# item_id = item['item_id'] +# category = item['category'] +# if i >= len(target_areas): +# # This should not happen if num_images <= 4 +# break +# +# # Target area dimensions (x_start, y_start, width, height) +# x_start, y_start, target_w, target_h = target_areas[i] +# +# # Calculate new size while maintaining aspect ratio +# original_w, original_h = img.size +# +# # Calculate the ratio needed to fit within the target area +# ratio_w = target_w / original_w +# ratio_h = target_h / original_h +# +# # Use the *smaller* of the two ratios to ensure the image fits entirely +# resize_ratio = min(ratio_w, ratio_h) +# +# # Calculate the new dimensions +# new_w = int(original_w * resize_ratio) +# new_h = int(original_h * resize_ratio) +# +# # Resize the image. Image.Resampling.LANCZOS provides high-quality scaling. +# # Pillow documentation recommends ANTIALIAS or BICUBIC for downscaling, +# # but LANCZOS is a good general high-quality filter. +# # Note: In Pillow versions > 9.0.0, Image.LANCZOS is now Image.Resampling.LANCZOS +# resized_img = img.resize((new_w, new_h), Image.Resampling.LANCZOS) +# +# # Calculate the paste position to center the resized image within its target area +# # Center X: (Target Width - New Width) / 2 + X Start +# paste_x = (target_w - new_w) // 2 + x_start +# # Center Y: (Target Height - New Height) / 2 + Y Start +# # paste_y = (target_h - new_h) // 2 + y_start +# +# TEXT_RESERVE_HEIGHT = 30 +# paste_y = (target_h - new_h - TEXT_RESERVE_HEIGHT) // 2 + y_start +# paste_y = max(paste_y, y_start) +# +# # Paste the resized image onto the canvas +# canvas.paste(resized_img, (paste_x, paste_y)) +# +# full_text = f"ID: {item_id}, Category: {category}" +# try: +# # 推荐使用:计算文本的实际尺寸 (width, height) +# bbox = draw.textbbox((0, 0), full_text, font=font) +# text_w = bbox[2] - bbox[0] +# text_h = bbox[3] - bbox[1] +# except AttributeError: +# # 兼容旧版本 Pillow +# text_w, text_h = draw.textsize(full_text, font=font) +# +# # 计算 X 轴起始位置:使其在目标区域 (target_w) 中居中 +# text_x_center = x_start + target_w // 2 +# text_x_start = text_x_center - text_w // 2 +# +# # 计算 Y 轴起始位置:将其放在目标区域的底部 +# # (目标区域的起始Y + 目标区域的高度 - 文本行的高度) +# text_y_start = y_start + target_h - text_h - 5 # 减去 5 像素作为边距 +# +# # 3. 绘制合并后的文本 +# if add_text: +# draw.text((text_x_start, text_y_start), +# full_text, +# fill='black', +# font=font) +# +# # Save as a high-quality JPG (quality=90 is a good balance) +# # canvas.save(output_path, 'JPEG', quality=90) +# +# return canvas