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

Migrating to 0.15.0

Breaking, and confined to the marker-board detector, its printable model, and the bindings that mirror them. If you do not use calib-targets-marker or MarkerBoardTargetSpec, nothing here applies and the upgrade is a version bump.

This is a minor rather than a patch for one reason: fields were removed from public structs. #[non_exhaustive] stops a downstream crate constructing or exhaustively destructuring them, but it never stopped reading a field, and #[serde(deny_unknown_fields)] means a stored JSON config naming a removed field now fails to deserialize rather than being ignored.

Why the marker board changed

A board rendered with inner_square_rel above ~0.3 could come back with the frame 90 or 180 degrees rotated while the detector reported three clean circle matches and no error (#96). Every corner then carried a wrong (i, j) label. Two things had to be fixed — the circle scorer had no test that a candidate was actually round, so the white inset drawn inside every black square scored as a marker disk; and frame resolution ranked hypotheses by contrast rather than by agreement, so two bright false candidates could outvote three real ones. The CHANGELOG has the full account.

The printed disk diameter moved

CircleScoreParams::diameter_frac is removed. It stated the same physical quantity as the printable spec’s circle_diameter_rel, and the two could disagree: a board printed at 0.45 was probed at the default 0.5, putting every radius the scorer uses 11% off. The board now says it once.

// Before
let params = MarkerBoardParams::for_board(board);
params.circle_score.diameter_frac = 0.45;
// After — on the board, which is what it describes.
let board = board.with_circle_diameter_rel(0.45);
let params = MarkerBoardParams::for_board(board);

In JSON (Python, WASM, stored configs), move circle_score.diameter_frac to board.circle_diameter_rel. Leaving it in place is a deserialization error, not a silent ignore. In C, ct_circle_score_params_t loses diameter_frac and ct_marker_board_layout_t gains circle_diameter_rel.

Circle matching is a frame sweep, so its distance knob is gone

CircleMatchParams::max_distance_cells and CircleMatch::distance_cells are removed, and CircleMatch::with_match drops its distance_cells argument.

Both encoded the same defunct idea: matching a circle by the distance between a board cell index and a detected cell index, which presumes the translation the alignment is supposed to establish. Resolution now enumerates every (rotation, translation) and verifies it, so a match is an exact integer coincidence and a distance could only ever have reported zero. Delete the field; there is nothing to replace it with.

min_offset_inliers defaults to 3

Was 1. The three circles exist for exactly one reason — to break the board’s 4-fold rotational symmetry — and fewer than all three cannot do it: any single circle is consistent with all four rotations, and two leave no redundancy to check a wrong pairing against.

This costs nothing on a board where all three markers are visible. It costs recall on one where a marker is occluded or blown out, which used to return a frame that was unverified rather than correct. If you need the old behaviour, set it explicitly and understand that you are trading the “never a wrong label” half of the detection contract for recall:

params.match_params.min_offset_inliers = 2;

A new failure, and a new rejection

  • MarkerBoardDetectError::AlignmentAmbiguous { inliers, runner_up } — two board frames explain the detected circles equally well, so neither is returned. The enum is #[non_exhaustive]; code with a wildcard arm compiles unchanged and now sees a case it previously received as a confidently wrong answer.
  • PrintableTargetError::InvisibleCircle { i, j } — a marker circle whose polarity matches the square underneath. The checkerboard’s colour phase is fixed (square (0, 0) is black), so a white disk is only visible on an even i + j and a black one only on an odd one; anything else rendered nothing at all. A spec that previously “worked” this way was producing a board with fewer markers than it claimed.

Corrected without action on your part

  • MarkerBoardTargetSpec::default_circles returned three invisible circles on any board whose centre cell has odd parity — a 6x8 board among them, where it emitted the exact polarity inverse of every shipped fixture. It now anchors the L on an even-parity cell. MarkerBoardSpec::default() on the detector side carried the same contradiction and now matches. If you passed either through unchanged, your board starts working.
  • The inner-square inset is no longer painted under a marker circle. It is drawn in the same white a disk needs, so it merged with one and — once inner_square_rel reached circle_diameter_rel — swallowed it. Regenerate any printed board that carries both.
  • A fully detected board no longer drops the id and target_position of its last row and column (13 of 48 corners on a 6x8). A circle layout names squares, so the resolved frame labelled corners 1..=cols — outside the board’s own index range, with a labelled bounding-box minimum of (1, 1) rather than (0, 0). Labels are inner-corner indices now, and target_position measures from the board’s outer corner, matching the printable spec’s resolved_points. If you stored marker-board corner ids or target positions from an earlier release, they are shifted by one cell in both axes relative to what 0.15.0 produces.

Diagnostics gained fields

CircleCandidate::squareness (what the new shape gate measured: 0 for a disk, 0.48..0.62 for a square inset), and MarkerBoardDiagnostics gains alignment_runner_up_inliers and alignment_ambiguous. Additive on an opt-in surface with a documented looser stability promise.

C ABI 4.0.0 -> 5.0.0

Three struct-layout-breaking changes: ct_marker_board_layout_t gains circle_diameter_rel, ct_circle_score_params_t loses diameter_frac, and ct_circle_match_params_t loses max_distance_cells. Relink, do not just recompile — a consumer built against 4.0.0 passes structs of the wrong size.


Migrating to 0.14.0

Additive on every published surface. No type is removed or renamed, the C ABI stays at 4.0.0, and detection results are unchanged for every board printed with the default one-cell marker border — which is every board the previous releases could describe.

There is exactly one behaviour change, below.

scan.border_bits is derived from the board

CharucoBoardSpec now carries border_bits, the width in marker cells of the black ring printed around each marker payload (OpenCV’s borderBits). It is a physical property of the printed target, so the board spec is where it lives, and CharucoDetector::new now overwrites params.scan.border_bits with the board’s value on every construction path.

If you were reaching past the board spec to describe a wide-ring board:

// Before — the only way to say "this board has a two-cell ring".
let mut params = CharucoParams::for_board(spec);
params.scan.border_bits = 2;
// After — say it on the board, which is what it describes.
let spec = spec.with_border_bits(2);
let params = CharucoParams::for_board(spec);

The old form now silently resolves back to the board’s value (1 unless you set it), so it must be migrated rather than left in place.

Why unconditionally, rather than repairing an “unset” scan value: the scan default is 1, which is indistinguishable from a caller who meant 1. A conditional repair would therefore leave CharucoParams::for_board as the only construction path whose scan config agreed with its own board — reassigning params.board, or deserialising a config that names the board and omits scan (the shape the Python bindings and any stored JSON config take), would keep sampling the ring of whatever board the params were seeded from.

Two fields describing one physical fact is the actual defect; deriving one from the other removes it.

What this does not mean

The previous behaviour was not a decode failure. Measured on a synthetic render, the board-level matcher aligns a border_bits = 2 — even = 3 — board correctly while sampling it with a one-cell ring, because the inset sampling grid lands inside the payload either way. What was genuinely wrong is everything computed from the ring: the min_border_score gate scored cells that are not the printed border, and diagnostics reported a ring the board does not have. The tolerance is a coincidence of one inset value and should not be relied on.

New, no action required

  • CharucoBoardSpec::border_bits / with_border_bits, mirrored on the printable CharucoTargetSpec and the Python CharucoBoardSpec. Defaults to 1; border_bits = 0 is now rejected with CharucoBoardError::InvalidBorderBits (a marker with no ring cannot be told from the white square under it).
  • render_target_bundle_json(doc) on the WASM surface takes a full PrintableTargetDocument — page size, orientation, margin, and every spec field the fixed-arity render_*_bundle helpers do not name. Those helpers are unchanged; handing the new function the page one of them would have synthesised returns a byte-identical SVG.
  • PrintableMarkerCircleSpec in the WASM TypeScript declarations. The printable model’s circle ({ i, j, polarity }) needs a name distinct from the detector’s MarkerCircleSpec ({ cell, polarity }): two Rust crates may share a type name, one .d.ts module may not.

TypeScript-only corrections

Three declarations in typescript-extras.d.ts described something the Rust side does not. Each is a narrowing of what type-checks, and in each case what stops type-checking never worked at runtime:

  • MarkerLayout no longer offers "bottom_left". The Rust enum has exactly one variant, so that value was always rejected at the boundary.
  • CharucoBoardSpec gains border_bits and marker_layout becomes optional, matching the serde shape.
  • The printable circles field is a 3-tuple rather than an array, matching Rust [MarkerCircleSpec; 3], which already rejected any other length at runtime.

If your code type-checked against the old declarations for a marker board, it did not compile at all — the duplicate MarkerCircleSpec merged into an interface requiring the fields of both shapes. Nothing to migrate; it starts working.