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;
18mod merge;
19mod neighbors;
20pub mod radon;
21
22pub use chess::detect::{
23    detect_corners_from_response, detect_corners_from_response_with_refiner,
24    detect_peaks_from_response_with_refine_radius, find_corners_u8, refine_corners_on_image,
25};
26pub use merge::merge_corners_simple;
27pub(crate) use neighbors::{count_positive_neighbors, is_local_max};
28
29/// A detected corner candidate (subpixel position with raw response strength).
30#[derive(Clone, Debug)]
31#[non_exhaustive]
32pub struct Corner {
33    /// Subpixel x coordinate in image pixels.
34    pub x: f32,
35    /// Subpixel y coordinate in image pixels.
36    pub y: f32,
37    /// Raw detector response at the integer peak (before refinement).
38    pub strength: f32,
39}
40
41impl Corner {
42    /// Construct a [`Corner`].
43    #[inline]
44    pub fn new(x: f32, y: f32, strength: f32) -> Self {
45        Self { x, y, strength }
46    }
47}
48
49/// Direction of one local grid axis with its 1σ angular uncertainty.
50#[derive(Clone, Copy, Debug)]
51#[non_exhaustive]
52pub struct AxisEstimate {
53    /// Axis direction, radians in `[0, 2π)`.
54    ///
55    /// See [`CornerDescriptor`] for the joint polarity convention.
56    pub angle: f32,
57    /// 1σ angular uncertainty (radians), from the fit's covariance.
58    pub sigma: f32,
59}
60
61impl AxisEstimate {
62    /// Construct an [`AxisEstimate`].
63    #[inline]
64    pub fn new(angle: f32, sigma: f32) -> Self {
65        Self { angle, sigma }
66    }
67}
68
69/// Describes a detected chessboard corner in full-resolution image coordinates.
70///
71/// # Axis polarity convention
72///
73/// Local chessboard corner intensity patterns have exact 180° symmetry,
74/// so assigning an absolute `[0, 2π)` direction to any single axis ray
75/// is not possible from ring-local data. Instead the two axes are
76/// reported jointly:
77///
78/// - `axes[0].angle` lies in `[0, π)` — the "line direction" of axis 1.
79/// - `axes[1].angle` lies in `(axes[0].angle, axes[0].angle + π) ⊂ [0, 2π)`.
80///
81/// Together they satisfy: rotating CCW (in the usual `atan2(dy, dx)`
82/// sense — note: in image pixel coordinates with y-axis pointing down,
83/// this is a clockwise visual rotation) from `axes[0].angle` toward
84/// `axes[1].angle` traverses a **dark** sector of the corner. The
85/// second half-turn (`axes[0].angle + π → axes[1].angle + π`) crosses
86/// the second dark sector; the two remaining sectors are bright.
87///
88/// Each axis direction is signed as a `f32` in `[0, 2π)`; the axes are
89/// **not** assumed orthogonal (holds up under projective warp).
90///
91/// All [`crate::OrientationMethod`] variants emit axes
92/// under this same convention, so consumers may compare `axes[0]`
93/// (e.g. for slot-parity matching between cardinal grid neighbours)
94/// across methods without method-aware translation.
95///
96/// # Orientation skipped
97///
98/// [`axes`](Self::axes) is `None` when the per-corner orientation fit was
99/// skipped — the dominant per-corner cost. Consumers that derive board
100/// geometry themselves can disable it (the orientation method is `None` in
101/// the detector configuration); position and response are still produced.
102#[derive(Clone, Copy, Debug)]
103#[non_exhaustive]
104pub struct CornerDescriptor {
105    /// Subpixel position in full-resolution image pixels.
106    pub x: f32,
107    /// Subpixel y position in full-resolution image pixels.
108    pub y: f32,
109
110    /// Raw, **unnormalized** detector response at the detected peak. For
111    /// the ChESS path this is `R = SR − DR − 16·MR`
112    /// (see [`chess_response_u8`](crate::chess_response_u8)). Units are 8-bit
113    /// pixel sums; data-dependent. Do not interpret it as a probability,
114    /// a contrast, or a normalized strength.
115    pub response: f32,
116
117    /// The two local grid axis directions with per-axis 1σ precision, or
118    /// `None` when the orientation fit was skipped (orientation disabled in
119    /// the detector configuration).
120    pub axes: Option<[AxisEstimate; 2]>,
121}
122
123impl CornerDescriptor {
124    /// Construct a [`CornerDescriptor`] with a fitted orientation.
125    #[inline]
126    pub fn new(x: f32, y: f32, response: f32, axes: [AxisEstimate; 2]) -> Self {
127        Self {
128            x,
129            y,
130            response,
131            axes: Some(axes),
132        }
133    }
134
135    /// Construct a [`CornerDescriptor`] with the orientation fit skipped, so
136    /// [`axes`](Self::axes) is `None`.
137    #[inline]
138    pub fn without_axes(x: f32, y: f32, response: f32) -> Self {
139        Self {
140            x,
141            y,
142            response,
143            axes: None,
144        }
145    }
146}