Spaces:
Running
Running
File size: 3,511 Bytes
f6f7b53 | 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 | """DINOv2 embeddings via ONNX Runtime.
Preprocessing is deliberately IDENTICAL to spike/measure.py. That file produced
the numbers we publish — rank-1 54.2%, AUC 0.77 — and if production resized or
normalised differently, those numbers would no longer describe this system. The
published figure has to be a measurement of the thing we shipped.
The one intentional difference: the spike cropped a camera-app timestamp
watermark off the bottom of every frame. App photos have no watermark, so the
crop is zero here and configurable rather than deleted, so the spike can be
re-run against production-shaped inputs.
"""
from __future__ import annotations
import functools
import threading
import numpy as np
from PIL import Image
MODEL_REPO = "onnx-community/dinov2-small"
# DINOv2 uses 14px patches; 224 = 16 x 14.
IMG_SIZE = 224
EMBED_DIM = 384
# ImageNet statistics — what DINOv2 was trained with.
_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
_STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
_lock = threading.Lock()
def _model_path() -> str:
"""Download the full-precision ONNX weights, skipping quantised variants.
Quantised weights would halve memory but shift every cosine similarity
slightly, which moves the thresholds we cross-validated. Not worth it at
~90 MB on a 16 GB box.
"""
from huggingface_hub import hf_hub_download, list_repo_files
onnx = [f for f in list_repo_files(MODEL_REPO) if f.endswith(".onnx")]
full = [
f
for f in onnx
if not any(q in f.lower() for q in ("quant", "int8", "uint8", "q4", "fp16", "bnb"))
]
return hf_hub_download(MODEL_REPO, sorted(full or onnx, key=len)[0])
@functools.lru_cache(maxsize=1)
def _session():
import onnxruntime as ort
opts = ort.SessionOptions()
# Free Spaces gives 2 vCPUs. Letting ORT spawn more threads than that makes
# it slower, not faster.
opts.intra_op_num_threads = 2
opts.inter_op_num_threads = 1
return ort.InferenceSession(
_model_path(), sess_options=opts, providers=["CPUExecutionProvider"]
)
def warm() -> None:
"""Force model download + graph load at startup rather than on first request."""
sess = _session()
name = sess.get_inputs()[0].name
sess.run(None, {name: np.zeros((1, 3, IMG_SIZE, IMG_SIZE), dtype=np.float32)})
def preprocess(img: Image.Image, watermark_crop: float = 0.0) -> np.ndarray:
img = img.convert("RGB")
if watermark_crop:
w, h = img.size
img = img.crop((0, 0, w, int(h * (1 - watermark_crop))))
# Resize the WHOLE frame rather than centre-cropping. For the wide shot the
# surroundings are a large part of the signal — the spike showed matching
# works partly off the background — and a centre crop throws exactly that
# away.
img = img.resize((IMG_SIZE, IMG_SIZE), Image.BICUBIC)
a = np.asarray(img, dtype=np.float32) / 255.0
return ((a - _MEAN) / _STD).transpose(2, 0, 1)
def embed(img: Image.Image) -> np.ndarray:
"""L2-normalised 384-d CLS embedding. Dot product of two of these is cosine."""
x = preprocess(img)[None, ...]
with _lock: # ORT sessions are not guaranteed thread-safe for concurrent run()
out = _session().run(None, {_session().get_inputs()[0].name: x})[0]
v = out[0, 0].astype(np.float32) # CLS token summarises the image
return v / (np.linalg.norm(v) + 1e-9)
def cosine(a: np.ndarray, b: np.ndarray) -> float:
return float(np.dot(a, b))
|