TL;DR
SURF combines a Hessian-based detector with Haar-wavelet descriptors, using integral images to evaluate rectangular filters efficiently. A box response needs a fixed number of table lookups regardless of filter size; evaluating more pixels and scale layers still adds work. The original paper reports faster detection and competitive matching on its evaluation sequences. The 64-dimensional descriptor reduces storage and distance-computation cost relative to a 128-dimensional descriptor, without guaranteeing a universal speedup.
Context: Why SIFT Was Not Enough
By the mid-2000s, Lowe's SIFT (1999/2004) had established itself as the dominant local feature pipeline. It was accurate, scale-invariant, and rotation-invariant — but it was also slow. SIFT's Difference-of-Gaussians (DoG) detector requires building a full Gaussian scale-space pyramid by repeatedly convolving the image with Gaussian kernels of increasing \sigma, then computing differences between adjacent scales. Its 128-dimensional descriptor, built from histograms of oriented gradients, is expensive to compute and match.
For offline tasks like panorama stitching or 3D reconstruction from photo collections, SIFT's speed was acceptable. But real-time applications — visual SLAM, augmented reality, video stabilization — needed something faster. SURF's contribution was showing that careful approximations at every stage of the pipeline could deliver comparable robustness at a fraction of the compute cost.
Integral Images: The Computational Foundation
The integral image (also called a summed-area table) is the data structure that makes SURF's speed possible. For an input image I, the integral image I_\Sigma at position (x, y) stores the sum of all pixel values in the rectangular region from the origin to (x, y):
Once I_\Sigma is computed in a single pass over the image (O(N) for N pixels), the sum of pixel intensities within any axis-aligned rectangle can be computed in constant time O(1) using four lookups and three additions, regardless of the rectangle's size. This property is what allows SURF to evaluate large-scale box filters at the same cost as small ones — eliminating the need for iterative Gaussian blurring entirely.
Interest Point Detection: Fast-Hessian Detector
SURF detects interest points using the determinant of the Hessian matrix, which responds to blob-like structures in the image. For a point \mathbf{x} = (x, y) at scale \sigma, the Hessian is:
where L_{xx}, L_{yy}, and L_{xy} are second-order Gaussian derivatives (convolutions of the image with second derivatives of the Gaussian kernel). The determinant \det(\mathcal{H}) = L_{xx} L_{yy} - L_{xy}^2 is large at blob-like structures and is preferred over the Laplacian (used in SIFT's DoG) because it penalizes elongated structures.
The critical approximation: SURF replaces the Gaussian derivative filters with axis-aligned box filters. A 9 \times 9 box filter approximates the Gaussian second derivative at \sigma = 1.2. Because the box filters are rectangular, their convolution can be computed using the integral image in O(1) per pixel regardless of filter size. The approximated determinant includes a weighting factor to correct for the energy difference between the box filter and the true Gaussian:
where D_{xx}, D_{yy}, D_{xy} are the box filter responses and w = 0.9 compensates for the approximation error in the D_{xy} filter.
Scale-Space Representation
SIFT builds its scale space by progressively downsampling and blurring the image — a process that requires multiple convolutions and image resizings. SURF takes a fundamentally different approach: instead of reducing the image size, it increases the filter size. Since integral image lookups cost O(1) regardless of the box filter dimensions, a 27 \times 27 filter is no more expensive to evaluate than a 9 \times 9 one.
SURF organizes scales into octaves. The first octave uses filter sizes 9, 15, 21, 27; the second uses 15, 27, 39, 51; and so on, with each octave doubling the step between consecutive filter sizes. Interest points are localized in scale and space by finding 3D maxima of the Hessian determinant response across a 3 \times 3 \times 3 neighborhood (spatial and scale dimensions). Sub-pixel and sub-scale precision is then achieved by fitting a 3D quadratic to the response values around the maximum.
Filters act directly on the original image, so SURF avoids constructing a repeatedly blurred image pyramid. The paper also allows larger spatial sampling intervals in later octaves; using the original image does not mean every response is evaluated at every pixel. Detection cost still depends on image size, the number of layers, and sampling density.
Orientation Assignment
For rotation-invariant matching, each detected keypoint needs a canonical orientation. SURF computes Haar wavelet responses in the x and y directions within a circular neighborhood of radius 6s around the interest point, where s is the detected scale. These responses are weighted by a Gaussian centered on the keypoint (\sigma = 2.5s) to give more weight to nearby points.
A sliding orientation window of angular size \pi/3 is rotated around the circle, and within each window position, the horizontal and vertical wavelet responses are summed to form a local orientation vector. The orientation yielding the longest resultant vector is assigned as the keypoint's dominant orientation.
The paper also introduces U-SURF (Upright SURF), which skips orientation assignment entirely. This variant is faster and works well when the camera does not undergo significant in-plane rotation — common in many practical scenarios like autonomous driving or web image retrieval.
Descriptor Construction
The SURF descriptor encodes the local image region around each keypoint. A square region of size 20s is constructed around the interest point, aligned to the assigned orientation. This region is divided into a 4 \times 4 grid of sub-regions.
Within each sub-region, Haar wavelet responses d_x and d_y are computed at 5 \times 5 regularly spaced sample points (relative to the dominant orientation). For each sub-region, four values are accumulated:
The sums \sum d_x and \sum d_y capture the dominant direction of intensity change, while \sum |d_x| and \sum |d_y| capture the overall amount of activity in the sub-region. With 4 values per sub-region and 4 \times 4 = 16 sub-regions, the final descriptor is a 64-dimensional vector, normalized to unit length to cancel positive contrast scaling. Equal-area Haar differences cancel an additive brightness offset; these properties do not cover arbitrary lighting changes.
This is half the dimensionality of SIFT's 128-D descriptor, reducing the arithmetic per descriptor distance. SURF-128 instead computes the sums of dx and |dx| separately for dy < 0 and dy ≥ 0, and splits the dy and |dy| sums by the sign of dx. The sign of the Laplacian is a separate matching index, not the descriptor's sign-splitting rule (§4.2, p. 9).
Matching and Indexing
For descriptor matching, SURF uses the ratio of distances to the nearest and second-nearest neighbors (following Lowe's ratio test). The sign of the Laplacian (trace of the Hessian) computed during detection serves as a fast pre-filter: only features with the same Laplacian sign are compared, reducing the candidate pool by an amount that depends on the data. Its sign can be retained from the derivative responses already computed during detection; the paper does not guarantee a 50% reduction.
Implementation
The stages above can be checked on actual pixel sums before considering the paper's benchmark evidence. The reference below carries one seeded synthetic image through detection and upright description, while keeping orientation assignment separate so its boundary is explicit.
Offline CPU mechanics with real summed-area, box-filter, Haar, and matching computations on a seeded synthetic image. This example contains a complete integer-grid upright 64-D descriptor and a separate orientation helper. It is not full SURF: rotated descriptor sampling, subpixel refinement, scale interpolation, and later octaves are omitted. There are no checkpoints, external images, or benchmark speed claims.
1. Build the zero-padded integral image
Original paper
The authors sum the rectangle from the origin to each pixel.
Math and tensor shapes
The example adds a zero row and column so coordinates describe half-open rectangles. Two cumulative sums construct the table; import itself performs no computation. The same seeded 73 × 73 synthetic image drives the smoke pipeline.
Image [H, W] → float64 SAT [H + 1, W + 1]
Python implementation
def integral_image(image):
"""Return a float64 SAT with a zero row and column, shape [H+1, W+1]."""
image = _real_array(image)
if image.ndim != 2 or not image.size or not np.isfinite(image).all():
raise ValueError("Expected a finite, nonempty 2-D image.")
sat = np.zeros((image.shape[0] + 1, image.shape[1] + 1))
sat[1:, 1:] = image.cumsum(axis=0).cumsum(axis=1)
if not np.isfinite(sat).all():
raise ValueError("Integral-image accumulation overflowed.")
return sat2. Read a rectangle with four corners
Original paper
The lookup count does not depend on rectangle area.
Math and tensor shapes
Empty rectangles return zero, including at the bottom and right border. Reversed or out-of-image bounds fail explicitly. The public wrapper validates the table; the four-corner arithmetic itself has constant lookup cost, not a promise about total detector runtime.
Bounds [y0:y1, x0:x1] → scalar sum
Python implementation
def _rectangle(sat, y0, x0, y1, x1):
"""Read four corners; callers establish valid half-open bounds."""
return float(sat[y1, x1] - sat[y0, x1] - sat[y1, x0] + sat[y0, x0])
def rectangle_sum(sat, y0, x0, y1, x1):
"""Sum [y0:y1, x0:x1], accepting borders and empty rectangles."""
sat = _validate_sat(sat)
if not all(_integer(v) for v in (y0, x0, y1, x1)):
raise ValueError("Rectangle coordinates must be integers.")
if not (0 <= y0 <= y1 < sat.shape[0] and 0 <= x0 <= x1 < sat.shape[1]):
raise ValueError(
"Rectangle lies outside the image or has reversed bounds."
)
return _rectangle(sat, y0, x0, y1, x1)3. Measure normalized box-Hessian responses
Original paper
Box filters approximate second derivatives; 0.9 balances Dxy.
Math and tensor shapes
Dxx uses three lobes weighted +1, −2, +1; Dyy transposes them. Dxy has four signed quadrants separated by a zero row and column. Dividing by f² normalizes filter area. The independent tests compare each size with dense masks applied directly to image pixels.
SAT + center + filter size f → three responses, h, sign
Python implementation
def box_hessian(sat, y, x, size):
"""Return normalized Dxx, Dyy, Dxy, determinant, and trace sign.
Odd lobes reproduce Figure 1: Dxx has three vertical lobes with weights
+1,-2,+1 and height 2*lobe-1; Dyy is its transpose. Dxy uses four lobe-sized
quadrants separated by a zero center row/column. Divide responses by size².
"""
sat = _validate_sat(sat)
if not _integer(size) or size < 9 or size % 6 != 3:
raise ValueError("Filter size must be 9, 15, 21, 27, ... .")
if not _integer(y) or not _integer(x):
raise ValueError("Filter centers must be integer coordinates.")
lobe, border = size // 3, size // 2
if not (
border <= y < sat.shape[0] - 1 - border
and border <= x < sat.shape[1] - 1 - border
):
raise ValueError("The full box filter must fit inside the image.")
def r(y0, x0, y1, x1):
return _rectangle(sat, y0, x0, y1, x1)
dxx = r(y - lobe + 1, x - border, y + lobe, x + border + 1)
dxx -= 3 * r(y - lobe + 1, x - lobe // 2, y + lobe, x + lobe // 2 + 1)
dyy = r(y - border, x - lobe + 1, y + border + 1, x + lobe)
dyy -= 3 * r(y - lobe // 2, x - lobe + 1, y + lobe // 2 + 1, x + lobe)
dxy = r(y - lobe, x - lobe, y, x) + r(
y + 1, x + 1, y + lobe + 1, x + lobe + 1
)
dxy -= r(y - lobe, x + 1, y, x + lobe + 1) + r(
y + 1, x - lobe, y + lobe + 1, x
)
dxx, dyy, dxy = np.array([dxx, dyy, dxy]) / size**2
determinant = dxx * dyy - (0.9 * dxy) ** 2
return (
float(dxx),
float(dyy),
float(dxy),
float(determinant),
int(np.sign(dxx + dyy)),
)4. Keep strict space-and-scale maxima
Original paper
The paper interpolates maxima after suppression; this example stops before that step.
Math and tensor shapes
Only the first octave is evaluated. Invalid filter borders are NaN, and a candidate needs all 27 valid values. Ties and boundary layers are rejected. Detector scale is reported separately from the rounded integer scale used by the descriptor. Subpixel refinement and scale interpolation are omitted.
Responses/signs [4, H, W] → (level, y, x, value, sign) list
Python implementation
def response_volume(sat):
"""Compute first-octave responses at every valid pixel, with NaN borders."""
sat = _validate_sat(sat)
height, width = sat.shape[0] - 1, sat.shape[1] - 1
responses = np.full((len(FILTER_SIZES), height, width), np.nan)
signs = np.zeros(responses.shape, dtype=np.int8)
for level, size in enumerate(FILTER_SIZES):
border = size // 2
for y in range(border, height - border):
for x in range(border, width - border):
*_, determinant, sign = box_hessian(sat, y, x, size)
responses[level, y, x] = determinant
signs[level, y, x] = sign
return responses, signs
def scale_maxima(responses, signs, threshold=1e-5):
"""Return strict interior 3x3x3 maxima as (level, y, x, value, sign)."""
responses, signs = np.asarray(responses), np.asarray(signs)
if (
responses.ndim != 3
or responses.shape[0] != len(FILTER_SIZES)
or signs.shape != responses.shape
or responses.dtype.kind not in "iuf"
or not np.isin(signs, [-1, 0, 1]).all()
or np.isinf(responses).any()
or not math.isfinite(threshold)
or threshold < 0
):
raise ValueError(
"Expected four response layers, matching signs, and a finite threshold >= 0."
)
maxima = []
for level in range(1, len(FILTER_SIZES) - 1):
for y in range(1, responses.shape[1] - 1):
for x in range(1, responses.shape[2] - 1):
value = responses[level, y, x]
neighbors = responses[
level - 1 : level + 2, y - 1 : y + 2, x - 1 : x + 2
].ravel()
others = np.delete(neighbors, 13)
if (
np.isfinite(neighbors).all()
and value > threshold
and np.all(value > others)
):
maxima.append(
(level, y, x, float(value), int(signs[level, y, x]))
)
return sorted(maxima, key=lambda point: -point[3])5. Compute signed Haar wavelets
Original paper
Orientation uses wavelets of size 4s; descriptor samples use 2s.
Math and tensor shapes
The full square must fit in the image; the example neither clips support nor pads image intensities. Positive x points right, and positive y points down. Equal-area subtraction cancels an additive intensity offset when support is complete.
Even square size + integer center → (dx, dy)
Python implementation
def haar_response(sat, y, x, size):
"""Return right-minus-left dx and bottom-minus-top dy for an even square."""
sat = _validate_sat(sat)
if (
not _integer(size)
or size < 2
or size % 2
or not _integer(y)
or not _integer(x)
):
raise ValueError(
"Haar size must be positive and even; center must be integer."
)
half = size // 2
if not (
half <= y <= sat.shape[0] - 1 - half
and half <= x <= sat.shape[1] - 1 - half
):
raise ValueError("The full Haar wavelet must fit inside the image.")
dx = _rectangle(sat, y - half, x, y + half, x + half)
dx -= _rectangle(sat, y - half, x - half, y + half, x)
dy = _rectangle(sat, y, x - half, y + half, x + half)
dy -= _rectangle(sat, y - half, x - half, y, x + half)
return dx, dy6. Estimate orientation in a separate helper
Original paper
Sum weighted response vectors within an angular window and select the longest resultant.
Math and tensor shapes
Candidate starts at response angles enumerate the distinct maximal membership sets, including wraparound. Zero energy returns angle zero; equal resultants keep the first candidate. The descriptor below remains upright: this helper does not rotate sample locations or wavelet responses and supplies no rotation-invariance guarantee.
Radius 6s, step s, 4s wavelets → radians in [0, 2π)
Python implementation
def orientation_from_responses(vectors):
"""Choose the longest sum in a half-open pi/3 angular window.
Candidate starts at each nonzero response angle enumerate distinct maximal
membership sets, including wraparound; first maximal candidate breaks ties.
"""
vectors = _real_array(vectors)
if (
vectors.ndim != 2
or vectors.shape[1] != 2
or not np.isfinite(vectors).all()
):
raise ValueError("Expected finite wavelet vectors [N, 2].")
vectors = vectors[np.any(vectors != 0, axis=1)]
if not len(vectors):
return 0.0
angles = np.mod(np.arctan2(vectors[:, 1], vectors[:, 0]), 2 * math.pi)
best, length = np.zeros(2), -1.0
for start in angles:
delta = np.mod(angles - start, 2 * math.pi)
resultant = vectors[delta < math.pi / 3].sum(axis=0)
squared_length = float(resultant @ resultant)
if squared_length > length:
best, length = resultant, squared_length
return float(math.atan2(best[1], best[0]) % (2 * math.pi))
def dominant_orientation(sat, y, x, scale):
"""Sample radius 6s at step s with 4s Haar filters and sigma 2.5s."""
if not _integer(scale) or scale < 1:
raise ValueError(
"This integer-grid helper requires integer scale >= 1."
)
vectors = []
for iy in range(-6, 7):
for ix in range(-6, 7):
if ix**2 + iy**2 <= 36:
dx, dy = haar_response(
sat, y + iy * scale, x + ix * scale, 4 * scale
)
weight = math.exp(-(ix**2 + iy**2) / (2 * 2.5**2))
vectors.append([weight * dx, weight * dy])
return orientation_from_responses(vectors)7. Accumulate the complete upright 64-D descriptor
Original paper
Each of 16 cells accumulates four Gaussian-weighted sums from 25 samples.
Math and tensor shapes
Samples use offsets −10s through 9s, wavelets of size 2s, and a Gaussian with σ = 3.3s centered on the keypoint. A zero-energy patch returns 64 finite zeros. Positive contrast scaling changes every raw sum proportionally, then normalization cancels it. These are tested photometric properties, not general illumination or rotation invariance.
4 × 4 cells × 5 × 5 samples → raw [64], normalized [64]
Python implementation
def upright_descriptor(sat, y, x, scale):
"""Return a complete upright 64-D descriptor and its unnormalized sums.
Integer scale s: samples use offsets [-10s, ..., 9s] in both axes. Each of
4x4 cells has 5x5 samples; Haar size is 2s, Gaussian sigma is 3.3s. No rotation
of coordinates or responses is applied. All wavelets must lie inside the
image; a zero-energy patch returns a finite zero descriptor.
"""
if not _integer(scale) or scale < 1:
raise ValueError("The upright descriptor requires integer scale >= 1.")
cells = np.zeros((4, 4, 4))
for row in range(4):
for col in range(4):
for iy in range(5):
for ix in range(5):
oy, ox = (5 * row + iy - 10) * scale, (
5 * col + ix - 10
) * scale
dx, dy = haar_response(sat, y + oy, x + ox, 2 * scale)
weight = math.exp(
-(ox**2 + oy**2) / (2 * (3.3 * scale) ** 2)
)
cells[row, col] += weight * np.array(
[dx, dy, abs(dx), abs(dy)]
)
raw = cells.ravel()
norm = float(np.linalg.norm(raw))
descriptor = raw / norm if norm > 0 else np.zeros(64)
return descriptor, raw8. Gate by trace sign and reject ambiguous neighbors
Original paper
Laplacian sign indexes matching; it is separate from SURF-128 sign splitting.
Math and tensor shapes
Only same-sign targets enter the distance ranking, and at least two are needed for the ratio test. Equal nearest distances, including zero/zero, fail the strict test. This example declares ρ = 0.8. The synthetic contrast-changed descriptor is compared with a real descriptor from another patch, without implying benchmark matching quality.
Queries [N, 64], targets [M, 64], signs → correspondence list
Python implementation
def match_descriptors(queries, query_signs, targets, target_signs, ratio=0.8):
"""Return (query, target, ratio) after same-sign gating and strict ratio test.
At least two eligible targets are required. Equal nearest distances and
zero/zero ties are rejected. The threshold 0.8 is a declared example choice,
not a guaranteed paper operating point. Distances are Euclidean, not squared.
"""
queries, targets = _real_array(queries), _real_array(targets)
query_signs, target_signs = np.asarray(query_signs), np.asarray(
target_signs
)
for descriptors, signs in [(queries, query_signs), (targets, target_signs)]:
if (
descriptors.ndim != 2
or descriptors.shape[1] != 64
or signs.shape != (len(descriptors),)
or not np.isfinite(descriptors).all()
or not np.isin(signs, [-1, 0, 1]).all()
):
raise ValueError(
"Expected finite [N,64] descriptors and N trace signs."
)
if not math.isfinite(ratio) or not 0 < ratio < 1:
raise ValueError(
"Ratio threshold must lie strictly between zero and one."
)
matches = []
for i, query in enumerate(queries):
eligible = np.flatnonzero(target_signs == query_signs[i])
if len(eligible) < 2:
continue
distances = np.linalg.norm(targets[eligible] - query, axis=1)
order = np.argsort(distances, kind="stable")
first, second = distances[order[:2]]
if first < ratio * second:
matches.append((i, int(eligible[order[0]]), float(first / second)))
return matchesRun the canonical source behind these highlighted excerpts. The smoke output reports the actual source SHA-256, seed, image and response shapes, detected maxima, descriptor norm, contrast errors, orientation, correspondences, and measured CPU time. Timing includes Python loops and validation; it does not reproduce the authors’ optimized release.
surf.pyrequirements-surf.txtAuthors’ release page
python -m pip install -r requirements-surf.txt python surf.py --smoke --seed 7
Key Results
The paper evaluates SURF against SIFT, GLOH (Gradient Location and Orientation Histogram), and several other descriptors using the standard benchmark of Mikolajczyk and Schmid (2005).
Speed: In the original evaluation, Fast-Hessian detection is more than three times faster than DoG and five times faster than Hessian-Laplace. These are detector comparisons, not a fivefold SURF-versus-SIFT descriptor claim. U-SURF skips orientation assignment. Timing depends on implementation, image content, and hardware; the synthetic CPU example above reproduces none of these benchmark timings (§5, p. 10).
Repeatability: The detector comparisons shown in the paper cover Graffiti and Wall viewpoint changes, Boat zoom and rotation, and Leuven lighting changes. Fast-Hessian is comparable on several sequences and better on Wall. This selected evidence does not establish an unconditional scale or rotation guarantee (§5, pp. 9–10).
Descriptor performance: The paper compares recall versus one minus precision across selected evaluation pairs. On the Graffiti comparison in Figure 4, SURF-128 is the strongest tested SURF variant. Descriptor dimensions, binning, and matching criteria all influence the curves; these experiments do not guarantee that one descriptor wins for every scene (§4.2, pp. 8–9).
Critical Analysis
Strengths:
- Constant-time filtering via integral images is the paper's central insight. It decouples filter size from computational cost, enabling direct evaluation of different filter sizes on the input image. The complete detector still pays for each sampled location and scale layer.
- The 64-D descriptor strikes a practical balance between distinctiveness and matching speed. A shorter vector reduces arithmetic per Euclidean distance; total matching time also depends on indexing and the candidate pool.
- U-SURF recognizes that full rotation invariance is unnecessary in many real-world applications and provides a principled faster alternative.
Limitations:
- Box filter approximation fidelity degrades at larger scales. The rectangular approximation of circular Gaussian derivatives introduces systematic errors that affect detection stability, particularly for features at coarse scales.
- U-SURF is not rotation invariant. In applications involving significant in-plane rotation (aerial imagery, robotics), the full orientation assignment is required, reducing the speed advantage.
- Less distinctive than SIFT under viewpoint changes. The Haar wavelet-based descriptor captures less fine-grained gradient information than SIFT's oriented gradient histograms, leading to higher false-match rates under perspective distortion.
- Axis-aligned box filters make the detector fundamentally less isotropic than Gaussian-based approaches. The rectangular kernels introduce orientation-dependent biases in the Hessian response.
- Superseded by learned features. Methods like SuperPoint (DeTone et al. 2018) and LoFTR (Sun et al. 2021) learn both detection and description end-to-end, outperforming handcrafted pipelines like SURF on modern benchmarks.
Impact and Legacy
SURF's practical impact was substantial. It made real-time local feature matching feasible on consumer hardware, enabling applications in augmented reality (e.g., Vuforia), visual odometry, and mobile image retrieval. OpenCV's inclusion of SURF (until patent-related removal) made it one of the most widely deployed feature detectors in industry.
Historically, SURF sits at an important transition point in computer vision. Together with SIFT, it represents the peak of handcrafted feature engineering — algorithms designed from first principles using signal processing and differential geometry. The subsequent shift toward learned features (LIFT, SuperPoint, D2-Net, R2D2) replaced these handcrafted pipelines with neural networks trained end-to-end, achieving better performance with less manual design. SURF's emphasis on computational efficiency, however, continues to influence the design of learned feature methods, many of which explicitly target real-time operation.
The integral image technique itself has found applications well beyond SURF, including in face detection (Viola-Jones), pedestrian detection, and as a building block in various convolutional architectures.
Related Reading
- DETR — end-to-end object detection with transformers, illustrating how learned features replaced handcrafted pipelines
- Faster R-CNN — region-based detection that moved from handcrafted features to learned feature hierarchies
- YOLO — real-time detection that shares SURF's emphasis on speed over theoretical purity
- Deep Residual Learning — the backbone architecture that enabled learned feature extractors to surpass handcrafted methods
- SAM — modern promptable segmentation showing how far vision has moved from keypoint-based representations
