import gradio as gr import numpy as np from PIL import Image import io import base64 import tempfile import os def calculate_crop_box(image, target_ratio=1170/391): """ 計算保持目標比例的最大裁切框 """ width, height = image.size current_ratio = width / height if current_ratio > target_ratio: # 圖片較寬,需要裁切寬度 new_width = int(height * target_ratio) left = (width - new_width) // 2 top = 0 right = left + new_width bottom = height else: # 圖片較高,需要裁切高度 new_height = int(width / target_ratio) left = 0 top = (height - new_height) // 2 right = width bottom = top + new_height return (left, top, right, bottom) def process_image(image, crop_data=None): """ 處理圖片:裁切並調整為目標尺寸,並保存為 JPG 格式 """ if image is None: return None, "請上傳圖片" # 如果有裁切數據,使用用戶選擇的區域 if crop_data is not None: # Gradio 的 Image 組件在編輯模式下會返回包含裁切信息的數據 # 這裡我們處理裁切後的圖片 processed_image = image else: # 如果沒有裁切數據,自動計算最佳裁切區域 crop_box = calculate_crop_box(image) processed_image = image.crop(crop_box) # 調整為目標尺寸 target_size = (1170, 391) final_image = processed_image.resize(target_size, Image.Resampling.LANCZOS) # 轉換為 RGB 模式以確保 JPG 格式輸出 if final_image.mode in ('RGBA', 'LA', 'P'): # 創建白色背景 rgb_image = Image.new('RGB', final_image.size, (255, 255, 255)) if final_image.mode == 'P': final_image = final_image.convert('RGBA') rgb_image.paste(final_image, mask=final_image.split()[-1] if final_image.mode == 'RGBA' else None) final_image = rgb_image # 保存為 JPG 格式的臨時檔案 temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') final_image.save(temp_file.name, format='JPEG', quality=95, optimize=True) temp_file.close() return temp_file.name, f"圖片已處理完成!尺寸:{final_image.size},格式:JPG" def auto_crop_preview(image): """ 自動裁切預覽 """ if image is None: return None, "請上傳圖片" crop_box = calculate_crop_box(image) cropped = image.crop(crop_box) # 創建預覽圖片,顯示裁切區域 preview = image.copy() from PIL import ImageDraw draw = ImageDraw.Draw(preview) # 繪製裁切框 draw.rectangle(crop_box, outline="red", width=3) return preview, f"建議裁切區域:{crop_box}" def create_crop_interface(image): """ 創建裁切界面,顯示帶有裁切框的圖片 """ if image is None: return None, "請先上傳圖片" # 計算建議的裁切區域 crop_box = calculate_crop_box(image) left, top, right, bottom = crop_box # 創建帶有裁切框的預覽圖 preview = image.copy() from PIL import ImageDraw, ImageFont draw = ImageDraw.Draw(preview) # 繪製裁切框 draw.rectangle(crop_box, outline="red", width=5) # 添加半透明遮罩到裁切區域外 overlay = Image.new('RGBA', image.size, (0, 0, 0, 100)) mask = Image.new('RGBA', image.size, (0, 0, 0, 0)) mask_draw = ImageDraw.Draw(mask) mask_draw.rectangle(crop_box, fill=(255, 255, 255, 255)) # 創建最終預覽 preview = Image.alpha_composite(preview.convert('RGBA'), overlay) preview = Image.alpha_composite(preview, Image.new('RGBA', image.size, (0, 0, 0, 0))) return preview.convert('RGB'), f"建議裁切區域:{crop_box}\n可以點擊「使用建議區域」或「自定義裁切」" def custom_crop_with_coordinates(image, x, y, width, height): """ 使用自定義座標裁切圖片 """ if image is None: return None, "請先上傳圖片" try: # 確保座標在圖片範圍內 img_width, img_height = image.size x = max(0, min(x, img_width)) y = max(0, min(y, img_height)) width = max(10, min(width, img_width - x)) height = max(10, min(height, img_height - y)) # 執行裁切 cropped = image.crop((x, y, x + width, y + height)) # 調整為目標尺寸 target_size = (1170, 391) final_image = cropped.resize(target_size, Image.Resampling.LANCZOS) # 轉換為 RGB 並保存為 JPG if final_image.mode in ('RGBA', 'LA', 'P'): rgb_image = Image.new('RGB', final_image.size, (255, 255, 255)) if final_image.mode == 'P': final_image = final_image.convert('RGBA') rgb_image.paste(final_image, mask=final_image.split()[-1] if final_image.mode == 'RGBA' else None) final_image = rgb_image # 保存為 JPG 檔案 temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') final_image.save(temp_file.name, format='JPEG', quality=95, optimize=True) temp_file.close() return temp_file.name, f"自定義裁切完成!裁切區域:({x}, {y}, {width}, {height})" except Exception as e: return None, f"裁切失敗:{str(e)}" def use_suggested_crop(image): """ 使用建議的裁切區域 """ if image is None: return None, "請先上傳圖片" crop_box = calculate_crop_box(image) cropped = image.crop(crop_box) # 調整為目標尺寸 target_size = (1170, 391) final_image = cropped.resize(target_size, Image.Resampling.LANCZOS) # 轉換為 RGB 並保存為 JPG if final_image.mode in ('RGBA', 'LA', 'P'): rgb_image = Image.new('RGB', final_image.size, (255, 255, 255)) if final_image.mode == 'P': final_image = final_image.convert('RGBA') rgb_image.paste(final_image, mask=final_image.split()[-1] if final_image.mode == 'RGBA' else None) final_image = rgb_image # 保存為 JPG 檔案 temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') final_image.save(temp_file.name, format='JPEG', quality=95, optimize=True) temp_file.close() return temp_file.name, f"使用建議裁切區域完成!區域:{crop_box}" # 創建 Gradio 界面 with gr.Blocks(title="圖片裁切工具", theme=gr.themes.Soft()) as demo: gr.Markdown("# 🖼️ 圖片裁切工具") gr.Markdown("### 將任意尺寸的圖片按 1170:391 比例裁切並調整為目標尺寸") with gr.Row(): with gr.Column(scale=1): # 圖片上傳區域 input_image = gr.Image( label="上傳圖片", type="pil", height=400, interactive=True ) # 手動裁切圖片區域 crop_image = gr.Image( label="手動裁切區域(點擊並拖拽選擇裁切區域)", type="pil", height=400, interactive=True, visible=False ) # 按鈕區域 with gr.Row(): preview_btn = gr.Button("🔍 顯示建議裁切區域", variant="secondary") suggest_btn = gr.Button("✅ 使用建議區域", variant="primary") with gr.Row(): edit_btn = gr.Button("✏️ 自定義裁切", variant="secondary") reset_btn = gr.Button("🔄 重置", variant="secondary") with gr.Column(scale=1): # 輸出區域 output_image = gr.Image( label="處理後圖片 (JPG格式)", type="filepath", height=400, interactive=False ) # 下載按鈕 download_btn = gr.DownloadButton( label="📥 下載 JPG 圖片", variant="primary", visible=False ) # 狀態信息 status_text = gr.Textbox( label="狀態", value="等待圖片上傳...", interactive=False ) # 預覽功能 with gr.Row(): preview_image = gr.Image( label="裁切區域預覽(紅框顯示建議裁切區域)", type="pil", height=300, visible=False ) # 使用說明 with gr.Accordion("📖 使用說明", open=False): gr.Markdown(""" ### 如何使用: 1. **上傳圖片**:點擊上傳區域選擇您的圖片 2. **查看建議**:點擊「顯示建議裁切區域」查看系統建議 3. **選擇方式**: - 點擊「使用建議區域」直接使用系統建議 - 點擊「自定義裁切」手動輸入座標和尺寸 4. **下載結果**:點擊「下載 JPG 圖片」按鈕下載處理後的圖片 ### 功能特色: - ✨ 自動計算最佳裁切比例 (1170:391) - 🎯 支援自定義座標裁切 - 📏 保持目標比例的精確輸出 - 🖼️ 輸出高品質 JPG 格式圖片 - 💾 簡單的一鍵下載功能 - 🔄 重置功能方便重新開始 ### 自定義裁切說明: - **X, Y 座標**:裁切區域的左上角位置 - **寬度, 高度**:裁切區域的大小 - 最終輸出會調整為 1170×391 像素 """) # 自定義裁切參數區域 with gr.Row(visible=False) as custom_crop_row: gr.Markdown("### 🎯 自定義裁切參數") with gr.Row(visible=False) as crop_controls: with gr.Column(): crop_x = gr.Number( label="X 座標 (左上角)", value=0, precision=0, minimum=0 ) crop_y = gr.Number( label="Y 座標 (左上角)", value=0, precision=0, minimum=0 ) with gr.Column(): crop_width = gr.Number( label="寬度", value=300, precision=0, minimum=10 ) crop_height = gr.Number( label="高度", value=100, precision=0, minimum=10 ) with gr.Column(): apply_crop_btn = gr.Button("🎯 套用自定義裁切", variant="primary") # 事件綁定 # 顯示建議裁切區域 preview_btn.click( fn=create_crop_interface, inputs=[input_image], outputs=[preview_image, status_text] ).then( fn=lambda: gr.update(visible=True), outputs=[preview_image] ) # 使用建議區域直接處理 def suggest_and_download(image): if image is None: return None, "請先上傳圖片", gr.update(visible=False) result_path, status = use_suggested_crop(image) if result_path: return result_path, status, gr.update(visible=True, value=result_path) else: return None, status, gr.update(visible=False) suggest_btn.click( fn=suggest_and_download, inputs=[input_image], outputs=[output_image, status_text, download_btn] ) # 顯示/隱藏自定義裁切控制項 def toggle_custom_controls(image): if image is None: return gr.update(visible=False), gr.update(visible=False) # 獲取圖片尺寸以設定預設值 width, height = image.size suggested_crop = calculate_crop_box(image) return ( gr.update(visible=True), gr.update(visible=True) ) edit_btn.click( fn=toggle_custom_controls, inputs=[input_image], outputs=[custom_crop_row, crop_controls] ) # 套用自定義裁切 def apply_custom_crop(image, x, y, width, height): if image is None: return None, "請先上傳圖片", gr.update(visible=False) result_path, status = custom_crop_with_coordinates(image, x, y, width, height) if result_path: return result_path, status, gr.update(visible=True, value=result_path) else: return None, status, gr.update(visible=False) apply_crop_btn.click( fn=apply_custom_crop, inputs=[input_image, crop_x, crop_y, crop_width, crop_height], outputs=[output_image, status_text, download_btn] ) # 重置功能 reset_btn.click( fn=lambda: ( None, None, None, gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), "請上傳圖片" ), outputs=[ input_image, crop_image, output_image, preview_image, custom_crop_row, crop_controls, download_btn, status_text ] ) # 當圖片上傳時自動更新狀態和建議值 def update_on_upload(image): if image is None: return "請上傳圖片", 0, 0, 300, 100 # 計算建議的裁切區域 crop_box = calculate_crop_box(image) left, top, right, bottom = crop_box width = right - left height = bottom - top return ( f"圖片已上傳!尺寸:{image.size[0]}×{image.size[1]}", left, top, width, height ) input_image.change( fn=update_on_upload, inputs=[input_image], outputs=[status_text, crop_x, crop_y, crop_width, crop_height] ) # 啟動應用程式 if __name__ == "__main__": demo.launch( server_name="0.0.0.0", server_port=7860, share=True )