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

Migration Guide — 0.10.0

This guide collects every breaking change in the 0.10.0 release for upstream consumers of the calib-targets workspace, with before/after snippets for Rust, the JSON config wire format, and the Python bindings.

The changes fall into six themes:

  1. Chessboard tuning is now opt-in (advanced)
  2. min_corner_strength is a stable top-level field
  3. Diagnostics moved behind a diagnostics cargo feature
  4. cell_size is back on ChessboardDetection
  5. #[non_exhaustive] + named constructors everywhere
  6. Chessboard algorithm renamed to SeedAndGrow

If you only read corners off a detection result and never construct config or result structs by literal, most of this does not affect you — the serialized JSON for a default detection is unchanged and detection behaviour is byte-identical.


1. Chessboard tuning is now opt-in

The ~40 per-stage chessboard tuning knobs that previously lived flat on DetectorParams (via a ChessboardTuning sub-struct flattened into the wire format) have moved behind an opt-in, semver-exempt advanced block.

  • ChessboardTuning was renamed AdvancedTuning. It is re-exported from the chessboard crate root and the calib_targets::chessboard facade. It is documented but explicitly unstable: its fields are NOT covered by semver and may be renamed, retyped, or removed between minor versions. It is #[non_exhaustive], so build it from AdvancedTuning::default() and mutate the knobs you need.

  • DetectorParams now carries four stable fieldsgraph_build_algorithm, min_labeled_corners, max_components, min_corner_strength — plus an opt-in advanced: Option<Box<AdvancedTuning>>. Attach overrides with with_advanced(...); read the effective tuning (configured or default) with effective_tuning(). With advanced unset, detection is byte-identical to the previous defaults.

Rust

#![allow(unused)]
fn main() {
// before
let mut params = DetectorParams::default();
params.tuning.cluster_tol_deg = 9.0;
params.tuning.seed_edge_tol = 0.18;

// after
use calib_targets::chessboard::{AdvancedTuning, DetectorParams};

let mut advanced = AdvancedTuning::default(); // #[non_exhaustive]: start from default
advanced.cluster_tol_deg = 9.0;
advanced.seed_edge_tol = 0.18;
let params = DetectorParams::default().with_advanced(advanced);

// read the knobs the detector will actually use (configured or default):
let tol = params.effective_tuning().cluster_tol_deg;
}

JSON config wire format

Every tuning knob except min_corner_strength now lives under a nested "advanced" object instead of at the top level. Old flat configs that set advanced knobs at the top level silently fall back to the defaults for those knobs — serde ignores the unknown top-level keys. Move them into an "advanced" block to carry them forward. The nested block is omitted entirely when no advanced tuning is set.

// before (flat)
{
  "graph_build_algorithm": "chessboard_v2",
  "min_labeled_corners": 8,
  "max_components": 3,
  "cluster_tol_deg": 9.0,
  "seed_edge_tol": 0.18
}

// after (advanced knobs nested)
{
  "graph_build_algorithm": "seed_and_grow",
  "min_labeled_corners": 8,
  "max_components": 3,
  "min_corner_strength": 0.0,
  "advanced": {
    "cluster_tol_deg": 9.0,
    "seed_edge_tol": 0.18
  }
}

Python

The Python ChessboardParams dataclass keeps the knobs flat for ergonomics, but to_dict() nests the advanced knobs under "advanced" to match the Rust wire format (and from_dict() reads them back from there). No code change is required to set a knob — but note:

  • The unused projective_line_tol_rel knob was removed. It was serialized into the advanced block but never read by the Rust detector, so it was a no-op. Drop the keyword from any ChessboardParams(...) call that set it. Serialized configs that still carry the key continue to deserialize (the extra key is ignored).
# before — projective_line_tol_rel had no effect and is now removed
params = ChessboardParams(cluster_tol_deg=9.0, projective_line_tol_rel=0.05)

# after
params = ChessboardParams(cluster_tol_deg=9.0)

FFI

The C ABI ct_chessboard_params_t keeps the stable fields directly and gates the advanced knobs behind a has_advanced flag plus a nested ct_chessboard_advanced_t. Initialise from ct_chessboard_params_default_values before flipping has_advanced so the advanced fields start from valid defaults, then regenerate against the updated header.


2. min_corner_strength is a stable top-level field

min_corner_strength (the Stage-1 ChESS-response pre-filter) was promoted out of the advanced knobs into the stable DetectorParams core. Its serialized key stays top-level "min_corner_strength", so that one key is wire-compatible with the previous flat layout — no migration needed for configs that set it. Setting it on a nested params.chessboard (ChArUco / PuzzleBoard / marker) keeps working.

#![allow(unused)]
fn main() {
// works the same before and after — now a documented stable field
let params = DetectorParams {
    min_corner_strength: 0.5,
    ..DetectorParams::default()
};
}

3. Diagnostics moved behind a diagnostics cargo feature

The chessboard detector previously assembled a full per-stage DebugFrame on every detect() / detect_all() call and then discarded it. That work is now skipped on the hot path, and the diagnostics surface is opt-in.

  • calib-targets-chessboard gains a diagnostics cargo feature (OFF by default). It gates the diagnostics module (DebugFrame, IterationTrace, StageCounts, the per-stage trace types, DEBUG_FRAME_SCHEMA) and the Detector::detect_with_diagnostics / detect_all_with_diagnostics entry points. Without the feature these names are absent from the public API.

  • The calib_targets facade gains a matching diagnostics feature (OFF by default) that forwards to calib-targets-chessboard/diagnostics and gates detect_chessboard_with_diagnostics.

  • The dataset feature implies diagnostics. The language bindings (Python, WASM, FFI) enable diagnostics unconditionally, so their diagnostic entry points are unchanged.

Behaviour on the detect() path is byte-identical — the same labelled ChessboardDetection.

Renamed entry point. The old detect_chessboard_debug / detect_chessboard_debug_with_config helpers are now the single detect_chessboard_with_diagnostics, which takes the ChESS DetectorConfig explicitly.

Rust

#![allow(unused)]
fn main() {
// before
let frame = detect_chessboard_debug(&img, &params);

// after — requires the `diagnostics` feature on `calib-targets`
use calib_targets::detect::{default_chess_config, detect_chessboard_with_diagnostics};

let frame = detect_chessboard_with_diagnostics(&img, &default_chess_config(), &params);
}
# Cargo.toml — turn the surface back on
calib-targets = { version = "...", features = ["diagnostics"] }

4. cell_size is back on ChessboardDetection

ChessboardDetection regained a stable cell_size: Option<f32> field, populated on the normal detect() path with the seed-derived grid pitch. The type stays #[non_exhaustive], so reading code is unaffected; code constructing it by literal across crates must route through the constructor + builder:

#![allow(unused)]
fn main() {
// constructing across crates (e.g. test fixtures)
let det = ChessboardDetection::new(corners).with_cell_size(31.4);

// reading
if let Some(pitch) = det.cell_size {
    // grid pitch in pixels
}
}

The field is mirrored across the bindings:

  • PythonChessboardDetectionResult.cell_size (float | None).
  • WASMcell_size: number | null on the chessboard result.
  • FFIct_chessboard_result_t.cell_size is now a ct_optional_f32_t (has_value == CT_TRUE carries the pitch). The generated header was regenerated; recompile against it.

5. #[non_exhaustive] + named constructors

Workspace-wide, the public config / spec / report / result types are now #[non_exhaustive] with named constructors. Reading fields is unaffected. External code can no longer build these types with a struct literal or match them exhaustively from another crate — route literal construction through the constructor (and, where present, with_* setters), and add .. to exhaustive patterns.

This is a pure API-surface change: detection behaviour, tuning defaults, and every serialized JSON shape are unchanged.

#![allow(unused)]
fn main() {
// before — struct literal across crates no longer compiles
let corner = LabeledCorner { position, score, grid: None, id: None, target_position: None };

// after — minimal `new` + builder setters
let corner = LabeledCorner::new(position, score).with_grid(grid);
}

Representative constructors (non-exhaustive list):

  • core: TargetDetection::new, LabeledCorner::new (+ with_grid / with_id / with_target_position).
  • chessboard: ChessboardDetection::new (+ with_cell_size), ChessboardCorner::new.
  • charuco: CharucoBoardSpec::new (+ with_marker_layout), CharucoDetectConfig::new, CharucoDetectReport::new.
  • puzzleboard: PuzzleBoardSpec::new (+ with_origin), PuzzleBoardDetectConfig::new, PuzzleBoardDetectReport::new.
  • marker: MarkerBoardSpec::new (+ with_cell_size), MarkerCircleSpec::new, MarkerBoardDetectConfig::new.
  • print: ChessboardTargetSpec::new, CharucoTargetSpec::new, PuzzleBoardTargetSpec::new, MarkerBoardTargetSpec::new.

When adding a field to one of these types later is a non-breaking change because they are #[non_exhaustive] — that is the point of the policy.


6. Chessboard algorithm renamed to SeedAndGrow

The chessboard grid-build algorithm formerly called ChessboardV2 is now SeedAndGrow — a name that describes what the pipeline does (find a self-consistent 4-corner seed, then grow the grid outward) instead of its development history. The companion Topological variant is unchanged, and SeedAndGrow is still the default, so code that relies on the default is unaffected. This is a clean break: the old chessboard_v2 spelling is gone everywhere, with no compatibility alias.

Rust

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

// before
let params = DetectorParams {
    graph_build_algorithm: GraphBuildAlgorithm::ChessboardV2,
    ..DetectorParams::default()
};

// after
let params = DetectorParams {
    graph_build_algorithm: GraphBuildAlgorithm::SeedAndGrow,
    ..DetectorParams::default()
};
}

JSON config wire format

The serialized value changed from "chessboard_v2" to "seed_and_grow". There is no alias, so a config that still sets "chessboard_v2" now fails to parse — update it. Configs that omit the key (the common case) are unaffected, since the default is seed_and_grow.

// before
{ "graph_build_algorithm": "chessboard_v2" }

// after
{ "graph_build_algorithm": "seed_and_grow" }

Python

# before
ChessboardParams(graph_build_algorithm="chessboard_v2")

# after
ChessboardParams(graph_build_algorithm="seed_and_grow")

FFI

The C ABI constant CT_GRAPH_BUILD_ALGORITHM_CHESSBOARD_V2 is now CT_GRAPH_BUILD_ALGORITHM_SEED_AND_GROW (its value is unchanged, 0). Update any references and recompile against the regenerated header.

WASM / TypeScript

The GraphBuildAlgorithm union type is now "topological" | "seed_and_grow".


Verifying your migration

  • A default detection’s serialized JSON is unchanged; if your pipeline round-trips config or result dicts, the only shape change is the nested "advanced" block (omitted when unset).
  • effective_tuning() with advanced unset is byte-identical to AdvancedTuning::default() — there is no silent behaviour drift from the split.
  • If a previously-tuned config stopped changing detection, you are almost certainly hitting the flat→nested advanced fallback (theme 1): move the knobs under "advanced".