chess_corners/lib.rs
1//! Ergonomic ChESS detector facade over `chess-corners-core`.
2//!
3//! # Overview
4//!
5//! This crate is the high-level entry point for the ChESS
6//! (Chess-board Extraction by Subtraction and Summation) corner
7//! detector. It re-exports the main configuration/result types
8//! from [`chess_corners_core`] and adds:
9//!
10//! - single-scale detection on raw grayscale buffers via
11//! [`find_chess_corners`],
12//! - optional `image::GrayImage` helpers (see
13//! `find_chess_corners_image`) when the `image` feature is
14//! enabled,
15//! - a coarse-to-fine multiscale detector configured through
16//! [`ChessConfig`] and [`CoarseToFineParams`].
17//!
18//! The detector returns subpixel [`CornerDescriptor`] values in
19//! full-resolution image coordinates. In most applications you
20//! construct a [`ChessConfig`], optionally tweak its fields, and call
21//! [`find_chess_corners`] or `find_chess_corners_image`.
22//!
23//! # Quick start
24//!
25//! ## Using `image` (default)
26//!
27//! The default feature set includes integration with the `image`
28//! crate:
29//!
30//! ```no_run
31//! use chess_corners::{ChessConfig, ChessParams, find_chess_corners_image};
32//! use image::io::Reader as ImageReader;
33//!
34//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
35//! // Load a grayscale chessboard image.
36//! let img = ImageReader::open("board.png")?
37//! .decode()?
38//! .to_luma8();
39//!
40//! // Start from the recommended coarse-to-fine preset.
41//! let mut cfg = ChessConfig::multiscale();
42//! cfg.params = ChessParams::default();
43//!
44//! let corners = find_chess_corners_image(&img, &cfg);
45//! println!("found {} corners", corners.len());
46//!
47//! for c in &corners {
48//! println!(
49//! "corner at ({:.2}, {:.2}), response {:.1}, theta {:.2} rad",
50//! c.x, c.y, c.response, c.orientation,
51//! );
52//! }
53//! # Ok(()) }
54//! ```
55//!
56//! ## Raw grayscale buffer
57//!
58//! If you already have an 8-bit grayscale buffer, you can call the
59//! detector directly without depending on `image`:
60//!
61//! ```no_run
62//! use chess_corners::{ChessConfig, ChessParams, find_chess_corners_u8};
63//!
64//! # fn detect(img: &[u8], width: u32, height: u32) {
65//! // Single-scale convenience configuration.
66//! let mut cfg = ChessConfig::single_scale();
67//! cfg.params = ChessParams::default();
68//!
69//! let corners = find_chess_corners_u8(img, width, height, &cfg);
70//! println!("found {} corners", corners.len());
71//! # let _ = corners;
72//! # }
73//! ```
74//!
75//! ## ML refiner (feature `ml-refiner`)
76//!
77//! ```no_run
78//! # #[cfg(feature = "ml-refiner")]
79//! # {
80//! use chess_corners::{ChessConfig, ChessParams, find_chess_corners_image_with_ml};
81//! use image::GrayImage;
82//!
83//! let img = GrayImage::new(1, 1);
84//! let mut cfg = ChessConfig::single_scale();
85//! cfg.params = ChessParams::default();
86//!
87//! let corners = find_chess_corners_image_with_ml(&img, &cfg);
88//! # let _ = corners;
89//! # }
90//! ```
91//!
92//! The ML refiner runs a small ONNX model on normalized intensity
93//! patches (uint8 / 255.0) centered at each candidate. The model
94//! predicts `[dx, dy, conf_logit]`, but the confidence output is
95//! currently ignored; the offsets are applied directly. Current
96//! benchmarks are synthetic; real-world accuracy still needs
97//! validation. It is also slower (about 23.5 ms vs 0.6 ms for 77
98//! corners on `testimages/mid.png`).
99//!
100//! ## Python bindings
101//!
102//! The workspace includes a PyO3-based Python extension crate at
103//! `crates/chess-corners-py`. It exposes `chess_corners.find_chess_corners`,
104//! which accepts a 2D `uint8` NumPy array and returns a float32 `(N, 4)` array
105//! with columns `[x, y, response, orientation]`. See
106//! `crates/chess-corners-py/README.md` for usage and configuration details.
107//!
108//! For tight processing loops you can also reuse pyramid storage
109//! explicitly via [`find_chess_corners_buff`] and the internal
110//! `pyramid` module; this avoids reallocating intermediate pyramid
111//! levels across frames. Most users should stick to
112//! [`find_chess_corners`] / `find_chess_corners_image` unless they
113//! need fine-grained control over allocations.
114//!
115//! # Configuration
116//!
117//! [`ChessConfig`] combines the low-level ChESS parameters with
118//! multiscale tuning:
119//!
120//! - [`ChessParams`] (re-exported from `chess-corners-core`) controls
121//! the response kernel and detector behavior: ring radius, relative
122//! or absolute threshold, non-maximum suppression radius, and the
123//! minimum cluster size for accepting a corner.
124//! - [`CoarseToFineParams`] describes how the multiscale detector
125//! behaves: number of pyramid levels, minimum level size, coarse
126//! ROI radius (at the smallest level) and merge radius for
127//! deduplicating refined corners.
128//! - [`RefinerKind`] (via [`ChessParams::refiner`]) selects the
129//! classic subpixel refinement backend (center-of-mass default,
130//! Förstner, saddle-point) and exposes per-refiner tuning knobs.
131//! - ML refinement is exposed via explicit `find_chess_corners_*_with_ml`
132//! entry points when the `ml-refiner` feature is enabled.
133//!
134//! [`ChessConfig::default`] and the shortcut
135//! [`ChessConfig::single_scale`] both configure a single-scale run.
136//! [`ChessConfig::multiscale`] returns the recommended coarse-to-fine
137//! starting point with a 3-level pyramid. Any
138//! `pyramid.num_levels > 1` triggers the coarse-to-fine path: corners
139//! are first detected on the smallest pyramid level and then refined
140//! inside base-image regions of interest.
141//!
142//! If you need raw response maps or more control, depend directly on
143//! `chess-corners-core` and use its [`chess_corners_core::response`]
144//! and [`chess_corners_core::detect`] modules alongside the
145//! re-exported [`ResponseMap`] and [`CornerDescriptor`] types.
146//!
147//! # Features
148//!
149//! - `image` *(default)* – enables `find_chess_corners_image` and
150//! `image::GrayImage` integration.
151//! - `rayon` – parallelizes response computation and multiscale
152//! refinement over image rows. Combine with `par_pyramid` to
153//! parallelize pyramid downsampling as well.
154//! - `ml-refiner` – enables the ML-backed refiner entry points via the
155//! `chess-corners-ml` crate and embedded ONNX model.
156//! - `simd` – enables portable-SIMD accelerated inner loops for the
157//! response kernel (requires a nightly compiler). Combine with
158//! `par_pyramid` to SIMD-accelerate pyramid downsampling.
159//! - `par_pyramid` – opt-in gate for SIMD/`rayon` acceleration inside
160//! the pyramid builder.
161//! - `tracing` – emits structured spans for multiscale detection,
162//! suitable for use with `tracing-subscriber` or JSON tracing from
163//! the CLI.
164//! - `cli` – builds the `chess-corners` binary shipped with this
165//! crate; it is not required when using the library as a
166//! dependency.
167//!
168//! The library API is stable across feature combinations; features
169//! only affect performance and observability, not numerical results.
170//!
171//! The ChESS idea was proposed in the papaer Bennett, Lasenby, *ChESS: A Fast and
172//! Accurate Chessboard Corner Detector*, CVIU 2014
173
174#[cfg(feature = "ml-refiner")]
175mod ml_refiner;
176mod multiscale;
177
178// Re-export a focused subset of core types for convenience. Consumers that
179// need lower-level primitives (rings, raw response functions, etc.) are
180// encouraged to depend on `chess-corners-core` directly.
181pub use chess_corners_core::{
182 CenterOfMassConfig, ChessParams, CornerDescriptor, CornerRefiner, ForstnerConfig, ImageView,
183 RefineResult, RefineStatus, Refiner, RefinerKind, ResponseMap, SaddlePointConfig,
184};
185
186// High-level helpers on `image::GrayImage`.
187#[cfg(feature = "image")]
188pub mod image;
189#[cfg(all(feature = "image", feature = "ml-refiner"))]
190pub use image::find_chess_corners_image_with_ml;
191#[cfg(feature = "image")]
192pub use image::{find_chess_corners_image, find_chess_corners_image_with_refiner};
193
194// Multiscale/coarse-to-fine API types.
195pub use crate::multiscale::{
196 find_chess_corners, find_chess_corners_buff, find_chess_corners_buff_with_refiner,
197 find_chess_corners_with_refiner, CoarseToFineParams,
198};
199#[cfg(feature = "ml-refiner")]
200pub use crate::multiscale::{find_chess_corners_buff_with_ml, find_chess_corners_with_ml};
201pub use box_image_pyramid::{ImageBuffer, PyramidBuffers, PyramidParams};
202
203/// Unified detector configuration combining response/detector params and
204/// multiscale/pyramid tuning.
205///
206/// [`ChessConfig::default`] keeps the detector in the single-scale regime.
207/// Use [`ChessConfig::multiscale`] for the recommended coarse-to-fine starting
208/// point on high-resolution, small-board, or textured projected patterns.
209#[derive(Clone, Debug, Default)]
210#[non_exhaustive]
211pub struct ChessConfig {
212 /// Low-level ChESS response/detector parameters (ring radius, thresholds,
213 /// NMS radius, minimum cluster size, and subpixel refinement backend).
214 pub params: ChessParams,
215 /// Coarse-to-fine multiscale configuration (pyramid shape, ROI radius,
216 /// merge radius).
217 pub multiscale: CoarseToFineParams,
218}
219
220impl ChessConfig {
221 /// Recommended coarse-to-fine starting point.
222 ///
223 /// This uses the canonical r=5 ChESS ring together with:
224 /// - 3 pyramid levels,
225 /// - `min_size = 128`,
226 /// - coarse ROI radius 3,
227 /// - merge radius 3.0.
228 pub fn multiscale() -> Self {
229 let mut cfg = Self::default();
230 cfg.multiscale.pyramid.num_levels = 3;
231 cfg.multiscale.pyramid.min_size = 128;
232 cfg
233 }
234
235 /// Convenience helper for single-scale detection.
236 ///
237 /// This is equivalent to [`ChessConfig::default`] but kept as an explicit
238 /// readability helper at call sites.
239 pub fn single_scale() -> Self {
240 Self::default()
241 }
242}
243
244/// Detect chessboard corners from a raw grayscale image buffer.
245///
246/// The `img` slice must be `width * height` bytes in row-major order.
247///
248/// # Panics
249///
250/// Panics if `img.len() != width * height`.
251#[must_use]
252pub fn find_chess_corners_u8(
253 img: &[u8],
254 width: u32,
255 height: u32,
256 cfg: &ChessConfig,
257) -> Vec<CornerDescriptor> {
258 let view = ImageView::from_u8_slice(width as usize, height as usize, img)
259 .expect("image dimensions must match buffer length");
260 multiscale::find_chess_corners(view, cfg)
261}
262
263/// Detect corners from a raw grayscale buffer with an explicit refiner choice.
264#[must_use]
265pub fn find_chess_corners_u8_with_refiner(
266 img: &[u8],
267 width: u32,
268 height: u32,
269 cfg: &ChessConfig,
270 refiner: &RefinerKind,
271) -> Vec<CornerDescriptor> {
272 let view = ImageView::from_u8_slice(width as usize, height as usize, img)
273 .expect("image dimensions must match buffer length");
274 multiscale::find_chess_corners_with_refiner(view, cfg, refiner)
275}
276
277/// Detect corners from a raw grayscale buffer using the ML refiner pipeline.
278#[must_use]
279#[cfg(feature = "ml-refiner")]
280pub fn find_chess_corners_u8_with_ml(
281 img: &[u8],
282 width: u32,
283 height: u32,
284 cfg: &ChessConfig,
285) -> Vec<CornerDescriptor> {
286 let view = ImageView::from_u8_slice(width as usize, height as usize, img)
287 .expect("image dimensions must match buffer length");
288 multiscale::find_chess_corners_with_ml(view, cfg)
289}