Recall, quad extraction & ICRA — lessons
Status: CLOSED — the diagnosed issues shipped fixes; max_recall_adaptive profile was since removed.
Last updated: 2026-07-19
Owning code: quad extraction (extract_quads_soa / extract_quads_soa_with_camera and pixel_count_descending_order in the quad module), the AdaptivePpb extraction router (quad.extraction_policy), the decoder Hamming/bit-sample path, and the shipped profiles under crates/locus-core/profiles/ (standard.json, high_accuracy.json).
TL;DR
Extraction-mode choice, not funnel/threshold/refinement knobs, governs recall vs. pose-tail trade-offs: EdLines wins the clean-render pose tail but culls ~50% of dense small-tag candidates; ContourRdp wins crude-render recall but destroys the render-tag rotation tail. No single static (extraction, refinement) pair wins both regimes — the durable fix is PPB-based adaptive routing (AdaptivePpb, threshold 2.5), which lets high_accuracy recover ICRA recall (+14.2 pt) while staying byte-identical on render-tag. A separate quad-truncation bug (truncating candidates by pixel_count before geometric filtering) silently dropped tag-sized candidates behind large background blobs; the fix moved truncation caller-side, post-filter. The max_recall_adaptive profile validated this routing but was later removed in the config consolidation — its behavior lives on inside high_accuracy's AdaptivePpb block.
Lessons
Quad truncation fix. pixel_count_descending_order bundled two things: (1) sort component indices by pixel_count descending (load-bearing — lifts ICRA standard recall ~2.8 pp and stabilizes order-sensitive dedup on crude renders), and (2) truncate component_stats to MAX_CANDIDATES = 1024 before per-component geometric filtering (the bug). Tag candidates have small pixel_count relative to background blobs (texture/shadow/noise), so pre-filter truncation kept giant blobs (which gates would reject anyway) and discarded real tags. Fix: drop only the pre-filter truncation; keep the desc ordering verbatim; truncate caller-side after extract_single_quad filters geometrically (Rayon collect() preserves input order, so truncation drops the smallest survivors — the intended 4K-recall behavior). Impact: distortion Brown–Conrady recall 0.8701→0.9354 (+6.5 pp), Kannala–Brandt 0.8088→0.8130; render-tag byte-identical. Same fix on the _with_camera path.
ICRA-forward high_accuracy diagnostic. high_accuracy (EdLines + refinement_mode=None) collapsed on ICRA forward frames — mean recall 0.4631 vs standard's 0.7236, with 5 of 6 dense frames detecting zero tags. The icra_forward_diagnostic.rs harness (per-frame funnel / rejected-size / decode-Hamming attribution) root-caused it: the funnel was not the bottleneck (both profiles pass 100% through contrast), so threshold/sharpening flips could not help. EdLines under-produced by ~50% (~110 candidates/frame vs ~206), and its imprecise corners pushed decode Hamming into the 6–10 bucket (97/102 rejected on one frame). Cause was attributable to extraction_mode=EdLines on dense small-tag scenes. Resolution: adopt the AdaptivePpb router (threshold 2.5; low = ContourRdp+Erf, high = EdLines+None) — a Pareto win: ICRA forward recall 0.4631→0.6053 (+14.2 pt), RMSE 0.7535→0.5572, render-tag / distortion / board snapshots byte-identical (well-resolved tags have PPB≫2.5 and stay on EdLines+None; far-field PPB<2.5 tags route to ContourRdp+Erf).
max_recall_adaptive calibration (REMOVED profile). A now-removed profile that first shipped the AdaptivePpb routing (threshold 2.5, sharpening on): ICRA forward recall 0.7380 (+27 pp vs high_accuracy, matching standard), every render-tag p99 rotation under budget. Its calibration sweep is the durable evidence: disabling sharpening cost -19 pp ICRA recall and worsened the render-tag p99 tail (3°+ outliers), so sharpening does real work on both extraction paths; lowering the threshold 2.5→1.5 had near-zero effect (candidates already sit above 2.5) with a small ICRA cost — the shipped config was a local Pareto optimum. Its residual render-tag RMSE gap (~0.6 px vs high_accuracy's ~0.2 px) is intrinsic to ContourRdp+Erf, which yields robust corners but no per-corner Fisher covariance prior. The profile was dropped in the config consolidation — its routing was absorbed into high_accuracy, so nothing was lost, but do not resurrect the standalone profile expecting new behavior.
Hub regression snapshot. A point-in-time perf/accuracy table (AMD EPYC-Milan, --release, single-threaded), now historical. Durable methodology notes: (a) high_accuracy is the accuracy baseline because its 4–6× tighter pose bounds catch pose-solver regressions that standard's wide thresholds mask; small-tag recall is instead carried by the standard-profile robustness suite. (b) Latency scales ~linearly with pixel count; high_accuracy is ~2× faster than standard (EdLines cheaper than ContourRdp). (c) The robustness subsets are KPI watchlists, not regressions: tag16h5 precision is codebook-bound (halving allowed Hamming is the dominant lever, remainder is dense-codebook ambiguity); low_key has a hard config recall ceiling (KPI for future contrast-robust threshold work); raw_pipeline is largely config-bound.
Re-attempt / watch-outs
- Do not reintroduce a standalone
max_recall_adaptiveprofile — itsAdaptivePpbblock already lives inhigh_accuracy. Any revival must justify why the shared router isn't sufficient. - Do not try to fix the ICRA/high_accuracy gap by knob-flipping (sharpening, static ContourRdp, static Erf): every static extraction/refinement pair either destroys the render-tag pose tail (
0.56°→102°) or blows mean RMSE 4× (0.20→0.86). "No static pair wins both regimes" still holds — PPB routing sidesteps it, it does not refute it. - Keep the quad-truncation edge case covered: truncation must stay caller-side, after geometric filtering (small tags hide behind large-pixel-count background blobs). Never truncate
component_statspre-filter. Preserve the pixel_count-descending order (with deterministic index tie-break) — load-bearing for order-sensitive dedup on crude renders. - The render-tag RMSE gap on ContourRdp+Erf routes is intrinsic (no Fisher covariance prior). Closing it needs post-decode corner re-refinement (emitting its own per-corner covariance) or an image pyramid, not a config change.
- Test-infra trap: a relative
LOCUS_ICRA_DATASET_DIRonce resolved against the crate root and silently fell through to a 1-frame stub, producing a false "byte-identical" result. Relative paths now resolve against the workspace root and missing dataset dirs panic rather than skip. Verify ICRA snapshots against the real 50-frame dataset.
Provenance
Distilled 2026-07-19 from (removed; see git history): icra_forward_high_accuracy_diagnostic_20260426, quad_truncation_fix_20260426, max_recall_adaptive_calibration_20260426, hub_regression_20260423.