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:
- Chessboard tuning is now opt-in (
advanced) min_corner_strengthis a stable top-level field- Diagnostics moved behind a
diagnosticscargo feature cell_sizeis back onChessboardDetection#[non_exhaustive]+ named constructors everywhere- 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.
-
ChessboardTuningwas renamedAdvancedTuning. It is re-exported from the chessboard crate root and thecalib_targets::chessboardfacade. 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 fromAdvancedTuning::default()and mutate the knobs you need. -
DetectorParamsnow carries four stable fields —graph_build_algorithm,min_labeled_corners,max_components,min_corner_strength— plus an opt-inadvanced: Option<Box<AdvancedTuning>>. Attach overrides withwith_advanced(...); read the effective tuning (configured or default) witheffective_tuning(). Withadvancedunset, 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_relknob 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 anyChessboardParams(...)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-chessboardgains adiagnosticscargo feature (OFF by default). It gates thediagnosticsmodule (DebugFrame,IterationTrace,StageCounts, the per-stage trace types,DEBUG_FRAME_SCHEMA) and theDetector::detect_with_diagnostics/detect_all_with_diagnosticsentry points. Without the feature these names are absent from the public API. -
The
calib_targetsfacade gains a matchingdiagnosticsfeature (OFF by default) that forwards tocalib-targets-chessboard/diagnosticsand gatesdetect_chessboard_with_diagnostics. -
The
datasetfeature impliesdiagnostics. The language bindings (Python, WASM, FFI) enablediagnosticsunconditionally, 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_confighelpers are now the singledetect_chessboard_with_diagnostics, which takes the ChESSDetectorConfigexplicitly.
Rust
#![allow(unused)]
fn main() {
// before
let frame = detect_chessboard_debug(&img, ¶ms);
// 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(), ¶ms);
}
# 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:
- Python —
ChessboardDetectionResult.cell_size(float | None). - WASM —
cell_size: number | nullon the chessboard result. - FFI —
ct_chessboard_result_t.cell_sizeis now act_optional_f32_t(has_value == CT_TRUEcarries 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()withadvancedunset is byte-identical toAdvancedTuning::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
advancedfallback (theme 1): move the knobs under"advanced".