Keetha, Mishra, Karhade, Jatavallabhula et al. (AnyLoc, RA-L 2023) · Izquierdo & Civera (SALAD, CVPR 2024) · Berton & Masone (MegaLoc, 2025)

Foundation-Model Visual Place Recognition

How frozen self-supervised features turned place recognition from a brittle, per-domain training problem into a training-free retrieval problem — and how optimal transport pushed it to state of the art.

Prerequisites: Image embeddings + cosine similarity + softmax. Helpful: a Vision Transformer mental model.
10
Chapters
9
Simulations
2
Code Labs

Chapter 0: The Domain-Gap Problem

A delivery robot rolls down the same street it mapped yesterday. Yesterday it was noon, dry, summer leaves on the trees. Today it is dusk, raining, the leaves are gone, and a parked truck blocks half the view. The robot snaps a photo and asks one deceptively simple question: "Have I been here before, and if so, where?" Answer it and you can re-localize, close a SLAM loop, or recover from a lost GPS fix. Get it wrong and the robot teleports across its map.

This is Visual Place Recognition (VPR): given a query image, retrieve the geographically nearest images from a large geo-tagged database (the "map"). It is a pure image retrieval problem — but a vicious one, because the same physical place looks wildly different across conditions (day/night, sun/rain/snow, summer/winter) and viewpoints (driving lane, sidewalk, drone, the opposite direction down a one-way street).

Here is what was broken before 2023. The dominant method, NetVLAD (Arandjelović et al., 2016), learned a single global descriptor end-to-end with a triplet loss on geo-tagged street-view data. It worked beautifully — on data that looked like its training set. Train on sunny urban driving and it cracked on aerial imagery, on indoor scenes, on underwater footage, on night driving, on anything outside its training distribution. Every new domain demanded a fresh round of collecting geo-tagged images, mining triplets, and retraining. VPR had become a per-domain engineering treadmill.

The misconception to drop right now: "more VPR-specific training data fixes generalization." It does not. NetVLAD trained on city A overfits the appearance statistics of city A — its building textures, its camera, its lighting. Adding city B helps city B and slightly hurts city A. The fix is not more in-domain data; it is a feature backbone that already saw everything. That backbone arrived as DINOv2 — a Vision Transformer self-supervised on 142M unlabeled images — and the realization that you can do VPR with its features frozen, with no VPR training at all.

This lesson walks the arc of three papers that, together, rebuilt VPR on foundation features:

AnyLoc (2023)
Frozen DINOv2 + classical VLAD, training-free. Works across cities, aerial, underwater, indoor, night — domains where NetVLAD collapses.
SALAD (2024)
Keep DINOv2, but replace hard VLAD assignment with a learned optimal-transport assignment solved by Sinkhorn. New state of the art with a tiny trained head.
MegaLoc (2025)
Train one retrieval model, on a megamix of datasets, that is simultaneously strong at VPR, landmark retrieval, and the retrieval step of visual localization.

The through-line is a single bet: the hard part of VPR is the feature, and a frozen foundation feature already solves the hard part. Everything after is about how you aggregate thousands of per-patch features into one searchable vector — and that aggregation is where the optimal-transport idea earns its place.

Why this is the right framing

A query image of a 224×224 crop, fed to DINOv2 (patch size 14), yields a grid of 16×16 = 256 local descriptors, each a vector in dimension D = 1024 (for the ViT-L/14 backbone). A database of N = 100,000 images is then 100,000 × 256 such vectors. You cannot match patch-to-patch across the whole database at query time — that is 100,000 × 256 × 256 dot products per query, hopelessly slow. So VPR compresses each image's patch grid into a single global vector and does one nearest-neighbour search. The entire game is: how do you collapse 256 local descriptors into one global descriptor without throwing away what makes a place recognizable?

Why VPR is hard: the appearance gap

The same place under shifting conditions. Drag the slider from "day" to "night/rain/winter". A descriptor trained to memorize day-time texture (left bars) drifts apart from the query; a descriptor built on stable structure (right bars) stays close. The number is descriptor distance to the day reference — lower is better.

Condition shift day
EraMethodBackboneVPR trainingCross-domain
Pre-2016Bag-of-Words, VLAD on SIFTHand-craftedNone (unsupervised)Weak — SIFT fragile to lighting
2016–2022NetVLAD & descendantsVGG/ResNet, trainedHeavy (triplets, per-domain)Poor — overfits training cities
2023AnyLocDINOv2, frozenNoneStrong, all domains
2024SALADDINOv2 + tiny headLight (one head, fast)SOTA on the standard benchmarks
2025MegaLocDINOv2 + aggregationMulti-dataset, multi-taskSOTA & unified across tasks
What was the core failure of NetVLAD-style VPR that the foundation-model approach targets?

Chapter 1: VPR as Retrieval

Strip away the vision and VPR is nearest-neighbour search in a vector space. Every database image i is reduced to a global descriptor gi ∈ ℝd. The query image becomes gq. We rank the database by similarity and return the top-K. If a returned image lies within a tolerance (typically 25 m) of the query's true position, it is a correct retrieval.

retrieve(q) = top-Ki   cos(gq, gi) = top-Ki   (gq · gi) / (‖gq‖ ‖gi‖)

Because descriptors are L2-normalized (‖g‖ = 1), cosine similarity reduces to a plain dot product, and "most similar" equals "smallest Euclidean distance." So the global descriptor is everything: the search is trivial; the descriptor is the science.

The metric: Recall@K

VPR is scored with Recall@K: the fraction of queries for which at least one of the top-K retrieved images is a true match. Recall@1 is the strict version (the single best match must be correct); Recall@5 and Recall@10 are more forgiving, which matters because a downstream geometric verifier can re-rank a short list. A method that gets the right place into the top-5 ninety-five times out of a hundred is, in practice, excellent.

Recall@K = (1/|Q|) ∑q∈Q 𝟙[ ∃ i ∈ topK(q) : dist(posq, posi) ≤ τ ]

where 𝟙[·] is the indicator (1 if true, 0 if false), τ is the localization tolerance (25 m is standard for outdoor driving benchmarks), and pos is ground-truth position.

A worked Recall@K

Suppose a query's true place is database image #42, and #42 sits at GPS coordinate p. We compute cosine similarity to all database descriptors and sort:

RankDB imagecos simdist to query (m)match?
1#1170.91140no
2#420.893yes
3#430.889yes

Recall@1 = 0 for this query (rank 1 is a false match — a different street that looks similar). Recall@5 = 1 (a true match appears by rank 2). This is the typical story: the right place is usually in the short list, so Recall@5 and @10 are high while Recall@1 is the demanding number that separates strong methods from weak ones.

Retrieval over a descriptor space

A 2D toy descriptor space: the query (warm) and the database (teal = same place, gray = different places). Drag the slider to change how well the descriptor separates places. Watch Recall@1 flip as the nearest neighbour becomes a true vs false match. The dashed circle is the top-1 ball.

Descriptor quality 0.30
Concept → realization. A database of N = 100k images with d = 8448-dim descriptors (AnyLoc's VLAD size) is a 100k×8448 float matrix ≈ 3.4 GB. One query is a single 8448-vector. The "search" is one matrix–vector product (or a FAISS index lookup): ≈100k dot products, milliseconds. All the engineering and all the research effort goes into producing those 8448 numbers per image — never into the search itself.
Why is Recall@1 a much harder number to improve than Recall@10?

Chapter 2: DINOv2 Local Features

The shared engine of all three papers is DINOv2 (Oquab et al., 2023): a Vision Transformer self-supervised — no labels — on a curated 142M-image dataset, with a discriminative self-distillation objective (student/teacher DINO loss plus an iBOT masked-patch loss). The payoff for VPR is empirical and decisive: DINOv2 patch tokens behave like a dense, semantically meaningful, condition-robust local feature, without ever being trained for places.

From image to patch grid: the real data flow

Take a 224×224 RGB image. The ViT-L/14 backbone splits it into non-overlapping 14×14 patches, giving a 16×16 = 256 patch grid (plus one CLS token). Each patch is linearly embedded and passed through the transformer. The output we use is the last-layer patch tokens:

image [3, 224, 224]  →DINOv2 ViT-L/14  F ∈ ℝN×D,   N = 256 patches,   D = 1024

So one image becomes a set of N = 256 local descriptors, each D = 1024-dimensional. This is the raw material. (AnyLoc uses larger input crops, giving more patches; SALAD uses 322×322, giving N = 16×16... the exact grid depends on the crop, but the structure is identical: a grid of D-dim patch tokens.)

Why frozen DINOv2 features are condition-robust. The self-supervised objective rewards features that are invariant under heavy augmentation (crops, color jitter, blur). It never had a "place" label to memorize, so it cannot overfit a city's texture. What survives is structure: edges, object parts, layout. A facade is a facade at noon or at dusk. That invariance is exactly the property VPR begged NetVLAD to learn from triplets — and DINOv2 hands it over for free.

Which layer and which token?

AnyLoc ran a careful probe: which backbone, which layer, and which facet (the raw token, or the internal query/key/value projections) give the best VPR features? The headline findings:

ChoiceAnyLoc findingWhy
BackboneDINOv2 > DINO > CLIP > MAE for VPRDiscriminative self-distillation yields the most place-discriminative locals
LayerA late-but-not-final layer is bestFinal layer over-specializes to the pretext task; mid-late layers keep spatial/structural detail
FacetThe "value" facet works wellValue vectors carry content rather than attention routing

A worked patch dot product

Take two tiny D = 3 patch tokens (real ones are 1024-dim; the arithmetic is identical). Patch a = [0.6, 0.0, 0.8] from a query window of a brick wall; patch b = [0.5, 0.1, 0.85] from the same wall at dusk; patch c = [-0.7, 0.7, 0.1] from foliage. L2-normalize, then dot:

cos(a, b) = (0.6·0.5 + 0 + 0.8·0.85) / (1 · √(0.25+0.01+0.7225)) = 0.98/0.991 ≈ 0.989 — same surface, near-identical even across the day→dusk shift. cos(a, c) = (-0.42 + 0 + 0.08) / (1 · 0.99) ≈ -0.343 — a wall patch and a foliage patch are far apart. The features cluster by what the patch is, not by lighting. This is the property the aggregator will exploit.

DINOv2 patch grid → local descriptors

A schematic image, its patch grid, and the resulting set of local descriptors. Click a patch to see it light up the descriptors that share its visual cluster (wall / sky / foliage / road). Notice descriptors group by content, not position.

Wall patches highlighted
python
# DINOv2 patch tokens are the raw material for foundation-model VPR
import torch

dinov2 = torch.hub.load('facebookresearch/dinov2', 'dinov2_vitl14')
dinov2.eval()
for p in dinov2.parameters(): p.requires_grad_(False)   # FROZEN — no VPR training

img = torch.randn(1, 3, 224, 224)
with torch.no_grad():
    out = dinov2.forward_features(img)
    patch_tokens = out['x_norm_patchtokens']   # [1, 256, 1024]

# patch_tokens[0] is a SET of 256 local descriptors, D=1024.
# Aggregating this set into ONE global vector is the whole problem.
print(patch_tokens.shape)   # torch.Size([1, 256, 1024])
The misconception to avoid: "we fine-tune DINOv2 for VPR." AnyLoc does not — the backbone is frozen and never sees a place label. SALAD and MegaLoc fine-tune only lightly and mostly train the small aggregation head on top. If you unfreeze the whole backbone and train it on city driving, you reintroduce the exact overfitting that wrecked NetVLAD's generalization. The frozen-ness is the robustness.
What does DINOv2 produce for a single 224×224 image, and why is it useful for VPR?

Chapter 3: AnyLoc — VLAD on Frozen Features

AnyLoc's recipe is almost provocatively simple: take the frozen DINOv2 local descriptors and aggregate them with VLAD (Vector of Locally Aggregated Descriptors, Jégou et al., 2010) — a classical, unsupervised aggregation from the pre-deep-learning era. No triplet loss, no place labels, no training. And it generalizes everywhere NetVLAD does not.

VLAD, derived from first principles

VLAD answers: how do I turn a variable-size set of N local descriptors into one fixed-size global vector that preserves their distribution? The idea: build a small vocabulary of K cluster centers (a "visual vocabulary," learned once by k-means on a pile of descriptors), then for each cluster, record the sum of residuals — how the descriptors assigned to that cluster deviate from its center.

Let xi ∈ ℝD be local descriptor i (i = 1..N), and ck ∈ ℝD be cluster center k (k = 1..K). Hard-assign each descriptor to its nearest center; let aik = 1 if xi's nearest center is ck, else 0. Then the VLAD descriptor stacks K residual sums:

Vk = ∑i=1N aik (xi − ck)  ∈ ℝD,    V = [V1; V2; ...; VK] ∈ ℝK·D

The full VLAD vector has dimension K·D. AnyLoc uses K = 32 clusters with D = 1536 (its chosen DINOv2 facet), giving a 32·... — actually a large vector that is then intra-normalized (each Vk block L2-normalized separately) and L2-normalized overall. Intra-normalization is the crucial trick: it stops one populous, bursty cluster (e.g. a huge patch of sky) from dominating the descriptor.

(1) residual sum: Vk = ∑i aik(xi−ck)    (2) intra-norm: Vk ← Vk/‖Vk‖    (3) global L2: V ← V/‖V‖

A fully worked VLAD (tiny example)

Take D = 2, K = 2, N = 3 descriptors. Centers c1 = [0, 0], c2 = [4, 4]. Descriptors x1 = [0.2, 0.1], x2 = [-0.1, 0.3], x3 = [4.1, 3.9].

Assign: x1, x2 are nearest to c1; x3 is nearest to c2. So a11=a21=1, a32=1, all else 0.

Residual sums:
V1 = (x1−c1) + (x2−c1) = [0.2, 0.1] + [-0.1, 0.3] = [0.1, 0.4].
V2 = (x3−c2) = [4.1−4, 3.9−4] = [0.1, -0.1].

Intra-normalize: ‖V1‖ = √(0.01+0.16) = 0.412, so V1 → [0.243, 0.970]. ‖V2‖ = √0.02 = 0.141, so V2 → [0.707, -0.707].
Stack & global L2: V = [0.243, 0.970, 0.707, -0.707], ‖V‖ = √(0.059+0.941+0.5+0.5) = √2 = 1.414, final V = [0.172, 0.686, 0.500, -0.500].

Notice what intra-normalization bought us: cluster 2 had only one descriptor and a tiny residual, yet after intra-norm it contributes as strongly as cluster 1's two-descriptor residual. Each region of feature space gets an equal vote, regardless of how many patches landed there. That is exactly why sky and pavement don't drown out the distinctive facade.

VLAD: residuals to centers

Local descriptors (dots) assigned to the nearest of K cluster centers (crosses). Each colored arrow is a residual x−c. The VLAD block for a cluster is the SUM of its residual arrows. Drag to move the descriptors and watch the per-cluster residual sums (bars, right) update.

Scatter spread 0.40

Universal vs map-specific vocabulary

AnyLoc made one more domain-spanning observation. The k-means vocabulary can be built once on a diverse mix of images (a "universal" vocabulary, the AnyLoc-VLAD-DINOv2 config) and reused everywhere; or it can be built per-domain ("map-specific") for a small extra gain. The universal vocabulary is what makes AnyLoc truly training-free and plug-and-play — drop it on aerial imagery it has never seen, and it works.

AnyLoc also showed that the DINOv2 feature space, after this aggregation, exposes interpretable domain clusters: aerial, urban, indoor, and sub-surface images occupy distinct regions, which is why a single vocabulary can serve them all without collapsing them together.

The trap: "VLAD just averages the patch features." It does not. Averaging throws away the distribution — two very different images can share a mean. VLAD records, per cluster, the direction and magnitude of deviation from the center, and keeps K of those. It is a first-order statistic of the descriptor distribution, not a zeroth-order one. That extra structure is why a 32×D VLAD vector discriminates places that a single D-dim mean cannot.
python
# AnyLoc = frozen DINOv2 locals + classical hard-assignment VLAD
import numpy as np

def vlad(X, C):                  # X: [N, D] locals, C: [K, D] centers
    N, D = X.shape; K = C.shape[0]
    # hard assignment: nearest center per descriptor
    d2 = ((X[:, None, :] - C[None]) ** 2).sum(-1)   # [N, K]
    a = d2.argmin(1)                                 # [N]
    V = np.zeros((K, D))
    for i in range(N):
        V[a[i]] += X[i] - C[a[i]]                  # residual sum
    V = V / (np.linalg.norm(V, axis=1, keepdims=True) + 1e-9)  # intra-norm
    V = V.flatten()
    return V / (np.linalg.norm(V) + 1e-9)            # global L2 -> [K*D]
What is the purpose of VLAD's intra-normalization step?

Chapter 4: SALAD — Optimal-Transport Assignment

AnyLoc proved frozen features are enough. SALAD (Izquierdo & Civera, CVPR 2024) — Sinkhorn Algorithm for Locally Aggregated Descriptors — asks: the aggregation is still a 14-year-old hard k-means assignment; what if we make the assignment itself soft, learned, and globally balanced? Their answer reframes "which descriptor goes to which cluster" as a problem of optimal transport, and it sets a new state of the art with a tiny trainable head on top of (mostly frozen) DINOv2.

The reframing: assignment is a transport plan

VLAD's hard assignment aik ∈ {0, 1} is a brutal commitment: descriptor i goes entirely to its single nearest cluster. A descriptor that sits between two clusters is forced to pick one, discarding information. Soft assignment (as in NetVLAD) lets aik ∈ [0, 1] spread across clusters — but row-wise softmax has no notion of balance: a single overwhelming cluster can soak up every descriptor.

SALAD instead treats the N×K assignment matrix as a transport plan P, where Pik is "how much mass of descriptor i is sent to cluster k." It is the soft assignment with two-sided constraints:

P ∈ ℝ+N×K,    ∑k Pik = ri (each descriptor is fully distributed),    ∑i Pik = ck (each cluster gets a target mass)

The row constraint says every descriptor's mass is conserved (it must go somewhere). The column constraint is the new idea: each cluster has a capacity, so no single cluster can monopolize the descriptors. This two-sided balance is exactly what hard VLAD and plain softmax lack, and it is what optimal transport gives you.

The score matrix and the dustbin

A small learned head turns each local descriptor into a score against each cluster, producing the cost/score matrix S ∈ ℝN×K (high score = good match). SALAD adds one more column — a "dustbin" — a (K+1)-th destination that absorbs uninformative descriptors (dynamic objects, sky, motion blur) so they don't pollute the aggregation. This is the same trick SuperGlue uses for unmatched keypoints, borrowed into VPR.

S ∈ ℝN×(K+1)   (last column = dustbin)  →Sinkhorn  P (a balanced soft assignment)

Solving for a balanced P given S is the optimal-transport problem. There is no closed form, but an entropy-regularized version has a beautifully simple iterative solver — Sinkhorn — which we derive in the next chapter. For now: SALAD feeds the patch tokens through a tiny MLP to get S, runs a handful of Sinkhorn iterations to get a balanced assignment P, then aggregates exactly like VLAD (residuals weighted by P instead of hard a), discards the dustbin, and L2-normalizes. The aggregation is differentiable end-to-end, so the head learns assignments that maximize retrieval — but the heavy DINOv2 backbone stays (mostly) frozen.

Frozen DINOv2 locals
F ∈ ℝN×D patch tokens — no place training
↓ tiny learned head
Scores + dustbin
S ∈ ℝN×(K+1) — how well each patch fits each cluster, plus a trash column
↓ Sinkhorn (a few iterations)
Balanced plan P
Rows = each descriptor fully assigned; columns = each cluster capacity-balanced
↓ weighted residual aggregation, drop dustbin, L2-norm
Global descriptor
One compact vector — searched by cosine

Why two-sided balance helps retrieval

Consider an image where 60% of patches are sky. Hard VLAD or softmax dumps them all into the "sky" cluster, whose huge residual block (even after intra-norm) reflects almost nothing place-specific. SALAD's column constraint caps the sky cluster's intake and forces the model to spread informative patches across clusters, so the descriptor's capacity is spent on the parts of the scene that actually distinguish this place. Empirically this — plus the dustbin and the learned scores — is what lifts Recall@1 past AnyLoc and the older trained methods.

Hard vs balanced (OT) assignment

Same descriptors, two assignment policies. Left: hard nearest-center (VLAD) — the over-populated cluster swallows everything. Right: optimal-transport assignment with column capacities — mass is balanced across clusters. The bars show per-cluster total mass; drag to over-populate one cluster and watch only the OT side stay balanced.

Over-populate cluster 1 0.50
Concept → realization. SALAD trades a global descriptor that is large and untrained (AnyLoc's VLAD is thousands of dims) for one that is compact and lightly trained. By assigning to a modest number of clusters with a small feature dimension per cluster, SALAD produces descriptors on the order of a few thousand dims while beating much larger ones — because the learned, balanced assignment packs more place-discriminative signal into each dimension. Smaller and smarter beats bigger and dumber.
What does SALAD's optimal-transport (Sinkhorn) assignment add over VLAD's hard assignment and plain softmax?

Chapter 5: Sinkhorn, Derived

Now the core mechanism. We want the balanced assignment P that best matches the learned scores S, subject to fixed row marginals r and column marginals c. This is entropy-regularized optimal transport, and Sinkhorn's algorithm solves it with nothing but row and column normalizations.

The optimization problem

Optimal transport finds the plan P that maximizes total matched score (equivalently minimizes a cost) while respecting both marginals. The raw OT problem is a linear program — expensive and non-differentiable at the vertices. Cuturi (2013) added an entropy regularizer H(P) = −∑ik Pik(log Pik − 1), which makes the objective strictly convex, the solution smooth (great for backprop), and — the magic — admits a closed-form structure:

maxP   ∑ik Pik Sik  +  ε H(P)    s.t.   P𝟙 = r,   PT𝟙 = c

Here ε > 0 is the regularization strength. The Lagrangian (one multiplier per row constraint, ui, and one per column, vk) has gradient zero exactly when:

Pik = exp(ui) · exp(Sik/ε) · exp(vk) = ai · Gik · bk,    where Gik = exp(Sik/ε)

Read that carefully: the optimal P is the kernel matrix G (the exponentiated, temperature-scaled scores) scaled by a positive vector a on the rows and a positive vector b on the columns. We only need to find the scaling vectors a and b that make the marginals come out right. That is what Sinkhorn iterations do.

The algorithm: alternate row and column normalization

Start with the kernel G = exp(S/ε). Then repeatedly: rescale rows so each row sums to its target ri; rescale columns so each column sums to its target ck. Each step fixes one marginal and breaks the other slightly; iterating drives both to convergence (guaranteed for positive G).

init: G = exp(S / ε)     repeat:   ai ← ri / ∑k Gik bk  (row scale),    bk ← ck / ∑i ai Gik  (col scale)

and read off Pik = ai Gik bk. In practice this is done in log-space for numerical stability — instead of multiplying by a and b you add log-scalings, and the row/column normalizations become log-sum-exp subtractions. SuperGlue and SALAD both run the log-space variant for just a handful of iterations.

A fully worked Sinkhorn (2×2, by hand)

Let the kernel be G = [[1, 0], [0, 1]]-ish... use a concrete non-trivial case. Scores give kernel G = [[4, 1], [1, 2]] (already exponentiated for simplicity). Targets: uniform rows r = [1, 1], columns c = [1, 1] (we want a doubly-balanced plan; here we normalize to total mass 2).

Iteration 1 — row normalize (each row should sum to 1; here ri=1 with 2 rows, total 2, but we'll target row-sum 1):
Row 0 sum = 4+1 = 5 → scale 1/5: [0.8, 0.2]. Row 1 sum = 1+2 = 3 → scale 1/3: [0.333, 0.667].
P = [[0.8, 0.2], [0.333, 0.667]].

Column normalize (each column should sum to 1):
Col 0 sum = 0.8+0.333 = 1.133 → scale 1/1.133: [0.706, 0.294]↓. Col 1 sum = 0.2+0.667 = 0.867 → scale 1/0.867: [0.231, 0.769]↓.
P = [[0.706, 0.231], [0.294, 0.769]].

Iteration 2 — row normalize again:
Row 0 sum = 0.937 → [0.753, 0.247]. Row 1 sum = 1.063 → [0.277, 0.723].
Column normalize: col 0 = 1.030 → [0.731, 0.269]; col 1 = 0.970 → [0.255, 0.745]. P = [[0.731, 0.255], [0.269, 0.745]].

After just two passes the plan is close to doubly-balanced (rows ≈ 0.986/1.014, columns within 1%) and it keeps the structure of G — entry (0,0) was the largest score and stays the largest mass (0.731), while the plan now respects both marginals. A few more iterations and the row/column sums are 1 to machine precision. That alternating normalize-rows / normalize-columns is the entire algorithm.

Sinkhorn iterations converging to a balanced plan

The transport plan P (heatmap, brighter = more mass) starts as the raw exponentiated scores and converges to a doubly-stochastic-style balanced plan. Step through iterations and watch the row sums and column sums (side bars) march toward their uniform targets. Drag the temperature to sharpen (ε small) or soften (ε large) the plan.

Iteration 0 — raw kernel
Temperature ε 0.60
python
# Sinkhorn: balanced soft assignment by alternating normalization
import numpy as np

def sinkhorn(S, r, c, eps=0.6, iters=20):
    G = np.exp(S / eps)          # kernel: exponentiated scaled scores
    b = np.ones(S.shape[1])
    for _ in range(iters):
        a = r / (G @ b + 1e-9)    # row scaling: hit row marginals r
        b = c / (G.T @ a + 1e-9)  # col scaling: hit col marginals c
    return a[:, None] * G * b[None, :]   # P = diag(a) G diag(b)

# SALAD: S from a tiny head on frozen DINOv2 locals, last col = dustbin
P = sinkhorn(scores, r=row_targets, c=col_targets)
P = P[:, :-1]                        # drop the dustbin column
# aggregate residuals weighted by P, then L2-normalize -> global descriptor
The trap: "softmax over clusters already gives a soft assignment, so why bother with Sinkhorn?" Row-softmax balances rows only — each patch sums to 1 — but nothing constrains the columns, so one dominant cluster can absorb most of the mass. Sinkhorn enforces both marginals at once. That column constraint is the entire point; without it you are back to an unbalanced aggregation where sky and road drown out the facade.
What is the single operation that the Sinkhorn algorithm repeats to solve entropy-regularized optimal transport?

Chapter 6: Results & Ablations

What did these design choices actually buy? The benchmarks that matter for VPR are MSLS (Mapillary Street-Level Sequences, including the hard MSLS-Challenge split), Pitts250k/Pitts30k (Pittsburgh street view), Nordland (extreme seasonal change, summer↔winter on a railway), Tokyo 24/7 (day/night), SPED, and AmsterTime. They probe different failure modes: viewpoint, season, illumination.

AnyLoc: generalization without training

AnyLoc's headline is qualitative and decisive. On structured, urban benchmarks that NetVLAD was trained for, AnyLoc is competitive. On out-of-distribution domains — aerial, indoor, underwater, night driving — where trained baselines collapse, AnyLoc roughly doubles the recall of prior methods. The paper reports up to a ~4× improvement in the very hardest unstructured cases. The lesson: trained-from-scratch VPR descriptors are sharp on their home turf and blind off it; frozen-foundation descriptors are broadly capable everywhere.

SALAD: state of the art with a light head

SALAD takes the standard MSLS / Pitts / Nordland / Tokyo24-7 benchmarks and sets a new state of the art, surpassing both AnyLoc and the strong trained baselines (e.g. MixVPR, EigenPlaces) — with a compact descriptor and a training recipe so light it converges in well under an hour on a single GPU, because only the small aggregation head (and a light backbone touch) is trained. The qualitative ablation findings the paper emphasizes:

AblationEffect on Recall@1Interpretation
Hard VLAD → Sinkhorn OT assignmentClear gainTwo-sided balance packs more place signal into the descriptor
Remove the dustbin columnDropUninformative patches (sky, dynamics) pollute the aggregation when they can't be discarded
Unfreeze whole DINOv2 vs light tuningWorse generalizationHeavy tuning re-introduces domain overfitting
Fewer Sinkhorn iterationsGracefulA handful of iterations already gives a usefully balanced plan
Verified pattern across all three papers: the gains come from how you aggregate the frozen features, not from training a bigger backbone on more places. AnyLoc fixes the aggregator (classical VLAD) and wins on breadth; SALAD learns a balanced aggregator (OT) and wins on the standard benchmarks; MegaLoc keeps the aggregator strong and wins by training it on the right mixture of data. The backbone is the constant; the aggregation and the data are the variables.

A worked Recall@1 comparison

Imagine 1000 Nordland queries (summer query, winter database — the same track six months apart). A NetVLAD-style model trained on urban summer driving might get the exact winter frame at rank 1 for ~150 of them (Recall@1 ≈ 15%): winter strips the trees, dumps snow, and the trained texture features fall apart. A frozen-DINOv2 method holds structural cues (track curvature, signal masts, tunnel mouths) that survive the season, lifting many more queries to a correct rank-1. The delta is largest exactly where the appearance gap is largest — which is the whole reason foundation features matter.

Recall@K curves: trained baseline vs frozen-foundation

Schematic Recall@K curves. The trained baseline (gray) is decent on its home domain but the frozen-foundation method (teal) dominates as the domain gap grows. Drag the slider from "in-domain" to "out-of-domain" and watch the gap between the curves open up — that gap is AnyLoc's headline result.

Domain gap in-domain

Why the compact descriptor still wins

It is counterintuitive that SALAD's smaller descriptor beats AnyLoc's much larger VLAD vector. The resolution: descriptor dimensions are not all equally informative. AnyLoc's hard assignment and untrained centers waste capacity on redundant or sky-dominated blocks; SALAD's learned, balanced assignment makes every dimension earn its keep. This is a recurring theme in retrieval — a well-trained 4k-dim descriptor routinely beats a poorly-structured 32k-dim one. Quality of the assignment, not raw dimensionality, sets the ceiling.

Where is the improvement from frozen-foundation VPR largest, and why?

Chapter 7: MegaLoc — One Retrieval Model

AnyLoc and SALAD focus on VPR. But "retrieve a similar image of a place" is one of three closely related retrieval tasks the robotics/vision stack actually needs:

Visual Place Recognition
Coarse "where am I?" — retrieve geographically near images (25 m tolerance)
 
Landmark retrieval
Find other photos of the same instance (this exact building/statue), tight, fine-grained
 
Visual localization (retrieval step)
Retrieve database images close enough to enable accurate 6-DoF pose estimation via local feature matching

Historically each task had its own specialist model trained on its own dataset. MegaLoc (Berton & Masone, 2025) asks: can one model be strong at all three? Its recipe is, again, frozen-foundation features plus a strong aggregation head — but the key contribution is the training mixture: a megamix of multiple datasets (street-view VPR data, landmark datasets, and localization-style data) trained with a multi-similarity / contrastive objective and careful mining, so the single descriptor is simultaneously good at coarse place recall, instance-level landmark matching, and the retrieval stage of a localization pipeline.

Why one model can serve all three

The tasks differ in their tolerance (25 m for VPR vs same-instance for landmarks) and their downstream consumer (a human-readable match vs a geometric pose solver), but they share the core need: a descriptor where "same physical place/object" lands close and "different" lands far. Training on a mixture forces the descriptor to satisfy all three notions of "same" at once. The result is a model that, at the time, matched or beat the per-task specialists on their own benchmarks — a single retrieval front-end for an entire localization stack.

Concept → realization. Practically, MegaLoc means a robotics pipeline ships one image-retrieval model instead of three. The same global descriptor that closes a SLAM loop (VPR) also retrieves the database frames a structure-from-motion localizer needs (visual localization) and powers instance search (landmarks). One index, one model, one set of descriptors to maintain — a real systems win on top of the accuracy win.
One descriptor, three tasks

A shared descriptor space serving three retrieval tasks. Click a task to see which neighbours count as "correct" — VPR accepts geographically near frames (wide ring), landmark retrieval accepts only same-instance photos (tight cluster), localization accepts frames close enough for pose. The same embedding satisfies all three.

VPR: geographically-near frames count
AspectAnyLocSALADMegaLoc
BackboneDINOv2, frozenDINOv2, mostly frozenDINOv2, mostly frozen
AggregationClassical VLAD (hard)Sinkhorn OT + dustbinStrong learned head
TrainingNone (training-free)Light, one headMulti-dataset megamix
TargetAny domain, plug-and-playVPR SOTA, compactVPR + landmark + localization
TaglineFrozen features are enoughBalance the assignmentOne model for all retrieval
The misconception: "MegaLoc is just SALAD trained on more data." The contribution is not raw data volume — it is the deliberate mixture of datasets spanning three distinct retrieval notions, with the mining and loss tuned so the single descriptor doesn't collapse to one task. Naïvely concatenating datasets and training would let the largest/easiest task dominate. Multi-task retrieval is a curation-and-objective problem, not a "throw more data in" problem.
What is MegaLoc's central contribution beyond AnyLoc/SALAD?

Chapter 8: Assignment Explorer

This is the payoff. The single mechanism that ties the whole arc together is how patches get assigned to clusters before aggregation — hard (AnyLoc/VLAD) vs balanced optimal transport (SALAD/Sinkhorn). Below, drive both policies on the same scene and watch the resulting global descriptor change.

Hard VLAD vs Sinkhorn OT — live assignment

Left: patches (dots) colored by their assigned cluster. Middle: the assignment matrix (rows = patches, cols = clusters, brighter = more mass). Right: the per-cluster aggregated mass that feeds the descriptor. Toggle the policy, raise the Sinkhorn temperature to soften, and add "sky" patches to see how only the OT policy keeps the descriptor balanced.

Hard nearest-center assignment
Sky patches (bursty cluster) 2
Sinkhorn temperature ε 0.50
What to explore:
1. Start on Hard VLAD and crank "sky patches" to 12 — the sky cluster's bar balloons and swallows the descriptor's capacity.
2. Switch to Sinkhorn OT with the same sky count — the column constraint caps the sky cluster and the bars stay balanced; the descriptor keeps room for the distinctive clusters.
3. On OT, drop temperature ε toward 0.1 — the soft plan sharpens toward a hard permutation (recovering hard-assignment behaviour).
4. Raise ε toward 2 — the plan blurs toward uniform; every patch spreads evenly, washing out discrimination.
5. The sweet spot is a moderate ε: balanced and discriminative. That is the regime SALAD operates in.

From assignment to descriptor, end to end

Once you have the assignment P (hard or balanced), the rest is mechanical and identical to VLAD: for each cluster k, sum the residuals of its assigned patches weighted by Pik, intra-normalize each block, concatenate, L2-normalize. The descriptor you get out is the thing that gets stored in the database and compared by cosine at query time. The only thing the three papers really change is the assignment policy and where the cluster definitions / scores come from.

g = L2norm( concatk   intranorm( ∑i Pik (xi − ck) ) )

Set P to a one-hot hard assignment and c to k-means centers and you have AnyLoc. Set P to a Sinkhorn-balanced soft plan with a learned head and a dustbin and you have SALAD. Train the head on a megamix and you have MegaLoc. One equation, three papers.

Chapter 9: Limits & Connections

Foundation-model VPR is a clear win, but it is not magic. Knowing where it breaks is what separates using it from understanding it.

Limitations

LimitationWhy it bitesMitigation
Perceptual aliasingDistinct places that genuinely look identical (repeated facades, generic hallways) collapse to similar descriptors — no descriptor can fix true ambiguitySequence-based VPR; geometric re-ranking on a short list
Global descriptor is coarseOne vector per image cannot do precise 6-DoF pose; it only retrieves candidatesTwo-stage: retrieve (this) → local feature matching for pose
Backbone costA ViT-L DINOv2 forward pass per image is heavy for tiny robotsDistilled/smaller backbones; precompute the database offline
Extreme viewpoint flipOpposite-direction traversal can still defeat appearance-only descriptorsMulti-view databases; panoramic capture
Dynamic / non-rigid scenesCrowds, traffic, foliage motion add distractor patchesSALAD's dustbin; semantic masking

The two-stage localization pipeline

In practice, foundation-VPR is the first stage of a hierarchical localizer. Stage 1 (retrieval, this lesson) narrows a 100k-image database to a top-K shortlist in milliseconds. Stage 2 (local matching, e.g. SuperPoint+SuperGlue or LightGlue, then PnP+RANSAC) does the heavy geometric work on just those K candidates to recover precise pose. The global descriptor doesn't need to be perfect — it needs to get the right place into the top-K, where the geometric verifier finishes the job. That is why Recall@5/@10 is the operationally relevant number.

Hierarchical localization: retrieve then verify

The full pipeline. The global descriptor (foundation VPR) narrows the database to a shortlist; local feature matching + RANSAC verifies and solves pose on just those candidates. Click a stage to highlight its cost and role.

Stage 1: global retrieval (this lesson)

Connections

The frozen self-supervised backbone every method here runs on — read it to understand why the patch tokens are condition-robust
The self-distillation idea DINOv2 scales up; AnyLoc found DINOv2 > DINO for VPR
The patches-as-tokens architecture that produces the N×D local-descriptor grid
The classical aggregation lineage (BoW/VLAD) that AnyLoc revives on modern features
How the N×d descriptor matrix is indexed and searched at query time (FAISS, ANN)
Cosine vs Euclidean and why L2-normalized descriptors make them equivalent
The pretext-task family that makes labels unnecessary — the engine behind frozen features

Cheat sheet

ConceptOne-line takeaway
VPRImage retrieval: query → geographically-near database images, scored by Recall@K
Domain gapSame place, different condition/viewpoint — the thing that broke trained descriptors
Frozen foundation featuresDINOv2 patch tokens: dense, condition-robust local descriptors with zero place supervision
VLADAggregate locals by summing residuals to k-means centers; intra-norm gives each cluster equal vote
AnyLocFrozen DINOv2 + VLAD, training-free, generalizes to every domain
Optimal transportBalanced soft assignment with two-sided marginal constraints (rows AND columns)
SinkhornSolve entropy-regularized OT by alternately normalizing rows then columns of exp(S/ε)
DustbinAn extra "trash" cluster that absorbs uninformative patches (sky, dynamics)
SALADDINOv2 + Sinkhorn OT assignment + dustbin: compact descriptor, SOTA VPR, light training
MegaLocOne frozen-foundation retrieval model for VPR + landmarks + localization, via a dataset megamix
Two-stage localizationRetrieve a shortlist (global descriptor) → verify pose (local matching) — Recall@K is what stage 1 owes stage 2
The one idea to keep: the hard part of place recognition is the feature, and a frozen foundation model already solves it. Everything since 2023 — AnyLoc's revival of VLAD, SALAD's optimal-transport assignment, MegaLoc's multi-task megamix — is about how you aggregate those frozen local features into one searchable vector. Master the assignment step (hard residuals vs Sinkhorn-balanced transport) and you understand the entire arc.