Part III: The ChESS response detector
ChESS (Chess-board Extraction by Subtraction and Summation,
Bennett & Lasenby, 2014; CVIU
118:197–210) is a ring-based corner detector specialized
for chessboard X-junctions. Given an 8-bit grayscale image, it produces
a dense response map; positive values mark chessboard-like corners,
while edges, blobs, and flat regions should have lower response on the
ideal checkerboard model. The benchmark chapter reports single-digit
millisecond timings for the measured camera-sized test images with the
simd or rayon features enabled.
For a self-contained overview of the algorithm, see the chess-corners atlas page on vitavision.dev.
This part covers the ChESS pipeline end-to-end: the ring geometry, the response formula, the dense response computation over the image, the detection pipeline (threshold + NMS + cluster filter + subpixel refinement), and the corner descriptor that both detectors (ChESS here and Radon in Part IV) feed into.
The core ChESS code lives under
crates/chess-corners-core/src/detect/chess/ and the descriptor code
under crates/chess-corners-core/src/orientation/.
Feature flags (std, rayon, simd, tracing) only affect
performance and observability, not the numerical output.
3.1 Rings and sampling geometry
The ChESS response is built around a fixed 16‑sample ring at a
given radius. The core crate encodes these rings in
crates/chess-corners-core/src/ring.rs.
3.1.1 Canonical rings
The main types are:
RingOffsets– an enum representing the supported ring radii (R5andR10).RING5/RING10– the actual offset tables for radius 5 and 10.ring_offsets(radius: u32)– helper returning the offset table for a given radius (anything other than 10 maps to 5).
The 16 offsets are ordered clockwise starting at the top, and are derived from the FAST‑16 pattern:
RING5is the canonicalr = 5ring used in the original ChESS paper.RING10is a scaled variant (r = 10) with the same angles, which improves robustness under heavier blur at the cost of a larger footprint and border margin.
The exact offsets are stored as integer (dx, dy) pairs, so sampling
around a pixel (x, y) means accessing (x + dx, y + dy) for each
ring point.
3.1.2 From parameters to rings
ChessParams in lib.rs controls which ring to use:
use_radius10– whentrue,ring_radius()returns 10 instead of 5.descriptor_use_radius10– optional override specifically for the descriptor ring; whenNone, it followsuse_radius10.
Convenience methods:
ring_radius()/descriptor_ring_radius()return the numeric radii.ring()/descriptor_ring()returnRingOffsetsvalues, which can be turned into offset tables viaoffsets().
The response path uses ring(), while descriptor estimation uses
descriptor_ring(). This allows you, for example, to detect corners
with a smaller ring but compute descriptors on a larger one.
3.2 Dense response computation
The main entry point in response.rs is:
#![allow(unused)]
fn main() {
pub fn chess_response_u8(img: &[u8], w: usize, h: usize, params: &ChessParams) -> ResponseMap
}
This function computes the ChESS response at each pixel center whose full ring fits entirely inside the image. Pixels that cannot support a full ring (near the border) get response zero.
3.2.1 ChESS formula
For each pixel center c, we gather 16 ring samples s[0..16) using
the offsets described in §3.1, and a small 5‑pixel cross at the
center:
- center
c, - north/south/east/west neighbors.
From these values we compute:
-
SR– a “square” term that compares opposite quadrants on the ring:SR = sum_{k=0..3} | (s[k] + s[k+8]) - (s[k+4] + s[k+12]) | -
DR– a “difference” term encouraging edge‑like structure:DR = sum_{k=0..7} | s[k] - s[k+8] | -
μₙ– the mean of all 16 ring samples. -
μₗ– the local mean of the 5‑pixel cross.
The final ChESS response is:
R = SR - DR - 16 * |μₙ - μₗ|
Intuitively:
SRis large when opposite quadrants have contrasting intensities (as in an X‑junction).DRis large for simple edges, and subtracting it de‑emphasizes edge‑like structures.|μₙ - μₗ|penalizes isolated blobs or local illumination changes.
High positive values of R correspond to chessboard‑like corners.
3.2.2 Implementation paths and borders
chess_response_u8 is implemented in a few interchangeable ways:
- Scalar sequential path (
compute_response_sequential/compute_row_range_scalar) – a straightforward nested loop over rows and columns. - Parallel path (
compute_response_parallel) – when therayonfeature is enabled, the outer loop is split across threads usingpar_chunks_mutover rows. - SIMD path (
compute_row_range_simd) – when thesimdfeature is enabled, the inner loop vectorizes overLANESpixels at a time, using portable SIMD to gather ring samples and accumulateSR,DR, andμₙin vector registers.
Regardless of the path, the function:
- respects a border margin equal to the ring radius so that all ring accesses are in bounds,
- writes responses into a
ResponseMap { w, h, data }in row‑major order, - keeps the scalar, parallel, and SIMD variants within the documented deterministic-output contract.
3.2.3 ROI support with Roi
For multiscale refinement, we rarely need the response over the entire image. Instead we compute it inside small regions of interest around coarse corner predictions.
The Roi struct:
#![allow(unused)]
fn main() {
pub struct Roi {
pub x0: usize,
pub y0: usize,
pub x1: usize,
pub y1: usize,
}
}
describes an axis‑aligned rectangle in image coordinates. A specialized function:
#![allow(unused)]
fn main() {
pub fn chess_response_u8_patch(
img: &[u8],
w: usize,
h: usize,
params: &ChessParams,
roi: Roi,
) -> ResponseMap
}
computes a response map only inside that ROI, treating the ROI as a small image with its own (0,0) origin. This is used in the multiscale pipeline to refine coarse corners without paying the cost of full‑frame response computation at the base resolution.
3.3 Detection pipeline
The response map is only the first half of the detector. The second
half—implemented in detect.rs—turns responses into subpixel corner
candidates.
3.3.1 Thresholding and NMS
The main stages are:
- Thresholding – we reject responses that are too small to be
meaningful. The paper’s contract is “any strictly positive
Ris a corner candidate”, which is what the default settings encode:- The default is
Threshold::Absolute(0.0)combined with a strictR > thrcomparison, i.e. accept iffR > 0. - Callers can opt into
Threshold::Relative(frac)(a fraction of the maximum response in the current frame) — useful as an adaptive policy on high‑contrast scenes where the raw positive‑response floor contains sensor noise. - Or tune the absolute threshold upward directly with
Threshold::Absolute(value)to suppress flat‑region noise without committing to a scene‑max policy.
- The default is
- Non‑maximum suppression (NMS) – in a window of radius
nms_radiusaround each pixel, we keep only local maxima and suppress weaker neighbors. - Cluster filtering – we require that each surviving corner have
at least
min_cluster_sizepositive‑response neighbors in its NMS window. This discards isolated noisy peaks that don’t belong to a structured corner.
The result of this stage is a set of raw corner candidates, each carrying:
- integer‑like peak position,
- response strength (before refinement).
3.3.2 Subpixel refinement
Each candidate is refined from its integer peak to a subpixel
position by one of the refiners in
Part V. The ChESS detector is selected via
DetectorConfig.refiner.kind; the default is CenterOfMass, which
operates directly on the response map, but any of the five refiners
can be used. Refinement is a per-candidate call and adds at most a
few tens of nanoseconds for the fastest options.
The internal type representing a refined candidate is
descriptor::Corner:
#![allow(unused)]
fn main() {
pub struct Corner {
/// Subpixel location in image coordinates (x, y).
pub xy: [f32; 2],
/// Raw ChESS response at the integer peak (before refinement).
pub strength: f32,
}
}
The refinement step preserves the detector’s noise robustness and adds subpixel precision. Measured accuracy and throughput for each refiner are in Part VIII.
3.4 Corner descriptors
Raw corners (position + strength) are enough for many applications,
but the core crate also offers a richer CornerDescriptor that fits
a parametric intensity model to the 16-sample ring around each corner.
The fit yields both local grid axes independently, their per-axis
1σ angular uncertainty, a bright/dark contrast amplitude, and the RMS
fit residual — all in one pass.
Both the ChESS detector and the Radon detector produce
CornerDescriptor values via the same describe_corners
function, so everything in this section applies to both pipelines.
3.4.1 CornerDescriptor
Defined in descriptor.rs:
#![allow(unused)]
fn main() {
pub struct CornerDescriptor {
pub x: f32,
pub y: f32,
pub response: f32,
pub contrast: f32,
pub fit_rms: f32,
pub axes: [AxisEstimate; 2],
}
pub struct AxisEstimate {
pub angle: f32,
pub sigma: f32,
}
}
Fields:
x,y– subpixel coordinates in full‑resolution image pixels.response– raw, unnormalized ChESS responseR = SR − DR − 16·MRat the detected peak. Units are 8‑bit pixel sums; the paper’s contract isR > 0.contrast– fitted bright/dark amplitude|A|in gray levels. Independent fromresponseand not comparable to it.fit_rms– root‑mean‑squared residual of the two‑axis intensity fit (gray levels). Smaller means the ring sampled cleanly through a chessboard‑like corner.axes[0],axes[1]– the two local grid axis directions and their 1σ uncertainties.
The axis convention:
axes[0].angle ∈ [0, π)— the “line direction” of axis 1.axes[1].angle ∈ (axes[0].angle, axes[0].angle + π).- Rotating CCW from
axes[0].angletowardaxes[1].angletraverses a dark sector; the second half‑turn crosses the other dark sector, and the remaining two sectors are bright. - The two axes are not assumed orthogonal — a projective warp (or strong lens distortion) tilts the two sectors independently.
3.4.2 Two‑axis intensity model
The ring samples s₀, …, s₁₅ at angles φ₀, …, φ₁₅ = atan2(dy, dx)
are fitted to
I(φ) = μ + A · tanh(β·sin(φ − θ₁)) · tanh(β·sin(φ − θ₂))
with fixed β = 4.0. The four free parameters are:
μ– ring‑level mean intensity,A– bright/dark amplitude (signed during optimization, canonicalized to non‑negative on exit),θ₁,θ₂– the two grid axis directions.
Intuition: each tanh(β · sin(φ − θᵢ)) is a smooth approximation of
sign(sin(φ − θᵢ)), i.e. +1 on one side of the axis line and −1 on
the other. Their product is +1 in two antipodal “bright” sectors and
−1 in the two “dark” sectors, matching a chessboard X‑junction. The
fixed β reflects the effective ring‑integration blur at the sampled
radius and is not a fit parameter.
3.4.3 Gauss–Newton solver
fit_two_axes runs a small Gauss–Newton iteration (up to 6 steps):
- Seed
θ₁,θ₂from the 2nd-harmonic Fourier coefficient of the centred ring samples, placed at the sector midpoint ± π/4. SeedAfrom the harmonic magnitude. The initial 90° spacing is only a seed — the two angles are independent free parameters during optimisation. - At each step, evaluate the residuals and the 16×4 Jacobian of
I(φᵢ)with respect to[μ, A, θ₁, θ₂]and solve the normal equationsJᵀJ · Δ = Jᵀ rwith partial pivoting. - Clamp angular updates to ±0.5 rad per step to prevent runaway.
- Stop once the update falls below
‖Δθ‖ < 10⁻⁴or the iteration cap is reached. - Canonicalize
(θ₁, θ₂, A)so thatA ≥ 0,θ₁ ∈ [0, π)and the CCW arc fromθ₁toθ₂spans a dark sector.
Flat or near‑flat rings (ring variance below 10⁻⁶, or 2nd‑harmonic
magnitude below 10⁻⁴) short‑circuit to a degenerate fit:
A = 0, θ₁ = 0, θ₂ = π/2, and σ = π on both axes so downstream
consumers can detect the “no signal” case via the uncertainty field.
3.4.4 Per‑axis 1σ uncertainty
The sigma field on each AxisEstimate is the standard 1σ angular
uncertainty from the linearised Gauss–Newton covariance at the
optimum:
- The sum of squared residuals is
SSR = Σᵢ (sᵢ − I(φᵢ))². - The unbiased residual variance is
σ̂² = SSR / (N − p) = SSR / (16 − 4) = SSR / 12. - The parameter covariance is
Σ = σ̂² · (JᵀJ)⁻¹, whereJᵀJis the final Gauss–Newton normal matrix. - The angle sigmas are the relevant diagonal entries:
σθ₁ = √Σ[2,2],σθ₂ = √Σ[3,3](clamped to ≥ 0, capped at π).
This is the textbook Cramér–Rao‑style uncertainty for nonlinear
least squares — it assumes residuals are approximately iid Gaussian
and the linearisation around the optimum is adequate. It does not
account for model mismatch (e.g. a corner that is not well described
by a separable two‑axis tanh product), but it scales correctly with
SNR: noisier rings produce proportionally larger sigma.
Practically, sigma is useful for:
- Weighting corners in downstream grid fitting (inverse‑variance weights, or rejecting corners whose axes are too uncertain).
- Flagging degenerate fits:
sigma ≈ πmeans the fit did not lock onto a well‑defined grid.
3.4.5 From corners to descriptors
The function:
#![allow(unused)]
fn main() {
pub fn describe_corners(
img: &[u8],
w: usize,
h: usize,
radius: u32,
corners: Vec<Corner>,
method: OrientationMethod,
) -> Vec<CornerDescriptor>
}
turns raw Corner values into full descriptors by:
- Sampling the 16‑point ring around each corner with bilinear
interpolation (
sample_ring). - Running
fit_two_axesto obtain(μ, A, θ₁, θ₂), the Gauss–Newton covariance, and the residual RMS. - Canonicalising the axes and packaging everything into a
CornerDescriptor.
The pass is deterministic and purely local — there is no global optimisation or topology reasoning at this stage.
3.4.6 When to use descriptors
You get CornerDescriptor values when you use the high‑level APIs:
chess-corners-coreusers can run the response and detector stages manually and then callchess_corners_core::describe_corners.chess-cornersusers getVec<CornerDescriptor>directly from theDetectorstruct’sdetect,detect_u8, ordetect_viewmethods.
For many tasks, x, y, and response are enough. When you need
more insight into local structure — grid fitting, lens‑distortion
modelling, calibration with per‑corner weights, or outlier rejection
before bundle adjustment — axes, sigma, contrast, and fit_rms
are the extra handles you get “for free” with each detection.
The axes, sigma_theta1, sigma_theta2, amp, and fit_rms
fields are produced by an orientation method shared with the Radon
detector. See Part VI: Orientation methods
for the API surface, the two available algorithms (RingFit and
DiskFit), and step-by-step descriptions of each.
Next: Part IV covers the alternative Radon response detector, which shares the descriptor pipeline above but uses a ray-based kernel instead of a ring.