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}