Spaces:
Running on Zero
Running on Zero
help2
Browse files- .gitignore +2 -0
- README.md +4 -4
- app.py +479 -0
- requirements.txt +4 -0
- tmp/config.log +0 -0
- tmp/engine_initializers.log +0 -0
- tmp/models.log +0 -0
- tmp/optimization.log +0 -0
.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.venv/
|
| 2 |
+
sample_images/
|
README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
---
|
| 2 |
-
title: UnReflectAnything
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: yellow
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
-
python_version:
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: mit
|
|
|
|
| 1 |
---
|
| 2 |
+
title: UnReflectAnything
|
| 3 |
+
emoji: 💻
|
| 4 |
colorFrom: yellow
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.5.1
|
| 8 |
+
python_version: 3.12
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: mit
|
app.py
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Gradio demo for UnReflectAnything: remove specular reflections from images."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import shutil
|
| 6 |
+
import sys
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import NamedTuple
|
| 9 |
+
|
| 10 |
+
# Allow importing unreflectanything when run from gradio_space (e.g. HF Space with root dir)
|
| 11 |
+
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
| 12 |
+
if _REPO_ROOT not in sys.path:
|
| 13 |
+
sys.path.insert(0, str(_REPO_ROOT))
|
| 14 |
+
|
| 15 |
+
# Guard against missing '__main__' in worker threads (wandb/pydantic compat)
|
| 16 |
+
if "__main__" not in sys.modules:
|
| 17 |
+
import types
|
| 18 |
+
sys.modules["__main__"] = types.ModuleType("__main__")
|
| 19 |
+
|
| 20 |
+
_GRADIO_DIR = Path(__file__).resolve().parent
|
| 21 |
+
try:
|
| 22 |
+
import spaces
|
| 23 |
+
except ModuleNotFoundError:
|
| 24 |
+
spaces = None
|
| 25 |
+
import gradio as gr
|
| 26 |
+
import numpy as np
|
| 27 |
+
import torch
|
| 28 |
+
|
| 29 |
+
from huggingface_hub import hf_hub_download, snapshot_download
|
| 30 |
+
|
| 31 |
+
HF_REPO = "AlbeRota/UnReflectAnything"
|
| 32 |
+
IMAGE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff", ".webp")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class HFAssets(NamedTuple):
|
| 36 |
+
"""Paths to assets downloaded from the Hugging Face repo."""
|
| 37 |
+
|
| 38 |
+
weights_path: str
|
| 39 |
+
config_path: str
|
| 40 |
+
logo_path: str
|
| 41 |
+
sample_images_dir: Path
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _download_from_hf() -> HFAssets:
|
| 45 |
+
"""Download weights, config, logo, and sample images from the HF repo. Returns paths to all assets."""
|
| 46 |
+
weights_path = hf_hub_download(
|
| 47 |
+
repo_id=HF_REPO,
|
| 48 |
+
revision="a0d3c7bff5ddb2c430e74b5f8ee67be7cc28fcbe",
|
| 49 |
+
filename="weights/full_model_weights.pt",
|
| 50 |
+
)
|
| 51 |
+
print("Weights path: ", weights_path)
|
| 52 |
+
# config_path = hf_hub_download(
|
| 53 |
+
# repo_id=HF_REPO,
|
| 54 |
+
# filename="configs/pretrained_config.yaml",
|
| 55 |
+
# )
|
| 56 |
+
logo_path = hf_hub_download(
|
| 57 |
+
repo_id=HF_REPO,
|
| 58 |
+
filename="assets/logo.png",
|
| 59 |
+
)
|
| 60 |
+
sample_images_root = Path(
|
| 61 |
+
snapshot_download(
|
| 62 |
+
repo_id=HF_REPO,
|
| 63 |
+
allow_patterns=["sample_images/*"],
|
| 64 |
+
)
|
| 65 |
+
)
|
| 66 |
+
sample_images_dir = sample_images_root / "sample_images"
|
| 67 |
+
return HFAssets(
|
| 68 |
+
weights_path=weights_path,
|
| 69 |
+
config_path=Path(__file__).parent / "pretrained_config.yaml",
|
| 70 |
+
logo_path=logo_path,
|
| 71 |
+
sample_images_dir=sample_images_dir,
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
_cached_assets: HFAssets | None = None
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _get_assets() -> HFAssets:
|
| 79 |
+
"""Return HF assets, downloading once and caching."""
|
| 80 |
+
global _cached_assets
|
| 81 |
+
if _cached_assets is None:
|
| 82 |
+
_cached_assets = _download_from_hf()
|
| 83 |
+
return _cached_assets
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# Local copy of sample images under cwd so Gradio never needs allowed_paths for examples
|
| 87 |
+
_SAMPLE_IMAGES_COPY_DIR: Path | None = None
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _get_sample_image_paths() -> list[str]:
|
| 91 |
+
"""Return paths of sample images under cwd (copied from HF cache) so Gradio can use them without allowed_paths."""
|
| 92 |
+
global _SAMPLE_IMAGES_COPY_DIR
|
| 93 |
+
assets = _get_assets()
|
| 94 |
+
src = assets.sample_images_dir
|
| 95 |
+
if not src.is_dir():
|
| 96 |
+
return []
|
| 97 |
+
dest = _GRADIO_DIR / "sample_images"
|
| 98 |
+
dest.mkdir(parents=True, exist_ok=True)
|
| 99 |
+
paths = []
|
| 100 |
+
for p in sorted(src.iterdir()):
|
| 101 |
+
if not p.is_file() or p.suffix.lower() not in IMAGE_EXTENSIONS:
|
| 102 |
+
continue
|
| 103 |
+
dst_file = dest / p.name
|
| 104 |
+
if not dst_file.exists() or dst_file.stat().st_mtime < p.stat().st_mtime:
|
| 105 |
+
shutil.copy2(p, dst_file)
|
| 106 |
+
paths.append(str(dst_file.resolve()))
|
| 107 |
+
_SAMPLE_IMAGES_COPY_DIR = dest
|
| 108 |
+
return paths
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _get_sample_image_arrays() -> list[np.ndarray]:
|
| 112 |
+
"""Load sample images as numpy arrays (H, W, 3) uint8 for gr.Examples so the input Image shows a preview."""
|
| 113 |
+
from PIL import Image
|
| 114 |
+
|
| 115 |
+
paths = _get_sample_image_paths()
|
| 116 |
+
arrays = []
|
| 117 |
+
for p in paths:
|
| 118 |
+
try:
|
| 119 |
+
img = Image.open(p).convert("RGB")
|
| 120 |
+
arrays.append(np.array(img))
|
| 121 |
+
except Exception:
|
| 122 |
+
continue
|
| 123 |
+
return arrays
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
# Single model instance; loaded in background at app start or on first inference.
|
| 127 |
+
_cached_ura_model = None
|
| 128 |
+
_cached_device = None
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def _get_model(device: str):
|
| 132 |
+
"""Return the pretrained model, loading it once and moving to the requested device."""
|
| 133 |
+
global _cached_ura_model, _cached_device
|
| 134 |
+
assets = _get_assets()
|
| 135 |
+
|
| 136 |
+
from unreflectanything import model
|
| 137 |
+
|
| 138 |
+
# If the model isn't loaded yet, initialize it
|
| 139 |
+
if _cached_ura_model is None:
|
| 140 |
+
print(f"Loading model initially on {device}...")
|
| 141 |
+
_cached_ura_model = model(
|
| 142 |
+
pretrained=True,
|
| 143 |
+
weights_path=assets.weights_path,
|
| 144 |
+
# config_path=assets.config_path,
|
| 145 |
+
device=device,
|
| 146 |
+
verbose=False,
|
| 147 |
+
)
|
| 148 |
+
_cached_device = device
|
| 149 |
+
|
| 150 |
+
# If the model is loaded but on the wrong device, move it
|
| 151 |
+
if _cached_device != device:
|
| 152 |
+
print(f"Moving model from {_cached_device} to {device}...")
|
| 153 |
+
_cached_ura_model.to(device)
|
| 154 |
+
_cached_device = device
|
| 155 |
+
|
| 156 |
+
return _cached_ura_model
|
| 157 |
+
|
| 158 |
+
def build_ui():
|
| 159 |
+
_get_assets()
|
| 160 |
+
# PREVENT: _get_model("cuda") here. It will crash ZeroGPU during startup.
|
| 161 |
+
print("UI building... Model will initialize on first inference.")
|
| 162 |
+
|
| 163 |
+
# Note: Use the decorator directly on the function that does the heavy lifting
|
| 164 |
+
def _extract_tokens_nc(tokens) -> torch.Tensor:
|
| 165 |
+
"""Extract [N, C] from list (last layer) or tensor [B, N, C] (first sample)."""
|
| 166 |
+
t = tokens[-1] if isinstance(tokens, list) else tokens
|
| 167 |
+
t = t[0].cpu().float() if t.dim() == 3 else t.cpu().float()
|
| 168 |
+
return t.squeeze(0) if t.dim() == 3 else t # [N, C]
|
| 169 |
+
|
| 170 |
+
def _tokens_pair_to_rgb(
|
| 171 |
+
tokens_completed,
|
| 172 |
+
tokens_input,
|
| 173 |
+
h: int,
|
| 174 |
+
w: int,
|
| 175 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 176 |
+
"""PCA fit once on completed tokens; apply same mean and V to both; joint min/max norm."""
|
| 177 |
+
from PIL import Image as PILImage
|
| 178 |
+
|
| 179 |
+
t_comp = _extract_tokens_nc(tokens_completed) # [N, C]
|
| 180 |
+
t_inp = _extract_tokens_nc(tokens_input) # [N, C]
|
| 181 |
+
mean = t_comp.mean(dim=0, keepdim=True) # [1, C] – fit on completed only
|
| 182 |
+
centered_comp = t_comp - mean # [N, C]
|
| 183 |
+
U, S, V = torch.svd_lowrank(centered_comp, q=3) # V: [C, 3]
|
| 184 |
+
# Project both with same parameters (same mean, same V)
|
| 185 |
+
proj_comp = (t_comp - mean) @ V # [N, 3]
|
| 186 |
+
proj_inp = (t_inp - mean) @ V # [N, 3]
|
| 187 |
+
# Joint min/max so both images share the same color scale
|
| 188 |
+
lo = min(proj_comp.min().item(), proj_inp.min().item())
|
| 189 |
+
hi = max(proj_comp.max().item(), proj_inp.max().item())
|
| 190 |
+
eps = 1e-8
|
| 191 |
+
proj_comp = (proj_comp - lo) / (hi - lo + eps)
|
| 192 |
+
proj_inp = (proj_inp - lo) / (hi - lo + eps)
|
| 193 |
+
grid = int(t_comp.shape[0] ** 0.5)
|
| 194 |
+
def to_img(proj: torch.Tensor) -> np.ndarray:
|
| 195 |
+
arr = (proj.reshape(grid, grid, 3).numpy() * 255).clip(0, 255).astype(np.uint8)
|
| 196 |
+
return np.array(PILImage.fromarray(arr).resize((w, h), PILImage.BILINEAR))
|
| 197 |
+
return to_img(proj_comp), to_img(proj_inp)
|
| 198 |
+
|
| 199 |
+
def _gray_to_rgb(tensor_1c: torch.Tensor, h: int, w: int) -> np.ndarray:
|
| 200 |
+
"""Convert [B, 1, H_model, W_model] to resized [H, W, 3] uint8 grayscale-as-RGB."""
|
| 201 |
+
from torchvision.transforms import functional as TF
|
| 202 |
+
|
| 203 |
+
resized = TF.resize(tensor_1c.cpu(), [h, w], antialias=True) # [B, 1, H, W]
|
| 204 |
+
gray = (resized[0, 0].numpy().clip(0.0, 1.0) * 255).astype(np.uint8) # [H, W]
|
| 205 |
+
return np.stack([gray] * 3, axis=-1) # [H, W, 3]
|
| 206 |
+
|
| 207 |
+
@spaces.GPU if spaces else lambda x: x
|
| 208 |
+
def run_inference(
|
| 209 |
+
image: np.ndarray | None,
|
| 210 |
+
threshold: float = 0.3,
|
| 211 |
+
dilation: int = 40,
|
| 212 |
+
) -> dict[str, np.ndarray] | None:
|
| 213 |
+
"""Run reflection removal; return all visualisable outputs as numpy arrays."""
|
| 214 |
+
if image is None:
|
| 215 |
+
return None
|
| 216 |
+
|
| 217 |
+
from torchvision.transforms import functional as TF
|
| 218 |
+
import time
|
| 219 |
+
|
| 220 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 221 |
+
ura_model = _get_model(device)
|
| 222 |
+
|
| 223 |
+
target_side = ura_model.image_size
|
| 224 |
+
h, w = image.shape[:2]
|
| 225 |
+
|
| 226 |
+
tensor = TF.to_tensor(image).unsqueeze(0) # [1, 3, H, W]
|
| 227 |
+
tensor = TF.resize(tensor, [target_side, target_side], antialias=True)
|
| 228 |
+
tensor = tensor.to(device, dtype=torch.float32)
|
| 229 |
+
|
| 230 |
+
with torch.no_grad():
|
| 231 |
+
start_time = time.time()
|
| 232 |
+
out = ura_model(
|
| 233 |
+
images=tensor,
|
| 234 |
+
threshold=threshold,
|
| 235 |
+
dilation=int(dilation),
|
| 236 |
+
return_dict=True,
|
| 237 |
+
)
|
| 238 |
+
end_time = time.time()
|
| 239 |
+
|
| 240 |
+
inference_time_ms = (end_time - start_time) * 1000
|
| 241 |
+
gr.Info(f"Inference complete in {inference_time_ms:.1f} ms")
|
| 242 |
+
|
| 243 |
+
results: dict[str, np.ndarray] = {}
|
| 244 |
+
|
| 245 |
+
# Diffuse: [1, 3, H, W] -> [H, W, 3] uint8
|
| 246 |
+
diffuse = TF.resize(out["diffuse"].cpu(), [h, w], antialias=True)
|
| 247 |
+
results["diffuse"] = (diffuse[0].numpy().transpose(1, 2, 0).clip(0.0, 1.0) * 255).astype(np.uint8)
|
| 248 |
+
|
| 249 |
+
# Detected highlight: RGBA overlay superimposed on darkened input
|
| 250 |
+
hl_data = out.get("highlight")
|
| 251 |
+
if hl_data is not None:
|
| 252 |
+
m = TF.resize(hl_data.cpu(), [h, w], antialias=True)[0, 0].numpy().clip(0.0, 1.0) # [H, W]
|
| 253 |
+
image_dark = (image.astype(np.float32) * 0.5).clip(0, 255) # [H, W, 3] base
|
| 254 |
+
overlay_rgb = np.array([255, 200, 0], dtype=np.float32) # amber
|
| 255 |
+
alpha = (0.5 * m)[:, :, np.newaxis] # [H, W, 1]
|
| 256 |
+
comp_rgb = (1 - alpha) * image_dark + alpha * overlay_rgb # [H, W, 3]
|
| 257 |
+
comp_uint8 = np.clip(comp_rgb, 0, 255).astype(np.uint8)
|
| 258 |
+
results["highlight_overlay"] = np.concatenate(
|
| 259 |
+
[comp_uint8, np.full((h, w, 1), 255, dtype=np.uint8)], axis=-1
|
| 260 |
+
) # [H, W, 4] RGBA
|
| 261 |
+
results["highlight_gray"] = _gray_to_rgb(hl_data, h, w)
|
| 262 |
+
|
| 263 |
+
# Highlight mask (binary/dilated)
|
| 264 |
+
mask_data = out.get("highlight_mask")
|
| 265 |
+
if mask_data is not None:
|
| 266 |
+
results["highlight_mask"] = _gray_to_rgb(mask_data, h, w)
|
| 267 |
+
|
| 268 |
+
# DINOv3 tokens (PCA visualization) – same PCA fit for both, joint color scale
|
| 269 |
+
tokens_completed_data = out.get("tokens_completed")
|
| 270 |
+
tokens_input_data = out.get("tokens_input")
|
| 271 |
+
if tokens_completed_data is not None and tokens_input_data is not None:
|
| 272 |
+
img_comp, img_inp = _tokens_pair_to_rgb(tokens_completed_data, tokens_input_data, h, w)
|
| 273 |
+
results["tokens_completed"] = img_comp
|
| 274 |
+
results["tokens_input"] = img_inp
|
| 275 |
+
elif tokens_completed_data is not None:
|
| 276 |
+
t = _extract_tokens_nc(tokens_completed_data)
|
| 277 |
+
mean = t.mean(dim=0, keepdim=True)
|
| 278 |
+
V = torch.svd_lowrank(t - mean, q=3)[2]
|
| 279 |
+
proj = (t - mean) @ V
|
| 280 |
+
lo, hi = proj.min().item(), proj.max().item()
|
| 281 |
+
proj = (proj - lo) / (hi - lo + 1e-8)
|
| 282 |
+
grid = int(t.shape[0] ** 0.5)
|
| 283 |
+
from PIL import Image as PILImage
|
| 284 |
+
arr = (proj.reshape(grid, grid, 3).numpy() * 255).clip(0, 255).astype(np.uint8)
|
| 285 |
+
results["tokens_completed"] = np.array(PILImage.fromarray(arr).resize((w, h), PILImage.BILINEAR))
|
| 286 |
+
results["tokens_input"] = results["tokens_completed"]
|
| 287 |
+
elif tokens_input_data is not None:
|
| 288 |
+
t = _extract_tokens_nc(tokens_input_data)
|
| 289 |
+
mean = t.mean(dim=0, keepdim=True)
|
| 290 |
+
V = torch.svd_lowrank(t - mean, q=3)[2]
|
| 291 |
+
proj = (t - mean) @ V
|
| 292 |
+
lo, hi = proj.min().item(), proj.max().item()
|
| 293 |
+
proj = (proj - lo) / (hi - lo + 1e-8)
|
| 294 |
+
grid = int(t.shape[0] ** 0.5)
|
| 295 |
+
from PIL import Image as PILImage
|
| 296 |
+
arr = (proj.reshape(grid, grid, 3).numpy() * 255).clip(0, 255).astype(np.uint8)
|
| 297 |
+
results["tokens_input"] = np.array(PILImage.fromarray(arr).resize((w, h), PILImage.BILINEAR))
|
| 298 |
+
results["tokens_completed"] = results["tokens_input"]
|
| 299 |
+
|
| 300 |
+
return results
|
| 301 |
+
|
| 302 |
+
VIEW_MODES = ["Diffuse", "Highlight", "Inpaint mask", "DINOv3 space"]
|
| 303 |
+
|
| 304 |
+
def run_inference_slider(
|
| 305 |
+
image: np.ndarray | None,
|
| 306 |
+
threshold: float,
|
| 307 |
+
dilation: int,
|
| 308 |
+
) -> tuple:
|
| 309 |
+
"""Return 4 slider tuples: (left, right) for each view mode."""
|
| 310 |
+
results = run_inference(image, threshold, dilation)
|
| 311 |
+
if results is None:
|
| 312 |
+
return (None,) * 4
|
| 313 |
+
|
| 314 |
+
diffuse = results["diffuse"]
|
| 315 |
+
# Darken input for slider so highlights are more visible; Gradio expects uint8 [0,255]
|
| 316 |
+
image_dark = (image.astype(np.float32) * 0.5).clip(0, 255).astype(np.uint8)
|
| 317 |
+
hl_overlay = results.get("highlight_overlay", diffuse)
|
| 318 |
+
hl_gray = results.get("highlight_gray", diffuse)
|
| 319 |
+
hl_mask = results.get("highlight_mask", diffuse)
|
| 320 |
+
tok_comp = results.get("tokens_completed", diffuse)
|
| 321 |
+
tok_inp = results.get("tokens_input", diffuse)
|
| 322 |
+
|
| 323 |
+
return (
|
| 324 |
+
(image, diffuse), # Diffuse
|
| 325 |
+
(image_dark, hl_overlay), # Detected highlight
|
| 326 |
+
(hl_gray, hl_mask), # Highlight mask
|
| 327 |
+
(tok_inp, tok_comp), # DINOv3 tokens
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
assets = _get_assets()
|
| 331 |
+
with gr.Blocks(title="UnReflectAnything") as demo:
|
| 332 |
+
with gr.Row(elem_classes="mobile-stack"):
|
| 333 |
+
with gr.Column(scale=0, min_width=100):
|
| 334 |
+
if Path(assets.logo_path).is_file():
|
| 335 |
+
gr.Image(
|
| 336 |
+
value=assets.logo_path,
|
| 337 |
+
show_label=False,
|
| 338 |
+
interactive=False,
|
| 339 |
+
height=100,
|
| 340 |
+
container=False,
|
| 341 |
+
buttons=[],
|
| 342 |
+
)
|
| 343 |
+
with gr.Column(scale=1):
|
| 344 |
+
gr.Markdown(
|
| 345 |
+
"""
|
| 346 |
+
# UnReflectAnything
|
| 347 |
+
UnReflectAnything inputs any RGB image and **removes specular highlights**,
|
| 348 |
+
returning a clean diffuse-only outputs. We trained UnReflectAnything by synthetizing
|
| 349 |
+
specularities and supervising in DINOv3 feature space.
|
| 350 |
+
UnReflectAnything works on both natural indoor and **surgical/endoscopic** domain data.
|
| 351 |
+
Visit the [Project Page](https://alberto-rota.github.io/UnReflectAnything/)!
|
| 352 |
+
"""
|
| 353 |
+
)
|
| 354 |
+
slider_labels = [
|
| 355 |
+
"Diffuse",
|
| 356 |
+
"Highlight",
|
| 357 |
+
"Inpaint",
|
| 358 |
+
"DINOv3 Space",
|
| 359 |
+
]
|
| 360 |
+
with gr.Row(elem_classes="mobile-stack"):
|
| 361 |
+
inp = gr.Image(
|
| 362 |
+
type="numpy",
|
| 363 |
+
label="Input",
|
| 364 |
+
height=600,
|
| 365 |
+
)
|
| 366 |
+
sliders = []
|
| 367 |
+
for i, lbl in enumerate(slider_labels):
|
| 368 |
+
sliders.append(
|
| 369 |
+
gr.ImageSlider(
|
| 370 |
+
label=lbl,
|
| 371 |
+
type="numpy",
|
| 372 |
+
height=600,
|
| 373 |
+
show_label=True,
|
| 374 |
+
visible=(i == 0),
|
| 375 |
+
)
|
| 376 |
+
)
|
| 377 |
+
with gr.Row(elem_classes="mobile-stack"):
|
| 378 |
+
threshold_slider = gr.Slider(
|
| 379 |
+
minimum=0.0, maximum=1.0, value=0.2, step=0.01,
|
| 380 |
+
label="Highlight Threshold",
|
| 381 |
+
info="Brightness threshold for detecting specular highlights",
|
| 382 |
+
)
|
| 383 |
+
dilation_slider = gr.Slider(
|
| 384 |
+
minimum=0, maximum=100, value=40, step=1,
|
| 385 |
+
label="Mask Dilation",
|
| 386 |
+
info="Dilation (px) applied to the detected highlight mask",
|
| 387 |
+
)
|
| 388 |
+
view_radio = gr.Radio(
|
| 389 |
+
choices=VIEW_MODES,
|
| 390 |
+
value=VIEW_MODES[0],
|
| 391 |
+
label="Output view",
|
| 392 |
+
)
|
| 393 |
+
run_btn = gr.Button("Run UnReflectAnything", variant="primary")
|
| 394 |
+
run_btn.click(
|
| 395 |
+
fn=run_inference_slider,
|
| 396 |
+
inputs=[inp, threshold_slider, dilation_slider],
|
| 397 |
+
outputs=sliders,
|
| 398 |
+
)
|
| 399 |
+
view_radio.change(
|
| 400 |
+
fn=lambda mode: [gr.update(visible=(m == mode)) for m in VIEW_MODES],
|
| 401 |
+
inputs=view_radio,
|
| 402 |
+
outputs=sliders,
|
| 403 |
+
)
|
| 404 |
+
sample_arrays = _get_sample_image_arrays()
|
| 405 |
+
if sample_arrays:
|
| 406 |
+
gr.Examples(
|
| 407 |
+
examples=[[arr] for arr in sample_arrays],
|
| 408 |
+
inputs=inp,
|
| 409 |
+
label="Pre-loaded examples",
|
| 410 |
+
examples_per_page=20,
|
| 411 |
+
)
|
| 412 |
+
gr.HTML("""<hr>""")
|
| 413 |
+
gr.Markdown("""
|
| 414 |
+
[Project Page](https://alberto-rota.github.io/UnReflectAnything/) ⋅
|
| 415 |
+
[GitHub](https://github.com/alberto-rota/UnReflectAnything) ⋅
|
| 416 |
+
[Model Card](https://huggingface.co/AlbeRota/UnReflectAnything) ⋅
|
| 417 |
+
[Paper](https://arxiv.org/abs/2512.09583) ⋅
|
| 418 |
+
[Contact](mailto:alberto1.rota@polimi.it)
|
| 419 |
+
""")
|
| 420 |
+
return demo
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
demo = build_ui()
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
def _launch_allowed_paths():
|
| 427 |
+
"""Paths Gradio is allowed to serve (e.g. for gr.Examples from HF cache)."""
|
| 428 |
+
paths = [str(_GRADIO_DIR)]
|
| 429 |
+
try:
|
| 430 |
+
assets = _get_assets()
|
| 431 |
+
sample_dir = assets.sample_images_dir
|
| 432 |
+
if sample_dir.is_dir():
|
| 433 |
+
paths.append(str(sample_dir.resolve()))
|
| 434 |
+
# Also allow parent (snapshot root) in case Gradio resolves paths from repo root
|
| 435 |
+
parent = sample_dir.parent
|
| 436 |
+
if parent.is_dir():
|
| 437 |
+
paths.append(str(parent.resolve()))
|
| 438 |
+
except Exception as e:
|
| 439 |
+
print(f"Warning: could not add HF sample_images to allowed_paths: {e}")
|
| 440 |
+
return paths
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
_MOBILE_CSS = """
|
| 444 |
+
@media (max-width: 768px) {
|
| 445 |
+
.mobile-stack { flex-direction: column !important; }
|
| 446 |
+
.mobile-stack > .gr-column,
|
| 447 |
+
.mobile-stack > div { min-width: 100% !important; }
|
| 448 |
+
}
|
| 449 |
+
"""
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
def _launch_kwargs():
|
| 453 |
+
"""Default kwargs for launch() so allowed_paths are always set (e.g. when HF Spaces runs demo.launch())."""
|
| 454 |
+
return {
|
| 455 |
+
"allowed_paths": _launch_allowed_paths(),
|
| 456 |
+
"theme": gr.themes.Soft(primary_hue="orange", secondary_hue="blue"),
|
| 457 |
+
"css": _MOBILE_CSS,
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
# Ensure launch() always receives allowed_paths (e.g. when HF Spaces runner calls demo.launch() without args)
|
| 462 |
+
_original_launch = demo.launch
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
def _launch_with_allowed_paths(*args, **kwargs):
|
| 466 |
+
for key, value in _launch_kwargs().items():
|
| 467 |
+
if key not in kwargs:
|
| 468 |
+
kwargs[key] = value
|
| 469 |
+
return _original_launch(*args, **kwargs)
|
| 470 |
+
|
| 471 |
+
|
| 472 |
+
demo.launch = _launch_with_allowed_paths
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
# Replace your existing launch logic at the very bottom of the file with this:
|
| 476 |
+
if __name__ == "__main__":
|
| 477 |
+
demo.launch(ssr_mode=True, server_name="0.0.0.0", server_port=7860)
|
| 478 |
+
else:
|
| 479 |
+
demo.launch(ssr_mode=True, server_name="0.0.0.0", server_port=7860)
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio
|
| 2 |
+
huggingface-hub
|
| 3 |
+
unreflectanything @ git+https://github.com/alberto-rota/UnReflectAnything.git@49fc52a351863257c0137f7c15397d6bda91b8ef
|
| 4 |
+
dotmap
|
tmp/config.log
ADDED
|
File without changes
|
tmp/engine_initializers.log
ADDED
|
File without changes
|
tmp/models.log
ADDED
|
File without changes
|
tmp/optimization.log
ADDED
|
File without changes
|