Skip to main content

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}