Part II: Using the library
This chapter is a walk through the public API on every binding target. Code-first; algorithms are covered in Part III (ChESS), Part IV (Radon), and Part V (refiners).
2.1 Configuration shape
DetectorConfig has one place for every knob. Cross-cutting fields
sit at the top level; detector-specific fields are nested inside the
active strategy variant.
| Top-level field | Type |
|---|---|
strategy | DetectionStrategy::Chess(ChessConfig) or DetectionStrategy::Radon(RadonConfig) — selects the detector and carries its tuning. |
threshold | A single f32. ChESS reads it as an absolute floor on the raw response (default 30); Radon reads it as a fraction in [0, 1] of the per-frame maximum (default 0.28). See Part III §3.3.1 and Part IV §4.4. |
detection | DetectionParams { nms_radius, min_cluster_size } — shared NMS and cluster-filter knobs honoured by both detectors. |
multiscale | MultiscaleConfig::SingleScale or MultiscaleConfig::Pyramid { levels, min_size, refinement_radius }. Honoured by both detectors. |
upscale | UpscaleConfig::Disabled or UpscaleConfig::Fixed(factor) (factor ∈ {2, 3, 4}). Pre-pipeline bilinear upscaling for low-resolution inputs. |
orientation_method | OrientationMethod::RingFit (default) or DiskFit. Drives the two-axis descriptor fit on both detectors; without_orientation() skips the fit and leaves each descriptor’s axes as None. |
merge_radius | Duplicate-suppression distance (base-image pixels) for the final cross-scale merge step. |
Inside DetectionParams (set via DetectorConfig.detection):
| Field | Meaning |
|---|---|
nms_radius | Non-maximum-suppression window half-radius, in input-image pixels. ChESS preset default: 2; Radon preset default: 4. |
min_cluster_size | Minimum positive-response neighbours inside the NMS window. Default: 2 for both presets. |
Inside ChessConfig:
| Field | Meaning |
|---|---|
ring | ChessRing::Canonical (r=5, paper default) or ChessRing::Broad (r=10, wider support window). Descriptors always sample at this same radius. |
refiner | ChessRefiner::CenterOfMass(_), Forstner(_), SaddlePoint(_), or Ml (with ml-refiner). Each variant carries its own tuning struct. |
Inside RadonConfig:
| Field | Meaning |
|---|---|
ray_radius | Half-length of each ray (working-resolution pixels). Paper default at image_upsample = 2 is 4. |
image_upsample | 1 (no supersample) or 2 (paper default). Values ≥ 3 are clamped to 2. |
response_blur_radius | Half-size of the box blur applied to the response map. 0 disables blurring. |
peak_fit | PeakFitMode::Parabolic or Gaussian for the 3-point subpixel refinement. |
Four presets cover the common cases:
| Preset | Detector | Scale |
|---|---|---|
DetectorConfig::chess() | ChESS | Single-scale |
DetectorConfig::chess_multiscale() | ChESS | 3-level pyramid |
DetectorConfig::radon() | Radon | Single-scale |
DetectorConfig::radon_multiscale() | Radon | 3-level pyramid |
Three guarantees follow from this shape:
- One place per knob.
cfg.strategy.chess.ring = ChessRing::Broadis the only way to request the wider ChESS sampling ring. There is no parallel top-level “broad” flag. - Per-detector refiners.
ChessRefinerlists the refiners that operate on ChESS output (CenterOfMass,Forstner,SaddlePoint,Ml). The Radon detector does not have a pluggable refiner; its subpixel step is the built-in Gaussian peak fit (peak_fit). - Enum-with-payload everywhere a knob has an “on/off + tuning”
shape.
MultiscaleConfig,UpscaleConfig, andChessRefinershare the same encoding, so the JSON shape and the binding surface stay symmetric across all of them. (thresholdis a plain number — a single scalar carries no tuning, so it needs no tag.)
2.2 Rust
Add the facade crate:
[dependencies]
chess-corners = "1.0"
image = "0.25" # optional, for GrayImage integration
2.2.1 Single-scale ChESS detection from an image file
use chess_corners::{Detector, DetectorConfig};
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(); // ChESS detector, defaults
let mut detector = Detector::new(cfg)?;
let corners = detector.detect(&img)?;
println!("found {} corners", corners.len());
Ok(())
}
corners is a Vec<CornerDescriptor> with subpixel positions and
per-corner intensity-fit metadata (Part I §1.4).
2.2.2 Radon detector instead of ChESS
#![allow(unused)]
fn main() {
use chess_corners::{Detector, DetectorConfig};
let cfg = DetectorConfig::radon(); // Radon detector, paper defaults
let mut detector = Detector::new(cfg)?;
let corners = detector.detect(&img)?;
}
DetectorConfig::radon() builds a DetectionStrategy::Radon(RadonConfig)
with the paper’s published defaults. The output type is the same
Vec<CornerDescriptor>.
Try Radon when ChESS’s 16-sample ring does not seed enough corners, especially on the small-cell, blur, and low-contrast cases covered by the repository tests. For throughput, ChESS is faster in the measured fixtures; see Part IV §4.5.
2.2.3 Swapping the subpixel refiner
#![allow(unused)]
fn main() {
use chess_corners::{ChessRefiner, DetectorConfig};
let cfg = DetectorConfig::chess_multiscale()
.with_chess(|c| c.refiner = ChessRefiner::forstner());
}
Each refiner variant carries its tuning struct inline:
#![allow(unused)]
fn main() {
use chess_corners::{ChessRefiner, ForstnerConfig};
// The per-refiner config structs are `#[non_exhaustive]`: start from
// `Default` and set only the fields you care about.
let mut f = ForstnerConfig::default();
f.max_offset = 2.0;
let refiner = ChessRefiner::Forstner(f);
}
The Radon detector does not have a pluggable refiner; its subpixel step
is the built-in Gaussian peak fit, configured via
RadonConfig.peak_fit. See Part V.
2.2.4 Raw buffer API
If your pixels come from a camera SDK, FFI, or GPU pipeline, skip
the image crate and feed a packed &[u8]:
#![allow(unused)]
fn main() {
use chess_corners::{Detector, DetectorConfig};
fn detect(img: &[u8], width: u32, height: u32) -> Result<(), chess_corners::ChessError> {
// ChESS reads `threshold` as an absolute floor on the raw response;
// the default is 30, raised here to suppress more textured-region noise.
let cfg = DetectorConfig::chess()
.with_threshold(60.0);
let mut detector = Detector::new(cfg)?;
let corners = detector.detect_u8(img, width, height)?;
println!("found {} corners", corners.len());
Ok(())
}
}
Requirements:
imgiswidth * heightbytes, row-major.0is black,255is white.
If your buffer has a stride or is interleaved RGB, copy the luminance channel to a packed buffer first. The facade does not resample stride; the only supported layout is tightly packed.
2.2.5 Inspecting corners
#![allow(unused)]
fn main() {
for c in &corners {
match &c.axes {
Some([a0, a1]) => println!(
"({:.2}, {:.2}) response={:.1} axes=({:.2}, {:.2}) rad",
c.x, c.y, c.response, a0.angle, a1.angle,
),
None => println!(
"({:.2}, {:.2}) response={:.1} (orientation skipped)",
c.x, c.y, c.response,
),
}
}
}
The axes field is Option<[AxisEstimate; 2]>. Some([a0, a1])
gives two directions, both in radians and not assumed orthogonal,
with a0.sigma / a1.sigma their 1σ angular uncertainties. It is
None when the orientation fit was disabled with
without_orientation() (see
Part VI §6.5).
See Part III §3.4
for the fit and the polarity convention.
2.3 Python
Install from PyPI:
python -m pip install chess-corners
import numpy as np
import chess_corners
img = np.zeros((128, 128), dtype=np.uint8)
cfg = chess_corners.DetectorConfig.chess_multiscale()
cfg.threshold = 60.0 # ChESS: absolute floor on the raw response (default 30)
# Nested getters return the live shared object, so direct attribute
# assignment propagates back to `cfg` — no rebuild needed:
cfg.strategy.chess.refiner = chess_corners.ChessRefiner.forstner()
detector = chess_corners.Detector(cfg)
det = detector.detect(img)
print(det.xy.shape) # (N, 2) float32
print(det.response.shape) # (N,) float32
Detector(cfg).detect(image) accepts a 2D uint8 array shaped
(H, W) and returns a Detections object with named arrays:
det.xy—(N, 2)float32, subpixel corner positions (x, y) in input pixelsdet.response—(N,)float32, raw detector response at each peakdet.angles—(N, 2)float32,[axis0_angle, axis1_angle]in radians[0, π), orNonewhen orientation is disableddet.sigmas—(N, 2)float32, 1σ uncertainty per axis in radians, orNonewhen orientation is disabled
When the orientation fit is skipped (cfg.without_orientation()),
det.angles and det.sigmas are None.
The Python DetectorConfig mirrors the Rust type field-for-field and
supports to_dict(), from_dict(), to_json(), from_json(),
pretty(), and print(). The factory methods are chess(),
chess_multiscale(), radon(), and radon_multiscale().
cfg.threshold is a plain float — assign it directly. The tagged
enum classes follow a common idiom: MultiscaleConfig.single_scale() /
.pyramid(...), UpscaleConfig.disabled() / .fixed(k),
ChessRefiner.center_of_mass(...) / .forstner(...) /
.saddle_point(...) / .ml(). Orientation can be turned off entirely
with cfg.without_orientation(), after which det.angles and
det.sigmas are None.
Nested getters (cfg.strategy, cfg.strategy.chess, cfg.threshold,
cfg.multiscale, …) all return the live shared object held by the
parent — direct attribute assignment is enough:
cfg.strategy.chess.refiner = chess_corners.ChessRefiner.forstner()
cfg.strategy.chess.ring = chess_corners.ChessRing.BROAD
For chainable single-expression edits, use the with_chess(**kwargs) /
with_radon(**kwargs) builders, which return a new config with only
the named fields replaced:
cfg = cfg.with_chess(
refiner=chess_corners.ChessRefiner.forstner(),
ring=chess_corners.ChessRing.BROAD,
)
The Radon strategy is selected the same way:
cfg = chess_corners.DetectorConfig.radon()
If the wheel was built with ml-refiner, the ML pipeline is reached
through the same Detector(cfg).detect(img) call once the active ChESS
refiner is the ml variant:
cfg.strategy.chess.refiner = chess_corners.ChessRefiner.ml()
2.4 JavaScript / WebAssembly
Build the wasm package from source:
wasm-pack build crates/chess-corners-wasm --target web
Or consume the published npm package @vitavision/chess-corners. Usage
from a web app:
import init, {
ChessDetector,
ChessConfig,
ChessRefiner,
ChessRing,
DetectionStrategy,
DetectorConfig,
ForstnerConfig,
MultiscaleConfig,
} from '@vitavision/chess-corners';
await init();
// Build a typed configuration tree.
const cfg = DetectorConfig.chessMultiscale();
cfg.threshold = 60.0; // ChESS: absolute floor on the raw response (default 30)
cfg.multiscale = MultiscaleConfig.pyramid(3, 128, 3); // levels, minSize, refinementRadius
const chess = new ChessConfig();
chess.ring = ChessRing.Broad;
chess.refiner = ChessRefiner.withForstner(new ForstnerConfig());
cfg.strategy = DetectionStrategy.fromChess(chess);
const detector = ChessDetector.withConfig(cfg);
// From a canvas (webcam frame, loaded image, etc.)
const imageData = ctx.getImageData(0, 0, width, height);
const corners = detector.detect_rgba(imageData.data, width, height);
// corners is Float32Array, stride 7 per corner — same layout as Python.
// (After cfg.withoutOrientation(), the four axis values are NaN per corner.)
for (let i = 0; i < corners.length; i += 7) {
console.log(`(${corners[i].toFixed(2)}, ${corners[i + 1].toFixed(2)})`);
}
// Raw response map, for heatmap visualisation (opt-in diagnostic).
const response = detector.diagnostics_response_rgba(imageData.data, width, height);
ChessDetector reads and writes its full configuration through the
typed tree — detector.getConfig() returns an independent
DetectorConfig snapshot and detector.applyConfig(cfg) commits
edits. The factory functions on the tagged classes follow the same
with* idiom: ChessRefiner.withForstner(cfg),
ChessRefiner.withCenterOfMass(cfg),
ChessRefiner.withSaddlePoint(cfg), MultiscaleConfig.singleScale(),
MultiscaleConfig.pyramid(levels, minSize, refinementRadius), UpscaleConfig.disabled(),
UpscaleConfig.fixed(factor).
2.5 CLI
cargo run -p chess-corners --release --bin chess-corners -- \
run config/chess_cli_config_example.json
The CLI:
- Loads the image at the config’s
imagefield. - Picks single-scale or multiscale from the top-level
multiscalefield. - Picks ChESS or Radon from
strategy(the top-level variant). - Picks the refiner from the strategy’s nested
refinerblock. - Writes a JSON summary and a PNG overlay with one mark per corner.
The JSON config is the same DetectorConfig schema as the Rust and
Python APIs, wrapped in a CLI envelope that adds image,
output_json, output_png, log_level, and ml:
{
"image": "testimages/mid.png",
"strategy": {
"chess": {
"ring": "canonical",
"refiner": { "center_of_mass": { "radius": 2 } }
}
},
"threshold": 30.0,
"detection": { "nms_radius": 2, "min_cluster_size": 2 },
"multiscale": "single_scale",
"upscale": "disabled",
"orientation_method": "ring_fit",
"merge_radius": 3.0,
"output_json": null,
"output_png": null,
"log_level": "info"
}
Example configs under config/:
chess_algorithm_config_example.json— just the algorithm fields, the pureDetectorConfigshape shared with the Rust and Python APIs.chess_cli_config_example.json— algorithm fields plus CLI I/O envelope.chess_cli_config_example_ml.json— same, with the ML refiner enabled. Requires a binary built with--features ml-refiner.
In JSON, threshold is written as a bare number (ChESS: absolute
response floor; Radon: fraction of the per-frame maximum), and
orientation_method may be null to skip the orientation fit — in
which case each corner record in the output JSON carries axes: null.
Per-flag overrides (applied on top of the JSON):
--threshold <v>(ChESS: absolute response floor; Radon: fraction of the per-frame max)--chess-ring canonical|broad--chess-refiner center_of_mass|forstner|saddle_point--pyramid-levels <n>(1 = single-scale)--upscale-factor 0|2|3|4
Overlay examples on the sample images in testdata/:



2.6 ML refiner
The ML refiner is a separate, optional code path. Enable it by
building with --features ml-refiner (Rust) or by installing a
wheel built with the same feature (Python), then pick the
Ml variant on the active ChESS refiner:
#![allow(unused)]
fn main() {
#[cfg(feature = "ml-refiner")]
{
use chess_corners::{ChessRefiner, Detector, DetectorConfig};
let cfg = DetectorConfig::chess_multiscale()
.with_chess(|c| c.refiner = ChessRefiner::Ml);
let mut detector = Detector::new(cfg).unwrap();
let corners = detector.detect(&img).unwrap();
}
}
The ML path:
- Runs the ChESS detector to produce seeds.
- Feeds each seed’s 21×21 neighborhood through the embedded
ONNX model (
chess_refiner_v4.onnx, ~180 K params). - Replaces the seed position with the model’s predicted
(dx, dy)offset. - Falls back to the configured classical refiner if the ML path rejects or times out.
The algorithm and its limits are covered in
Part V §5.5. Compared to the
geometric refiners: SaddlePoint has lower error on blurred data while
ML leads on the heaviest noise row (see the comparison in Part VIII).
The ML refiner is available on the ChESS strategy only. The Radon detector does not use a pluggable refiner.
2.7 Radon heatmap (visualization)
The Radon detector computes a dense (max_α S_α − min_α S_α)²
response heatmap as an intermediate step. The heatmap is exposed
across all wrappers for visualization, debugging, and downstream
tooling — useful when tuning ray_radius, image_upsample, or the
threshold floor.
The heatmap is returned at working resolution: the input is
optionally upscaled (DetectorConfig.upscale) and then internally
supersampled by the Radon detector (RadonConfig.image_upsample,
default 2). The working-to-input scale factor is therefore
upscale_factor * image_upsample. Multiply input-pixel coordinates by
this factor to land on heatmap pixels.
Rust:
#![allow(unused)]
fn main() {
use chess_corners::diagnostics::radon_heatmap_u8;
use chess_corners::DetectorConfig;
fn run(img: &[u8], width: u32, height: u32) {
let cfg = DetectorConfig::radon();
let map = radon_heatmap_u8(img, width, height, &cfg);
println!("heatmap {}×{}, max = {:.1}",
map.width(), map.height(),
map.data().iter().copied().fold(f32::NEG_INFINITY, f32::max));
}
}
Python:
import chess_corners
import numpy as np
cfg = chess_corners.DetectorConfig.radon()
detector = chess_corners.Detector(cfg)
heatmap = detector.radon_heatmap(img) # (H', W') float32
print(heatmap.shape, heatmap.dtype, float(heatmap.max()))
WebAssembly (JS):
import init, { ChessDetector, DetectorConfig } from '@vitavision/chess-corners';
await init();
const det = ChessDetector.withConfig(DetectorConfig.radon());
const heatmap = det.diagnostics_radon_heatmap(grayPixels, width, height);
const w = det.diagnostics_radon_heatmap_width();
const h = det.diagnostics_radon_heatmap_height();
const scale = det.diagnostics_radon_heatmap_scale(); // working-to-input factor
console.log('heatmap', w, 'x', h, 'scale', scale);
The heatmap is independent from corner detection: calling it does not require the active strategy to be Radon, and it does not return corners.
In this part we focused on the public faces of the detector: the
image helper, the raw buffer API, the CLI, and the Python and JS
bindings. In the next parts we will look under the hood at how the
ChESS response is computed, how the detector turns responses into
subpixel corners, and how the multiscale pipeline is structured.
Next: Part III describes the ChESS response kernel, the detection pipeline, and the corner descriptor fit. Part IV covers the Radon detector.