import gradio as gr import numpy as np from PIL import Image import io import tempfile import os def resize_images_to_match(images, resize_mode="height"): if not images or len(images) == 0: return [] valid_images = [img for img in images if img is not None] if not valid_images: return [] if resize_mode == "height": target_height = min(img.size[1] for img in valid_images) resized_images = [] for img in valid_images: aspect_ratio = img.size[0] / img.size[1] new_width = int(target_height * aspect_ratio) resized_img = img.resize((new_width, target_height), Image.Resampling.LANCZOS) resized_images.append(resized_img) else: target_width = min(img.size[0] for img in valid_images) resized_images = [] for img in valid_images: aspect_ratio = img.size[1] / img.size[0] new_height = int(target_width * aspect_ratio) resized_img = img.resize((target_width, new_height), Image.Resampling.LANCZOS) resized_images.append(resized_img) return resized_images def concatenate_images(images, direction="horizontal", spacing=0, background_color="white"): if not images or len(images) == 0: return None valid_images = [img for img in images if img is not None] if not valid_images: return None rgb_images = [] for img in valid_images: if img.mode != 'RGB': if img.mode in ('RGBA', 'LA'): background = Image.new('RGB', img.size, 'white') background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None) rgb_images.append(background) else: rgb_images.append(img.convert('RGB')) else: rgb_images.append(img) if len(rgb_images) == 1: return rgb_images[0] if direction == "horizontal": total_width = sum(img.size[0] for img in rgb_images) + spacing * (len(rgb_images) - 1) max_height = max(img.size[1] for img in rgb_images) result = Image.new('RGB', (total_width, max_height), background_color) x_offset = 0 for img in rgb_images: y_offset = (max_height - img.size[1]) // 2 result.paste(img, (x_offset, y_offset)) x_offset += img.size[0] + spacing else: max_width = max(img.size[0] for img in rgb_images) total_height = sum(img.size[1] for img in rgb_images) + spacing * (len(rgb_images) - 1) result = Image.new('RGB', (max_width, total_height), background_color) y_offset = 0 for img in rgb_images: x_offset = (max_width - img.size[0]) // 2 result.paste(img, (x_offset, y_offset)) y_offset += img.size[1] + spacing return result def save_as_jpg(image, quality=95): if image is None: return None if image.mode != 'RGB': if image.mode in ('RGBA', 'LA'): background = Image.new('RGB', image.size, 'white') background.paste(image, mask=image.split()[-1]) image = background else: image = image.convert('RGB') buffer = io.BytesIO() image.save(buffer, format='JPEG', quality=quality, optimize=True) buffer.seek(0) return Image.open(buffer) def process_images( image1, image2, image3, image4, image5, image6, resize_mode, concat_direction, spacing, bg_color, jpg_quality ): uploaded_images = [image1, image2, image3, image4, image5, image6] valid_images = [img for img in uploaded_images if img is not None] if len(valid_images) == 0: return None, "❌ 請至少上傳一張圖片", None if len(valid_images) == 1: return valid_images[0], f"✅ 只有一張圖片,無需拼接", None try: if resize_mode == "等高度 (水平拼接)": resized_images = resize_images_to_match(valid_images, "height") direction = "horizontal" else: resized_images = resize_images_to_match(valid_images, "width") direction = "vertical" if concat_direction != "自動 (根據調整模式)": direction = "horizontal" if concat_direction == "水平拼接" else "vertical" result = concatenate_images(resized_images, direction, spacing, bg_color) if result is None: return None, "❌ 拼接失敗", None result = save_as_jpg(result, quality=jpg_quality) temp_path = os.path.join(tempfile.gettempdir(), "result.jpg") result.save(temp_path, format="JPEG", quality=jpg_quality) info = f"✅ 成功拼接 {len(valid_images)} 張圖片\n" info += f"📐 最終尺寸: {result.size[0]} x {result.size[1]} 像素\n" info += f"🔄 調整模式: {resize_mode}\n" info += f"➡️ 拼接方向: {'水平' if direction == 'horizontal' else '垂直'}\n" info += f"📏 間距: {spacing} 像素\n" info += f"💾 輸出品質: {jpg_quality}%" return result, info, temp_path except Exception as e: return None, f"❌ 處理過程中出現錯誤: {str(e)}", None def create_interface(): with gr.Blocks(title="圖片拼接比較工具", theme=gr.themes.Soft()) as iface: gr.Markdown("# 🖼️ 圖片拼接比較工具") with gr.Row(): with gr.Column(scale=2): gr.Markdown("### 📁 上傳圖片") with gr.Row(): image1 = gr.Image(type="pil", label="圖片 1") image2 = gr.Image(type="pil", label="圖片 2") image3 = gr.Image(type="pil", label="圖片 3") with gr.Row(): image4 = gr.Image(type="pil", label="圖片 4") image5 = gr.Image(type="pil", label="圖片 5") image6 = gr.Image(type="pil", label="圖片 6") gr.Markdown("### ⚙️ 處理設定") with gr.Row(): resize_mode = gr.Dropdown( choices=["等高度 (水平拼接)", "等寬度 (垂直拼接)"], value="等高度 (水平拼接)", label="尺寸調整模式" ) concat_direction = gr.Dropdown( choices=["自動 (根據調整模式)", "水平拼接", "垂直拼接"], value="自動 (根據調整模式)", label="拼接方向" ) with gr.Row(): spacing = gr.Slider(0, 50, value=2, step=1, label="圖片間距 (像素)") bg_color = gr.Dropdown( choices=["white", "black", "gray"], value="white", label="背景顏色" ) jpg_quality = gr.Slider(60, 100, value=95, step=5, label="JPG 輸出品質 (%)") process_btn = gr.Button("🚀 開始處理", variant="primary", size="lg") with gr.Column(scale=2): gr.Markdown("### 🎯 處理結果") result_image = gr.Image(type="pil", label="拼接結果") result_info = gr.Textbox(label="處理信息", lines=6, max_lines=10) result_file = gr.File(label="下載 JPG 檔案") process_btn.click( fn=process_images, inputs=[image1, image2, image3, image4, image5, image6, resize_mode, concat_direction, spacing, bg_color, jpg_quality], outputs=[result_image, result_info, result_file] ) return iface if __name__ == "__main__": app = create_interface() app.launch(server_name="0.0.0.0", server_port=7860, share=True)