Skip to main content

chess_corners_core/detect/
mod.rs

1//! Feature-detection pipelines.
2//!
3//! Two independent detector families share a common output type:
4//!
5//! - [`chess`] — the ChESS (Chess-board Extraction by Subtraction and
6//!   Summation) detector: 16-sample ring kernel, NMS, cluster filtering,
7//!   pluggable subpixel refinement.
8//! - [`radon`] — the Duda-Frese (2018) localized Radon detector:
9//!   integral-image (SAT) ray sums, peak detection, Gaussian peak fit.
10//!
11//! Both families produce the same [`Corner`] / [`CornerDescriptor`]
12//! values, then go through the orthogonal subpixel-refinement and
13//! orientation-fit stages defined in [`crate::refine`] and
14//! [`crate::orientation`].
15
16pub mod chess;
17pub mod dense;
18pub mod radon;
19
20pub(crate) use chess::detect::{count_positive_neighbors, is_local_max};
21pub use chess::detect::{
22    detect_corners_from_response, detect_corners_from_response_with_refiner,
23    detect_peaks_from_response, find_corners_u8, find_corners_u8_with_refiner,
24    merge_corners_simple, refine_corners_on_image,
25};
26pub use dense::{ChessBuffers, ChessDetector, DenseDetector, RadonDetector};
27
28/// A detected corner candidate (subpixel position with raw response strength).
29#[derive(Clone, Debug)]
30#[non_exhaustive]
31pub struct Corner {
32    /// Subpixel x coordinate in image pixels.
33    pub x: f32,
34    /// Subpixel y coordinate in image pixels.
35    pub y: f32,
36    /// Raw detector response at the integer peak (before refinement).
37    pub strength: f32,
38}
39
40impl Corner {
41    /// Construct a [`Corner`].
42    #[inline]
43    pub fn new(x: f32, y: f32, strength: f32) -> Self {
44        Self { x, y, strength }
45    }
46}
47
48/// Direction of one local grid axis with its 1σ angular uncertainty.
49#[derive(Clone, Copy, Debug)]
50#[non_exhaustive]
51pub struct AxisEstimate {
52    /// Axis direction, radians in `[0, 2π)`.
53    ///
54    /// See [`CornerDescriptor`] for the joint polarity convention.
55    pub angle: f32,
56    /// 1σ angular uncertainty (radians), from the fit's covariance.
57    pub sigma: f32,
58}
59
60impl AxisEstimate {
61    /// Construct an [`AxisEstimate`].
62    #[inline]
63    pub fn new(angle: f32, sigma: f32) -> Self {
64        Self { angle, sigma }
65    }
66}
67
68/// Describes a detected chessboard corner in full-resolution image coordinates.
69///
70/// # Axis polarity convention
71///
72/// Local chessboard corner intensity patterns have exact 180° symmetry,
73/// so assigning an absolute `[0, 2π)` direction to any single axis ray
74/// is not possible from ring-local data. Instead the two axes are
75/// reported jointly:
76///
77/// - `axes[0].angle` lies in `[0, π)` — the "line direction" of axis 1.
78/// - `axes[1].angle` lies in `(axes[0].angle, axes[0].angle + π) ⊂ [0, 2π)`.
79///
80/// Together they satisfy: rotating CCW (in the usual `atan2(dy, dx)`
81/// sense — note: in image pixel coordinates with y-axis pointing down,
82/// this is a clockwise visual rotation) from `axes[0].angle` toward
83/// `axes[1].angle` traverses a **dark** sector of the corner. The
84/// second half-turn (`axes[0].angle + π → axes[1].angle + π`) crosses
85/// the second dark sector; the two remaining sectors are bright.
86///
87/// Each axis direction is signed as a `f32` in `[0, 2π)`; the axes are
88/// **not** assumed orthogonal (holds up under projective warp).
89///
90/// All [`crate::orientation::OrientationMethod`] variants emit axes
91/// under this same convention, so consumers may compare `axes[0]`
92/// (e.g. for slot-parity matching between cardinal grid neighbours)
93/// across methods without method-aware translation.
94#[derive(Clone, Copy, Debug)]
95#[non_exhaustive]
96pub struct CornerDescriptor {
97    /// Subpixel position in full-resolution image pixels.
98    pub x: f32,
99    /// Subpixel y position in full-resolution image pixels.
100    pub y: f32,
101
102    /// Raw, **unnormalized** detector response at the detected peak. For
103    /// the ChESS path this is `R = SR − DR − 16·MR`
104    /// (see [`chess::response::chess_response_u8`]). Units are 8-bit
105    /// pixel sums; data-dependent. Do not interpret it as a probability,
106    /// a contrast, or a normalized strength.
107    pub response: f32,
108
109    /// Bright/dark amplitude (`|A|`, ≥ 0) recovered by the two-axis
110    /// orientation fit (see [`crate::orientation`]). Units are gray
111    /// levels. Larger means a stronger bright/dark separation at the
112    /// ring radius. This is an independent quantity from
113    /// [`Self::response`] — they are computed by different estimators
114    /// and must not be compared against each other or against the same
115    /// threshold.
116    pub contrast: f32,
117
118    /// RMS fit residual of the two-axis intensity model (gray levels).
119    /// Smaller = tighter match to an ideal chessboard corner.
120    pub fit_rms: f32,
121
122    /// The two local grid axis directions with per-axis 1σ precision.
123    pub axes: [AxisEstimate; 2],
124}
125
126impl CornerDescriptor {
127    /// Construct a [`CornerDescriptor`].
128    #[inline]
129    #[allow(clippy::too_many_arguments)]
130    pub fn new(
131        x: f32,
132        y: f32,
133        response: f32,
134        contrast: f32,
135        fit_rms: f32,
136        axes: [AxisEstimate; 2],
137    ) -> Self {
138        Self {
139            x,
140            y,
141            response,
142            contrast,
143            fit_rms,
144            axes,
145        }
146    }
147}