Img2LocJakarta — DINOv3 street-view geolocation for Jakarta
Predict (latitude, longitude) of a Google Street View perspective crop taken on a Jakarta road. A frozen DINOv3 ViT-L/16 backbone extracts features; a U-shaped MLP head regresses a local flat-earth (x, y) offset which is converted to WGS-84 degrees.
| Input | RGB street-level image (any size, resized to 224×224) |
| Output | {"lat": float, "lon": float} — WGS-84 degrees |
| Scope | Trained on Jakarta (5 administrative cities, motorway + primary roads) |
| Backbone | DINOv3 ViT-L/16 — frozen; only the regression head was trained |
Quick Start
pip install torch torchvision pillow huggingface_hub safetensors
from huggingface_hub import hf_hub_download
import sys, os
# Download the inference code
for fname in ["inference.py", "modeling_geotag.py"]:
hf_hub_download("nadh0708/Img2LocJakarta", fname, local_dir=".")
from inference import GeoTagPredictor
predictor = GeoTagPredictor("nadh0708/Img2LocJakarta")
print(predictor.predict("street.jpg"))
# {'lat': -6.2261, 'lon': 106.8123}
# Batched
print(predictor.predict(["a.jpg", "b.jpg"]))
From a local checkpoint
predictor = GeoTagPredictor("modelD_40e_2.pth")
CLI
python inference.py nadh0708/Img2LocJakarta street.jpg
Low-level API
import torch
from torchvision import transforms
from PIL import Image
from modeling_geotag import DinoGeoRegressor
model = DinoGeoRegressor.from_pretrained("nadh0708/Img2LocJakarta")
model.eval()
tf = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])
x = tf(Image.open("street.jpg").convert("RGB")).unsqueeze(0)
lonlat = model.predict_lonlat(x) # tensor([[lon, lat]])
Architecture
DinoGeoRegressor
├─ encoder : DINOv3 ViT-L/16 (frozen, patch_size=16)
│ last block tokens (B, 201, 1024) ──flatten──► (B, 205824)
│ tokens: 196 patch + 1 CLS + 4 storage
└─ head : UNet-MLP
205824 ──enc1──► 512 ──enc2──► 512
↓ bottleneck 512
512 ◄──dec2── 1024 (cat)
512 ◄──dec1── 1024 (cat) skip connections from enc1, enc2
↓ out linear
2 (x_metres, y_metres)
──coord_convert──► (lon, lat) degrees
The output is a flat-earth offset in metres relative to Jakarta origin
(lon 106.828320, lat -6.227468), inverted to degrees at inference time.
The published checkpoint bundles the frozen backbone + trained head — no
separate LVD-1689M weight download is required. Only the DINOv3 code is
pulled from torch.hub on first load.
Performance
Evaluated on 137,173 test samples (Google Street View perspective crops of Jakarta roads, 8 headings: 0°–315° in 45° steps).
| Epoch | Mean error | Median error | % < 1 km | % < 5 km | % < 25 km |
|---|---|---|---|---|---|
| e15 | 3.313 km | 1.966 km | 25.6% | 79.4% | 100.0% |
| e35 | 3.158 km | 1.588 km | 33.8% | 80.2% | 100.0% |
| e40 | 2.743 km | 1.272 km | 42.2% | 83.1% | 100.0% |
Error is geodesic (haversine) distance between the predicted and true GPS
coordinate. This checkpoint is epoch 40 (modelD_40e_2.pth).
Training
| Hyperparameter | Value |
|---|---|
| Backbone | DINOv3 ViT-L/16 (facebookresearch/dinov3) — frozen |
| Head | UNet-MLP, hidden 512, skip connections |
| Optimiser | AdamW |
| Learning rate | 1e-3 |
| Weight decay | 1e-4 |
| Batch size | 64 |
| Epochs | 40 |
| Input size | 224 × 224 |
| Loss | MSE on flat-earth (x, y) metres |
Dataset: nadh0708/JKTSV-primary —
685,848 perspective crops (548,675 train / 137,173 test) sampled along
Jakarta motorway and primary road segments at 50 m intervals, 8 headings.
Preprocessing
Images are resized to 224 × 224 and normalised with ImageNet statistics:
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
GeoTagPredictor applies this automatically. If you call DinoGeoRegressor
directly, apply the transform before passing pixel_values.
Limitations
- Geographically restricted to Jakarta. Out-of-domain images return coordinates near the projection origin.
- Trained on 2023+ Street View panoramas projected to 8 fixed headings (0°, 45°, 90°, 135°, 180°, 225°, 270°, 315°).
- Backbone is frozen; only the 3 M-parameter regression head was trained.
- Hard examples (48% of the test set, defined as error > 1 km across all measured epochs) tend to cluster in peripheral and coastal areas with low visual distinctiveness.
Files
| File | Purpose |
|---|---|
modeling_geotag.py |
Model classes + from_pretrained loader + coordinate conversion |
inference.py |
GeoTagPredictor high-level API + CLI |
model.safetensors |
Full state dict (frozen backbone + trained head, ~1.2 GB) |
requirements.txt |
Runtime dependencies |
Citation
If you use this model or the dataset, please cite:
@misc{img2locjakarta2025,
author = {Nadhif},
title = {Img2LocJakarta: DINOv3 Street-View Geolocation for Jakarta},
year = {2025},
url = {https://huggingface.co/nadh0708/Img2LocJakarta}
}
License
This model is released under the Apache License 2.0, consistent with the
DINOv3 backbone license. See LICENSE for details.