from __future__ import annotations import gc import os import sys import threading import time import warnings from pathlib import Path from typing import Any warnings.filterwarnings( "ignore", message=r".*HTTP_422_UNPROCESSABLE_ENTITY.*", module=r"gradio\.routes", ) os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1" import gradio as gr import numpy as np from huggingface_hub import snapshot_download from PIL import Image MODEL_REPO_ID = os.getenv("MODEL_REPO_ID", "heyh97791/NCF") HF_TOKEN = os.environ.get("HF_TOKEN") SPACE_DIR = Path(__file__).resolve().parent SEGMENTATION_MODEL_DIR = SPACE_DIR / "segformer-b5-finetuned-ade-640-640" RESOLUTION_MAP = { "Original": 0, "4K": 3840 * 2160, "2K": 2560 * 1440, "1080p": 1920 * 1080, "720p": 1280 * 720, "512": 512 * 512, } _RUN_LOCK = threading.Lock() _FLOW_MODULES: dict[str, Any] = {} I18N = { "lang_btn": { "en": "中文", "zh": "English", }, "title": { "en": "# 🎨 ColorFM-O [CPU]", "zh": "# 🎨 ColorFM-O [CPU]", }, "paper_code": { "en": """ 📄 **Paper:** [ColorFM: An Optimization-to-Learning Framework for Color Transfer via Flow Matching (ECCV 2026)](https://github.com/cszn/ColorFM) | 💻 **Code:** [GitHub](https://github.com/cszn/ColorFM) | ⚙️ **Model:** Optimization method for color transfer """, "zh": """ 📄 **论文:** [ColorFM: An Optimization-to-Learning Framework for Color Transfer via Flow Matching (ECCV 2026)](https://github.com/cszn/ColorFM) | 💻 **代码:** [GitHub](https://github.com/cszn/ColorFM) | ⚙️ **模型:** 基于优化的色彩转移方法 """, }, "desc_header": { "en": "💡 What is Color Transfer?", "zh": "💡 什么是追色?", }, "desc_content": { "en": """ **1. What is "Color Transfer"?** It is a technique that extracts the **color palette and atmosphere** from a reference image (Style) and applies it to your target image (Content), while preserving the original structure. **2. Use Cases:** * 📷 **Photography:** Instantly mimic color grading styles from master photographers. * 🎨 **Art & Design:** Unify the color theme of different assets quickly. **3. Tips for Best Results:** * ✅ **Match Content:** Results are best when the content and style images share similar scenes (e.g., Landscape to Landscape). * 🎨 **Hue Similarity:** Images with closer hues between content and style may produce better color transfer results. * ⚡ **High Resolution:** Supports processing high-resolution images (use the Settings menu to optimize speed). **🛡️ Privacy Disclaimer:** This demo runs entirely on the cloud instance. Your uploaded images are processed in memory and are **NOT saved** or stored permanently on our servers. """, "zh": """ **1. 什么是“追色”?** 追色(Color Transfer)是指从一张参考图(风格图)中提取**色调与氛围**,并将其迁移到你的目标图片(内容图)上,同时保留原图的细节纹理。 **2. 主要用途:** * 📷 **摄影后期:** 一键复刻摄影大师的调色风格,无需手动调参。 * 🎨 **设计创作:** 快速统一多张素材的色调风格,提高创作效率。 **3. 如何获得最佳效果?** * ✅ **内容匹配:** 当内容图与风格图的场景相似时(例如都是风景),效果通常最好。 * 🎨 **色相相似:** 内容图像和风格图像之间色相相似,可能会呈现更好的结果。 * ⚡ **高清支持:** 支持高分辨率图片处理(可在“设置”中调整分辨率以获得更快速度)。 **🛡️ 免责声明:** 本 Demo 仅供演示体验。您的图片仅在内存中进行临时处理,**不会被保存**、存储或用于任何其他用途,处理结束后即刻销毁。 """, }, "label_content": { "en": "Content Image", "zh": "内容图", }, "label_style": { "en": "Style Reference", "zh": "色彩参考图", }, "settings_header": { "en": "⚙️ Settings", "zh": "⚙️ 设置", }, "steps_label": { "en": "Fit Steps", "zh": "迭代步数", }, "seg_label": { "en": "Use Semantic Segmentation", "zh": "使用语义分割", }, "res_label": { "en": "Max Resolution Limit", "zh": "最大分辨率限制", }, "res_info": { "en": "Limits the input image size before optimization. Original keeps the uploaded size.", "zh": "在优化前限制输入图像尺寸;Original 保持上传尺寸。", }, "btn_run": { "en": "🚀 Start Color Transfer", "zh": "🚀 开始追色", }, "label_result": { "en": "Result Image", "zh": "追色结果", }, } def default_device() -> str: try: import torch return "cuda" if torch.cuda.is_available() else "cpu" except Exception: return "cpu" def normalize_image(image: np.ndarray) -> np.ndarray: if image.ndim == 2: image = np.stack([image, image, image], axis=-1) if image.shape[-1] == 4: image = image[..., :3] if image.dtype != np.uint8: max_value = float(np.nanmax(image)) if image.size else 0.0 if np.issubdtype(image.dtype, np.floating) and max_value <= 1.0: image = image * 255.0 image = np.clip(image, 0, 255).astype(np.uint8) return image def resize_to_limit(image: np.ndarray, resolution_choice: str) -> np.ndarray: image = normalize_image(image) max_pixels = RESOLUTION_MAP.get(str(resolution_choice), 0) if max_pixels <= 0: return image height, width = image.shape[:2] pixels = height * width if pixels <= max_pixels: return image scale = (max_pixels / pixels) ** 0.5 new_width = max(1, int(width * scale)) new_height = max(1, int(height * scale)) return np.asarray(Image.fromarray(image).resize((new_width, new_height), Image.Resampling.LANCZOS)) def missing_segmentation_files() -> list[str]: required_files = [ "config.json", "preprocessor_config.json", "pytorch_model.bin", ] return [name for name in required_files if not (SEGMENTATION_MODEL_DIR / name).exists()] def load_flow_modules(): if MODEL_REPO_ID in _FLOW_MODULES: return _FLOW_MODULES[MODEL_REPO_ID] try: repo_dir = Path( snapshot_download( repo_id=MODEL_REPO_ID, token=HF_TOKEN, allow_patterns=[ "flow_interface.py", "config/flow.yaml", "dataset/**", "models/**", "solvers/**", ], ) ) except Exception as exc: raise gr.Error( "Failed to download the model repository. Make sure HF_TOKEN has read access. " f"Original error: {exc}" ) from exc repo_path = str(repo_dir) if repo_path not in sys.path: sys.path.insert(0, repo_path) try: from flow_interface import ColorFlowOptimizer, FlowRunOptions except Exception as exc: raise gr.Error(f"Failed to import flow_interface from the model repository: {exc}") from exc optimizer = ColorFlowOptimizer(repo_dir / "config" / "flow.yaml") _FLOW_MODULES[MODEL_REPO_ID] = { "optimizer": optimizer, "FlowRunOptions": FlowRunOptions, } return _FLOW_MODULES[MODEL_REPO_ID] def run_optimization( content_image: np.ndarray | None, style_image: np.ndarray | None, fit_steps: int, use_segmentation: bool, resolution_choice: str, progress=gr.Progress(track_tqdm=True), ) -> np.ndarray | None: if content_image is None or style_image is None: raise gr.Error("Please upload both content and style/reference images.") fit_steps = int(fit_steps) if fit_steps <= 0: raise gr.Error("Fit steps must be greater than 0.") if use_segmentation: missing_files = missing_segmentation_files() if missing_files: raise gr.Error( "Segmentation model files are missing. Put these files under " f"{SEGMENTATION_MODEL_DIR}: {', '.join(missing_files)}" ) def update_progress(step: int, total_steps: int, stage: str) -> None: total_steps = max(int(total_steps or 1), 1) step = max(0, min(int(step), total_steps)) progress((step, total_steps), desc=stage) progress(0, desc="Waiting for worker") with _RUN_LOCK: try: start_time = time.time() modules = load_flow_modules() FlowRunOptions = modules["FlowRunOptions"] optimizer = modules["optimizer"] content = resize_to_limit(content_image, resolution_choice) style = resize_to_limit(style_image, resolution_choice) device_choice = default_device() options = FlowRunOptions( total_steps=fit_steps, max_epochs=1, num_workers=0, accelerator=device_choice, devices="auto" if device_choice == "cuda" else None, full=True, seg_mode=use_segmentation, allow_downloads=False, segmentation_model_name=str(SEGMENTATION_MODEL_DIR) if use_segmentation else None, show_progress=False, progress_callback=update_progress, ) result_pil = optimizer.optimize(content, style, options=options) output = np.asarray(result_pil.convert("RGB")) print( f"Finished in {time.time() - start_time:.2f}s | " f"steps={fit_steps} | segmentation={'on' if use_segmentation else 'off'} | " f"output={output.shape[1]}x{output.shape[0]}" ) progress(1.0, desc="Finished") return output except gr.Error: raise except Exception as exc: if "out of memory" in str(exc).lower(): raise gr.Error("Out of memory. Try fewer fit steps or a lower resolution.") from exc raise gr.Error(str(exc)) from exc finally: gc.collect() try: import torch if torch.cuda.is_available(): torch.cuda.empty_cache() except Exception: pass def toggle_language(current_lang): target_lang = "zh" if current_lang == "en" else "en" return ( target_lang, gr.update(value=I18N["lang_btn"][target_lang]), gr.update(value=I18N["title"][target_lang]), gr.update(label=I18N["desc_header"][target_lang]), gr.update(value=I18N["desc_content"][target_lang]), gr.update(label=I18N["label_content"][target_lang]), gr.update(label=I18N["label_style"][target_lang]), gr.update(label=I18N["settings_header"][target_lang]), gr.update(label=I18N["steps_label"][target_lang]), gr.update(label=I18N["seg_label"][target_lang]), gr.update(label=I18N["res_label"][target_lang], info=I18N["res_info"][target_lang]), gr.update(value=I18N["btn_run"][target_lang]), gr.update(label=I18N["label_result"][target_lang]), ) custom_css = """ #col-container { margin: 0 auto; max-width: 1100px; } .gallery-container img{ object-fit: contain; } """ with gr.Blocks(css=custom_css, title="Color-Transfer") as demo: lang_state = gr.State("en") with gr.Column(elem_id="col-container"): with gr.Row(): with gr.Column(scale=5): md_title = gr.Markdown(I18N["title"]["en"]) with gr.Column(scale=0, min_width=80): btn_lang = gr.Button( value=I18N["lang_btn"]["en"], variant="secondary", size="sm", ) gr.Markdown(I18N["paper_code"]["en"]) with gr.Accordion(label=I18N["desc_header"]["en"], open=False) as acc_desc: md_desc = gr.Markdown(I18N["desc_content"]["en"]) with gr.Row(): with gr.Column(): input_content = gr.Image(label=I18N["label_content"]["en"], type="numpy", height=300, image_mode="RGB") with gr.Column(): input_style = gr.Image(label=I18N["label_style"]["en"], type="numpy", height=300, image_mode="RGB") with gr.Accordion(label=I18N["settings_header"]["en"], open=True) as acc_settings: with gr.Row(): fit_steps = gr.Slider(1, 1000, value=300, step=1, label=I18N["steps_label"]["en"]) chk_seg = gr.Checkbox(value=True, label=I18N["seg_label"]["en"]) radio_res = gr.Radio( choices=list(RESOLUTION_MAP.keys()), value="Original", label=I18N["res_label"]["en"], info=I18N["res_info"]["en"], interactive=True, ) with gr.Row(): btn_run = gr.Button(I18N["btn_run"]["en"], variant="primary") output_result = gr.Image(label=I18N["label_result"]["en"], type="numpy", interactive=False, height=450, format="png") btn_run.click( fn=lambda: None, inputs=None, outputs=output_result, ).then( fn=run_optimization, inputs=[input_content, input_style, fit_steps, chk_seg, radio_res], outputs=output_result, ) btn_lang.click( fn=toggle_language, inputs=[lang_state], outputs=[ lang_state, btn_lang, md_title, acc_desc, md_desc, input_content, input_style, acc_settings, fit_steps, chk_seg, radio_res, btn_run, output_result, ], ) if __name__ == "__main__": demo.launch()