chess-corners 0.11.0

High-level chessboard / ChESS corner detection API
Documentation
//! Ergonomic ChESS / Radon corner-detector facade over
//! `chess-corners-core`.
//!
//! # Overview
//!
//! This crate is the high-level entry point for two chessboard-corner
//! detectors that share the same output surface:
//!
//! - **ChESS** (Chess-board Extraction by Subtraction and Summation)
//!   — a dense ring-difference response with NMS and a pluggable
//!   subpixel refiner. This is the default path and the fastest preset
//!   in the repository's clean-image benchmark.
//! - **Radon** — a whole-image Duda-Frese accumulator that scores
//!   corners by summing ray intensities through each pixel. It is useful
//!   when the ChESS ring does not produce enough seeds, especially in
//!   the small-cell, blur, and low-contrast fixtures covered by the
//!   tests.
//!
//! The [`Detector`] struct ties together the active strategy, the
//! orientation fit, and the multiscale / upscale scratch buffers
//! behind a single `detect` call. It returns subpixel
//! [`CornerDescriptor`] values in full-resolution input coordinates.
//! In most applications you construct a [`DetectorConfig`] (typically
//! via [`DetectorConfig::chess`], [`DetectorConfig::chess_multiscale`],
//! [`DetectorConfig::radon`], or [`DetectorConfig::radon_multiscale`]),
//! optionally tweak its fields, build a [`Detector`], and call
//! [`Detector::detect`].
//!
//! Building a [`Detector`] once and calling [`Detector::detect`] in a
//! loop reuses the pyramid, response, and upscale scratch buffers
//! across frames — no per-frame allocation.
//!
//! # Quick start
//!
//! ## Using `image` (default)
//!
//! The default feature set includes integration with the `image`
//! crate. This example reads from disk and is marked `no_run`:
//!
//! ```no_run
//! use chess_corners::{ChessRefiner, Detector, DetectorConfig, Threshold};
//! use image::io::Reader as ImageReader;
//!
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let img = ImageReader::open("board.png")?
//!     .decode()?
//!     .to_luma8();
//!
//! let cfg = DetectorConfig::chess_multiscale()
//!     .with_threshold(Threshold::Relative(0.15))
//!     .with_chess(|c| c.refiner = ChessRefiner::forstner());
//!
//! let mut detector = Detector::new(cfg)?;
//! let corners = detector.detect(&img)?;
//! println!("found {} corners", corners.len());
//!
//! for c in &corners {
//!     println!(
//!         "corner at ({:.2}, {:.2}), response {:.1}, axes [{:.2}, {:.2}] rad",
//!         c.x, c.y, c.response, c.axes[0].angle, c.axes[1].angle,
//!     );
//! }
//! # Ok(()) }
//! ```
//!
//! ## Raw grayscale buffer
//!
//! If you already have an 8-bit grayscale buffer, call
//! [`Detector::detect_u8`]:
//!
//! ```
//! use chess_corners::{Detector, DetectorConfig};
//!
//! // 8×8 black/white checkerboard of 16-pixel squares (128×128).
//! let mut img = vec![0u8; 128 * 128];
//! for y in 0..128 {
//!     for x in 0..128 {
//!         if ((x / 16) + (y / 16)) % 2 == 0 {
//!             img[y * 128 + x] = 255;
//!         }
//!     }
//! }
//!
//! let cfg = DetectorConfig::chess();
//! let mut detector = Detector::new(cfg)?;
//! let corners = detector.detect_u8(&img, 128, 128)?;
//! assert!(!corners.is_empty());
//! # Ok::<(), chess_corners::ChessError>(())
//! ```
//!
//! ## Radon strategy
//!
//! Switch to the whole-image Radon detector when ChESS misses corners on
//! the images you care about. The strategy lives inside
//! [`DetectorConfig::strategy`]; pick a Radon preset to get sensible
//! defaults:
//!
//! ```
//! use chess_corners::{Detector, DetectorConfig};
//!
//! let mut img = vec![0u8; 128 * 128];
//! for y in 0..128 {
//!     for x in 0..128 {
//!         if ((x / 16) + (y / 16)) % 2 == 0 {
//!             img[y * 128 + x] = 255;
//!         }
//!     }
//! }
//!
//! let cfg = DetectorConfig::radon();
//! let mut detector = Detector::new(cfg)?;
//! let corners = detector.detect_u8(&img, 128, 128)?;
//! assert!(!corners.is_empty());
//! # Ok::<(), chess_corners::ChessError>(())
//! ```
//!
//! ## ML refiner (feature `ml-refiner`)
//!
//! Pick the ML pipeline by selecting `ChessRefiner::Ml` inside the
//! ChESS strategy. The example is marked `no_run` because loading the
//! embedded ONNX model on first use is not appropriate for a doctest:
//!
//! ```no_run
//! # #[cfg(feature = "ml-refiner")]
//! # {
//! use chess_corners::{ChessRefiner, Detector, DetectorConfig};
//! use image::GrayImage;
//!
//! let cfg = DetectorConfig::chess()
//!     .with_chess(|c| c.refiner = ChessRefiner::Ml);
//!
//! let img: GrayImage = image::open("board.png").unwrap().to_luma8();
//! let mut detector = Detector::new(cfg).unwrap();
//! let corners = detector.detect(&img).unwrap();
//! # let _ = corners;
//! # }
//! ```
//!
//! The ML refiner runs a small ONNX model on normalized intensity
//! patches (uint8 / 255.0) centered at each candidate. The model
//! predicts `[dx, dy, conf_logit]`, but the confidence output is
//! currently ignored; the offsets are applied directly. Current
//! accuracy benchmarks are synthetic; real-world accuracy still needs
//! validation. Per-refiner cost is measured in Part VIII §7.6 of the
//! book. The ML path is slower than the hand-coded refiners and should
//! be chosen only after measuring that its behavior helps your data.
//!
//! ## Python and JavaScript bindings
//!
//! The workspace also ships bindings that wrap this facade:
//!
//! - `crates/chess-corners-py` (PyO3 / maturin) exposes a
//!   `chess_corners.Detector` class whose `detect(image)` method
//!   accepts a 2D `uint8` NumPy array and returns a `float32`
//!   `(N, 9)` array with columns `[x, y, response, contrast, fit_rms,
//!   axis0_angle, axis0_sigma, axis1_angle, axis1_sigma]`. See its
//!   README for usage and configuration details.
//! - `crates/chess-corners-wasm` (wasm-bindgen / wasm-pack) exposes
//!   the same surface to JavaScript / TypeScript via the
//!   `@vitavision/chess-corners` npm package.
//!
//! # Configuration
//!
//! [`DetectorConfig`] is strategy-typed: the [`DetectorConfig::strategy`]
//! field is a [`DetectionStrategy`] enum carrying either a
//! [`ChessConfig`] (detector ring, descriptor ring, NMS, refiner) or
//! a [`RadonConfig`] (whole-image Duda-Frese parameters). Acceptance
//! is a single [`Threshold`] enum (`Absolute` or `Relative`).
//! [`MultiscaleConfig`] and [`UpscaleConfig`] live at the top level
//! and apply to both strategies. The detector translates this into
//! lower-level parameter structs internally; those structs
//! (`ChessParams`, `RadonDetectorParams`) are exposed for hand-composed
//! pipelines in [`low_level`].
//!
//! Two opt-in channels sit alongside the primary [`Detector`] API for
//! callers who need more than a finished detection result. The curated
//! low-level surface for hand-composing the detection pipeline
//! (response -> detect -> describe), including its parameter structs and
//! scratch buffers, lives in [`low_level`]; intermediate response maps
//! and Radon heatmaps for debugging and visualization live in
//! [`diagnostics`]. Both carry a weaker stability promise than the
//! facade root and are not needed by typical consumers. For deeper
//! internals (ring offsets, SAT views, scalar reference paths) depend on
//! `chess-corners-core` directly.
//!
//! # Features
//!
//! - `image` *(default)* – enables [`Detector::detect`] and
//!   `image::GrayImage` integration.
//! - `rayon` – parallelizes response computation and multiscale
//!   refinement over image rows. Combine with `par_pyramid` to
//!   parallelize pyramid downsampling as well.
//! - `ml-refiner` – enables the ML-backed refiner entry points via the
//!   `chess-corners-ml` crate and embedded ONNX model.
//! - `simd` – enables portable-SIMD accelerated inner loops for the
//!   response kernel (requires a nightly compiler). Combine with
//!   `par_pyramid` to SIMD-accelerate pyramid downsampling.
//! - `par_pyramid` – opt-in gate for SIMD/`rayon` acceleration inside
//!   the pyramid builder.
//! - `tracing` – emits structured spans for multiscale detection,
//!   suitable for use with `tracing-subscriber` or JSON tracing from
//!   the CLI.
//! - `cli` – builds the `chess-corners` binary shipped with this
//!   crate; it is not required when using the library as a
//!   dependency.
//!
//! The library API is stable across feature combinations; features
//! only affect performance and observability, not numerical results.
//!
//! # References
//!
//! - Bennett, Lasenby. *ChESS: A Fast and Accurate Chessboard Corner
//!   Detector*. CVIU 2014.
//! - Duda, Frese. *Accurate Detection and Localization of Checkerboard
//!   Corners for Calibration*. BMVC 2018.

mod config;
mod detector;
pub mod diagnostics;
mod error;
pub mod low_level;
#[cfg(feature = "ml-refiner")]
mod ml_refiner;
mod multiscale;
mod radon;
mod upscale;

// The crate root surfaces only the stable facade: the detector, its
// configuration and result types, and errors. Diagnostic outputs are
// reachable via [`diagnostics`]; low-level pipeline stages, parameter
// structs, and scratch buffers via [`low_level`]; deeper internals
// (ring offsets, SAT views, scalar reference paths) via a direct
// `chess-corners-core` dependency.
pub use crate::config::{
    ChessConfig, ChessRefiner, ChessRing, DescriptorRing, DetectionStrategy, DetectorConfig,
    MultiscaleConfig, RadonConfig, RadonRefiner, Threshold,
};
pub use crate::error::ChessError;
pub use crate::upscale::{UpscaleConfig, UpscaleError};
pub use chess_corners_core::{
    AxisEstimate, CenterOfMassConfig, CornerDescriptor, ForstnerConfig, OrientationMethod,
    PeakFitMode, RadonPeakConfig, SaddlePointConfig,
};

// Primary detector entry point.
pub use crate::detector::Detector;