chess_corners_core/lib.rs
1#![cfg_attr(not(feature = "std"), no_std)]
2#![cfg_attr(feature = "simd", feature(portable_simd))]
3//! Core primitives for computing ChESS responses and extracting subpixel corners.
4//!
5//! The crate is organized along the three orthogonal axes the
6//! detector pipeline composes:
7//!
8//! | Module | Description |
9//! |--------|-------------|
10//! | [`detect`] | Feature-detection pipelines. Two independent families share a common output type: [`detect::chess`] (ChESS response + NMS) and [`detect::radon`] (Radon SAT + peak detection). |
11//! | [`refine`] | Pluggable subpixel-refinement backends. The [`refine::CornerRefiner`] trait dispatches across center-of-mass, Förstner, saddle-point, and Radon-peak refiners. |
12//! | [`orientation`] | Two-axis orientation fit (`OrientationMethod::RingFit` ring fit, `OrientationMethod::DiskFit` full-disk crossing-line) shared between detectors. The [`orientation::describe_corners`] entry point produces [`CornerDescriptor`] values with subpixel position, two-axis orientation, per-axis 1σ uncertainty, contrast, and fit residual. |
13//! | [`imageview`] | Zero-copy [`ImageView`] into a borrowed grayscale buffer, with optional `origin` offset for pyramid/ROI support. |
14//!
15//! Most users should work through the `chess-corners` facade crate rather than
16//! depending on `chess-corners-core` directly. Depend on this crate only when
17//! you need raw response maps, custom refiners, or the Radon detector primitives.
18//!
19//! # Features
20//!
21//! - `std` *(default)* – enables use of the Rust standard library. When
22//! disabled, the crate is `no_std` + `alloc`.
23//! - `rayon` – parallelizes the dense response computation and Radon accumulation
24//! over image rows using the `rayon` crate. Does not change numerical results.
25//! - `simd` – enables a SIMD‑accelerated inner loop for the ChESS response
26//! kernel, based on `portable_simd`. Requires a nightly compiler; the
27//! scalar path remains the reference implementation.
28//! - `tracing` – emits structured spans around response and detector functions
29//! using the [`tracing`](https://docs.rs/tracing) ecosystem, useful for
30//! profiling and diagnostics.
31//!
32//! Feature combinations:
33//!
34//! - no features / `std` only – single‑threaded scalar implementation.
35//! - `rayon` – same scalar math, but rows are processed in parallel.
36//! - `simd` – single‑threaded, but the inner ring computation is vectorized.
37//! - `rayon + simd` – rows are processed in parallel *and* each row uses the
38//! SIMD‑accelerated inner loop.
39//!
40//! The detector in [`detect`] is independent of `rayon`/`simd`, and `tracing`
41//! only adds observability; none of these features change the numerical
42//! results, only performance and instrumentation.
43//!
44//! The ChESS idea is proposed in Bennett, Lasenby, *ChESS: A Fast and
45//! Accurate Chessboard Corner Detector*, CVIU 2014.
46
47pub mod detect;
48pub mod imageview;
49pub mod orientation;
50pub mod refine;
51
52use crate::detect::chess::ring::RingOffsets;
53use serde::{Deserialize, Serialize};
54
55pub use crate::detect::dense::{ChessBuffers, ChessDetector, DenseDetector, RadonDetector};
56pub use crate::detect::radon::primitives::{fit_peak_frac, PeakFitMode};
57pub use crate::detect::radon::{
58 detect_peaks_from_radon, radon_response_u8, RadonBuffers, RadonDetectorParams,
59 RadonResponseView, SatElem,
60};
61pub use crate::detect::{AxisEstimate, Corner, CornerDescriptor};
62pub use crate::orientation::{
63 fit_axes_at_point, fit_axes_from_samples, AxisFitResult, OrientationMethod,
64};
65pub use crate::refine::{
66 CenterOfMassConfig, CenterOfMassRefiner, CornerRefiner, ForstnerConfig, ForstnerRefiner,
67 RadonPeakConfig, RadonPeakRefiner, RefineContext, RefineResult, RefineStatus, Refiner,
68 RefinerKind, SaddlePointConfig, SaddlePointRefiner,
69};
70pub use imageview::ImageView;
71/// Tunable parameters for the ChESS response computation and corner detection.
72#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
73#[serde(default)]
74#[non_exhaustive]
75pub struct ChessParams {
76 /// Use the larger r=10 ring instead of the canonical r=5.
77 pub use_radius10: bool,
78 /// Optional override for descriptor sampling ring (r=5 vs r=10). Falls back
79 /// to `use_radius10` when `None`.
80 pub descriptor_use_radius10: Option<bool>,
81 /// Relative threshold as a fraction of max response (e.g. 0.2 = 20%).
82 pub threshold_rel: f32,
83 /// Absolute threshold override; if `Some`, this is used instead of `threshold_rel`.
84 pub threshold_abs: Option<f32>,
85 /// Non-maximum suppression radius (in pixels).
86 pub nms_radius: u32,
87 /// Minimum count of positive-response neighbors in NMS window
88 /// to accept a corner (rejects isolated noise).
89 pub min_cluster_size: u32,
90 /// Subpixel refinement backend and its configuration. Defaults to the legacy
91 /// center-of-mass refiner on the response map.
92 pub refiner: RefinerKind,
93 /// Orientation-fit method used to estimate the two grid axes at
94 /// each detected corner. Default [`OrientationMethod::RingFit`]
95 /// fits the parametric two-axis model with robust seeding and
96 /// calibrated per-axis uncertainties.
97 #[serde(default)]
98 pub orientation_method: OrientationMethod,
99}
100
101impl Default for ChessParams {
102 fn default() -> Self {
103 Self {
104 use_radius10: false,
105 descriptor_use_radius10: None,
106 // Paper's contract: accept every strictly-positive ChESS
107 // response. `threshold_abs = Some(0.0)` combined with the
108 // strict comparison in `detect_corners_from_response` gives
109 // "R > 0 ⇒ corner". `threshold_rel = 0.2` is kept as a
110 // default-sized opt-in value for callers that explicitly
111 // switch to `threshold_abs = None`.
112 threshold_rel: 0.2,
113 threshold_abs: Some(0.0),
114 nms_radius: 2,
115 min_cluster_size: 2,
116 refiner: RefinerKind::default(),
117 orientation_method: OrientationMethod::default(),
118 }
119 }
120}
121
122impl ChessParams {
123 #[inline]
124 pub fn ring_radius(&self) -> u32 {
125 if self.use_radius10 {
126 10
127 } else {
128 5
129 }
130 }
131
132 #[inline]
133 pub fn descriptor_ring_radius(&self) -> u32 {
134 match self.descriptor_use_radius10 {
135 Some(true) => 10,
136 Some(false) => 5,
137 None => self.ring_radius(),
138 }
139 }
140
141 #[inline]
142 pub fn ring(&self) -> RingOffsets {
143 RingOffsets::from_radius(self.ring_radius())
144 }
145
146 #[inline]
147 pub fn descriptor_ring(&self) -> RingOffsets {
148 RingOffsets::from_radius(self.descriptor_ring_radius())
149 }
150}
151
152/// Dense response map in row-major layout.
153#[derive(Clone, Debug, Default)]
154pub struct ResponseMap {
155 pub(crate) w: usize,
156 pub(crate) h: usize,
157 pub(crate) data: Vec<f32>,
158}
159
160impl ResponseMap {
161 /// Create a new response map. `data` must have exactly `w * h` elements.
162 ///
163 /// # Panics
164 ///
165 /// Panics if `data.len() != w * h`.
166 pub fn new(w: usize, h: usize, data: Vec<f32>) -> Self {
167 assert_eq!(data.len(), w * h, "ResponseMap data length mismatch");
168 Self { w, h, data }
169 }
170
171 /// Width of the response map.
172 #[inline]
173 pub fn width(&self) -> usize {
174 self.w
175 }
176
177 /// Height of the response map.
178 #[inline]
179 pub fn height(&self) -> usize {
180 self.h
181 }
182
183 /// Raw response data in row-major order.
184 #[inline]
185 pub fn data(&self) -> &[f32] {
186 &self.data
187 }
188
189 /// Mutable access to the raw response data.
190 #[inline]
191 pub fn data_mut(&mut self) -> &mut [f32] {
192 &mut self.data
193 }
194
195 #[inline]
196 /// Response value at an integer coordinate.
197 pub fn at(&self, x: usize, y: usize) -> f32 {
198 self.data[y * self.w + x]
199 }
200}