Skip to content

0.9.0 — 2026-10-04

← Back to the changelog index

Real-camera recall, sub-pixel corners, and a reproducible comparison against OpenCV and aruco_nano. - Comparison method: the figures compare standard against v0.8.0 on the same data with cargo xtask sota, using full datasets. - Hardware: AMD EPYC-Milan KVM, 8 vCPU, AVX2, Linux 6.8, rustc 1.92, --release. - Reports: EuRoC, scoreboard.

Highlights

  • Real cameras. On EuRoC cam_april (Kalibr AprilGrid), standard decodes 86.0 % of the in-view tags at 99.996 % precision.
  • Recall: v0.8.0 20.8 %; best reference OpenCV, 57.1 %.
  • Leave-one-tag-out corner error: 0.283 px. v0.8.0: 0.912 px; OpenCV APRILTAG: 0.516 px.
  • Corners. A sub-pixel corner stage, decoder.corner_subpix:
  • fuses a junction estimate with the marker's whole-edge lines;
  • repairs gross seed corners;
  • measures and removes, on each decoded marker, the photometric inset that every gradient estimator has on gamma-encoded images.

Debiased corner error on render-tag drops from 0.44–0.58 px to 0.05–0.21 px; references score 0.22–0.24 px. - Precision. A decoded marker must show its dark border ring, within a per-family error budget. tag16h5 precision rises from 83 % to 100 %, and every benchmark is at 99.9 % or better. - Hard light and lenses. Recall: - low_key: 14 → 66 %; - raw_pipeline: 60 → 96 %; - Kannala–Brandt AprilGrid: 83 → 96 %; - EuRoC: 21 → 86 %. - Boards. The board pose models each frame's photometric inset. Board translation error on render-tag boards falls 5–9×. - New: the ArUcoMip36h12 tag family; cargo xtask data for pinned datasets; and cargo xtask sota for pinned reference detectors and a win table. - Faster. 1-thread latency of standard against v0.8.0, measured serially and back to back on the same machine:

Benchmark v0.8.0 (ms) v0.9.0 (ms) Change
render-tag 1080p 39.1 21.7 −45 %
render-tag 4K 151.0 85.0 −44 %
ICRA forward 96.9 54.9 −43 %
AprilGrid board 48.9 25.8 −47 %
EuRoC 7.6 7.4 −2 %

EuRoC now decodes four times as many tags in the same time. - Cost: - ICRA forward recall: 73.8 → 72.0 %. - ChArUco marker recall: 99.8 → 99.2 %.

Breaking changes

Default output. standard, grid and DetectorConfig::default() produce different corners, poses and detection sets. high_accuracy changes only through the fixes listed under Fixed. - standard and grid decode first and refine only what decodes. - They require the dark border ring, with a per-family budget. - They run decoder.corner_subpix. - standard also segments with 4-connectivity and disables the blob-shape quad gates (min_fill_ratio, min_density, max_elongation = 0). - Every profile rejects markers that the image frame cuts.

Profile JSON and Pydantic config:

Change Migration
extends removed (never implemented) Delete it.
threshold.min_radius, max_radius, gradient_threshold, min_range, constant removed Delete them. LocalMean subtracts clamp(round(noise_k·σ̂ₙ), 2, 20).
threshold.noise_k must be > 0, default 4.0 Remove "noise_k": 0.0.
threshold.local_mean_radius default 24 → 7 Set it if you relied on 24.
quad.refine_before_decode removed Delete it. Decode-first applies to Static + Erf at native resolution.
decoder.refinement_mode: "Gwlf" removed, with decoder.gwlf_transversal_alpha Use "Erf" or "None".
pose.pose_consistency_min_decisive_ratio removed (fixed at 5.0) Delete it.
Boolean quad.edlines_imbalance_gate rejected Use "Enabled" / "Disabled".
AdaptivePpb requires decoder.refinement_mode: "None" Set it.
pose.structure_tensor_radius: 0 rejected Use 1–8.
decoder.max_border_error_rate added: None = per-family budget, 1.0 = off Set 1.0 to disable the ring check.
Every key is optional and defaults to standard in both Rust and Python An omitted decoder.corner_subpix used to mean false in Rust; set it explicitly if you relied on that.

Rust API (locus-core): - DetectorConfig: - Fields removed: threshold_min_radius, threshold_max_radius, threshold_gradient_threshold, threshold_min_range, adaptive_threshold_constant, pose_consistency_min_decisive_ratio, gwlf_transversal_alpha, quad_refine_before_decode. - Fields added: threshold_mode, threshold_local_mean_radius, threshold_noise_k, decoder_max_border_error_rate, decoder_corner_subpix. Struct literals need them, or ..DetectorConfig::default(). - DetectorConfigBuilder and DetectorConfig::builder() removed. Use struct update syntax or from_profile(..). - DetectorBuilder keeps only new, with_config, with_family, with_decimation, with_threads, with_max_concurrent_frames, build, validated_build and build_engine. Set config fields and pass them with with_config. - DetectOptions / DetectOptionsBuilder removed. Families go to with_family / set_families; intrinsics and tag size go to detect. - CornerRefinementMode::Gwlf removed; TagFamily::ArUcoMip36h12 added (the enum is exhaustive). - ConfigError: - InvalidPoseConsistencyMinDecisiveRatio removed. - AdaptivePolicyStaticRefinement added. - InvalidNoiseK and InvalidStructureTensorRadius now cover 0. - DetectionBatch::corner_refined added; TelemetryPayload::{gwlf_fallback_count, gwlf_avg_delta} removed.

Python API (locus): - DetectorBuilder is a pure-Python builder that validates through DetectorConfig. - It keeps with_family, with_decimation, with_threads, with_max_concurrent_frames and build, and adds with_profile and with_config. - Every per-knob setter is removed. - build() returns locus.Detector. - Removed: CornerRefinementMode.Gwlf, locus.DetectOptions, and PipelineTelemetry GWLF counters. - Added: TagFamily.ArUcoMip36h12, locus.ThresholdMode, Detector(..., max_concurrent_frames=n).

Tooling: - scripts/fetch_euroc_calibration.sh is removed; use cargo xtask data fetch euroc. - sync_hub.py --repo-id is removed. - Hub datasets are pinned to a revision. - bench real uses the selected profile unless an option is given explicitly: --constant / --min-range are now --noise-k, and Gwlf is gone.

Migration

To reproduce v0.8.0's standard, start from standard and set:

{
  "quad": { "min_fill_ratio": 0.10, "min_density": 0.15, "max_elongation": 20.0 },
  "decoder": { "max_border_error_rate": 1.0, "corner_subpix": false },
  "segmentation": { "connectivity": "Eight" }
}

You cannot switch decode-first ordering off for the Erf route.

The new corner stage and the frame-clipping rule apply to undistorted cameras. With a declared distortion model, the distortion-aware path still refines before decoding; its redesign is planned.

Added

  • decoder.corner_subpix: a sub-pixel corner stage for decoded markers. It is on in standard and grid, and off in high_accuracy.
  • Junction estimate: the gradient-orthogonality (cornerSubPix) model. The window comes from the marker cell; among windows of 0.3, 0.5 and 0.75 cell (2–4 px), it keeps the largest one that is not significantly less certain. A junction check rejects captures by outside structure.
  • Fusion: the junction is fused, by inverse covariance, with the intersection of the marker's two whole-edge lines. This is done only when they agree (χ²₂ 99 %) and all four corners qualify. X-junctions, such as AprilGrid connectors, keep the junction.
  • Gross-corner repair: a corner the junction model rejects while both neighbours pass is re-placed where its two edges cross. The edges are fitted from the good neighbours, and the junction model must confirm the result.
  • Photometric calibration (marker_inset.rs):
    • Why: cameras blur in linear light and then apply a tone curve, so every gradient estimator finds edges shifted toward the dark side. Corners sit 0.4–0.8 px inside the printed ones, OpenCV and aruco_nano included. On a single tag that becomes depth and rotation error.
    • Method: a robust per-marker fit to the decoded bit boundaries separates the inset from the layout scale, the per-side shrink and a lens bow (kept only when an F-test finds it). It needs no camera or tone-curve model.
  • Skipped: markers with cells under about 3.3 px, and declared lens distortion.
  • Dark border-ring evidence (decoder.max_border_error_rate). A decoded candidate must show the one-cell black ring around its payload.
  • Budget: by default it is the codeword's own error density, max_hamming / bit_count: 1 cell of 28 for tag36h11, 0 for tag16h5.
  • Decode-first ordering. Candidates decode from their contour corners. Only decoded candidates and near misses are refined, and a match stands only if the refined quad decodes the same id within budget.
  • Near-miss recovery budgeted by probability. Recovery runs within each family's recovery window, the Hamming distance random bits reach with probability ≤ 0.1. It also requires the seed to show a dark ring.
  • Board inset. BoardEstimator estimates a per-frame photometric edge offset for each corner estimator, kept when significant. DetectionBatch::corner_refined records which estimator placed each corner.
  • Opt-in threshold.mode = "LocalMean". A per-pixel local mean minus a noise-calibrated offset: radius 7, noise_k 4, using an allocation-free Immerkær noise estimate. No shipped profile uses it yet.
  • ArUcoMip36h12 (cv2.aruco.DICT_ARUCO_MIP_36h12) tag family.

Changed

  • standard segments dark regions with 4-connectivity and judges candidates by marker evidence, not blob shape.
  • Connectivity: 8-connectivity links squares that touch at a corner (AprilGrid connectors) and fuses the whole board into one component.
  • Blob gates: they reject hollow-ring and merged markers that decode.
  • Cost: markers of about 2 px per cell, whose one-pixel ring connects only diagonally.
  • Codebook search is one branch-free pass, dispatched at runtime to popcnt/AVX2/NEON. Results are identical.

Fixed

  • Gamma inset: corners on gamma-encoded images sat 0.4–0.8 px inside the printed outline (see Added).
  • Frame-clipped markers: markers cut by the frame were reported with a clipped corner. A marker is now rejected when a corner lies outside the image, or a side's middle lies within 3 px of the border. A corner may still touch the border.
  • Multi-family decoding:
  • Decoding kept the last matching family instead of the one at the lowest Hamming distance.
  • The corner stage now reads the matched family's layout.
  • Resampling maps: they were off by 1 px at decimation = 2 and by 0.25 px at upscale_factor = 2. decimate_to now area-averages, and upscale_to rounds.
  • Smaller fixes:
  • The gradient sampler's border fallback was 0.5 px off.
  • The AVX2 branch of the ERF gradient projection missed the pixel-centre offset.
  • Unchecked sampler: the marker calibration's unchecked sampler needed the homography depth to keep one sign. Strongly foreshortened quads now take the checked sampler.
  • Stale corner-class bits: the distortion-aware decoder left corner-class bits from earlier frames.
  • More than eight families registered through set_families panicked.
  • Custom profile defaults: a Rust custom profile that omitted decoder.corner_subpix got false; Python got true.

Performance

Net 1-thread latency against v0.8.0 is under Highlights (−2 to −47 %). The output-identical speed-ups behind it: - the cached and band-restricted ERF scans; - one edge fit per quad side; - a shared exp(−s²); - one decode-first seed refinement per candidate; - tracing without the full-frame label image (built only for EdLines); - contour pre-rejects (Pick's-theorem decodability floor, compactness); - runtime-dispatched codebook search.

Tooling and benchmarks

  • cargo xtask data provisions every dataset (Hub suites, ICRA 2020, EuRoC, Liu4K) from one pinned manifest: list, fetch and verify.
  • cargo xtask sota builds pinned, unpatched references: OpenCV 4.10 with the NONE, SUBPIX and APRILTAG refiners, aruco_nano 961b18b, and AprilTag 3.
  • It runs all detectors under one timing protocol, scores 17 benchmarks, and writes a win table against the best reference operating point.
  • Corner cells are judged on debiased error, with each detector's photometric offset reported separately.
  • EuRoC scoring:
  • It now judges only within the published lens model's valid radius.
  • It measures error in image pixels.
  • It pools its reference only from self-consistent detectors.
  • Distortion benchmarks: hub-aprilgrid-bc and hub-aprilgrid-kb.

Documentation

  • Updated for the current pipeline: pipeline, algorithm, architecture, memory-model, guide, FFI-contract and profile docs.
  • New benchmark reports and lessons under docs/engineering/, linked from benchmarking.