Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tuning the Detector

This chapter answers the question: “My detection fails or gives poor results — what do I change?”

Background first. Every parameter below acts on the grid-recovery pipeline — its input-feature kinds, the topological grid builder, and the per-stage contract. If a knob’s name reads as jargon, read The Grid Model first; the tuning reference assumes that vocabulary.

Start here: use the built-in defaults

Before tuning anything, confirm you are starting from the library defaults:

#![allow(unused)]
fn main() {
use calib_targets::detect::detect_chessboard;
use calib_targets::chessboard::DetectorParams;

let params = DetectorParams::default();
}

For ChArUco:

#![allow(unused)]
fn main() {
use calib_targets::charuco::CharucoParams;
let board = todo!();

let params = CharucoParams::for_board(&board);
}

The chessboard detector’s ChESS corner config is not carried inside DetectorParams — it’s a separate argument via calib_targets::detect::default_chess_config() (used automatically by the detect_chessboard* facade helpers). If you need to override it, call calib_targets::detect::detect_corners(&img, &custom_chess_config) directly and pass the resulting corner cloud into calib_targets::chessboard::Detector::new(params).detect(&corners).

For ChArUco, CharucoParams.chessboard is a DetectorParams: a stable core of four fields plus an opt-in advanced block (see the per-parameter reference below). Board sampling scale is controlled separately by CharucoParams::for_board, which starts with px_per_square = 60. If marker decoding is the problem and the board appears at a very different pixel scale, adjust px_per_square before touching other parameters.

Challenging images: multi-config sweep

For images with uneven lighting, Scheimpflug optics, or narrow focus strips, a single threshold may miss corners in some regions. Use the multi-config sweep to try several parameter variants and keep the best result:

#![allow(unused)]
fn main() {
use calib_targets::detect::{detect_chessboard_best, detect_charuco_best};
use calib_targets::chessboard::DetectorParams;
use calib_targets::charuco::CharucoParams;
let img: image::GrayImage = todo!();
let board = todo!();

let chess_configs = DetectorParams::sweep_default();
let chess_result = detect_chessboard_best(&img, &chess_configs);

let charuco_configs = CharucoParams::sweep_for_board(&board);
let charuco_result = detect_charuco_best(&img, &charuco_configs);
}

DetectorParams::sweep_default() returns three configs: default + tighter + looser on cluster_tol_deg, attach_axis_tol_deg, and related tolerances. All three preserve the detector’s precision- by-construction invariants; only recall-affecting tolerances are varied.

For PuzzleBoard, use PuzzleBoardParams::sweep_for_board(&spec).

Multi-component detection (via Detector::detect_all / the facade detect_chessboard_all) recovers fragmented grids where markers break contiguity — each disconnected piece comes back as its own Detection with its own locally-rebased (i, j) labels. Capped by DetectorParams::max_components (default 3).


Symptom → parameter table

min_corner_strength, min_labeled_corners, and max_components are stable top-level fields; every other chessboard knob below is an advanced knob set via DetectorParams::with_advanced(...). ChArUco / PuzzleBoard decode.* knobs sit on their own config structs.

SymptomParameter to adjust
detect_chessboard returns Nonemin_corner_strength ↓, cluster_tol_deg ↑, min_peak_weight_fraction ↓, or try detect_chessboard_best
Partial board, many holesattach_search_rel ↑, attach_axis_tol_deg
Scene has multiple chessboard componentsuse detect_chessboard_all (cap with max_components)
Fast perspective / wide-angle lensedge_axis_tol_deg ↑, geometry_check_local_h_tol_rel
Corners falsely labelled (wrong (i, j))Do not tune — file a bug. precision contract forbids this.
NoMarkers on blurry ChArUcomin_border_score ↓, multi_threshold: true
AlignmentFailed (low inlier count)min_marker_inliers
DecodeFailed on PuzzleBoarddecode.min_bit_confidence ↓, decode.max_bit_error_rate

Per-parameter reference: chessboard::DetectorParams

DetectorParams is a #[non_exhaustive] struct split into two surfaces:

  • a stable core of four fields covered by semver — graph_build_algorithm (single-variant, Topological; retained as a reserved config seam), min_labeled_corners, max_components, and min_corner_strength (see Output gates and Stage 1 below);
  • an opt-in advanced sub-struct (Option<Box<AdvancedTuning>>) holding the ~40 per-stage knobs. AdvancedTuning is NOT covered by semver — leave it unset unless a specific input fails and you have evidence for the change.

Attach overrides with DetectorParams::with_advanced(tuning) and read the effective tuning with effective_tuning(). AdvancedTuning is #[non_exhaustive], so build it from AdvancedTuning::default() and mutate the knobs you need:

#![allow(unused)]
fn main() {
use calib_targets::chessboard::{AdvancedTuning, DetectorParams};

let mut advanced = AdvancedTuning::default();
advanced.cluster_tol_deg = 16.0;
advanced.attach_search_rel = 0.5;
let params = DetectorParams::default().with_advanced(advanced);
}

All knobs in the Stage 2-8 tables below are advanced knobs set on the advanced block; min_corner_strength (Stage 1) and the output gates are stable top-level fields. See the chessboard chapter for the full invariant-to-parameter mapping and crates/calib-targets-chessboard/src/params/ for defaults.

Stage 1 — pre-filter

min_corner_strength is a stable top-level field; max_fit_rms_ratio is an advanced knob.

FieldDefaultGuidance
min_corner_strength0.0Raise to 0.30.5 on noisy scenes with many spurious saddles. Drops weak corners before clustering.
max_fit_rms_ratio0.5ChESS fit_rms must be ≤ ratio × contrast. Raise to 0.8 when accepting softer corners; lower tightens the pre-filter.

Stages 2-3 — grid-direction clustering

FieldDefaultGuidance
num_bins90Histogram resolution (π / n per bin). Rarely adjusted.
cluster_tol_deg12.0Per-axis absolute tolerance vs cluster centre for a corner to be labelled. Raise to 16 on noisy axes; tighter risks unclustering legitimate corners.
peak_min_separation_deg60.0Minimum angle between the two returned peaks. Guards against twin-peak collisions.
min_peak_weight_fraction0.02Fraction of total axis-vote weight a peak must carry. Lower on dense boards where each real peak only carries a few percent; higher rejects spurious noise peaks.

Stage 5 — seed

Seed-finding tolerances are internal to the topological grid builder and are not exposed as public tuning knobs. If seeding consistently fails, use detect_chessboard_best with DetectorParams::sweep_default() which varies the upstream clustering and attachment tolerances.

Stage 6 — grow

FieldDefaultGuidance
attach_search_rel0.35KD-tree search radius around each prediction (fraction of cell_size). Raise to 0.450.55 on images with noticeable perspective; tighter rejects more holes.
attach_axis_tol_deg15.0Candidate’s axes must match both cluster centres within this tolerance.
attach_ambiguity_factor1.5If the second-nearest candidate is within factor × nearest, attachment is skipped (the position is marked ambiguous).
step_tol0.25Edge-length window at attachment ([1 − step_tol, 1 + step_tol] × s).
edge_axis_tol_deg15.0Induced-edge axis alignment at attachment.

Stage 7 — validate

FieldDefaultGuidance
geometry_check_local_h_tol_rel0.20Local 4-point homography residual tolerance for the final geometry check.
line_min_members3Minimum row/column length for a line fit to be attempted.

Stage 8 — recall boosters

Per-stage toggle (an advanced knob): enable_weak_cluster_rescue (default true). Leave it on unless the weak-cluster booster is producing false positives for you. Line extrapolation, gap fill, and component merge run unconditionally and are no longer configurable.

Output gates

min_labeled_corners and max_components are stable top-level fields.

FieldDefaultGuidance
min_labeled_corners8Detection rejected below this labelled count. Raise for validation boards with an expected floor.
max_components3Cap for detect_all. Raise if a scene legitimately fragments into more pieces of the same board (rare).

Per-parameter reference: ScanDecodeConfig / ChArUco

These parameters live inside CharucoParams.

min_border_score

Default: 0.75 for ChArUco.

Guidance: Minimum contrast score for the black border ring around a marker. Lower cautiously to 0.65 for very blurry images. Values below 0.60 risk accepting non-marker regions.

multi_threshold

Default: true.

Guidance: When enabled, the decoder tries several Otsu-style binarization thresholds until a dictionary match is found. This handles uneven lighting and motion blur at the cost of a small speed penalty. Disable only when speed is critical and lighting is controlled.

inset_frac

Default: 0.06 for ChArUco.

Guidance: Fraction of the cell size inset from the cell boundary before sampling the marker interior. Raise to 0.100.12 when the cell boundary visibly bleeds into the bit area (common with thick printed borders or strong blur).

marker_size_rel

Source: Board specification — must match the printed board exactly.

Guidance: Ratio of the ArUco marker side to the chessboard square side. A mismatch here causes systematic decoding failures even when all other parameters are correct. Verify against the printed board or the JSON spec used to generate it.


Quick checklist

  1. Start with defaults; run with RUST_LOG=debug to see corner counts and per-stage counters.
  2. If no corners are found: loosen min_corner_strength, check image resolution and contrast.
  3. If corners found but no grid (detect_chessboard returns None): run calib_targets_chessboard::trace_topological — few usable corners means the prefilter / clustering is too tight (try lowering min_corner_strength or the advanced min_peak_weight_fraction), Err(NoComponents) means the topological builder assembled no grid (try detect_chessboard_best), and components found but a refused detection points at the final geometry check (inspect GeometryCheckTrace.dropped_*; try a wider config). See Troubleshooting for the full chain.
  4. If grid found but no ChArUco markers: enable multi_threshold, lower min_border_score.
  5. If alignment fails: verify board spec (rows, cols, dictionary, marker_size_rel).
  6. If you observe wrong (i, j) labels, that’s a precision- contract bug — file an issue rather than tuning around it. The detector is engineered to drop corners before it labels them wrong.

See also: Troubleshooting for per-error checklists and the Chessboard Detector chapter for the full invariant stack.