Goal
Detect corners in a grayscale image on pixel domain . Output: a set of integer pixel locations at which intensity varies strongly in two independent directions. The detector responds to local image structure where both principal curvatures of the local autocorrelation function are large — flat regions and edges are suppressed.
Algorithm
Let denote the image derivatives in the horizontal and vertical directions, approximated by central differences or Sobel operators. Let denote a Gaussian window with standard deviation :
Let denote the eigenvalues of at a given pixel.
The symmetric matrix summarising gradient covariance in a local neighbourhood.
with entries , , , so that and .
A rotation-invariant corner score expressed in terms of and to avoid explicit eigendecomposition.
where is an empirical sensitivity constant, typically in the range –. in corner regions, on edges, and small in flat regions.
Procedure
- Compute and across the image via central differences or Sobel operators.
- Form the per-pixel products , , and .
- Convolve each product with the Gaussian window to obtain the entries , , of at every pixel.
- Compute at every pixel.
- Discard pixels with .
- Apply non-maximum suppression: retain only pixels whose value is an 8-way local maximum.
Implementation
The per-pixel response computation in Rust:
fn harris_response(img: &[f32], w: usize, h: usize, sigma: f32, k: f32) -> Vec<f32> {
// Step 1: gradients via central differences
let (ix, iy) = gradients(img, w, h);
// Step 2: per-pixel products
let ixx: Vec<f32> = ix.iter().zip(&ix).map(|(a, b)| a * b).collect();
let iyy: Vec<f32> = iy.iter().zip(&iy).map(|(a, b)| a * b).collect();
let ixy: Vec<f32> = ix.iter().zip(&iy).map(|(a, b)| a * b).collect();
// Step 3: Gaussian-blur each component to obtain M's entries A, B, C
let a = gaussian_blur(&ixx, w, h, sigma);
let b = gaussian_blur(&iyy, w, h, sigma);
let c = gaussian_blur(&ixy, w, h, sigma);
// Step 4: R = det(M) - k * tr(M)^2 per pixel
a.iter().zip(&b).zip(&c)
.map(|((&aa, &bb), &cc)| (aa * bb - cc * cc) - k * (aa + bb).powi(2))
.collect()
}
gradients and gaussian_blur are standard helpers; gradients returns central-difference derivatives and gaussian_blur applies a separable Gaussian kernel. Each line of the kernel corresponds directly to a step in the Procedure above.
Remarks
- Complexity: per scale — gradient computation, three convolutions (one per product , , ), and a fixed scalar arithmetic pass per pixel. Separable Gaussian convolutions vectorize over rows.
- The sensitivity constant is typically in 0.04–0.06. The response is a continuous function of ; smaller values suppress edge responses less aggressively.
- The response is rotation-invariant by construction: and are both rotation-invariant quantities.
- The detector is not scale-invariant. Response peaks at the scale of the integration window . Multi-scale detection requires running the detector on a Gaussian image pyramid.
- The detector responds to general 2D intensity variation; it does not encode X-junction geometry. For chessboard calibration targets, a domain-specific detector yields higher selectivity.
References
- C. Harris, M. J. Stephens. A Combined Corner and Edge Detector. Alvey Vision Conference, 1988. DOI: 10.5244/c.2.23
- J. Shi, C. Tomasi. Good Features to Track. IEEE CVPR, 1994. DOI: 10.1109/CVPR.1994.323794