Goal
Detect chessboard X-junctions in a grayscale image. Input: an image on pixel domain . Output: a set of integer pixel locations at which the local neighborhood matches an alternating bright-dark quadrant pattern. The detector is specific to X-junctions and rejects generic corners, straight edges, and narrow stripes.
Algorithm
Sampling is done at fixed integer offsets — no interpolation. Around a candidate pixel , 16 ring samples are read at the offsets
arranged cyclically around the ring so that consecutive indices are adjacent on the ring and index is opposite . The offsets are the FAST-16 pattern scaled to radius 5. The angular spacing alternates between 21.8° and 23.2°, close to the ideal 22.5°. Indices are taken modulo 16.
Two local means anchor the response. The local mean averages the 5-pixel cross at the candidate:
The neighbour mean averages the 16 ring samples:
Three per-pixel responses follow.
Large when the ring shows the two-cycle alternation of a true X-junction.
Large on straight edges, where opposite samples disagree.
Separates X-junctions from narrow stripes by comparing the two means.
The detector score. The factor 16 zeros out the degenerate case where only and are bright — a narrow stripe — making there.
Procedure
- For every pixel at distance from the image border, compute from the 16 ring samples and the 5-pixel cross.
- Positive threshold. Discard pixels with .
- Non-maximum suppression. In a small neighborhood (typically or ), keep only local maxima of .
- Response connectivity. Discard isolated positive-response pixels; a true X-junction produces a connected cluster.
- Neighbourhood comparison. Suppress maxima whose magnitude is small relative to nearby maxima, using an application-specific threshold.
Subpixel localization, orientation recovery, and multiscale detection via a Gaussian pyramid are standard extensions, not part of the base detector.
Implementation
The per-pixel response in Rust, given a row-major image:
const RING5: [(i32, i32); 16] = [
(0, -5), (2, -5), (3, -3), (5, -2), (5, 0), (5, 2), (3, 3), (2, 5),
(0, 5), (-2, 5), (-3, 3), (-5, 2), (-5, 0), (-5, -2), (-3, -3), (-2, -5),
];
fn chess_response(img: &[u8], w: usize, x: i32, y: i32) -> f32 {
let mut s = [0i32; 16];
for k in 0..16 {
let (dx, dy) = RING5[k];
s[k] = img[((y + dy) as usize) * w + (x + dx) as usize] as i32;
}
let mut sr = 0i32;
for k in 0..4 {
sr += ((s[k] + s[k + 8]) - (s[k + 4] + s[k + 12])).abs();
}
let mut dr = 0i32;
for k in 0..8 {
dr += (s[k] - s[k + 8]).abs();
}
let mu_n = s.iter().sum::<i32>() as f32 / 16.0;
let c = |dx: i32, dy: i32| img[((y + dy) as usize) * w + (x + dx) as usize] as f32;
let mu_l = (c(0, 0) + c(-1, 0) + c(1, 0) + c(0, -1) + c(0, 1)) / 5.0;
(sr - dr) as f32 - 16.0 * (mu_n - mu_l).abs()
}
The 16 ring accesses and 5 cross accesses are fixed offsets, so the hot loop compiles to straight-line integer code with no branches and no interpolation. The full response map is computed by running chess_response over every pixel at distance from the border; on wide images the outer loop vectorizes cleanly over rows.
Remarks
- Complexity: per scale — 16 ring reads, 5 cross reads, and a fixed number of integer additions per pixel. No trigonometry, no interpolation.
- Selective for chessboard-like X-junctions; not a general-purpose corner detector.
- Ring radius is fixed to 5 pixels in the canonical design. For heavy blur or low-resolution targets, the paper also defines a radius-10 ring with the same angular pattern.
- Orientation is ambiguous from ring samples alone — the response is symmetric under rotation by . Orientation is recovered post-detection by maximizing the signed measure over the eight discrete directions.
References
- S. Bennett, J. Lasenby. ChESS — Quick and Robust Detection of Chess-board Features. arXiv.5491, 2013. arxiv.org/abs/1301.5491
- E. Rosten, T. Drummond. Machine Learning for High-Speed Corner Detection. ECCV, 2006. — origin of the FAST-16 sampling pattern adopted for the ring.