Skip to main content

chess_corners/
lib.rs

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