Upload app.py with huggingface_hub
Browse files
app.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Chipoint โ image geolocation demo (ZeroGPU).
|
| 2 |
+
|
| 3 |
+
Full 3-encoder model + density-weighted consensus, on a ZeroGPU (Blackwell) GPU. Weights are pulled from the
|
| 4 |
+
public chiikabu-labs/chipoint repo; DINOv3 comes ungated from ModelScope. Each guess takes a few seconds.
|
| 5 |
+
"""
|
| 6 |
+
import os
|
| 7 |
+
import numpy as np, torch, torch.nn as nn, torch.nn.functional as F
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from PIL import Image
|
| 10 |
+
import gradio as gr
|
| 11 |
+
import spaces # ZeroGPU
|
| 12 |
+
from huggingface_hub import snapshot_download
|
| 13 |
+
|
| 14 |
+
DEV = "cuda" # ZeroGPU: load on cuda at module level (emulated until @spaces.GPU)
|
| 15 |
+
M, K = 200, 64
|
| 16 |
+
Gv, LAM, MU, BW1, BW2 = 0.8, 1.7, 0.8, 1492.7, 750.0
|
| 17 |
+
ENC_D = {"so400m256": 1152, "dinov3": 1024, "gopt": 1536}
|
| 18 |
+
|
| 19 |
+
print("downloading weights ...", flush=True)
|
| 20 |
+
W = Path(snapshot_download("chiikabu-labs/chipoint", allow_patterns=["*.pt", "*.npy"]))
|
| 21 |
+
|
| 22 |
+
class Head(nn.Module):
|
| 23 |
+
def __init__(s, D):
|
| 24 |
+
super().__init__(); s.net = nn.Sequential(nn.Linear(D, 1024), nn.GELU(), nn.Dropout(0.1), nn.Linear(1024, 512))
|
| 25 |
+
def forward(s, x): return F.normalize(s.net(x), dim=-1)
|
| 26 |
+
|
| 27 |
+
def load_head(tag, D):
|
| 28 |
+
h = Head(D); sd = torch.load(W / f"proj_head_{tag}.pt", map_location="cpu")
|
| 29 |
+
h.load_state_dict({k: v for k, v in sd.items() if k.startswith("net.")})
|
| 30 |
+
return h.to(DEV).eval()
|
| 31 |
+
|
| 32 |
+
print("loading heads + galleries + encoders ...", flush=True)
|
| 33 |
+
HEADS = {t: load_head(t, D) for t, D in ENC_D.items()}
|
| 34 |
+
GAL = {t: np.load(W / f"proj_head_gallery_512_{t}.npy", mmap_mode="r") for t in ENC_D} # mmap, streamed to GPU per query
|
| 35 |
+
GC = np.load(W / "gallery_all_C_so400m256.npy").astype("float32")
|
| 36 |
+
def cell_key(ll, C): return (np.round(ll[..., 0]/C).astype(np.int64) << 20) ^ (np.round(ll[..., 1]/C).astype(np.int64) & 0xFFFFF)
|
| 37 |
+
DENS = {C: dict(zip(*[a.tolist() for a in np.unique(cell_key(GC, C), return_counts=True)])) for C in (0.25, 0.5, 1.0, 2.0)}
|
| 38 |
+
|
| 39 |
+
import open_clip, torchvision.transforms as T
|
| 40 |
+
from transformers import AutoModel, AutoImageProcessor
|
| 41 |
+
from modelscope import snapshot_download as ms_snapshot
|
| 42 |
+
_OC = {}
|
| 43 |
+
for tag, name in [("so400m256", "ViT-SO400M-16-SigLIP2-256"), ("gopt", "ViT-gopt-16-SigLIP2-256")]:
|
| 44 |
+
m, _, pre = open_clip.create_model_and_transforms(name, pretrained="webli"); _OC[tag] = (m.to(DEV).eval(), pre)
|
| 45 |
+
_DINO = ms_snapshot("facebook/dinov3-vitl16-pretrain-lvd1689m")
|
| 46 |
+
_DM = AutoModel.from_pretrained(_DINO).to(DEV).eval()
|
| 47 |
+
_DP = AutoImageProcessor.from_pretrained(_DINO)
|
| 48 |
+
_MEAN = torch.tensor(_DP.image_mean).view(1, 3, 1, 1).to(DEV); _STD = torch.tensor(_DP.image_std).view(1, 3, 1, 1).to(DEV)
|
| 49 |
+
_DPRE = T.Compose([T.Resize(256), T.CenterCrop(224), T.ToTensor()])
|
| 50 |
+
|
| 51 |
+
def zc(a): return (a - a.mean()) / (a.std() + 1e-6)
|
| 52 |
+
|
| 53 |
+
@torch.no_grad()
|
| 54 |
+
def _encode(tag, pil):
|
| 55 |
+
if tag == "dinov3":
|
| 56 |
+
x = ((_DPRE(pil).unsqueeze(0).to(DEV) - _MEAN) / _STD)
|
| 57 |
+
o = _DM(x); e = o.pooler_output if getattr(o, "pooler_output", None) is not None else o.last_hidden_state[:, 0]
|
| 58 |
+
else:
|
| 59 |
+
m, pre = _OC[tag]; e = m.encode_image(pre(pil).unsqueeze(0).to(DEV))
|
| 60 |
+
return F.normalize(e.float(), dim=-1)
|
| 61 |
+
|
| 62 |
+
@torch.no_grad()
|
| 63 |
+
def _retrieve(tag, q): # q:[1,512] cuda -> top-M over the gallery
|
| 64 |
+
g = GAL[tag]; step = 500000
|
| 65 |
+
bs = torch.full((M,), -1e9, device=DEV); bi = torch.zeros(M, dtype=torch.long, device=DEV)
|
| 66 |
+
for i in range(0, g.shape[0], step):
|
| 67 |
+
gg = torch.from_numpy(np.asarray(g[i:i+step], dtype="float16")).to(DEV)
|
| 68 |
+
s = (q.half() @ gg.T)[0].float(); cs, ci = s.topk(min(M, s.shape[0]))
|
| 69 |
+
al = torch.cat([bs, cs]); ai = torch.cat([bi, ci + i]); o = al.topk(M).indices
|
| 70 |
+
bs, bi = al[o], ai[o]; del gg, s
|
| 71 |
+
return bi.cpu().numpy(), bs.cpu().numpy()
|
| 72 |
+
|
| 73 |
+
@spaces.GPU(duration=120)
|
| 74 |
+
def locate(pil):
|
| 75 |
+
pil = pil.convert("RGB"); sc = {}
|
| 76 |
+
for t in ENC_D:
|
| 77 |
+
q = F.normalize(HEADS[t](_encode(t, pil)), dim=-1)
|
| 78 |
+
idx, sim = _retrieve(t, q); z = zc(sim)
|
| 79 |
+
for j, zz in zip(idx, z): sc[int(j)] = sc.get(int(j), 0.0) + float(zz)
|
| 80 |
+
ci = np.array(sorted(sc, key=lambda k: -sc[k])[:K], np.int64); csim = np.array([sc[i] for i in ci], "float32")
|
| 81 |
+
cc = GC[ci]
|
| 82 |
+
DW = np.mean([[-np.log(DENS[C].get(int(k), 1)+1.0) for k in cell_key(cc, C)] for C in (0.25,0.5,1.0,2.0)], 0)
|
| 83 |
+
la, lo = np.radians(cc[:, 0]), np.radians(cc[:, 1])
|
| 84 |
+
D = 2*6371.0*np.arcsin(np.sqrt(np.clip(np.sin((la[:,None]-la[None,:])/2)**2 +
|
| 85 |
+
np.cos(la)[:,None]*np.cos(la)[None,:]*np.sin((lo[:,None]-lo[None,:])/2)**2, 0, 1)))
|
| 86 |
+
vote = np.maximum(csim, 0)*np.exp(Gv*zc(DW))
|
| 87 |
+
KV1 = (vote[None,:]*np.exp(-D/BW1)).sum(1); KV2 = (vote[None,:]*np.exp(-D/BW2)).sum(1)
|
| 88 |
+
score = zc(csim) + 0.3*zc(DW) + LAM*zc(KV1) + MU*zc(KV2)
|
| 89 |
+
lat, lon = cc[int(score.argmax())]; return float(lat), float(lon)
|
| 90 |
+
|
| 91 |
+
def predict(img):
|
| 92 |
+
if img is None: return "Upload a photo first.", ""
|
| 93 |
+
lat, lon = locate(img)
|
| 94 |
+
md = f"### ๐ {lat:.4f}, {lon:.4f}\n[Open in Google Maps](https://maps.google.com/?q={lat:.5f},{lon:.5f})"
|
| 95 |
+
d = 1.5; bbox = f"{lon-d}%2C{lat-d}%2C{lon+d}%2C{lat+d}"
|
| 96 |
+
iframe = (f'<iframe width="100%" height="360" frameborder="0" '
|
| 97 |
+
f'src="https://www.openstreetmap.org/export/embed.html?bbox={bbox}&marker={lat}%2C{lon}"></iframe>')
|
| 98 |
+
return md, iframe
|
| 99 |
+
|
| 100 |
+
with gr.Blocks(title="Chipoint") as demo:
|
| 101 |
+
gr.Markdown("# Chipoint โ where was this photo taken?\n"
|
| 102 |
+
"Upload an outdoor photo; the model guesses its GPS by matching it against 4.9M geotagged images "
|
| 103 |
+
"and beats the published state of the art on OSV-5M. It's a region/city model, not street-address.")
|
| 104 |
+
with gr.Row():
|
| 105 |
+
inp = gr.Image(type="pil", label="Photo")
|
| 106 |
+
with gr.Column():
|
| 107 |
+
out_md = gr.Markdown(); out_map = gr.HTML()
|
| 108 |
+
gr.Button("Locate", variant="primary").click(predict, inp, [out_md, out_map])
|
| 109 |
+
|
| 110 |
+
demo.queue(max_size=8).launch()
|