Spaces:
Running
Running
Add MAE and CLIP encoders alongside DINOv3 (per-model prefix/grid/normalization; MAE ids_restore un-shuffle)
Browse files
app.py
CHANGED
|
@@ -15,50 +15,72 @@ from PIL import Image
|
|
| 15 |
print("[app] transformers import...", flush=True)
|
| 16 |
from scipy.cluster.hierarchy import linkage, to_tree
|
| 17 |
from scipy.spatial.distance import squareform
|
| 18 |
-
from transformers import AutoModel
|
| 19 |
|
| 20 |
RES, MS = 224, 4
|
| 21 |
-
GRID = RES // 16 # 14x14 = 196 patches
|
| 22 |
DEV = "cuda" if torch.cuda.is_available() else "cpu"
|
| 23 |
TOKEN = os.environ.get("HF_TOKEN")
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
MODELS = {
|
| 26 |
-
"ViT-L · 0.3B (fast)": "facebook/dinov3-vitl16-pretrain-lvd1689m",
|
| 27 |
-
"ViT-H+ · 0.84B (slower)": "facebook/dinov3-vith16plus-pretrain-lvd1689m",
|
| 28 |
-
"ViT-7B · 6.7B (needs GPU)": "facebook/dinov3-vit7b16-pretrain-lvd1689m",
|
|
|
|
|
|
|
| 29 |
}
|
| 30 |
DEFAULT_MODEL = next(iter(MODELS))
|
| 31 |
|
| 32 |
-
MEAN = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1)
|
| 33 |
-
STD = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1)
|
| 34 |
TREE_HTML = open("tree_widget.html").read()
|
| 35 |
REVEAL_HTML = open("reveal_widget.html").read()
|
| 36 |
|
| 37 |
# single-slot cache: keep only the most-recently-used model (avoids OOM when switching)
|
| 38 |
_cur = {"repo": None, "model": None}
|
| 39 |
-
def get_model(
|
|
|
|
| 40 |
if _cur["repo"] != repo:
|
| 41 |
_cur["model"] = None
|
| 42 |
import gc; gc.collect()
|
| 43 |
dt = torch.bfloat16 if "vit7b16" in repo else torch.float32 # 7B in bf16 to fit
|
| 44 |
print(f"[app] loading {repo} ({dt})", flush=True)
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
| 47 |
_cur["repo"] = repo
|
| 48 |
return _cur["model"]
|
| 49 |
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
def build_tree(image, model_label):
|
| 52 |
-
|
|
|
|
| 53 |
mdt = next(model.parameters()).dtype
|
|
|
|
| 54 |
img = image.convert("RGB").resize((RES, RES), Image.BICUBIC)
|
| 55 |
arr = np.asarray(img).astype(np.float32) / 255
|
| 56 |
-
x = ((torch.from_numpy(arr).permute(2, 0, 1) -
|
| 57 |
-
|
| 58 |
-
o = model(x, output_attentions=True)
|
| 59 |
-
prefix = o.last_hidden_state.shape[1] - GRID * GRID # strip CLS + register tokens
|
| 60 |
-
feat = o.last_hidden_state[0, prefix:].float().cpu().numpy()
|
| 61 |
-
att = o.attentions[-1][0][:, 0, prefix:].mean(0).float().cpu().numpy()
|
| 62 |
att_pct = 100.0 * att / att.sum()
|
| 63 |
P = feat.shape[0]; g = int(round(P ** 0.5))
|
| 64 |
fn = feat / (np.linalg.norm(feat, axis=1, keepdims=True) + 1e-8)
|
|
@@ -127,9 +149,12 @@ if gr is not None:
|
|
| 127 |
with gr.Row():
|
| 128 |
inp = gr.Image(type="pil", label="Upload an image", height=300)
|
| 129 |
with gr.Column(scale=0):
|
| 130 |
-
model_sel = gr.Dropdown(choices=list(MODELS.keys()), value=DEFAULT_MODEL, label="
|
| 131 |
btn = gr.Button("Build tree + reveal", variant="primary")
|
| 132 |
-
gr.Markdown("<small>
|
|
|
|
|
|
|
|
|
|
| 133 |
"First use of each model downloads its weights (one-time).</small>")
|
| 134 |
with gr.Tab("Condensed tree (hover)"):
|
| 135 |
out_tree = gr.HTML()
|
|
|
|
| 15 |
print("[app] transformers import...", flush=True)
|
| 16 |
from scipy.cluster.hierarchy import linkage, to_tree
|
| 17 |
from scipy.spatial.distance import squareform
|
| 18 |
+
from transformers import AutoModel, ViTMAEModel, CLIPVisionModel
|
| 19 |
|
| 20 |
RES, MS = 224, 4
|
|
|
|
| 21 |
DEV = "cuda" if torch.cuda.is_available() else "cpu"
|
| 22 |
TOKEN = os.environ.get("HF_TOKEN")
|
| 23 |
|
| 24 |
+
IMNET = (torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1),
|
| 25 |
+
torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1))
|
| 26 |
+
CLIPN = (torch.tensor([0.48145466, 0.4578275, 0.40821073]).view(3, 1, 1),
|
| 27 |
+
torch.tensor([0.26862954, 0.26130258, 0.27577711]).view(3, 1, 1))
|
| 28 |
+
|
| 29 |
+
# kind: how to strip prefix tokens / un-shuffle; patch: 224/patch = grid side
|
| 30 |
MODELS = {
|
| 31 |
+
"DINOv3 ViT-L · 0.3B (fast)": dict(repo="facebook/dinov3-vitl16-pretrain-lvd1689m", kind="dinov3", patch=16, norm=IMNET),
|
| 32 |
+
"DINOv3 ViT-H+ · 0.84B (slower)": dict(repo="facebook/dinov3-vith16plus-pretrain-lvd1689m", kind="dinov3", patch=16, norm=IMNET),
|
| 33 |
+
"DINOv3 ViT-7B · 6.7B (needs GPU)": dict(repo="facebook/dinov3-vit7b16-pretrain-lvd1689m", kind="dinov3", patch=16, norm=IMNET),
|
| 34 |
+
"MAE ViT-L (pixel-reconstruction)": dict(repo="facebook/vit-mae-large", kind="mae", patch=16, norm=IMNET),
|
| 35 |
+
"CLIP ViT-L/14 (language-aligned)": dict(repo="openai/clip-vit-large-patch14", kind="clip", patch=14, norm=CLIPN),
|
| 36 |
}
|
| 37 |
DEFAULT_MODEL = next(iter(MODELS))
|
| 38 |
|
|
|
|
|
|
|
| 39 |
TREE_HTML = open("tree_widget.html").read()
|
| 40 |
REVEAL_HTML = open("reveal_widget.html").read()
|
| 41 |
|
| 42 |
# single-slot cache: keep only the most-recently-used model (avoids OOM when switching)
|
| 43 |
_cur = {"repo": None, "model": None}
|
| 44 |
+
def get_model(spec):
|
| 45 |
+
repo = spec["repo"]
|
| 46 |
if _cur["repo"] != repo:
|
| 47 |
_cur["model"] = None
|
| 48 |
import gc; gc.collect()
|
| 49 |
dt = torch.bfloat16 if "vit7b16" in repo else torch.float32 # 7B in bf16 to fit
|
| 50 |
print(f"[app] loading {repo} ({dt})", flush=True)
|
| 51 |
+
cls = {"dinov3": AutoModel, "mae": ViTMAEModel, "clip": CLIPVisionModel}[spec["kind"]]
|
| 52 |
+
mdl = cls.from_pretrained(repo, dtype=dt, token=TOKEN, attn_implementation="eager")
|
| 53 |
+
if spec["kind"] == "mae":
|
| 54 |
+
mdl.config.mask_ratio = 0.0 # keep ALL patches (MAE masks 75% by default)
|
| 55 |
+
_cur["model"] = mdl.eval().to(DEV)
|
| 56 |
_cur["repo"] = repo
|
| 57 |
return _cur["model"]
|
| 58 |
|
| 59 |
|
| 60 |
+
def encode(model, spec, x):
|
| 61 |
+
"""-> (feat[P,C], att[P]) with patches in spatial row-major order."""
|
| 62 |
+
with torch.no_grad():
|
| 63 |
+
o = model(x, output_attentions=True)
|
| 64 |
+
h, a = o.last_hidden_state, o.attentions[-1][0] # a: (heads, S, S)
|
| 65 |
+
if spec["kind"] == "mae":
|
| 66 |
+
ids = o.ids_restore[0] # MAE shuffles tokens -> un-shuffle
|
| 67 |
+
feat, att = h[0, 1:][ids], a[:, 0, 1:][:, ids].mean(0)
|
| 68 |
+
else:
|
| 69 |
+
g = RES // spec["patch"]
|
| 70 |
+
prefix = h.shape[1] - g * g # strip CLS (+ registers for DINOv3)
|
| 71 |
+
feat, att = h[0, prefix:], a[:, 0, prefix:].mean(0)
|
| 72 |
+
return feat.float().cpu().numpy(), att.float().cpu().numpy()
|
| 73 |
+
|
| 74 |
+
|
| 75 |
def build_tree(image, model_label):
|
| 76 |
+
spec = MODELS.get(model_label, MODELS[DEFAULT_MODEL])
|
| 77 |
+
model = get_model(spec)
|
| 78 |
mdt = next(model.parameters()).dtype
|
| 79 |
+
mean, std = spec["norm"]
|
| 80 |
img = image.convert("RGB").resize((RES, RES), Image.BICUBIC)
|
| 81 |
arr = np.asarray(img).astype(np.float32) / 255
|
| 82 |
+
x = ((torch.from_numpy(arr).permute(2, 0, 1) - mean) / std).unsqueeze(0).to(DEV, mdt)
|
| 83 |
+
feat, att = encode(model, spec, x)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
att_pct = 100.0 * att / att.sum()
|
| 85 |
P = feat.shape[0]; g = int(round(P ** 0.5))
|
| 86 |
fn = feat / (np.linalg.norm(feat, axis=1, keepdims=True) + 1e-8)
|
|
|
|
| 149 |
with gr.Row():
|
| 150 |
inp = gr.Image(type="pil", label="Upload an image", height=300)
|
| 151 |
with gr.Column(scale=0):
|
| 152 |
+
model_sel = gr.Dropdown(choices=list(MODELS.keys()), value=DEFAULT_MODEL, label="Encoder")
|
| 153 |
btn = gr.Button("Build tree + reveal", variant="primary")
|
| 154 |
+
gr.Markdown("<small>Compare encoder families: **DINOv3** (self-supervised, semantic), "
|
| 155 |
+
"**MAE** (pixel-reconstruction — more texture/appearance driven), "
|
| 156 |
+
"**CLIP** (language-aligned — 16×16 grid, patch-14).<br>"
|
| 157 |
+
"ViT-H+/7B are large — slow on the free CPU; **ViT-7B realistically needs a GPU Space**. "
|
| 158 |
"First use of each model downloads its weights (one-time).</small>")
|
| 159 |
with gr.Tab("Condensed tree (hover)"):
|
| 160 |
out_tree = gr.HTML()
|