Features & Representation
LoFTR
A detector-free local feature matcher (Sun et al., CVPR 2021) that transforms coarse convolutional feature maps of two images with interleaved self- and cross-attention, matches every coarse position by dual-softmax or optimal transport, and refines the confident matches to sub-pixel accuracy by an expectation over a small correlation window.
advanced
LoFTR (Local Feature TRansformer) is a model for feature matching between two images, introduced by Jiaming Sun, Zehong Shen, Yuang Wang, Hujun Bao and Xiaowei Zhou at CVPR 2021. It has no keypoint detector. A convolutional network computes feature maps of both images. A transformer of interleaved self-attention and cross-attention layers rewrites the coarse map at 1/8 of the image resolution so that each feature depends on both images. Every coarse position of one image is then scored against every coarse position of the other. The confident mutual matches are refined at 1/2 resolution by an expectation over a small correlation window. The result is a semi-dense set of matches, in the authors’ words, that can include points in low-texture regions where detectors find few repeatable keypoints. LoFTR belongs to the family of learned local features and learned matchers, and it adapts the attention design of SuperGlue to dense feature maps.
Problem Addressed
The classical matching pipeline runs in three stages: detect interest points in each image, describe a patch around each one, then match the descriptors by nearest-neighbour search or a learned matcher. SuperPoint learned the first two stages, and SuperGlue learned the third. LoFTR’s authors argue that the detector itself limits the pipeline. A detector may not fire on the same scene points in both images, because of poor texture, repetitive patterns, viewpoint or illumination change and motion blur. The authors add that, without repeatable interest points, correct correspondences cannot be found even with perfect descriptors (§1). They name indoor scenes, where low-texture regions or repetitive patterns sometimes occupy most of the field of view, as the case where this is most prominent.
Detector-free methods existed before LoFTR. The authors’ related work traces dense feature matching to SIFT Flow, and it names NCNet as the method that learned dense correspondences end to end. NCNet builds a 4D cost volume over all pairs of positions and regularizes it with 4D convolutions to enforce neighbourhood consensus; Sparse NCNet and the concurrent DRC-Net followed it. LoFTR’s authors argue that these convolutional methods see only a limited neighbourhood of each match. Their alternative is consensus over the whole image, from the global receptive field of attention. Their example is a white wall, whose positions can be told apart by where they lie relative to the edges of the wall (§1).
Architecture
LoFTR has four components, numbered in Fig. 2 of the paper:
- a local feature CNN that produces coarse and fine feature maps;
- the LoFTR module, a transformer on the coarse maps;
- a differentiable matching layer that turns the transformed features into a confidence matrix and selects coarse matches;
- a coarse-to-fine module that refines each selected match.
The notation follows the paper: images and , coarse maps at 1/8 resolution, and fine maps at 1/2 resolution.
Local Feature CNN
The backbone is a modified ResNet-18 with a feature pyramid network, trained from random initialization. The authors give two reasons for the CNN: its inductive bias of translation equivariance and locality suits local feature extraction, and its downsampling shortens the sequence the transformer must process (§3.1). In the official configuration (cvpr_ds_config.py), the coarse features have 256 channels and the fine features 128.
LoFTR Module
The coarse maps are flattened to sequences of vectors. A 2D sinusoidal positional encoding is added once, following the 2D extension of DETR; unlike DETR, it is not added again at every layer (§3.2). The sequences then pass through pairs of layers, each a self-attention layer within each image followed by a cross-attention layer between the two. This interleaving follows SuperGlue, which the authors describe as a graph neural network over two sets of detected keypoints. In LoFTR every coarse position takes part, so attention is not limited to detected points.
A sequence of positions makes full attention cost ; at 1/8 of a image, per image. The authors call vanilla attention impractical here even at 1/8 resolution. They use the linear transformer of Katharopoulos et al. (2020), as they describe it. The softmax kernel is replaced by with . Associativity then lets be computed first, which costs because the feature dimension is much smaller than . The official linear_attention.py implements this kernel. Attention mechanism places linear attention among the other efficient variants.
The authors attribute two properties to the transformed features . They are context-dependent, through the global receptive field, and position-dependent, through the positional encoding. Fig. 4(c) visualizes the features with PCA: on a uniform white wall, the transformed features vary smoothly with position, so each position is distinct even though the input colour is not.
Matching Layer
The score matrix between all coarse positions is
The paper offers two differentiable layers. The first is an optimal-transport layer as in SuperGlue, with as the cost of a partial assignment, solved with three Sinkhorn iterations in LoFTR’s implementation. The second is the dual-softmax, credited to NCNet and DISK, which applies a softmax along both dimensions:
The paper calls this the probability of soft mutual nearest-neighbour matching. Coarse matches are the pairs whose confidence reaches a threshold and that are mutual nearest neighbours in :
with (§3.6). In the official coarse_matching.py, each feature is first divided by , so the dot product is scaled by as well as by . The default temperature is 0.1. The threshold test is strict (). Two border cells are masked out in both images, and the mutual check compares each entry with the maxima of its row and column over the full matrix. The main paper does not state the scaling or the border mask. The optimal-transport variant imports log_optimal_transport from SuperGlue’s superglue.py and adds a learnable dustbin score; the README says the authors cannot redistribute that file because of its licence.
Coarse-to-Fine Module
Each coarse match is located on the fine maps as , and a window with is cropped around each position. A smaller LoFTR module with self- and cross-attention pair transforms the two windows. The centre vector of A’s window is correlated with all vectors of B’s window. The softmax of these correlations is a heatmap of match probability, and its expectation gives the refined position in to sub-pixel accuracy (§3.4). The authors say this correlation-based approach is inspired by Wang et al. (2020). A’s point stays fixed: only the position in B is refined.
In the official fine_matching.py, the heatmap is , and the expectation is taken in normalized window coordinates. It is scaled by times the fine stride of 2 pixels and added to the coarse position in B. fine_preprocess.py crops the windows with stride 4 on the 1/2-resolution map, centred on the coarse cell, and adds coarse context before the fine transformer. §3.6 says the coarse transformer features are upsampled and concatenated with the fine features. In fine_preprocess.py the coarse feature of each matched cell is projected and concatenated with every vector of its window. Coarse position maps to pixel in the code, and fine position to pixel . With at stride 2, the window spans pixels, exactly half a coarse cell, so a correct coarse match always leaves the true position inside the window.
This expectation is a soft-argmax over a window. Heatmap regression describes its failure mode: the softmax gives every cell some weight, which pulls the expectation towards the centre of the map. In LoFTR the centre of the window is the coarse match, so the pull is back towards the coarse position. Coarse-to-fine estimation and PWC-Net refine dense flow across a pyramid by warping. LoFTR instead refines each sparse match once, in a fixed window, without warping.
Training
The indoor model was trained on ScanNet and the outdoor model on MegaDepth, following SuperGlue (§3.6). Ground-truth matches come from camera poses and depth maps. The coarse ground truth is the set of mutual nearest neighbours between the two 1/8-resolution grids, with the distance between grid cells measured by reprojecting their centres. In the official supervision.py, the grid points are warped, rounded to the nearest cell and kept when the round trip returns to the starting cell.
The loss is . For the dual-softmax, the coarse loss is the negative log-likelihood of the ground-truth cells:
With optimal transport, the authors use SuperGlue’s loss. The fine loss is an distance weighted by the inverse of the heatmap’s total variance , so that confident refinements count more:
where is warped with the ground-truth pose and depth. Matches whose warped point falls outside the window are ignored, and no gradient flows through (§3.5). On ScanNet, training used Adam with an initial learning rate of and a batch of 64, and converged after 24 hours on 64 GTX 1080Ti GPUs. The authors sampled 230 million training pairs with overlap scores between 0.4 and 0.8, following SuperGlue’s procedure (§4.2). During training, the official coarse_matching.py pads the coarse matches sent to the fine level with ground-truth matches, so the fine stage also trains on ground-truth coarse matches, not only on predicted ones.
Example: Dual-Softmax and Window Refinement with Fixed Features
The example trains no network. It keeps LoFTR’s matching layer and coarse-to-fine module as the official code implements them, and replaces the CNN and both transformers with fixed, hand-set descriptors. This shows what the two stages do with the features they are given, and where they fail when the features are not distinctive.
Data and conventions:
- Images: two bundled scikit-image images in grey,
coffee() androcket(, with a large sky). - Pairs: image B is image A warped by a known homography : a rotation of 4° and a scale of 1.04 about the image centre, a shift of pixels and a mild perspective term.
- Points are = (column, row), maps A’s pixels to B’s, and , with bilinear interpolation and zeros outside.
- Noise: each image is run clean and with Gaussian noise of standard deviation 0.03 added to B, with noise seeds 0, 1 and 2, for eight pairs.
Features and matching:
- Coarse descriptors. For the cell at pixel : a patch of the image blurred with , sampled every 3 pixels and normalized to zero mean and unit length (81 values).
- Coarse matching. The dual-softmax is applied to cosines divided by . This equals the code’s for feature vectors of norm ; the equivalence is our reasoning. Selection follows
coarse_matching.py: threshold 0.2 (tested with rather than the code’s ), two border cells removed, and a mutual maximum over the full matrix. - Fine descriptors. patches of the half-resolution image (blur , even pixels), normalized the same way.
- Refinement. The heatmap is the softmax of the 25 cosines divided by , and the refined position is for offsets . The value is the code’s for the fine width of 128, again for vectors of norm . The argmax limit () and the uniform limit () are computed exactly.
Measures:
- Ground truth. Coarse ground truth follows
supervision.py. A selected coarse match is correct if it is a ground-truth pair. - End-point error (EPE). The distance in pixels between a match’s position in B and applied to A’s point .
- Teacher-forced refinement. The fine table refines the ground-truth coarse matches, as in the ground-truth padding of LoFTR’s fine training, so refinement is measured apart from coarse errors.
- Default pipeline. A last block runs the code’s defaults, , and .
Checks printed before the tables:
- E1: the dual-softmax against its log-domain form , within .
- E2: the uniform limit returns the coarse position, within pixels.
- E3 and D1: the coarse-only EPE and the ground-truth counts equal values computed in advance from the homography alone: 3.0461 and 3.0453 pixels, and 2,976 and 3,395 pairs (3,105, 3,552, 3,220 and 3,675 for the base pairs below).
- E4: reproduces the argmax, within pixels.
- B1: under a whole-cell shift of pixels, the mutual nearest neighbours of the cosine matrix, written MNN(), and the full coarse rule at each recover at least 0.99 of the ground-truth pairs, and the correct matches have a coarse EPE of exactly 0.
- B2: under a shift of pixels, which falls on the fine grid, the argmax limit finds the true position exactly for at least 0.99 of matches, with a mean EPE of at most 0.01 pixels, against a mean coarse-only EPE of 2.8284 pixels.
Before the run, our plan stated these hypotheses for every pair, with the noisy conditions counting only if they held for each seed:
- H1a: the number of selected coarse matches does not increase with .
- H1b: precision at the largest with at least 20 selected matches is at least that at .
- H1c: at the selected matches are exactly MNN(). The plan added, in its words, “selected ⊆ MNN by construction”.
- H2: at the refined EPE is below the coarse-only EPE.
- H3: the smallest EPE over the sweep is at a finite positive and is below the argmax limit.
- H4: above that , the EPE rises steadily to the coarse-only value.
- H5: in the default pipeline, refinement lowers the mean EPE of the correct selected matches.
The script uses NumPy 2.3.5, SciPy 1.16.3 and scikit-image 0.25.2, and runs in about 15 seconds.
import os
os.environ["OPENBLAS_NUM_THREADS"] = "1"; os.environ["OMP_NUM_THREADS"] = "1"; os.environ["MKL_NUM_THREADS"] = "1"
import numpy as np
import skimage
from skimage import io, color, filters, transform
from scipy.special import logsumexp
DATA = os.path.join(os.path.dirname(skimage.__file__), "data")
C, F, W, BORDER, THETA = 8, 2, 5, 2, 0.2 # coarse stride, fine stride, window, border cells, threshold
TAU_C = [0.005, 0.01, 0.02, 0.05, 0.1, 0.2]
TAU_F = [0.0, 0.005, 0.01, 0.02, 0.05, 1 / np.sqrt(128), 0.2, 0.5, 1.0, np.inf]
def load(name):
return color.rgb2gray(io.imread(os.path.join(DATA, name)))
def homography(h, w):
"""Rotation 4 deg and scale 1.04 about the centre, shift (11.3, -6.6), mild perspective."""
cx, cy, a, s = w / 2, h / 2, np.deg2rad(4.0), 1.04
T1 = np.array([[1, 0, -cx], [0, 1, -cy], [0, 0, 1.0]])
RS = np.array([[s * np.cos(a), -s * np.sin(a), 0], [s * np.sin(a), s * np.cos(a), 0], [0, 0, 1.0]])
T2 = np.array([[1, 0, cx + 11.3], [0, 1, cy - 6.6], [0, 0, 1.0]])
P = np.array([[1, 0, 0], [0, 1, 0], [2e-5, -1.5e-5, 1.0]])
return T2 @ RS @ T1 @ P
def shift(tx, ty):
return np.array([[1, 0, tx], [0, 1, ty], [0, 0, 1.0]])
def apply(H, xy):
p = np.c_[xy, np.ones(len(xy))] @ H.T
return p[:, :2] / p[:, 2:]
def unit(v):
v = v - v.mean(axis=-1, keepdims=True)
return v / (np.linalg.norm(v, axis=-1, keepdims=True) + 1e-12)
def coarse_desc(img):
"""Cell k (pixel 8k): 25x25 patch of the sigma-2 image sampled every 3 px, normalized."""
g = np.pad(filters.gaussian(img, 2.0), 12, mode="edge")
hc, wc = img.shape[0] // C, img.shape[1] // C
ys, xs = np.mgrid[0:hc, 0:wc] * C
oy, ox = np.meshgrid(np.arange(-12, 13, 3), np.arange(-12, 13, 3), indexing="ij")
return unit(g[ys.ravel()[:, None] + oy.ravel() + 12, xs.ravel()[:, None] + ox.ravel() + 12])
def fine_desc(fmap, u, v):
"""5x5 patch of the half-resolution map at fine index (u, v), normalized."""
p = np.pad(fmap, 2 + W, mode="edge")
oy, ox = np.meshgrid(np.arange(-2, 3), np.arange(-2, 3), indexing="ij")
return unit(p[v[:, None] + oy.ravel() + 2 + W, u[:, None] + ox.ravel() + 2 + W])
def interior(idx, hc, wc):
y, x = idx // wc, idx % wc
return (y >= BORDER) & (y < hc - BORDER) & (x >= BORDER) & (x < wc - BORDER)
def ground_truth(H, hc, wc):
"""LoFTR's coarse labels: warp grid points, round to a cell, keep mutual pairs."""
pts = np.c_[np.arange(hc * wc) % wc, np.arange(hc * wc) // wc] * C
r01 = np.round(apply(H, pts) / C).astype(int)
r10 = np.round(apply(np.linalg.inv(H), pts) / C).astype(int)
inb = lambda r: (r[:, 0] >= 0) & (r[:, 0] < wc) & (r[:, 1] >= 0) & (r[:, 1] < hc)
n1 = np.where(inb(r01), r01[:, 0] + r01[:, 1] * wc, -1)
n0 = np.where(inb(r10), r10[:, 0] + r10[:, 1] * wc, -1)
i = np.arange(hc * wc)
ok = n1 >= 0
ok[ok] &= n0[n1[ok]] == i[ok]
ok &= interior(i, hc, wc)
ok[ok] &= interior(n1[ok], hc, wc)
return dict(zip(i[ok], n1[ok]))
def dual_softmax(cos, tau):
S = cos / tau
R = np.exp(S - S.max(1, keepdims=True)); R /= R.sum(1, keepdims=True)
K = np.exp(S - S.max(0, keepdims=True)); K /= K.sum(0, keepdims=True)
return R * K
def select(P, hc, wc, theta):
"""Threshold, remove border cells, then mutual maximum of P (coarse_matching.py)."""
keep = interior(np.arange(P.shape[0]), hc, wc)
i = np.arange(P.shape[0]); j = P.argmax(1)
ok = (P[i, j] >= theta) & (P.argmax(0)[j] == i) & keep & keep[j]
return dict(zip(i[ok], j[ok]))
def refine(fa, fb, pairs, wc, tau):
"""Expectation over the 5x5 window of B around each coarse match, in pixels."""
i = np.array([p[0] for p in pairs]); j = np.array([p[1] for p in pairs])
ia = np.c_[i % wc, i // wc] * (C // F); jb = np.c_[j % wc, j // wc] * (C // F)
q = fine_desc(fa, ia[:, 0], ia[:, 1])
dy, dx = np.meshgrid(np.arange(-2, 3), np.arange(-2, 3), indexing="ij")
d = np.c_[dx.ravel(), dy.ravel()]
cand = np.stack([fine_desc(fb, jb[:, 0] + a, jb[:, 1] + b) for a, b in d], 1)
sim = np.einsum("mc,mkc->mk", q, cand)
if tau == 0: # argmax limit
wgt = np.eye(W * W)[sim.argmax(1)]
elif np.isinf(tau): # uniform limit
wgt = np.full(sim.shape, 1.0 / (W * W))
else:
z = sim / tau; wgt = np.exp(z - z.max(1, keepdims=True)); wgt /= wgt.sum(1, keepdims=True)
return jb * F + F * (wgt @ d)
def epe(H, pairs, wc, pos):
i = np.array([p[0] for p in pairs])
truth = apply(H, np.c_[i % wc, i // wc] * C)
return np.linalg.norm(pos - truth, axis=1)
def make_pair(name, H, noise_seed):
A = load(name)
B = transform.warp(A, transform.ProjectiveTransform(np.linalg.inv(H)), order=1, cval=0.0)
if noise_seed is not None:
B = B + np.random.default_rng(noise_seed).normal(0, 0.03, B.shape)
return A, B
def coarse_pos(pairs, wc):
j = np.array([p[1] for p in pairs])
return np.c_[j % wc, j // wc].astype(float) * C
conds = [(n, s) for n in ["coffee.png", "rocket.jpg"] for s in [None, 0, 1, 2]]
labels = [f"{n[:6]}{'' if s is None else '/s' + str(s)}" for n, s in conds]
coarse_rows, fine_rows, pipe_rows, checks = {}, {}, [], {"E1": 0.0, "E2": 0.0, "E4": 0.0}
for (name, seed), lab in zip(conds, labels):
A, B = make_pair(name, homography(*load(name).shape), seed)
H = homography(*A.shape)
hc, wc = A.shape[0] // C, A.shape[1] // C
gt = ground_truth(H, hc, wc)
cos = coarse_desc(A) @ coarse_desc(B).T
for tau in TAU_C:
P = dual_softmax(cos, tau)
if tau == 0.1:
S = cos / tau
Plog = np.exp(2 * S - logsumexp(S, 1)[:, None] - logsumexp(S, 0)[None, :])
checks["E1"] = max(checks["E1"], np.abs(P - Plog).max())
sel = select(P, hc, wc, THETA)
correct = [(i, j) for i, j in sel.items() if gt.get(i) == j]
coarse_rows.setdefault(f"{tau:g}", []).append(f"{len(sel)}/{len(correct)}")
if tau == 0.1:
default_correct = correct
mnn = select(cos, hc, wc, -np.inf) # mutual maximum of S itself
coarse_rows.setdefault("MNN(S)", []).append(f"{len(mnn)}/{sum(gt.get(i) == j for i, j in mnn.items())}")
fa = filters.gaussian(A, 1.0)[::F, ::F]; fb = filters.gaussian(B, 1.0)[::F, ::F]
pairs = sorted(gt.items())
cpos = coarse_pos(pairs, wc)
fine_rows.setdefault("coarse only", []).append(epe(H, pairs, wc, cpos).mean())
for tau in TAU_F:
pos = refine(fa, fb, pairs, wc, tau)
key = "argmax" if tau == 0 else ("uniform" if np.isinf(tau) else f"{tau:.4g}")
fine_rows.setdefault(key, []).append(epe(H, pairs, wc, pos).mean())
if np.isinf(tau):
checks["E2"] = max(checks["E2"], np.abs(pos - cpos).max())
if tau == 0:
pos0 = pos
checks["E4"] = max(checks["E4"], np.abs(refine(fa, fb, pairs, wc, 1e-5) - pos0).max())
if seed is None:
checks[f"E3 {name[:6]}"] = epe(H, pairs, wc, cpos).mean()
checks[f"D1 {name[:6]}"] = len(gt)
if default_correct:
pc = coarse_pos(default_correct, wc)
pf = refine(fa, fb, default_correct, wc, 1 / np.sqrt(128))
pipe_rows.append(f"{lab:10s} {len(default_correct):5d} {epe(H, default_correct, wc, pc).mean():9.4f} {epe(H, default_correct, wc, pf).mean():9.4f}")
else:
pipe_rows.append(f"{lab:10s} {0:5d} n/a n/a")
# Base checks: B1 coarse stage under a whole-cell shift, B2 fine stage under a fine-grid shift.
base = []
for name in ["coffee.png", "rocket.jpg"]:
A, B = make_pair(name, shift(16, 8), None)
hc, wc = A.shape[0] // C, A.shape[1] // C
gt = ground_truth(shift(16, 8), hc, wc)
cos = coarse_desc(A) @ coarse_desc(B).T
mnn = select(cos, hc, wc, -np.inf)
sel = select(dual_softmax(cos, 0.005), hc, wc, THETA)
good = [(i, j) for i, j in sel.items() if gt.get(i) == j]
e = epe(shift(16, 8), good, wc, coarse_pos(good, wc)).max() if good else np.nan
base.append(f"B1 {name[:6]}: GT {len(gt)} in MNN(S) {sum(gt.get(i) == j for i, j in mnn.items()) / len(gt):.4f}"
f" selected at tau_c=0.005 {len(good) / len(gt):.4f} max coarse EPE of correct {e:.1e}")
A, B = make_pair(name, shift(6, 2), None)
gt = ground_truth(shift(6, 2), hc, wc)
pairs = sorted(gt.items())
fa = filters.gaussian(A, 1.0)[::F, ::F]; fb = filters.gaussian(B, 1.0)[::F, ::F]
e0 = epe(shift(6, 2), pairs, wc, refine(fa, fb, pairs, wc, 0.0))
ec = epe(shift(6, 2), pairs, wc, coarse_pos(pairs, wc))
base.append(f"B2 {name[:6]}: GT {len(gt)} argmax EPE exactly 0 in {np.mean(e0 == 0):.4f}"
f" mean argmax EPE {e0.mean():.4f} mean coarse EPE {ec.mean():.4f}")
print("Checks")
for k, v in checks.items():
print(f" {k:10s} {v:.1e}" if k[0] in "E" and not k.startswith("E3") else f" {k:10s} {v:.4f}" if k.startswith("E3") else f" {k:10s} {v}")
for line in base:
print(" ", line)
print("\nCoarse matches, selected/correct (theta_c = 0.2, mutual check, 2 border cells removed)")
print(f"{'tau_c':>8s} " + " ".join(f"{l:>11s}" for l in labels))
for k, v in coarse_rows.items():
print(f"{k:>8s} " + " ".join(f"{x:>11s}" for x in v))
print("\nMean end-point error (px) of the ground-truth coarse matches, by fine temperature tau_f")
print(f"{'tau_f':>11s} " + " ".join(f"{l:>9s}" for l in labels))
for k, v in fine_rows.items():
print(f"{k:>11s} " + " ".join(f"{x:9.4f}" for x in v))
print("\nDefault pipeline (tau_c = 0.1, theta_c = 0.2, tau_f = 1/sqrt(128)): correct matches, mean EPE coarse, fine")
for r in pipe_rows:
print(r)
Checks
E1 7.8e-16
E2 0.0e+00
E4 2.4e+00
E3 coffee 3.0461
D1 coffee 2976
E3 rocket 3.0453
D1 rocket 3395
B1 coffee: GT 3105 in MNN(S) 1.0000 selected at tau_c=0.005 0.9961 max coarse EPE of correct 0.0e+00
B2 coffee: GT 3220 argmax EPE exactly 0 in 1.0000 mean argmax EPE 0.0000 mean coarse EPE 2.8284
B1 rocket: GT 3552 in MNN(S) 0.9983 selected at tau_c=0.005 0.5512 max coarse EPE of correct 0.0e+00
B2 rocket: GT 3675 argmax EPE exactly 0 in 1.0000 mean argmax EPE 0.0000 mean coarse EPE 2.8284
Coarse matches, selected/correct (theta_c = 0.2, mutual check, 2 border cells removed)
tau_c coffee coffee/s0 coffee/s1 coffee/s2 rocket rocket/s0 rocket/s1 rocket/s2
0.005 1869/1315 1717/1188 1721/1193 1734/1199 931/604 788/480 788/487 799/479
0.01 1684/1251 1550/1129 1551/1129 1562/1145 781/514 707/452 726/464 711/447
0.02 1206/977 1131/901 1120/894 1136/911 581/405 522/365 529/367 539/372
0.05 371/337 351/317 347/312 355/318 175/139 157/120 166/131 158/115
0.1 0/0 0/0 0/0 0/0 1/1 1/1 1/1 1/1
0.2 0/0 0/0 0/0 0/0 0/0 0/0 0/0 0/0
MNN(S) 1798/1270 1660/1142 1660/1163 1665/1155 1643/1186 798/469 785/478 823/468
Mean end-point error (px) of the ground-truth coarse matches, by fine temperature tau_f
tau_f coffee coffee/s0 coffee/s1 coffee/s2 rocket rocket/s0 rocket/s1 rocket/s2
coarse only 3.0461 3.0461 3.0461 3.0461 3.0453 3.0453 3.0453 3.0453
argmax 1.2104 2.2250 2.1811 2.1736 0.9997 3.5936 3.5854 3.5642
0.005 1.0568 2.0435 2.0180 2.0353 0.8846 3.5298 3.5023 3.4820
0.01 1.0429 1.9524 1.9351 1.9512 0.8205 3.4669 3.4366 3.4125
0.02 1.0656 1.8624 1.8585 1.8654 0.8405 3.3509 3.3165 3.2893
0.05 1.1729 1.7865 1.7867 1.7941 1.2223 3.0836 3.0605 3.0327
0.08839 1.3354 1.8159 1.8168 1.8174 1.6212 2.8843 2.8727 2.8472
0.2 1.7917 2.0662 2.0713 2.0684 2.1604 2.7273 2.7253 2.7111
0.5 2.3719 2.4853 2.4878 2.4902 2.6046 2.7988 2.8008 2.7949
1 2.6732 2.7295 2.7312 2.7322 2.8073 2.8948 2.8961 2.8928
uniform 3.0461 3.0461 3.0461 3.0461 3.0453 3.0453 3.0453 3.0453
Default pipeline (tau_c = 0.1, theta_c = 0.2, tau_f = 1/sqrt(128)): correct matches, mean EPE coarse, fine
coffee 0 n/a n/a
coffee/s0 0 n/a n/a
coffee/s1 0 n/a n/a
coffee/s2 0 n/a n/a
rocket 1 0.4500 1.6541
rocket/s0 1 0.4500 2.0950
rocket/s1 1 0.4500 3.0320
rocket/s2 1 0.4500 2.8244
Checks. E1, E2, E3 and D1 pass: the dual-softmax matches its log-domain form to , the uniform limit returns the coarse position exactly, and the coarse-only EPE and the ground-truth counts equal the values computed in advance. B2 passes for both images. Under a fine-grid shift, the argmax limit finds the true position for every ground-truth match, where the coarse cells alone are 2.8284 pixels off on average.
E4 failed. At the refined positions differ from the argmax by up to 2.4 pixels on one axis, against a tolerance of . The following derivation is ours. If the best candidate in a window has cosine and another candidate has , the softmax gives the second a weight times that of the first, so the expectation moves towards it by a share of their separation. Offsets in a window differ by at most 8 pixels on an axis, so a 2.4-pixel shift needs at least 0.3 of the weight off the argmax. With 24 other candidates, at least one must then hold a weight of or more, against at most 0.7 for the argmax. Its gap is therefore at most . At least one window thus had a runner-up within that distance of its best cosine, and a softmax at any finite temperature splits such near-ties. Our tolerance assumed no near-ties at that scale.
B1 coffee passed; B1 rocket failed for the full rule. For coffee, MNN() recovers 1.0000 of the ground-truth pairs under the whole-cell shift, and the full rule 0.9961. For rocket, MNN() recovers 0.9983, which passes, but the full coarse rule at recovers only 0.5512 against the criterion of at least 0.99. So at least 0.447 of the ground-truth pairs are mutual nearest neighbours of the cosines that the full rule rejects, at its threshold or at its mutual check on . In both images the correct matches have a coarse EPE of exactly 0, as planned. A derivation (ours) shows when the threshold rejects a mutual nearest neighbour. Write and , so that
For a mutual best pair, , and , where and are the sums over its row and its column. The pair fails the threshold of 0.2 when : when its row and column together hold the equivalent of more than five cells within a few of the best cosine. In our reading, this is rocket’s sky, where many patches of smooth gradient look alike to a normalized-patch descriptor. LoFTR’s transformer is meant to remove exactly this ambiguity by making features position-dependent.
Coarse selection (H1).
- H1a held in all eight pairs. Counts fall with , to 0 matches at for every pair, and 0 at 0.1 for coffee.
- H1b held in all eight pairs. At , the largest temperature with at least 20 matches everywhere, precision is 0.9084, 0.9031, 0.8991 and 0.8958 for coffee (clean, seeds 0–2), against 0.7036, 0.6919, 0.6932 and 0.6915 at 0.005. For rocket it is 0.7943, 0.7643, 0.7892 and 0.7278, against 0.6488, 0.6091, 0.6180 and 0.5995. A higher temperature keeps fewer matches, and a larger share of them is correct.
- H1c failed in all eight pairs. At , coffee selects 1,869, 1,717, 1,721 and 1,734 matches, against 1,798, 1,660, 1,660 and 1,665 in MNN(). Rocket selects 931, 788, 788 and 799, against 1,643, 798, 785 and 823.
The plan’s “selected ⊆ MNN by construction” was wrong, as the coffee counts show. The reason, by our derivation, is the identity above. The mutual check in LoFTR’s rule is a mutual maximum of , not of . A row’s best column under can be a column that is less contested, rather than the column with the highest cosine. At the same time, the threshold implies , which is at . So every selected pair is within in cosine of being a mutual nearest neighbour, without having to be one. Only in the limit do both sets coincide. The dual-softmax is a soft mutual-nearest-neighbour rule, as the paper calls it, and at finite temperature it differs from the hard rule in both directions: in clean rocket it selects 712 fewer pairs than MNN() contains, and in clean coffee it selects 71 more, so at least 71 of its pairs are not mutual nearest neighbours of the cosines.
Refinement (H2 to H4).
- H2 held in all eight pairs. At the refined EPE is 1.3354, 1.8159, 1.8168 and 1.8174 pixels for coffee, and 1.6212, 2.8843, 2.8727 and 2.8472 for rocket, against 3.0461 and 3.0453 for the coarse positions.
- H3 held in all eight pairs. The minimum is at for clean coffee (1.0429, against 1.2104 for the argmax) and clean rocket (0.8205 against 0.9997). It is at 0.05 for noisy coffee (1.7865, 1.7867 and 1.7941 against 2.2250, 2.1811 and 2.1736), and at 0.2 for noisy rocket (2.7273, 2.7253 and 2.7111 against 3.5936, 3.5854 and 3.5642).
- H4 held in all eight pairs. Above each minimum, the EPE rises at every step of the sweep to the coarse-only value at the uniform limit.
The sweep shows the trade-off a soft-argmax over a window has to make. A sharp heatmap picks one candidate, so its accuracy is limited by the 2-pixel spacing of the fine grid. A flat heatmap returns the window centre, the coarse match, which is the centre bias Heatmap Regression describes. In between, the expectation interpolates between neighbouring candidates and beats the argmax. The best temperature moves up with noise, from 0.01 clean to 0.05 and 0.2: a flatter heatmap averages out noisy correlations.
The plan did not predict one result in the table. With noise, rocket’s argmax-limit EPE (3.5936, 3.5854 and 3.5642) is worse than leaving the coarse match alone (3.0453). In our reading, the noise dominates the small fine patches where the image is smooth, so the argmax lands on an arbitrary offset in the window, which on average is farther from the truth than the window centre.
Default pipeline (H5). H5 failed for rocket and is undecidable for coffee. At the code’s defaults, coffee gives 0 correct matches in all four pairs. Rocket gives one correct match per pair, whose coarse EPE is 0.4500 pixels and whose refined EPE is 1.6541, 2.0950, 3.0320 and 2.8244. One match per pair also cannot show an average effect.
The coarse collapse follows from the threshold (our reasoning). needs both factors to be at least 0.2, so all other cells of a row must together weigh at most four times the best one: . With coffee’s 3,750 cells and , Jensen’s inequality then requires the other cosines to lie on average at least below the best one. In our reading, normalized intensity patches from one image rarely do that, which is consistent with the printed counts at . LoFTR’s trained features are optimized so that the dual-softmax concentrates on the right cell, and its threshold and temperature are set for such features. With fixed features, only temperatures well below the default keep matches, at a lower precision (H1b).
Inference
At test time, LoFTR takes two images and returns the fine matches with their coarse confidences. A’s points lie on the 1/8 grid, and B’s are refined to sub-pixel positions. Because every coarse cell can match, the number of matches depends on the image pair and the threshold rather than on a keypoint budget. For HPatches, the authors capped the output at 1,000 matches (§4.1). The full dual-softmax model ran in 116 ms for a pair on an RTX 2080Ti, and the optimal-transport model in 130 ms (§3.6). Downstream geometry uses RANSAC: OpenCV’s homography estimation for HPatches, and the essential matrix for relative pose (§4).
Performance
The authors compared LoFTR with three groups of baselines: detector-based features matched by mutual nearest neighbours (D2-Net, R2D2, DISK); SuperPoint with SuperGlue; and the detector-free Sparse-NCNet and DRC-Net. All numbers below are from the paper, as printed.
- Homography estimation on HPatches (Table 1). The measure is the area under the curve (AUC) of the corner error at 3, 5 and 10 pixels, with images resized to a shorter side of 480. LoFTR-DS, trained on MegaDepth, reached 65.9, 75.6 and 84.6. SuperPoint + SuperGlue reached 53.9, 68.3 and 81.7, and DRC-Net 50.6, 56.2 and 68.3. The table’s caption cites HPatches as reference [7], which in the paper’s list is ScanNet; the text cites it correctly as [1].
- Indoor relative pose on ScanNet (Table 2). On the 1,500 test pairs of SuperGlue’s evaluation, the measure is the AUC of the pose error at 5°, 10° and 20°. LoFTR-DS reached 22.06, 40.8 and 57.62, and LoFTR-OT 21.51, 40.39 and 57.96. SuperPoint + SuperGlue reached 16.16, 33.81 and 51.84. Trained on MegaDepth, LoFTR-OT reached 16.88, 33.62 and 50.62, against 7.69, 17.93 and 30.49 for DRC-Net trained on MegaDepth.
- Outdoor relative pose on MegaDepth (Table 3). On 1,500 pairs from two scenes, LoFTR-DS reached 52.8, 69.19 and 81.18, and LoFTR-OT 50.31, 67.14 and 79.93. SuperPoint + SuperGlue (with HLoc’s set-up) reached 42.18, 61.16 and 75.96, and DRC-Net 27.01, 42.96 and 58.31. The text’s 61% and 13% gains at AUC@10° are relative: 69.19 against 42.96 and against 61.16.
- Visual localization (Tables 4 and 5). On the Aachen Day-Night v1.1 local-feature track (night queries, within 0.25 m and 2°, 0.5 m and 5°, 1 m and 10°), LoFTR-DS scored 72.8, 88.5 and 99.0, against 73.3, 88.0 and 98.4 for SuperPoint + SuperGlue. The text says LoFTR-DS outperforms all baselines on this track, but at the tightest threshold the table puts it 0.5 below SuperPoint + SuperGlue and LISRD + SuperPoint + AdaLAM (73.3). On InLoc with HLoc, LoFTR-OT scored 47.5, 72.2 and 84.8 on DUC1 and 54.2, 74.8 and 85.5 on DUC2. HLoc + SuperPoint + SuperGlue scored 49.0, 68.7 and 80.8, and 53.4, 77.1 and 82.4. The text says LoFTR-OT outperforms all published methods on InLoc, but the table shows SuperPoint + SuperGlue higher at DUC1’s tightest threshold and at DUC2’s middle one.
- Ablation on ScanNet (Table 6). These runs use optimal-transport matching, according to the text. Replacing the LoFTR module with convolutions of a comparable parameter count gave 14.98, 32.04 and 49.92. Coarse and fine resolutions of 1/16 and 1/4 gave 16.75, 34.82 and 54.0, at 104 ms. Positional encoding at every layer, as in DETR, gave 18.02, 35.64 and 52.77. Doubling the layers (, ) gave 20.87, 40.23 and 57.56.
The paper’s tables disagree on the full model. Table 6’s full model (, ) scores 20.06, 40.8 and 57.62. Its last two values equal LoFTR-DS in Table 2, and none of the three equals LoFTR-OT in Table 2 (21.51, 40.39, 57.96), although the text says the ablations used optimal transport.
Strengths
- No repeatability requirement. Matches are not limited to points a detector fires on twice. The authors’ Fig. 1 shows LoFTR finding correspondences on a texture-less wall and a floor with repetitive patterns, where, in the caption’s words, detector-based methods struggle to find repeatable interest points; they also attribute the HPatches gain partly to the larger number of match candidates.
- Context and position in the descriptor. Attention over both whole images gives each coarse feature global context and, through the positional encoding, a position. The ablation shows the largest drop when the transformer is replaced by convolutions (Table 6, row 1).
- Sub-pixel output from a coarse search. The search runs at 1/8 resolution, and the expectation in the fine window recovers sub-pixel positions. The example shows that the expectation beats both the coarse cell and the argmax over the fine grid, at a suitable temperature.
- Two interchangeable matching layers. Dual-softmax and optimal transport performed comparably indoors, and dual-softmax was better outdoors (§4.2).
Limitations
- Coarse decisions are final. The fine stage searches only pixels around a coarse match, so a wrong coarse cell cannot be repaired. The example’s teacher-forced table measures refinement only on correct coarse matches.
- Cost grows with resolution. The coarse sequence grows with the image area, and the 4,800-position sequences of a pair already needed linear attention. Coarser grids (1/16) were faster but less accurate (Table 6).
- Fixed grid in A. A’s matched points lie on the 1/8 grid, and only B’s side is refined. Matches are semi-dense rather than placed at salient points.
- Thresholds tied to the learned features. In the example, the code’s defaults (, ) kept almost no matches with fixed features. The confidence threshold presupposes features as distinctive as the trained ones.
- Training data. Training needs posed images with depth (ScanNet, MegaDepth) and large compute (24 hours on 64 GPUs for ScanNet).
Historical Importance
LoFTR brought SuperGlue’s interleaved self- and cross-attention from detected keypoints to dense feature maps. In its own tables, a detector-free matcher outperformed SuperPoint + SuperGlue on HPatches homographies and on ScanNet and MegaDepth pose. Before it, NCNet-style methods regularized 4D cost volumes with local convolutions; LoFTR replaced that local consensus with attention over the whole image.
As of November 2026, the official README points to QuadTreeAttention as a more efficient attention mechanism for LoFTR, and states that LoFTR is part of the kornia library from version 0.5.11. The DUSt3R article reports that MASt3R’s authors, on the Map-free benchmark, called LoFTR with KBR depth the second-best published method, although their table also lists other methods above it.
Variants
- LoFTR-DS and LoFTR-OT. The same network with the dual-softmax or the optimal-transport matching layer. Pretrained indoor and outdoor models of each were released (README, as of November 2026).
- Resolution (8, 2) and (16, 4). The official configuration accepts coarse and fine strides of 8 and 2, or 16 and 4. The second is the faster variant of Table 6.
- Detector-based counterpart. SuperPoint + SuperGlue keeps the detector and matches keypoint sets with attention and optimal transport. Feature matching summarizes it and LightGlue, its faster revision.
Implementations
- Official repository.
zju3dv/LoFTRis released under the Apache License 2.0 (as of November 2026). It contains inference and training code, with the configuration values cited above at commit df7ca80. The README links pretrained weights for the indoor and outdoor models with both matching layers, and the ScanNet and MegaDepth test sets. - kornia. According to the README, LoFTR is available as
kornia.feature.LoFTRfrom kornia 0.5.11.
Related
- SuperPoint
A fully convolutional network from Magic Leap (2018) that detects interest points and computes 256-dimensional descriptors for a whole image in one forward pass, trained without manual labels through synthetic pre-training and homographic adaptation.
- Learned Local Features
Keypoint detectors and local descriptors trained from data rather than designed by hand, from patch descriptors such as L2-Net and HardNet to networks that detect and describe in one pass such as D2-Net, and why a point that is detected again is not necessarily one that can be matched.
- Cross-Attention
Attention in which one set of tokens asks the questions and another set supplies the answers, so that image features can read a text prompt, learned object queries can read an image, or a small set of latents can summarize a large input; the output has one row per query and the cost grows with the product of the two set sizes.
- Heatmap Regression
Locating keypoints by training a network to output one image-like map per keypoint, usually a Gaussian bump at the true position, and reading the coordinate off the map; target design, resolution and quantization error, sub-pixel decoding (quarter shift, DARK, offsets), integral regression, and the coordinate alternatives DeepPose, RLE and SimCC.
- Cost Volume
A tensor of matching costs or similarities between each pixel of one image and its candidate correspondences in another, indexed by pixel position and candidate disparity, displacement, or depth.
- Visual Localization
Estimating the full 6-DoF position and orientation of a camera from an image, against a map of a known scene; structure-based 2D–3D matching with PnP and RANSAC, hierarchical localization, scene coordinate regression, absolute and relative pose regression and their documented limits, and the 7-Scenes, Cambridge Landmarks and Aachen Day-Night benchmarks with their metrics.
References
- Sun, J., Shen, Z., Wang, Y., Bao, H. & Zhou, X. (2021). LoFTR: Detector-Free Local Feature Matching with Transformers. IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR), 8918–8927.
- Sun, J., Shen, Z., Wang, Y., Bao, H. & Zhou, X. (2021). LoFTR: official implementation, src/loftr and src/config at commit df7ca80 (GitHub repository, Apache License 2.0).
- Sarlin, P.-E., DeTone, D., Malisiewicz, T. & Rabinovich, A. (2020). SuperGlue: Learning Feature Matching with Graph Neural Networks. IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR), 4937–4946 (not read).
- Katharopoulos, A., Vyas, A., Pappas, N. & Fleuret, F. (2020). Transformers are RNNs: Fast Autoregressive Transformers with Linear Attention. Proceedings of the 37th International Conference on Machine Learning (ICML), PMLR 119, 5156–5165 (not read).
- Wang, Q., Zhou, X., Hariharan, B. & Snavely, N. (2020). Learning Feature Descriptors Using Camera Pose Supervision. European Conference on Computer Vision (ECCV), Lecture Notes in Computer Science (not read).
- DeTone, D., Malisiewicz, T. & Rabinovich, A. (2018). SuperPoint: Self-Supervised Interest Point Detection and Description. IEEE/CVF Conference on Computer Vision and Pattern Recognition Workshops (CVPRW).