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.

Downloads last month

-

Downloads are not tracked for this model. How to track
Safetensors
Model size
0.4B params
Tensor type
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Dataset used to train nadh0708/Img2LocJakarta