Spaces:
Running
Running
File size: 16,128 Bytes
6648d87 b41713e 6648d87 b41713e 8e4d005 f91c3ff 8e4d005 6648d87 b41713e c4d1f69 6648d87 c4d1f69 31bde8c c4d1f69 31bde8c c4d1f69 31bde8c c4d1f69 31bde8c c4d1f69 31bde8c c4d1f69 31bde8c c4d1f69 31bde8c 6648d87 8fa38ba 31def4c 8fa38ba 6648d87 8fb7d86 6648d87 b9599e0 354d532 53b68ad c3b1e23 53b68ad c3b1e23 354d532 53b68ad b9599e0 354d532 b9599e0 354d532 6648d87 354d532 7b7cfb8 88b4b62 7b7cfb8 b9599e0 9b325c4 a1b4bb5 92937eb dfbc724 62569eb dfbc724 92937eb dfbc724 92937eb 9c5bcae 92937eb dfbc724 6648d87 c4d1f69 6648d87 53b68ad 6648d87 53b68ad 6648d87 a1b4bb5 6648d87 9b325c4 6648d87 b41713e 8511c64 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 | from __future__ import annotations
import os
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
import gradio as gr
import numpy as np
import torch
from diffusers.image_processor import VaeImageProcessor
from huggingface_hub import snapshot_download
from PIL import Image, ImageOps
# Functions used by runtime.load()/run(); implemented in CatVTON/utils.py.
# The runtime sys.path injection makes `CatVTON/utils.py` importable as `utils`.
from CatVTON.utils import init_weight_dtype, resize_and_crop, resize_and_padding
APP_TITLE = "ChitraTech Virtual Try-On"
APP_DESCRIPTION = (
"Upload a shopper photo and clothing image to run on-demand CatVTON virtual try-on inference "
"using the Zheng-Chong CatVTON implementation."
)
CATVTON_REPO_DIR_ENV = os.getenv("CATVTON_REPO_DIR")
CATVTON_REPO_DIR = Path(CATVTON_REPO_DIR_ENV) if CATVTON_REPO_DIR_ENV else Path("./CatVTON")
CATVTON_RESUME_PATH = os.getenv("CATVTON_RESUME_PATH", "zhengchong/CatVTON")
def resolve_catvton_repo_dir(start_dir: Path) -> Path:
"""Find the CatVTON repo root that contains `model/cloth_masker.py`.
HF Spaces sometimes mount code in unexpected places; relying on fixed paths like
`/app/CatVTON` can be wrong. We therefore:
1) try a few common candidates
2) then scan under `/app` (and `/workspace` if present) for `model/cloth_masker.py`
"""
def looks_like_repo_dir(p: Path) -> bool:
return (p / "model" / "cloth_masker.py").exists() and (p / "model" / "pipeline.py").exists()
candidates: list[Path] = []
if start_dir is not None:
candidates.append(start_dir)
if CATVTON_REPO_DIR_ENV:
candidates.append(Path(CATVTON_REPO_DIR_ENV))
candidates.extend([
Path("/app/CatVTON"),
Path("/app"),
Path("./CatVTON"),
Path("./"),
Path("/workspace"),
])
for c in candidates:
if c is not None and looks_like_repo_dir(c):
return c.resolve()
# Broad scan for the actual code root.
scan_roots = [Path("/app"), Path("/workspace")]
for root in scan_roots:
if not root.exists():
continue
for cloth_masker in root.rglob("model/cloth_masker.py"):
repo_root = cloth_masker.parent.parent # .../<repo_root>/model/cloth_masker.py
if looks_like_repo_dir(repo_root):
return repo_root.resolve()
# Fallback: return the provided start_dir (so error message includes candidates).
return start_dir.resolve()
CATVTON_BASE_MODEL = os.getenv("CATVTON_BASE_MODEL", "booksforcharlie/stable-diffusion-inpainting")
CATVTON_OUTPUT_DIR = Path(os.getenv("CATVTON_OUTPUT_DIR", "./outputs"))
DEFAULT_DEVICE = os.getenv("CATVTON_DEVICE", "cuda")
# If container has no CUDA driver (CPU-only), fall back to CPU to avoid crash.
DEVICE = DEFAULT_DEVICE
if DEVICE.startswith("cuda"):
# CPU-only Spaces may still have torch built with CUDA, but no driver at runtime.
# Robustly detect that case and fall back to CPU.
if not torch.cuda.is_available():
DEVICE = "cpu"
else:
try:
_ = torch.cuda.current_device()
except Exception:
DEVICE = "cpu"
DEFAULT_WIDTH = int(os.getenv("CATVTON_WIDTH", "768"))
DEFAULT_HEIGHT = int(os.getenv("CATVTON_HEIGHT", "1024"))
DEFAULT_STEPS = int(os.getenv("CATVTON_STEPS", "50"))
DEFAULT_GUIDANCE_SCALE = float(os.getenv("CATVTON_GUIDANCE_SCALE", "2.5"))
DEFAULT_MIXED_PRECISION = os.getenv("CATVTON_MIXED_PRECISION", "bf16")
DEFAULT_SEED = int(os.getenv("CATVTON_SEED", "42"))
@dataclass
class CatVTONRuntime:
repo_dir: Path
device: str
pipeline: object | None = field(default=None, init=False, repr=False)
automasker: object | None = field(default=None, init=False, repr=False)
mask_processor: object | None = field(default=None, init=False, repr=False)
resize_and_crop: object | None = field(default=None, init=False, repr=False)
resize_and_padding: object | None = field(default=None, init=False, repr=False)
vis_mask: object | None = field(default=None, init=False, repr=False)
ready: bool = False
status: str = "not loaded"
def load(self) -> None:
if self.ready:
return
# Help debug missing code/weights on HF Spaces.
# (This will show up in Space logs during first request/build.)
# NOTE: keep as lightweight prints.
print("[CatVTON] load(): repo_dir=", self.repo_dir)
print("[CatVTON] CATVTON_RESUME_PATH=", CATVTON_RESUME_PATH)
print("[CatVTON] CATVTON_MODEL_DIR=", os.getenv("CATVTON_MODEL_DIR"))
if not self.repo_dir.exists():
raise RuntimeError(f"CatVTON repository not found at '{self.repo_dir}'.")
# --- Resolve real python root that contains `model/` ---
# Some HF environments mount the code differently; env/debug values can be wrong.
# We therefore detect the repo root by searching for `model/cloth_masker.py`.
# Force importability in HF Spaces: repo root is typically the working directory.
# Ensure both repo root and repo_root/CatVTON are importable.
repo_root = Path.cwd().resolve()
# Ensure that imports like `import model.*` work in HF.
# Different Space layouts may put the actual CatVTON code under:
# - ./CatVTON/model/...
# - ./model/...
# - /app/CatVTON/model/...
candidate_code_roots = [
repo_root,
repo_root / "CatVTON",
self.repo_dir,
self.repo_dir / "CatVTON",
]
for p in candidate_code_roots:
ps = str(p)
if ps not in sys.path and p.exists():
sys.path.insert(0, ps)
# Also add the directory that directly contains `model/` if present.
direct_model_root = None
for p in candidate_code_roots:
if (p / "model" / "cloth_masker.py").exists():
direct_model_root = p
break
if direct_model_root is not None:
dm = str(direct_model_root)
if dm not in sys.path:
sys.path.insert(0, dm)
found_model_parent: Path | None = None
# Search for CatVTON's `model/` package starting from the working directory.
# (Avoid scanning large absolute paths like /app that may not exist in the container.)
for cloth_masker in repo_root.rglob("model/cloth_masker.py"):
candidate_root = cloth_masker.parent.parent # .../<repo_root>/model/cloth_masker.py
if (candidate_root / "model" / "pipeline.py").exists():
found_model_parent = candidate_root.resolve()
break
if found_model_parent is None:
# Keep existing behavior as last resort.
found_model_parent = self.repo_dir.resolve()
repo_path = str(found_model_parent)
if repo_path not in sys.path:
sys.path.insert(0, repo_path)
self.repo_dir = found_model_parent
# If CatVTON code is under `<repo_root>/CatVTON/` then `import model.*` expects
# `sys.path` to include that inner code root (so `model/` is importable).
# Ensure this regardless of which candidate_root was selected.
inner_code_root = repo_root / "CatVTON"
if (inner_code_root / "model" / "cloth_masker.py").exists():
sys.path.insert(0, str(inner_code_root.resolve()))
else:
# If the HF Space layout is different, fall back to adding `repo_root/model`.
fallback_model_root = repo_root / "model"
if (fallback_model_root / "cloth_masker.py").exists():
sys.path.insert(0, str(fallback_model_root.resolve().parent))
# If this still fails inside HF, add debugging info.
try:
from model.cloth_masker import AutoMasker, vis_mask
from model.pipeline import CatVTONPipeline
except Exception as import_exc:
# Helpful diagnostics for HF Spaces.
repo_model_exists = (self.repo_dir / "model").exists()
candidate_roots = [
self.repo_dir,
self.repo_dir / "model",
(self.repo_dir / "model").parent,
]
candidate_roots_str = ", ".join(str(p) for p in candidate_roots)
raise RuntimeError(
"CatVTON import failed. "
f"repo_dir={self.repo_dir} "
f"repo_dir/model_exists={repo_model_exists} "
f"repo_model_candidate_roots={candidate_roots_str} "
f"sys.path[0:10]={sys.path[:10]} "
f"import_error={import_exc}"
) from import_exc
repo_weights_dir = Path(snapshot_download(repo_id=CATVTON_RESUME_PATH))
self.pipeline = CatVTONPipeline(
base_ckpt=CATVTON_BASE_MODEL,
attn_ckpt=str(repo_weights_dir),
attn_ckpt_version="mix",
weight_dtype=init_weight_dtype(DEFAULT_MIXED_PRECISION),
use_tf32=True,
device=self.device,
)
self.mask_processor = VaeImageProcessor(
vae_scale_factor=8,
do_normalize=False,
do_binarize=True,
do_convert_grayscale=True,
)
self.automasker = AutoMasker(
densepose_ckpt=os.path.join(repo_weights_dir, "DensePose"),
schp_ckpt=os.path.join(repo_weights_dir, "SCHP"),
device=self.device,
)
self.resize_and_crop = resize_and_crop
self.resize_and_padding = resize_and_padding
self.vis_mask = vis_mask
CATVTON_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
self.ready = True
self.status = "loaded"
def run(
self,
person_image: Image.Image,
garment_image: Image.Image,
cloth_type: str,
num_inference_steps: int,
guidance_scale: float,
seed: int,
show_type: str,
) -> Image.Image:
self.load()
assert self.pipeline is not None
assert self.automasker is not None
assert self.mask_processor is not None
assert self.resize_and_crop is not None
assert self.resize_and_padding is not None
assert self.vis_mask is not None
person_image = self.resize_and_crop(person_image.convert("RGB"), (DEFAULT_WIDTH, DEFAULT_HEIGHT))
garment_image = self.resize_and_padding(garment_image.convert("RGB"), (DEFAULT_WIDTH, DEFAULT_HEIGHT))
generated_mask = self.automasker(person_image, cloth_type)["mask"]
generated_mask = self.mask_processor.blur(generated_mask, blur_factor=9)
generator = None
if seed != -1:
generator = torch.Generator(device=self.device).manual_seed(seed)
result_image = self.pipeline(
image=person_image,
condition_image=garment_image,
mask=generated_mask,
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
generator=generator,
)[0]
if show_type == "result only":
return result_image.convert("RGB")
masked_person = self.vis_mask(person_image, generated_mask)
return compose_preview(person_image, garment_image, masked_person, result_image, show_type)
runtime = CatVTONRuntime(repo_dir=resolve_catvton_repo_dir(CATVTON_REPO_DIR), device=DEVICE)
def prepare_image(image: Image.Image) -> Image.Image:
return ImageOps.exif_transpose(image).convert("RGB")
def image_grid(images: list[Image.Image], rows: int, cols: int) -> Image.Image:
if len(images) != rows * cols:
raise ValueError("The number of images does not match the grid shape.")
width, height = images[0].size
grid = Image.new("RGB", size=(cols * width, rows * height))
for index, image in enumerate(images):
grid.paste(image, box=(index % cols * width, index // cols * height))
return grid
def compose_preview(
person_image: Image.Image,
garment_image: Image.Image,
masked_person: Image.Image,
result_image: Image.Image,
show_type: str,
) -> Image.Image:
width, height = person_image.size
if show_type == "input & result":
side_panel = image_grid([person_image, garment_image], 2, 1).resize((width // 2, height), Image.NEAREST)
else:
side_panel = image_grid([person_image, masked_person, garment_image], 3, 1).resize((width // 3, height), Image.NEAREST)
preview = Image.new("RGB", (side_panel.width + 5 + width, height), color=(255, 255, 255))
preview.paste(side_panel, (0, 0))
preview.paste(result_image.convert("RGB"), (side_panel.width + 5, 0))
return preview
def try_on(
person_image: Optional[Image.Image],
garment_image: Optional[Image.Image],
cloth_type: str,
num_inference_steps: int,
guidance_scale: float,
seed: int,
show_type: str,
) -> Image.Image:
if person_image is None or garment_image is None:
raise gr.Error("Please upload both a shopper photo and a clothing image.")
prepared_person = prepare_image(person_image)
prepared_garment = prepare_image(garment_image)
try:
return runtime.run(
person_image=prepared_person,
garment_image=prepared_garment,
cloth_type=cloth_type,
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
seed=seed,
show_type=show_type,
)
except Exception as exc:
raise gr.Error(f"CatVTON inference failed: {exc}") from exc
if __name__ == "__main__":
# HF debugging: confirm CatVTON code presence inside container
_p1 = Path('/app/CatVTON/model/cloth_masker.py')
_p2 = Path('/app/CatVTON/model/pipeline.py')
_p3 = Path('./CatVTON/model/cloth_masker.py')
print('[HF Debug] /app/CatVTON/model/cloth_masker.py exists:', _p1.exists())
print('[HF Debug] /app/CatVTON/model/pipeline.py exists:', _p2.exists())
print('[HF Debug] ./CatVTON/model/cloth_masker.py exists:', _p3.exists())
with gr.Blocks(theme=gr.themes.Soft(), title=APP_TITLE) as demo:
gr.Markdown(f"# {APP_TITLE}")
gr.Markdown(APP_DESCRIPTION)
gr.Markdown(
f"**Runtime:** repo=`{CATVTON_REPO_DIR}` | weights=`{CATVTON_RESUME_PATH}` | device=`{DEVICE}`"
)
with gr.Row():
with gr.Column(scale=1):
person_input = gr.Image(type="pil", label="Shopper photo")
garment_input = gr.Image(type="pil", label="Clothing image")
cloth_type_input = gr.Radio(
label="Garment type",
choices=["upper", "lower", "overall"],
value="upper",
)
submit_button = gr.Button("Try On", variant="primary")
with gr.Accordion("Advanced options", open=False):
step_input = gr.Slider(label="Inference steps", minimum=10, maximum=100, step=5, value=DEFAULT_STEPS)
guidance_input = gr.Slider(label="Guidance scale", minimum=0.0, maximum=7.5, step=0.5, value=DEFAULT_GUIDANCE_SCALE)
seed_input = gr.Slider(label="Seed", minimum=-1, maximum=10000, step=1, value=DEFAULT_SEED)
show_type_input = gr.Radio(
label="Preview mode",
choices=["result only", "input & result", "input & mask & result"],
value="result only",
)
with gr.Column(scale=1):
result_output = gr.Image(type="pil", label="Try-on result")
gr.Markdown(
"""
### Notes
- This app is just for testing `CatVTON/` codebase.
- Model weights are downloaded on demand from Hugging Face using `zhengchong/CatVTON` by default.
- Just for testing purposes only.
"""
)
submit_button.click(
fn=try_on,
inputs=[person_input, garment_input, cloth_type_input, step_input, guidance_input, seed_input, show_type_input],
outputs=result_output,
)
demo.queue().launch(show_error=True)
|