chess_corners/lib.rs
1//! Ergonomic ChESS / Radon corner-detector facade over
2//! `chess-corners-core`.
3//!
4//! # Overview
5//!
6//! This crate is the high-level entry point for two chessboard-corner
7//! detectors that share the same output surface:
8//!
9//! - **ChESS** (Chess-board Extraction by Subtraction and Summation)
10//! — a dense ring-difference response with NMS and a pluggable
11//! subpixel refiner. This is the default path and the fastest preset
12//! in the repository's clean-image benchmark.
13//! - **Radon** — a whole-image Duda-Frese accumulator that scores
14//! corners by summing ray intensities through each pixel. It is useful
15//! when the ChESS ring does not produce enough seeds, especially in
16//! the small-cell, blur, and low-contrast fixtures covered by the
17//! tests.
18//!
19//! The [`Detector`] struct ties together the active strategy, the
20//! orientation fit, and the multiscale / upscale scratch buffers
21//! behind a single `detect` call. It returns subpixel
22//! [`CornerDescriptor`] values in full-resolution input coordinates.
23//! In most applications you construct a [`DetectorConfig`] (typically
24//! via [`DetectorConfig::chess`], [`DetectorConfig::chess_multiscale`],
25//! [`DetectorConfig::radon`], or [`DetectorConfig::radon_multiscale`]),
26//! optionally tweak its fields, build a [`Detector`], and call
27//! [`Detector::detect`].
28//!
29//! Building a [`Detector`] once and calling [`Detector::detect`] in a
30//! loop reuses the pyramid, response, and upscale scratch buffers
31//! across frames — no per-frame allocation.
32//!
33//! # Quick start
34//!
35//! ## Using `image` (default)
36//!
37//! The default feature set includes integration with the `image`
38//! crate. This example reads from disk and is marked `no_run`:
39//!
40//! ```no_run
41//! use chess_corners::{ChessRefiner, Detector, DetectorConfig, Threshold};
42//! use image::io::Reader as ImageReader;
43//!
44//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
45//! let img = ImageReader::open("board.png")?
46//! .decode()?
47//! .to_luma8();
48//!
49//! let cfg = DetectorConfig::chess_multiscale()
50//! .with_threshold(Threshold::Relative(0.15))
51//! .with_chess(|c| c.refiner = ChessRefiner::forstner());
52//!
53//! let mut detector = Detector::new(cfg)?;
54//! let corners = detector.detect(&img)?;
55//! println!("found {} corners", corners.len());
56//!
57//! for c in &corners {
58//! println!(
59//! "corner at ({:.2}, {:.2}), response {:.1}, axes [{:.2}, {:.2}] rad",
60//! c.x, c.y, c.response, c.axes[0].angle, c.axes[1].angle,
61//! );
62//! }
63//! # Ok(()) }
64//! ```
65//!
66//! ## Raw grayscale buffer
67//!
68//! If you already have an 8-bit grayscale buffer, call
69//! [`Detector::detect_u8`]:
70//!
71//! ```
72//! use chess_corners::{Detector, DetectorConfig};
73//!
74//! // 8×8 black/white checkerboard of 16-pixel squares (128×128).
75//! let mut img = vec![0u8; 128 * 128];
76//! for y in 0..128 {
77//! for x in 0..128 {
78//! if ((x / 16) + (y / 16)) % 2 == 0 {
79//! img[y * 128 + x] = 255;
80//! }
81//! }
82//! }
83//!
84//! let cfg = DetectorConfig::chess();
85//! let mut detector = Detector::new(cfg)?;
86//! let corners = detector.detect_u8(&img, 128, 128)?;
87//! assert!(!corners.is_empty());
88//! # Ok::<(), chess_corners::ChessError>(())
89//! ```
90//!
91//! ## Radon strategy
92//!
93//! Switch to the whole-image Radon detector when ChESS misses corners on
94//! the images you care about. The strategy lives inside
95//! [`DetectorConfig::strategy`]; pick a Radon preset to get sensible
96//! defaults:
97//!
98//! ```
99//! use chess_corners::{Detector, DetectorConfig};
100//!
101//! let mut img = vec![0u8; 128 * 128];
102//! for y in 0..128 {
103//! for x in 0..128 {
104//! if ((x / 16) + (y / 16)) % 2 == 0 {
105//! img[y * 128 + x] = 255;
106//! }
107//! }
108//! }
109//!
110//! let cfg = DetectorConfig::radon();
111//! let mut detector = Detector::new(cfg)?;
112//! let corners = detector.detect_u8(&img, 128, 128)?;
113//! assert!(!corners.is_empty());
114//! # Ok::<(), chess_corners::ChessError>(())
115//! ```
116//!
117//! ## ML refiner (feature `ml-refiner`)
118//!
119//! Pick the ML pipeline by selecting [`ChessRefiner::Ml`] inside the
120//! ChESS strategy. The example is marked `no_run` because loading the
121//! embedded ONNX model on first use is not appropriate for a doctest:
122//!
123//! ```no_run
124//! # #[cfg(feature = "ml-refiner")]
125//! # {
126//! use chess_corners::{ChessRefiner, Detector, DetectorConfig};
127//! use image::GrayImage;
128//!
129//! let cfg = DetectorConfig::chess()
130//! .with_chess(|c| c.refiner = ChessRefiner::Ml);
131//!
132//! let img: GrayImage = image::open("board.png").unwrap().to_luma8();
133//! let mut detector = Detector::new(cfg).unwrap();
134//! let corners = detector.detect(&img).unwrap();
135//! # let _ = corners;
136//! # }
137//! ```
138//!
139//! The ML refiner runs a small ONNX model on normalized intensity
140//! patches (uint8 / 255.0) centered at each candidate. The model
141//! predicts `[dx, dy, conf_logit]`, but the confidence output is
142//! currently ignored; the offsets are applied directly. Current
143//! accuracy benchmarks are synthetic; real-world accuracy still needs
144//! validation. Per-refiner cost is measured in Part VIII §7.6 of the
145//! book. The ML path is slower than the hand-coded refiners and should
146//! be chosen only after measuring that its behavior helps your data.
147//!
148//! ## Python and JavaScript bindings
149//!
150//! The workspace also ships bindings that wrap this facade:
151//!
152//! - `crates/chess-corners-py` (PyO3 / maturin) exposes a
153//! `chess_corners.Detector` class whose `detect(image)` method
154//! accepts a 2D `uint8` NumPy array and returns a `float32`
155//! `(N, 9)` array with columns `[x, y, response, contrast, fit_rms,
156//! axis0_angle, axis0_sigma, axis1_angle, axis1_sigma]`. See its
157//! README for usage and configuration details.
158//! - `crates/chess-corners-wasm` (wasm-bindgen / wasm-pack) exposes
159//! the same surface to JavaScript / TypeScript via the
160//! `@vitavision/chess-corners` npm package.
161//!
162//! # Configuration
163//!
164//! [`DetectorConfig`] is strategy-typed: the [`DetectorConfig::strategy`]
165//! field is a [`DetectionStrategy`] enum carrying either a
166//! [`ChessConfig`] (detector ring, descriptor ring, NMS, refiner) or
167//! a [`RadonConfig`] (whole-image Duda-Frese parameters). Acceptance
168//! is a single [`Threshold`] enum (`Absolute` or `Relative`).
169//! [`MultiscaleConfig`] and [`UpscaleConfig`] live at the top level
170//! and apply to both strategies. The detector translates this into
171//! the lower-level [`ChessParams`] / [`RadonDetectorParams`] structs
172//! internally.
173//!
174//! If you need raw response maps or more control, the most useful
175//! low-level primitives are re-exported here:
176//! [`chess_response_u8`], [`chess_response_u8_patch`], [`Roi`],
177//! [`detect_corners_from_response_with_refiner`], [`Corner`], and
178//! [`describe_corners`]. For deeper internals (ring offsets,
179//! SAT views, scalar reference paths) depend on `chess-corners-core`
180//! directly.
181//!
182//! # Features
183//!
184//! - `image` *(default)* – enables [`Detector::detect`] and
185//! `image::GrayImage` integration.
186//! - `rayon` – parallelizes response computation and multiscale
187//! refinement over image rows. Combine with `par_pyramid` to
188//! parallelize pyramid downsampling as well.
189//! - `ml-refiner` – enables the ML-backed refiner entry points via the
190//! `chess-corners-ml` crate and embedded ONNX model.
191//! - `simd` – enables portable-SIMD accelerated inner loops for the
192//! response kernel (requires a nightly compiler). Combine with
193//! `par_pyramid` to SIMD-accelerate pyramid downsampling.
194//! - `par_pyramid` – opt-in gate for SIMD/`rayon` acceleration inside
195//! the pyramid builder.
196//! - `tracing` – emits structured spans for multiscale detection,
197//! suitable for use with `tracing-subscriber` or JSON tracing from
198//! the CLI.
199//! - `cli` – builds the `chess-corners` binary shipped with this
200//! crate; it is not required when using the library as a
201//! dependency.
202//!
203//! The library API is stable across feature combinations; features
204//! only affect performance and observability, not numerical results.
205//!
206//! # References
207//!
208//! - Bennett, Lasenby. *ChESS: A Fast and Accurate Chessboard Corner
209//! Detector*. CVIU 2014.
210//! - Duda, Frese. *Accurate Detection and Localization of Checkerboard
211//! Corners for Calibration*. BMVC 2018.
212
213mod config;
214mod detector;
215mod error;
216#[cfg(feature = "ml-refiner")]
217mod ml_refiner;
218mod multiscale;
219mod radon;
220mod upscale;
221
222// Re-export a focused subset of core types for convenience. The facade also
223// surfaces the most useful low-level primitives (response, detect,
224// describe) below so callers composing custom pipelines don't need a
225// separate `chess-corners-core` dependency. Deeper internals (ring offsets,
226// SAT views, scalar reference paths) remain reachable only via a direct
227// `chess-corners-core` dep.
228pub use crate::config::{
229 ChessConfig, ChessRefiner, ChessRing, DescriptorRing, DetectionStrategy, DetectorConfig,
230 MultiscaleConfig, RadonConfig, RadonRefiner, Threshold,
231};
232pub use crate::error::ChessError;
233pub use crate::upscale::{
234 rescale_descriptors_to_input, upscale_bilinear_u8, UpscaleBuffers, UpscaleConfig, UpscaleError,
235};
236pub use chess_corners_core::{
237 AxisEstimate, AxisFitResult, CenterOfMassConfig, ChessParams, CornerDescriptor, CornerRefiner,
238 ForstnerConfig, ImageView, OrientationMethod, PeakFitMode, RadonBuffers, RadonDetectorParams,
239 RadonPeakConfig, RefineResult, RefineStatus, Refiner, RefinerKind, ResponseMap,
240 SaddlePointConfig,
241};
242
243// Low-level building blocks for callers composing custom pipelines:
244// response → detect → describe. Surfaced from core's submodules.
245pub use chess_corners_core::detect::chess::response::{
246 chess_response_u8, chess_response_u8_patch, Roi,
247};
248pub use chess_corners_core::detect::Corner;
249pub use chess_corners_core::detect::{
250 detect_corners_from_response, detect_corners_from_response_with_refiner,
251};
252pub use chess_corners_core::orientation::describe_corners;
253
254// Primary detector entry point.
255pub use crate::detector::Detector;
256
257// Pyramid types remain re-exported because [`MultiscaleConfig::Pyramid`]
258// ultimately binds to [`PyramidParams`].
259pub use crate::multiscale::CoarseToFineParams;
260pub use box_image_pyramid::{ImageBuffer, PyramidBuffers, PyramidParams};
261
262// Radon-detector heatmap helpers (visualisation only).
263#[cfg(feature = "image")]
264pub use crate::radon::radon_heatmap_image;
265pub use crate::radon::radon_heatmap_u8;