DINOv3 geo pre-filter
A binary image classifier that runs before GeoCLIP geo-estimation. It decides whether an image shows a place that can plausibly be located, such as streets, buildings, landscape or landmarks. Images without one go no further, so GeoCLIP never assigns a location to them. That covers portraits and selfies, close-ups, screenshots, logos, documents and other graphics.
Files
| File | Backbone | Size | Input batch | sha256 |
|---|---|---|---|---|
25.09.25-dinov3_vitb16_geo_filter.onnx |
DINOv3 ViT-B/16 (frozen) + MLP head | 346 MB | dynamic | 49bb52b6d5bc22153cd2a4f433ac2ec00ea9a0e144712a73d45501e907119599 |
04.10.25-dino_v3_linear.onnx |
DINOv3 + linear head (previous version) | 87 MB | 1 | 5ca53cc5a07d5b3102c6d68e85fdb8670f3c8e94622fa48964f61bf5ab54efb3 |
Both files have the same interface, so either is a drop-in replacement for the other.
Interface
- Input
pixel_values: float32[batch, 3, 224, 224]. RGB, resized to 224ร224 without keeping aspect ratio or cropping (bilinear), scaled to [0, 1], then normalised with ImageNet mean[0.485, 0.456, 0.406]and std[0.229, 0.224, 0.225]. - Output
classification: float32[batch, 2]logits. Index0means usable for geo-estimation, index1means reject. - Decision: take the
argmax. The operating threshold is built into the head's bias, so callers need no threshold of their own. Apply a softmax for probabilities. - ONNX opset 17. Tested with onnxruntime 1.22.0.
import numpy as np
import onnxruntime as ort
from PIL import Image
session = ort.InferenceSession("25.09.25-dinov3_vitb16_geo_filter.onnx")
image = Image.open("photo.jpg").convert("RGB").resize((224, 224), Image.BILINEAR)
x = (np.asarray(image, np.float32) / 255 - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225]
logits = session.run(None, {"pixel_values": x.transpose(2, 0, 1)[None].astype(np.float32)})[0][0]
usable_for_geo = int(np.argmax(logits)) == 0
Model (25.09.25)
- Backbone:
facebook/dinov3-vitb16-pretrain-lvd1689m, frozen. - Features: the CLS token concatenated with the mean of the patch tokens (register tokens excluded), 1536 values.
- Head: dropout โ linear 1536โ512 โ GELU โ dropout โ linear 512โ2. Feature standardisation is folded into the first layer's weights.
- Trained with class-weighted cross-entropy and AdamW (lr 3e-4, weight decay 0.05, cosine schedule), early-stopped on validation average precision for the usable class.
Training data
- 40,000 de-duplicated images sampled at random from an internal collection of user-supplied images. The images are not published.
- Each image was labelled by
SmolVLM2-2.2B-Instruct. It answered two yes/no questions, read from next-token probabilities:- whether the photo shows enough of a real place to guess where in the world it was taken;
- whether a person, face, animal or object fills most of the picture.
- A logistic regression fitted on 200 hand-labelled images combined the two answers into a score with two cut-offs. Images between the cut-offs were dropped as uncertain.
- 36,163 labelled images remained, split by image hash:
| Split | Images | Usable for geo |
|---|---|---|
| train | 25,307 | 2,185 |
| validation | 5,477 | 492 |
| test | 5,379 | 439 |
Evaluation
VLM-labelled test split (5,379 images, 439 usable): ROC AUC 0.995, average precision 0.963. At the built-in threshold, 89.6% of the images it passes are usable, and it keeps 90.7% of the usable images.
Hand-labelled held-out set: 300 random images, never used for training or tuning. Ambiguous images are excluded from these counts.
04.10.25 (previous) |
25.09.25 |
|
|---|---|---|
| Usable images kept (of 10) | 10 | 8 |
| Reject images passed (of 264) | 143 | 9 |
| Passed images that are usable | 6.5% | 47% |
Fresh unlabelled images (3,000, never seen in training): the previous filter passes 60.1% of them and the new one 11.9%.
Known weakness: people posed outdoors with some scenery behind them, such as group photos or a selfie on a street, are the most common remaining false passes. Dense street crowds can also pass.
Runtime
Per image, batch 1, onnxruntime 1.22.0 on CPU (AMD EPYC 7401P), median:
| Threads | 04.10.25 |
25.09.25 |
|---|---|---|
| 6 | 97 ms | 260 ms |
| 24 | 76 ms | 177 ms |
The new backbone needs about 4ร the compute of the previous one. GPU latency was not measured.
License
Both files contain DINOv3 weights, so they are distributed under the DINOv3 License.
Model tree for matanrich/dinov3_onnx_classifier
Base model
facebook/dinov3-vit7b16-pretrain-lvd1689m