ColorFM-O / app.py
heyh97791's picture
update app
beb1f65
Raw
History Blame Contribute Delete
14.4 kB
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()