feat(examples): add simple_nft_dual.rs with DualFreakMatcher and per-frame divergence reporting (#157)#159
Merged
Conversation
…frame divergence reporting (#157) Closes #157. Diagnostic sibling of simple_nft.rs that drives DualFreakMatcher to compare the C++ and pure-Rust FREAK backends end-to-end on the pinball reference image. The example prints both backend homographies, the divergence count and reason, the C++-derived 3x4 KPM pose, and the AR2 refined pose. Two-phase structure: Phase A queries the DualFreakMatcher directly to capture per-backend state (KpmHandle wraps the matcher in Box<dyn FreakMatcherBackend>, which forbids recovering the concrete type post-move); Phase B uses a fresh KpmHandle + CppFreakMatcher for the production pose/AR2 pipeline, which is equivalent since DualFreakMatcher::query returns C++ as ground truth (M9-2 D5). Adds two ~3-line accessors on DualFreakMatcher (cpp_matched_geometry and rust_matched_geometry, both #[cfg(feature = "dual-mode")]-gated) so the example can read each backend's homography. New file uses arlog_*! macros from day one per CLAUDE.md §2; existing simple_nft.rs is left alone (issue #90 PR 4's scope). Cargo.toml entry declares required-features = ["dual-mode", "log-helpers"] so cargo auto-enables both when running the example. Measurement note: on pinball-demo.jpg both backends agree on matched_id but the tier-2 corner reprojection diverges (~13.8 px > 2.0 px tolerance), producing divergence_count = 1. This is the cross-language BHC-variance envelope §10 of docs/design/m9-2-rust-backend.md discusses, not a regression — the C++ pose still matches §10 (KPM error 7.1455, pose row 0 [0.9862, 0.1671, 0.0641, -182.1635]) and AR2 behaves identically to simple_nft.rs. Refs #141 (M9-2), #156 (M9-2 PR landing matched_geometry accessors). See docs/design/m9-2-simple-nft-dual.md for the full decision log. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…mparison (#157) Refs #157. Followup on PR #159. The "max corner displacement" line and module docstring in simple_nft_dual.rs could be misread as a pose-level comparison. Re-labels the metric as "Homography agreement (M9 #152 tier-2 metric): max corner displacement between H_cpp and H_rust", and adds an explicit note in both the module docstring and Phase A output that: - Side-by-side comparison is at the 3×3 homography level (what matched_geometry() exposes per backend). - Only one 3×4 camera pose is computed (C++-derived, fed to AR2). - The Rust 3×4 pose is intentionally not printed — would require running kpm_util_get_pose_binary separately on Rust inliers. No behavioural change. Matches PR #159 design-doc D2. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #157.
Summary
Adds
crates/core/examples/simple_nft_dual.rs— a diagnostic sibling ofsimple_nft.rsthat drivesDualFreakMatcher(M9-2, #141) to compare the C++ and pure-Rust FREAK backends end-to-end on the pinball reference image. Output includes both backend homographies, the divergence count and reason, the C++-derived 3×4 KPM pose, and the AR2 refined pose.Also adds two tiny
#[cfg(feature = "dual-mode")]-gated accessors onDualFreakMatcher(cpp_matched_geometry(),rust_matched_geometry()) so the example can read each backend's homography without anAny-downcast on the trait.Design
Two-phase structure (see
docs/design/m9-2-simple-nft-dual.mdfor the full decision log):KpmRefDataSetin via an inline replica ofKpmHandle::set_ref_data_set's loop, queries directly, then readsdivergence_count()/last_divergence_reason()/cpp_matched_geometry()/rust_matched_geometry(). Computes max corner displacement against the matched id's reference dimensions so the number matchesDualFreakMatcher's internal tier-2 metric to four decimals.KpmHandle+CppFreakMatcherrunsset_ref_data_set→kpm_matching→get_pose→ AR2 tracking, mirroringsimple_nft.rsexactly. Equivalent to running throughDualFreakMatchersinceDualFreakMatcher::queryreturns C++ as ground truth (M9-2 D5).Why two phases:
KpmHandlewraps the backend inBox<dyn FreakMatcherBackend>, which forbids recovering the concreteDualFreakMatchertype after the move. Splitting cleanly into "diagnostic" and "production" phases avoids any API surface changes to the trait.Conventions
.claude/HEADER.txt.arlog_*!macros from day one per CLAUDE.md §2 (arlog_i!for narrative output,arlog_e!for error sites,ar_log_init_default()at startup). Existingsimple_nft.rsis intentionally left untouched — that conversion is Convert remaining println!/eprintln! in examples and benches to arlog_*! #90 PR 4's scope.Cargo.toml[[example]]entry declaresrequired-features = ["dual-mode", "log-helpers"]socargo run --example simple_nft_dualauto-enables both. The issue's literal spec said["dual-mode"]only; we addedlog-helpersso the arlog macros actually emit output.Measurement note
On
pinball-demo.jpgthe example reportsdivergence_count = 1after one query — tier-1 (matched_id) agrees on both backends, but tier-2 corner reprojection comes out at ~13.8 px ondb_id=2(one of 9 internal db_ids spanning the page's image-scale variants), exceeding the 2.0 px M9 #152 tolerance.This is not a regression. It's the cross-language BHC-variance envelope
docs/design/m9-2-rust-backend.md§10 already discusses — sub-degree rotation and sub-percent translation differences manifesting as ~14 px at the smaller matched scale. The milestone-gate test (test_dual_mode_no_divergence_on_pinball) covers thefound.jpg/img.jpgpair where divergence stays at zero; pinball is a separate dataset where it doesn't. The C++ pose still matches §10 exactly (KPM error 7.1455, pose row 0[0.9862, 0.1671, 0.0641, -182.1635]) and AR2 behaves identically tosimple_nft.rs.The example surfaces this honestly so contributors investigating future regressions know what "normal" looks like on each fixture. Design doc §3.7 documents the correction relative to issue #157's original "expected zero divergences" wording.
Files changed
crates/core/examples/simple_nft_dual.rscrates/core/src/kpm/rust_backend.rsDualFreakMatcher)crates/core/Cargo.toml[[example]]entry)docs/design/m9-2-simple-nft-dual.mdTest plan
cargo fmt --all -- --checkcleancargo build --all-featuressucceeds (no errors; pre-existing 17 warnings unchanged)cargo clippy --all-targets --all-features -- --deny warningsexit 0cargo test --all-features --lib kpm::rust_backend— 5 passed, includingtest_dual_mode_no_divergence_on_pinballcargo run --features "dual-mode log-helpers" --example simple_nft_dualruns to completion on pinball assets; produces internally-consistent output (example's reported corner displacement matchesDualFreakMatcher's internal warning exactly: 13.8016 px)Refs
matched_geometryaccessors), feat(kpm): M9-3 - remove FFI as default, pure Rust backend complete #142 (M9-3 — flips default offffi-backend; this example is most valuable while both backends are still routinely available)simple_nft.rsarlog conversion — out of scope here)🤖 Generated with Claude Code