Skip to main content

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}