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 exposes:
8//!
9//! - single-scale detection on raw grayscale buffers via
10//! [`find_chess_corners`],
11//! - optional `image::GrayImage` helpers (see
12//! `find_chess_corners_image`) when the `image` feature is
13//! enabled,
14//! - a flat user-facing [`ChessConfig`] with explicit modes for
15//! thresholding, ring selection, and multiscale tuning.
16//!
17//! The detector returns subpixel [`CornerDescriptor`] values in
18//! full-resolution image coordinates. In most applications you
19//! construct a [`ChessConfig`], optionally tweak its fields, and call
20//! [`find_chess_corners`] or `find_chess_corners_image`.
21//!
22//! # Quick start
23//!
24//! ## Using `image` (default)
25//!
26//! The default feature set includes integration with the `image`
27//! crate:
28//!
29//! ```no_run
30//! use chess_corners::{ChessConfig, RefinementMethod, find_chess_corners_image};
31//! use image::io::Reader as ImageReader;
32//!
33//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
34//! // Load a grayscale chessboard image.
35//! let img = ImageReader::open("board.png")?
36//! .decode()?
37//! .to_luma8();
38//!
39//! // Start from the recommended coarse-to-fine preset.
40//! let mut cfg = ChessConfig::multiscale();
41//! cfg.threshold_value = 0.15;
42//! cfg.refiner.kind = RefinementMethod::Forstner;
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}, axes [{:.2}, {:.2}] rad",
50//! c.x, c.y, c.response, c.axes[0].angle, c.axes[1].angle,
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, find_chess_corners_u8};
63//!
64//! # fn detect(img: &[u8], width: u32, height: u32) -> Result<(), chess_corners::ChessError> {
65//! // Single-scale convenience configuration.
66//! let cfg = ChessConfig::single_scale();
67//!
68//! let corners = find_chess_corners_u8(img, width, height, &cfg)?;
69//! println!("found {} corners", corners.len());
70//! # let _ = corners;
71//! # Ok(()) }
72//! ```
73//!
74//! ## ML refiner (feature `ml-refiner`)
75//!
76//! ```no_run
77//! # #[cfg(feature = "ml-refiner")]
78//! # {
79//! use chess_corners::{ChessConfig, find_chess_corners_image_with_ml};
80//! use image::GrayImage;
81//!
82//! let img = GrayImage::new(1, 1);
83//! let cfg = ChessConfig::single_scale();
84//!
85//! let corners = find_chess_corners_image_with_ml(&img, &cfg).unwrap();
86//! # let _ = corners;
87//! # }
88//! ```
89//!
90//! The ML refiner runs a small ONNX model on normalized intensity
91//! patches (uint8 / 255.0) centered at each candidate. The model
92//! predicts `[dx, dy, conf_logit]`, but the confidence output is
93//! currently ignored; the offsets are applied directly. Current
94//! benchmarks are synthetic; real-world accuracy still needs
95//! validation. It is also slower (about 23.5 ms vs 0.6 ms for 77
96//! corners on `testimages/mid.png`).
97//!
98//! ## Python bindings
99//!
100//! The workspace includes a PyO3-based Python extension crate at
101//! `crates/chess-corners-py`. It exposes `chess_corners.find_chess_corners`,
102//! which accepts a 2D `uint8` NumPy array and returns a float32 `(N, 9)` array
103//! with columns `[x, y, response, contrast, fit_rms, axis0_angle,
104//! axis0_sigma, axis1_angle, axis1_sigma]`. See
105//! `crates/chess-corners-py/README.md` for usage and configuration details.
106//!
107//! For tight processing loops you can also reuse pyramid storage
108//! explicitly via [`find_chess_corners_buff`] and the internal
109//! `pyramid` module; this avoids reallocating intermediate pyramid
110//! levels across frames. Most users should stick to
111//! [`find_chess_corners`] / `find_chess_corners_image` unless they
112//! need fine-grained control over allocations.
113//!
114//! # Configuration
115//!
116//! [`ChessConfig`] is intentionally flat. It exposes detector ring,
117//! descriptor ring, threshold mode/value, NMS controls, refiner
118//! choice, and multiscale settings directly. The detector translates
119//! that high-level config into lower-level [`ChessParams`] and
120//! [`CoarseToFineParams`] internally.
121//!
122//! If you need raw response maps or more control, the most useful
123//! low-level primitives are re-exported here:
124//! [`chess_response_u8`], [`chess_response_u8_patch`], [`Roi`],
125//! [`detect_corners_from_response_with_refiner`], [`Corner`], and
126//! [`corners_to_descriptors`]. For deeper internals (ring offsets,
127//! SAT views, scalar reference paths) depend on `chess-corners-core`
128//! directly.
129//!
130//! # Features
131//!
132//! - `image` *(default)* – enables `find_chess_corners_image` and
133//! `image::GrayImage` integration.
134//! - `rayon` – parallelizes response computation and multiscale
135//! refinement over image rows. Combine with `par_pyramid` to
136//! parallelize pyramid downsampling as well.
137//! - `ml-refiner` – enables the ML-backed refiner entry points via the
138//! `chess-corners-ml` crate and embedded ONNX model.
139//! - `simd` – enables portable-SIMD accelerated inner loops for the
140//! response kernel (requires a nightly compiler). Combine with
141//! `par_pyramid` to SIMD-accelerate pyramid downsampling.
142//! - `par_pyramid` – opt-in gate for SIMD/`rayon` acceleration inside
143//! the pyramid builder.
144//! - `tracing` – emits structured spans for multiscale detection,
145//! suitable for use with `tracing-subscriber` or JSON tracing from
146//! the CLI.
147//! - `cli` – builds the `chess-corners` binary shipped with this
148//! crate; it is not required when using the library as a
149//! dependency.
150//!
151//! The library API is stable across feature combinations; features
152//! only affect performance and observability, not numerical results.
153//!
154//! The ChESS idea was proposed in the papaer Bennett, Lasenby, *ChESS: A Fast and
155//! Accurate Chessboard Corner Detector*, CVIU 2014
156
157mod config;
158mod error;
159#[cfg(feature = "ml-refiner")]
160mod ml_refiner;
161mod multiscale;
162mod radon;
163mod upscale;
164
165// Re-export a focused subset of core types for convenience. The facade also
166// surfaces the most useful low-level primitives (response, detect,
167// describe) below so callers composing custom pipelines don't need a
168// separate `chess-corners-core` dependency. Deeper internals (ring offsets,
169// SAT views, scalar reference paths) remain reachable only via a direct
170// `chess-corners-core` dep.
171pub use crate::config::{
172 ChessConfig, DescriptorMode, DetectorMode, RefinementMethod, RefinerConfig, ThresholdMode,
173};
174pub use crate::error::ChessError;
175pub use crate::upscale::{
176 rescale_descriptors_to_input, upscale_bilinear_u8, UpscaleBuffers, UpscaleConfig, UpscaleError,
177 UpscaleMode,
178};
179pub use chess_corners_core::{
180 AxisEstimate, CenterOfMassConfig, ChessParams, CornerDescriptor, CornerRefiner, ForstnerConfig,
181 ImageView, PeakFitMode, RadonBuffers, RadonDetectorParams, RadonPeakConfig, RefineResult,
182 RefineStatus, Refiner, RefinerKind, ResponseMap, SaddlePointConfig,
183};
184
185// Low-level building blocks for callers composing custom pipelines:
186// response → detect → describe. Surfaced from core's submodules.
187pub use chess_corners_core::descriptor::{corners_to_descriptors, Corner};
188pub use chess_corners_core::detect::{
189 detect_corners_from_response, detect_corners_from_response_with_refiner,
190};
191pub use chess_corners_core::response::{chess_response_u8, chess_response_u8_patch, Roi};
192
193// High-level helpers on `image::GrayImage`.
194#[cfg(feature = "image")]
195pub mod image;
196#[cfg(all(feature = "image", feature = "ml-refiner"))]
197pub use image::find_chess_corners_image_with_ml;
198#[cfg(feature = "image")]
199pub use image::{find_chess_corners_image, find_chess_corners_image_with_refiner};
200
201// Multiscale/coarse-to-fine API types.
202pub use crate::multiscale::{
203 find_chess_corners, find_chess_corners_buff, find_chess_corners_buff_with_refiner,
204 find_chess_corners_with_refiner, CoarseToFineParams,
205};
206#[cfg(feature = "ml-refiner")]
207pub use crate::multiscale::{find_chess_corners_buff_with_ml, find_chess_corners_with_ml};
208pub use box_image_pyramid::{ImageBuffer, PyramidBuffers, PyramidParams};
209
210// Radon-detector convenience entry points.
211#[cfg(feature = "image")]
212pub use crate::radon::radon_heatmap_image;
213pub use crate::radon::radon_heatmap_u8;
214
215/// Detect chessboard corners from a raw grayscale image buffer.
216///
217/// The `img` slice must be `width * height` bytes in row-major order.
218/// If `cfg.upscale` is enabled, the image is upscaled internally and
219/// output corner coordinates are rescaled back to the original input
220/// pixel frame.
221///
222/// # Errors
223///
224/// Returns [`ChessError::DimensionMismatch`] if `img.len() != width * height`.
225/// Returns [`ChessError::Upscale`] if the upscale configuration is invalid.
226pub fn find_chess_corners_u8(
227 img: &[u8],
228 width: u32,
229 height: u32,
230 cfg: &ChessConfig,
231) -> Result<Vec<CornerDescriptor>, ChessError> {
232 run_with_upscale(img, width, height, cfg, |view, cfg| {
233 multiscale::find_chess_corners(view, cfg)
234 })
235}
236
237/// Detect corners from a raw grayscale buffer with an explicit refiner choice.
238///
239/// # Errors
240///
241/// Returns [`ChessError::DimensionMismatch`] if `img.len() != width * height`.
242/// Returns [`ChessError::Upscale`] if the upscale configuration is invalid.
243pub fn find_chess_corners_u8_with_refiner(
244 img: &[u8],
245 width: u32,
246 height: u32,
247 cfg: &ChessConfig,
248 refiner: &RefinerKind,
249) -> Result<Vec<CornerDescriptor>, ChessError> {
250 run_with_upscale(img, width, height, cfg, |view, cfg| {
251 multiscale::find_chess_corners_with_refiner(view, cfg, refiner)
252 })
253}
254
255/// Detect corners from a raw grayscale buffer using the ML refiner pipeline.
256///
257/// # Errors
258///
259/// Returns [`ChessError::DimensionMismatch`] if `img.len() != width * height`.
260/// Returns [`ChessError::Upscale`] if the upscale configuration is invalid.
261#[cfg(feature = "ml-refiner")]
262pub fn find_chess_corners_u8_with_ml(
263 img: &[u8],
264 width: u32,
265 height: u32,
266 cfg: &ChessConfig,
267) -> Result<Vec<CornerDescriptor>, ChessError> {
268 run_with_upscale(img, width, height, cfg, |view, cfg| {
269 multiscale::find_chess_corners_with_ml(view, cfg)
270 })
271}
272
273/// Thread the optional upscaling stage around the detection closure.
274/// Allocates a single-use `UpscaleBuffers`; callers with their own
275/// buffer reuse pattern should drive the pipeline directly via the
276/// `multiscale` module plus `upscale_bilinear_u8`.
277fn run_with_upscale(
278 img: &[u8],
279 width: u32,
280 height: u32,
281 cfg: &ChessConfig,
282 detect: impl FnOnce(ImageView<'_>, &ChessConfig) -> Vec<CornerDescriptor>,
283) -> Result<Vec<CornerDescriptor>, ChessError> {
284 let src_w = width as usize;
285 let src_h = height as usize;
286 let expected = src_w * src_h;
287 if img.len() != expected {
288 return Err(ChessError::DimensionMismatch {
289 expected,
290 actual: img.len(),
291 });
292 }
293 // SAFETY: length check above guarantees dimensions match.
294 let view = ImageView::from_u8_slice(src_w, src_h, img).expect("dimensions were checked above");
295
296 // Enforce the upscale invariants up-front so a misconfigured
297 // `UpscaleMode::Fixed` with factor 0 or 1 fails clearly.
298 cfg.upscale.validate()?;
299
300 let factor = cfg.upscale.effective_factor();
301 if factor <= 1 {
302 return Ok(detect(view, cfg));
303 }
304
305 let mut buffers = UpscaleBuffers::new();
306 let upscaled = upscale::upscale_bilinear_u8(img, src_w, src_h, factor, &mut buffers)?;
307 let mut corners = detect(upscaled, cfg);
308 upscale::rescale_descriptors_to_input(&mut corners, factor);
309 Ok(corners)
310}