Berton et al. (CVPR 2022, ICCV 2023) · Ali-bey et al. (WACV 2023) · Visual Place Recognition

Classification & Aggregation VPR

Three papers that rebuilt visual place recognition: CosPlace turns geo-localization into classification with a large-margin loss; EigenPlaces makes that training viewpoint-robust; MixVPR replaces the aggregation head with a stack of feature-mixing MLPs. Together they set the modern standard for compact, accurate place descriptors.

Prerequisites: Dot products & cosine similarity + softmax + basic CNN feature maps. That's it.
10
Chapters
10+
Simulations
2
Code Labs

Chapter 0: The City-Scale Localization Problem

You have a single photo taken somewhere in San Francisco — a street corner, a row of houses, a tree-lined block. You also have a database of tens of millions of geo-tagged street-view images covering the whole city. Your job: find which database images depict the same place as the query, so you can read off its GPS coordinate. This is Visual Place Recognition (VPR), the visual front-end of every "where am I?" system — autonomous driving, AR navigation, loop closure in SLAM, and image-based geo-localization.

The obvious approach is image retrieval: turn every image into a fixed-length vector (a descriptor), then for a query, find the database vectors closest to it. If two photos show the same place, their descriptors should be near each other; if they show different places, far apart. The whole game is learning a function that maps a pixel grid to a descriptor with exactly that property.

So what was broken before 2022? The dominant recipe was NetVLAD (Arandjelovic et al., 2016) trained with a triplet loss: pull a query toward a matching "positive" image and push it away from a non-matching "negative." That recipe has two structural problems that get worse as the map grows.

Why triplet mining doesn't scale

Left: triplet training must, for each anchor, search the whole database for a "hard negative" that looks similar but is a different place. Right: classification training assigns each image a class label up front — no per-step search. Press "Add images" and watch the mining cost (left, quadratic-ish) explode while classification cost (right) stays flat.

Database: 1k images

First, mining. A triplet is only informative if the negative is hard — a different place that nevertheless looks similar. Finding hard negatives means, at each step, comparing the anchor against a huge pool of database descriptors. As the database grows from thousands to tens of millions of images, mining dominates the wall-clock cost and you must constantly re-cache descriptors that drift as the network trains.

Second, memory and label noise. To form a positive pair you need to know two images depict the same place; GPS gives you a noisy hint, but two photos taken 10 meters apart facing opposite directions show completely different scenes. Triplet training quietly assumes geographic proximity equals visual similarity, which is often false.

The misconception to kill early: "Bigger map → just mine harder." It is tempting to think the fix for scale is a faster nearest-neighbor index for mining. But the cost isn't only the search — it's that every gradient step needs a fresh comparison against a moving target (the descriptors keep changing as weights update). CosPlace's insight is to remove mining entirely by reframing VPR as classification, where each image carries a static label and the loss never needs to look at other images.

That reframing is the through-line of this Veanor. CosPlace (Berton et al., CVPR 2022) trains VPR as a classification problem with a face-recognition loss. EigenPlaces (Berton et al., ICCV 2023) fixes a viewpoint blind spot in that scheme. MixVPR (Ali-bey et al., WACV 2023) attacks a different bottleneck — the aggregation step that turns a CNN feature map into a single descriptor — replacing it with a stack of feature-mixing MLPs. By the end you will be able to derive the CosFace margin and implement the MixVPR mixer from scratch.

What is the core scaling problem with triplet-loss VPR (NetVLAD-style) that CosPlace was designed to remove?

Chapter 1: VPR as Retrieval

Before we change how we train, let's nail how VPR is used at inference, because that constraint shapes every design decision. At test time there is no classifier and no labels — only nearest-neighbor search in descriptor space.

Formally, an image I is passed through a backbone CNN f (a ResNet, frozen-then-finetuned) to produce a feature map of shape [C, H, W] — C channels over an H×W spatial grid. An aggregation module g collapses that grid into a single vector and an L2 normalization makes it a unit vector:

d = g(f(I)) / ‖g(f(I))‖2     d ∈ ℝD, ‖d‖ = 1

Here D is the descriptor dimension — 512 is the canonical choice (compact enough that 10M descriptors fit comfortably in memory, expressive enough to separate places). Because every descriptor is unit-norm, the dot product between two descriptors equals their cosine similarity:

sim(dq, dm) = dq · dm = cosθqm ∈ [-1, 1]

At inference, the query descriptor is compared against every database descriptor by this dot product, and the database images are ranked by similarity. The top-ranked image's GPS is the predicted location. Performance is reported as Recall@N (R@N): the fraction of queries for which at least one of the top-N retrieved images is within a threshold distance (commonly 25 m) of the true location.

Let's do a tiny worked example. Suppose D = 3 and we have unit descriptors:

ImageDescriptord · dqRank
Query q[0.80, 0.60, 0.00]
m1 (same place)[0.77, 0.64, 0.00]0.80·0.77 + 0.60·0.64 = 0.9991
m2 (nearby, diff view)[0.00, 0.80, 0.60]0.80·0 + 0.60·0.80 = 0.4802
m3 (other place)[-0.60, 0.00, 0.80]0.80·(-0.6) = -0.4803

The match m1 wins because its descriptor points in almost the same direction as the query. Notice m2 is geographically nearby but faces a different way — its descriptor is only mildly similar. That single fact (same GPS, different appearance) is the wedge EigenPlaces will later drive into.

Why cosine, not Euclidean? Once descriptors are L2-normalized onto the unit sphere, Euclidean distance and cosine similarity are monotonically related: ‖a−b‖2 = 2 − 2(a·b). So ranking by smallest Euclidean distance is identical to ranking by largest cosine. Working on the sphere means the loss only has to shape angles, never magnitudes — which is exactly what the CosFace margin operates on.

Descriptor space: ranking by cosine

A query (warm) and three database descriptors on a 2D unit circle. Drag the query angle; the database images re-rank live by cosine similarity (dot product). The thicker the connecting arc, the higher the similarity. Watch how a small angle change flips the top match.

Query angle 35°
Why do VPR systems L2-normalize descriptors before comparing them?

Chapter 2: GeoClassification — VPR Without Mining

CosPlace's central move: stop training on pairs/triplets and instead train on a classification proxy. The label space of VPR is continuous — every image has a (latitude, longitude, heading). CosPlace discretizes that continuous space into a finite set of classes, then trains the backbone to classify which class an image belongs to. The classifier head is thrown away at the end; what we keep is the backbone that learned descriptors which separate the classes.

How is a class defined? By a cell in geographic space. The world is divided into a grid of square cells of side length M meters (e.g. M = 10 m). The heading (compass) is divided into bins of α degrees (e.g. α = 30°). A class is one (cell, heading-bin) pair — all images taken roughly from the same spot, facing roughly the same direction.

class(I) = ( ⌊ eastI / M ⌋, ⌊ northI / M ⌋, ⌊ headingI / α ⌋ )

The heading bin matters a great deal. Two photos taken at the same GPS but facing opposite directions share no visual content — making them the same class would force the network to map two unrelated scenes to one label, which is impossible and harms training. Splitting by heading keeps each class visually coherent: same place, same direction, genuinely similar pictures.

Discretizing the continuous label space

A city map (top-down). Cells of side M tile the ground; each cell is split into heading wedges of α. Click a cell to highlight its classes — every (cell, heading) wedge is one classification target. Drag the sliders to change M and α and watch the class count change.

Cell M (m) 10
Heading α (°) 90

Worked example. Suppose an image is at east = 327 m, north = 154 m (relative to a city origin), heading = 71°, with M = 10 m and α = 30°:

Now training is a plain classification problem with cross-entropy: feed an image, predict its class, backprop. No mining, no triplets, no per-step database search. Each image's label is fixed for the whole run — computed once from its metadata. This is the property that lets CosPlace train on San Francisco eXtra Large (SF-XL), a dataset of tens of millions of images covering the entire city.

The trap with naive classification: a plain softmax classifier learns to separate classes, but it does nothing to make classes compact or to leave margin between them. At test time we never use the classifier — we use cosine nearest-neighbor on the descriptors. A descriptor that is barely on the correct side of a decision boundary classifies correctly during training yet retrieves terribly, because a slightly different photo of the same place lands on the wrong side. We need a loss that doesn't just classify, but pushes each class into a tight angular cluster with a buffer around it. That loss is CosFace, the subject of the next chapter.

Why does CosPlace split each geographic cell further by heading (compass) angle?

Chapter 3: The CosFace Large-Margin Loss

This is the mathematical heart of CosPlace, borrowed from face recognition (Wang et al., CosFace / Large Margin Cosine Loss, 2018). We derive it from ordinary softmax in four steps, because each step removes a specific weakness.

Step 0: ordinary softmax cross-entropy

A linear classifier has a weight vector Wj per class j and a bias bj. The logit for class j on a descriptor x (the backbone output before normalization) is zj = WjTx + bj. The softmax loss for an example with true class y is:

L = −log ( ezy / ∑j ezj )

Step 1: drop the bias, factor the logit into magnitude and angle

Set bj = 0. A dot product equals the product of norms times the cosine of the angle between the vectors:

zj = WjTx = ‖Wj‖ ⋅ ‖x‖ ⋅ cosθj

where θj is the angle between the descriptor x and the class direction Wj. The problem: the loss can be reduced by inflating the norms ‖Wj‖ or ‖x‖ instead of improving the angle — but at test time, only the angle (cosine) is used. The training objective and the test metric disagree.

Step 2: normalize away the magnitudes

Force ‖Wj‖ = 1 (normalize each class weight) and ‖x‖ = s (rescale every descriptor to a fixed radius s). Now the logit is purely the cosine, scaled by s:

zj = s ⋅ cosθj

The constant s (the scale, e.g. s = 30) is needed because cosines live in [−1, 1]; without scaling, the largest possible logit gap is 2, and softmax can never become confident. Multiplying by s widens the usable range of logits so the softmax can sharpen.

Step 3: subtract a margin on the true class

Here is the CosFace idea. We make the network's job harder for the correct class by subtracting a fixed margin m from its cosine before the softmax:

L = −log ( es(cosθy − m) / [ es(cosθy − m) + ∑j≠y es⋅cosθj ] )

To classify the example correctly, the network can no longer settle for cosθy just barely exceeding the other cosines. It must satisfy:

cosθy − m > cosθj   for all j ≠ y  ⟹  cosθy > cosθj + m

The correct-class cosine must beat every other cosine by at least m. This carves an angular decision margin between classes. The descriptors of one place are pulled into a tight cone and a buffer of width set by m is left empty around it — exactly the compactness that retrieval needs.

Why subtract on the cosine (CosFace) rather than on the angle (ArcFace)? CosFace does cosθ − m; ArcFace (Deng 2019) does cos(θ + m). CosFace's additive cosine margin has a constant gradient with respect to cosθ and is numerically gentle — there is no inverse-cosine to compute and no domain issues near θ = 0 or θ = π. For city-scale VPR with hundreds of thousands of classes, that stability and speed is why CosPlace chose the cosine margin.

Worked numerical example. Take s = 30, m = 0.35, and a 3-class problem. Suppose for some descriptor the cosines to the three class directions are cosθ = [0.70 (true class y=0), 0.55, 0.20].

So the margin keeps the gradient alive long after a plain softmax would have given up, relentlessly compacting each class. Let's see the geometry.

The angular margin in action

Two class directions (warm, teal) on the unit circle, with their decision boundary. Increase the margin m: the single boundary splits into two, opening a forbidden gap of angular width set by m. Descriptors of each class are forced strictly into their own cone. Drag s to see how scale sharpens the softmax (the green probability bar).

Margin m 0.35
Scale s 30
python
import torch
import torch.nn.functional as F

def cosface_loss(x, W, labels, s=30.0, m=0.35):
    """
    x:      [B, D]   backbone descriptors (pre-norm)
    W:      [D, C]   class direction matrix
    labels: [B]      integer class ids
    """
    x  = F.normalize(x, dim=1)        # put descriptors on unit sphere
    Wn = F.normalize(W, dim=0)        # unit class directions
    cos = x @ Wn                        # [B, C] = cos(theta) to every class

    # subtract margin ONLY from the true-class column
    onehot = F.one_hot(labels, cos.size(1)).float()
    logits = s * (cos - m * onehot)    # [B, C]
    return F.cross_entropy(logits, labels)
What does subtracting the margin m from the true-class cosine accomplish?

Chapter 4: CosPlace Groups — Training at City Scale

GeoClassification plus CosFace gives a beautiful objective, but there's a practical wall: SF-XL has hundreds of thousands of classes. The CosFace classifier head is a matrix W of shape [D, C]. With D = 512 and C in the hundreds of thousands, that single layer is enormous, and worse, every class direction is updated on every batch through the softmax denominator, which is both slow and memory-hungry.

CosPlace's fix is the CosPlace Group. The key observation: adjacent cells in the grid are visually correlated (a photo in cell (32,15) and one in the neighbor cell (33,15) might overlap). If two classes can appear in the same training batch and they're nearly the same scene, the margin loss fights itself. So CosPlace partitions the grid so that classes within one group are guaranteed far apart, and trains on one group at a time.

Concretely, group membership is defined by the cell indices modulo a spacing N:

group(I) = ( eastcell mod N, northcell mod N, headingbin )

With N = 2, this creates groups in a checkerboard pattern: group (0,0) contains cells (0,0), (0,2), (2,0), (2,2), … — every class in this group is at least N×M meters from every other, so no two classes in a group can be confused by overlap. Each group has far fewer classes than the full set, so its CosFace head is small. Training cycles through groups round-robin: load group's classifier, train some iterations, save it, move to the next group.

CosPlace Groups: the checkerboard partition

The grid of cells, colored by group membership under (east mod N, north mod N). Increase N: cells of the same color spread further apart, guaranteeing classes within a group never overlap. Each group is trained with its own small CosFace head. Click "Cycle group" to watch training rotate through the groups.

Spacing N 2
Active group: 0

Let's count. Say the city covers 1000×1000 cells (M = 10 m → 10×10 km) with 12 heading bins (α = 30°). That's 1000×1000×12 = 12 million classes total. With N = 2, there are N×N = 4 spatial groups (times heading), so each group holds about 12M / 4 = 3 million classes — and at any moment only one group's head (3M, not 12M directions) is in memory and being updated. The backbone, shared across all groups, accumulates everything it learns from each group, while no single CosFace head ever has to be the full size.

Why this is correct, not just convenient: the backbone f is the only part kept at test time, and it is shared across all groups — so it integrates the discriminative signal from every group. The per-group heads are scaffolding: each one teaches the backbone to separate the (well-spaced, non-overlapping) classes in its group. Because groups are non-overlapping by construction, the margin loss within a group is never sabotaged by two near-identical classes, so the gradient it sends to the backbone is clean.

Misconception: "Groups are just mini-batches of classes — any random split would do." No. A random split could put two overlapping cells in the same group, reintroducing exactly the confusion CosFace is trying to remove (two near-identical scenes with different labels, fighting over the margin). The modulo-N spacing is what guarantees intra-group separation. The partition is a correctness device, not just a memory trick.

QuantitySymbolTypical valueRole
Cell sizeM10–15 mSpatial granularity of a class
Heading binα30°Splits a cell by viewing direction
Group spacingN2–5Guarantees intra-group separation
Descriptor dimD512Compact, fits 10M+ in memory
Scale / margins, m30, 0.4CosFace hyperparameters
Why does CosPlace partition classes into groups by cell-index-modulo-N and train one group at a time?

Chapter 5: EigenPlaces — Viewpoint Robustness

CosPlace works beautifully when the query and database images share a viewpoint. But a fundamental VPR failure mode remains: lateral viewpoint shift. A car-mounted camera drives down a street facing forward; later a pedestrian photographs the same building from across the street, facing sideways. Same place, radically different image. CosPlace's heading-bin split actually worsens this — by definition it puts the two views in different classes, so the loss never directly tells the network they depict one place.

EigenPlaces (Berton et al., ICCV 2023) fixes this at the level of how classes are constructed. The idea: instead of one class per (cell, heading), build classes that deliberately span multiple viewpoints of the same physical location, so that during training the margin loss is forced to pull together images of one place taken from genuinely different angles.

The eigen-view: finding the street direction

Where does "eigen" come from? For each cell, EigenPlaces collects the 2D ground positions of all cameras that took images there. Those positions, for a typical street, lie roughly along a line — the street. EigenPlaces runs PCA on the camera positions: the top eigenvector of their covariance is the dominant axis of camera motion (the street direction). That direction defines two canonical viewpoints to look at the place from:

Mechanically: take the cameras in a cell, compute the covariance of their (x, y) positions, eigendecompose it. The eigenvector with the larger eigenvalue is the street axis e1; the smaller is e2, the across-street axis. EigenPlaces then defines a focal point — a target 3D location a fixed distance out along e2 — and groups together all images whose camera is positioned-and-oriented to be looking toward that focal point, regardless of which street segment the camera sits on.

PCA on camera positions → street axis → focal point

Dots are camera positions in one cell. The warm arrow is the top eigenvector e₁ (street direction); the teal arrow is e₂ (across-street). The star is the lateral focal point a fixed distance along e₂. Drag the spread slider to make the cameras more/less collinear and watch the eigenvectors snap to the dominant axis.

Camera spread 0.25

Worked example of the PCA step. Suppose four cameras sit at positions (0,0), (2,0.1), (4,−0.1), (6,0). Their mean is (3, 0). The mean-centered points are (−3,0), (−1,0.1), (1,−0.1), (3,0). The covariance:

C = (1/4) XTX = [[ 5.0, −0.05 ], [ −0.05, 0.005 ]]

The large diagonal entry (5.0) is along x and the tiny one (0.005) along y, so the top eigenvector is essentially e1 ≈ (1, 0) — the street runs east–west, exactly as the points suggest. The across-street axis is e2 ≈ (0, 1). A lateral class then collects every camera (on any nearby street segment) oriented to look in the ±e2 direction at a focal point — a building facade. Forced into one class, the frontal-driving and lateral-photographing views of that facade get pulled together by the margin loss.

The payoff: CosPlace optimizes for "same camera pose"; EigenPlaces optimizes for "same place, possibly different pose." On benchmarks with strong viewpoint variation (e.g. images taken from the sidewalk vs the road), EigenPlaces substantially improves recall over CosPlace while keeping the entire classification-without-mining machinery and the compact 512-D descriptor. It is a change to which images share a label, not to the loss.

Misconception: "EigenPlaces uses a different, fancier loss." It does not. EigenPlaces keeps the same CosFace large-margin loss and the same group-based training. The only thing that changes is the class definition: classes are built from focal-point-oriented viewpoint clusters (via PCA on camera positions) instead of raw heading bins. Better labels, same machine.

What is the role of PCA (the eigenvectors) in EigenPlaces?

Chapter 6: MixVPR — Feature-Mixing Aggregation

CosPlace and EigenPlaces are about training. MixVPR (Ali-bey et al., WACV 2023) is about a different component entirely: the aggregation g that turns the CNN feature map into the descriptor. This is the step that NetVLAD, GeM pooling, and average pooling all try to do. MixVPR replaces it with a stack of MLPs and beats them all while being smaller.

The input: a stack of feature maps

Run an image through a backbone (ResNet-50, truncated) and take an intermediate feature map of shape [C, H, W] — for example [C=1024, H=20, W=20]. MixVPR's first move is to view this not as one 3D tensor but as C feature maps, each a flattened H·W vector of length P = H×W (here 400). Reshape to a matrix:

F ∈ ℝC × P,   P = H·W   (each row = one channel's flattened spatial map)

The Feature-Mixer: an MLP across space

The heart of MixVPR is the Feature-Mixer block, lifted from the MLP-Mixer idea (Tolstikhin 2021). A Feature-Mixer is a tiny two-layer MLP applied across the spatial dimension P, shared across all C channels, wrapped in a residual connection:

FM(F) = F + W2 σ( W1 ⋅ LN(F)T )T

Read it carefully. LN is layer norm. We transpose so the MLP mixes along P (the H·W positions): W1 is [P′, P], σ is ReLU/GELU, W2 is [P, P′]. The MLP takes a length-P vector (one channel's spatial map) and returns a length-P vector — a learned, global recombination of every spatial location. The same MLP weights are applied to all C channels. The residual F + means each block refines rather than replaces.

Why mix across space? A convolution only ever combines a local neighborhood. NetVLAD aggregates by soft-assigning each location to cluster centers. The Feature-Mixer instead lets every spatial position talk to every other through a dense MLP — a global, content-adaptive aggregation. Crucially it operates on the flattened map, so it discards the rigid 2D grid and learns which spatial regions matter for distinguishing places (e.g. building facades over sky/road).

MixVPR stacks L Feature-Mixer blocks (e.g. L = 4). Because each is residual and shares weights across channels, the parameter count stays modest. After the stack, two linear projections compress the [C, P] matrix to the final descriptor:

Z = Wrow ⋅ ( Wdepth ⋅ FmixedT )T  →  flatten → L2-normalize → d

Wdepth projects the channel dimension C → C′ (e.g. 1024 → 1024 or fewer), Wrow projects the spatial dimension P → R (e.g. 400 → 4). The result is a [C′, R] matrix, flattened and normalized into the descriptor of dimension D = C′·R (e.g. 1024×4 = 4096, or projected down to 512). No clustering, no parameters-per-cluster, just MLPs.

Feature-Mixer: global spatial recombination

A single channel's flattened spatial map (left, P values) is fed through a two-layer MLP shared across all channels, then added back via the residual (right). Increase "mixing strength" to see the MLP blend distant spatial locations together — something convolution and pooling cannot do. Each row is one channel; watch all channels transform with the same weights.

Mixing strength 0.50
Blocks L 3
python
import torch
import torch.nn as nn
import torch.nn.functional as F

class FeatureMixer(nn.Module):
    # a 2-layer MLP that mixes ACROSS the P=H*W spatial positions,
    # shared across all channels, with a residual connection
    def __init__(self, P, hidden):
        super().__init__()
        self.ln = nn.LayerNorm(P)
        self.fc1 = nn.Linear(P, hidden)
        self.fc2 = nn.Linear(hidden, P)

    def forward(self, F_):           # F_: [B, C, P]
        h = self.ln(F_)
        h = self.fc2(F.relu(self.fc1(h)))  # mix along P
        return F_ + h                  # residual

class MixVPR(nn.Module):
    def __init__(self, C, P, L=4, hidden=512, c_out=1024, r_out=4):
        super().__init__()
        self.mixers = nn.Sequential(*[FeatureMixer(P, hidden) for _ in range(L)])
        self.depth = nn.Linear(C, c_out)   # project channels  C -> c_out
        self.row   = nn.Linear(P, r_out)   # project positions P -> r_out

    def forward(self, fmap):          # fmap: [B, C, H, W]
        B, C, H, Wd = fmap.shape
        F_ = fmap.flatten(2)              # [B, C, P]  P = H*W
        F_ = self.mixers(F_)              # L Feature-Mixer blocks
        F_ = self.depth(F_.transpose(1,2)).transpose(1,2)  # [B, c_out, P]
        F_ = self.row(F_)                 # [B, c_out, r_out]
        d = F.normalize(F_.flatten(1), dim=1)  # [B, c_out*r_out]
        return d
What makes the MixVPR Feature-Mixer fundamentally different from convolution or average pooling for aggregation?

Chapter 7: Results & Ablations

What did these three papers actually buy? Let's read the headline numbers and the ablations that justify each design choice. (Exact figures depend on backbone and benchmark; the relationships below are the robust, reported findings.)

CosPlace

CosPlace's two claims are scale and compactness. It introduced SF-XL (tens of millions of images, the whole of San Francisco) and showed that classification-without-mining trains efficiently at that scale where triplet mining becomes impractical. Critically, it reached state-of-the-art recall with a compact 512-D descriptor using a small ResNet-18 backbone that fits in under 4 GB of VRAM — and improved further with ResNet-50. Trained on one city, it generalized strongly to entirely different test sets (Pitts250k, Tokyo, etc.), evidence that GeoClassification learns transferable place features, not city-specific shortcuts.

EigenPlaces

EigenPlaces' claim is viewpoint robustness at no extra inference cost. By rebuilding classes around PCA-derived focal points, it improved recall over CosPlace specifically on benchmarks with strong lateral viewpoint variation, while keeping the same compact descriptor, the same CosFace loss, and the same fast classification training. It became a standard strong baseline for the 2023 generation of VPR systems.

MixVPR

MixVPR's claim is better aggregation, fewer parameters. With a ResNet-50 backbone and the Feature-Mixer stack, it reported recall@1 around 94.6% on Pitts250k-test, 88.0% on MapillarySLS, and strong results on the very hard Nordland (seasonal change) benchmark — outperforming NetVLAD and CosPlace while using less than half the parameters of those aggregations. The win comes from replacing cluster-based or pooling aggregation with global feature mixing.

PaperCore changeWhat it buysDescriptor
CosPlace (CVPR'22)VPR as classification + CosFace + GroupsCity-scale training, no mining, SOTA at 512-D512-D, compact
EigenPlaces (ICCV'23)Viewpoint classes via PCA focal pointsRobustness to lateral viewpoint shift512-D, same
MixVPR (WACV'23)Feature-Mixer (MLP) aggregationHigher recall, <half the params of NetVLAD/CosPlacee.g. 4096 or 512-D

The ablations that matter

Recall@N curves: the shape of a good VPR system

Illustrative Recall@N curves (R@1 … R@20) for a weak baseline vs a strong modern system. A good system has high R@1 AND the curve saturates fast (the right answer is almost always in the top few). Toggle which curve is highlighted.

Showing: strong (modern)
Misconception: "MixVPR beats CosPlace, so it replaces it." They are orthogonal contributions. CosPlace/EigenPlaces are training methods (how to build labels and a loss); MixVPR is an aggregation head (how to pool the feature map). You can — and people do — combine a classification-style training scheme with a feature-mixing aggregation head. Comparing them as rivals on a single benchmark hides that they fix different bottlenecks.

The CosPlace margin ablation shows recall is best at a moderate m (around 0.4), not at m = 0 or very large m. What does this tell us?

Chapter 8: VPR Studio — Train & Retrieve Live

This is the payoff. Below is a complete, live VPR pipeline on a toy 2D "city." Database images are 2D descriptors on the unit circle, colored by the place they belong to. You control the CosFace margin m and scale s, run gradient steps to see the classes compact under the margin, then issue a query and watch nearest-neighbor retrieval rank the database. Everything you learned is here at once.

Live CosFace training + cosine retrieval

Left circle: descriptors of 3 places (warm/teal/blue), with the CosFace decision wedges. Press "Train step" repeatedly to apply the margin loss — watch each place's descriptors tighten into a cone with a gap (margin) around it. Then "Query" drops a random query (white) and the right panel ranks the database by cosine similarity; the predicted place is the top bar. Raise the margin and re-train to see retrieval get cleaner.

Margin m 0.35
Scale s 20
Step 0 — scattered classes

Things to try, and what they reveal:

The whole story in one widget: training shapes angles (margin + scale), inference is pure cosine nearest-neighbor, and the descriptor never leaves the unit circle. Swap "tighten the cones" for "do it at city scale with groups," and you have CosPlace. Swap "raw heading classes" for "PCA focal-point classes," and you have EigenPlaces. Swap the toy descriptor for "a Feature-Mixer over a ResNet map," and you have MixVPR.

Chapter 9: Limitations & Connections

These three papers define the modern classification-and-aggregation lineage of VPR, but they are not the end of the story. Honest limitations:

Where this connects

The CosFace margin and the feature-mixing aggregator both reuse ideas you can study elsewhere on Engineermaxxing:

Cheat sheet

ConceptOne-line summary
GeoClassificationDiscretize (GPS, heading) into classes; train VPR as classification — no mining, no triplets.
Class definitionclass = (⌊east/M⌋, ⌊north/M⌋, ⌊heading/α⌋); each class = same spot, same direction.
CosFace logitzj = s·cosθj, with the true class using s·(cosθy − m).
Margin effectForces cosθy > cosθj + m → tight, separated class cones.
Scale sWidens the logit range (cosines are in [−1,1]) so softmax can become confident.
CosPlace Groupgroup = (east mod N, north mod N, heading); guarantees intra-group separation, small head.
EigenPlacesPCA on camera positions → street axis → frontal/lateral focal-point classes → viewpoint robustness.
Feature-MixerFM(F) = F + W2σ(W1·LN(F)T)T; 2-layer MLP mixing across P=H·W, residual, shared over channels.
MixVPR aggregationStack L mixers, then depth + row projections → flatten → L2-norm → descriptor.
Inferenced = normalize(g(f(I))); rank database by dq·dm; report Recall@N.
The big picture: VPR was reframed from "learn a metric by mining pairs" to "learn a classifier whose backbone happens to produce a great retrieval descriptor." CosPlace made that practical at city scale, EigenPlaces made it viewpoint-aware, and MixVPR upgraded the aggregator that turns the feature map into the descriptor. The recurring lesson: when retrieval depends only on angles, design the training loss to shape angles directly — and aggregate globally, not locally.

"What I cannot create, I do not understand." — You have now created the CosFace loss and the MixVPR mixer by hand. You understand them.