feat(sprt): Implement SPRT for RANSAC: - #1098
Open
AdarshGuptaa wants to merge 6 commits into
Open
Conversation
- Implemented `SPRTConfig` to store hyperparameters. - Implemented `SPRTState` to track sessions. - Implemented Log Likelihood Ratio update logic per inlier/outlier. - Implemented `is_rejected()` fn to reject models where llr exceeds a threshold. - Updated crates/kornia-3d/src/ransac/mod.rs to expose `SPRTConfig` and `SPRTState` publicly. - Added unit tests for core math and threshold verification: `test_verify_threshold_calculation` `test_likelihood_ratio_inliers` `test_dynamic_epsilon_update` `test_rejection_logic`.
|
Wire Wald's Sequential Probability Ratio Test into both PnP RANSAC paths and the Python bindings, and fix the issues that kept it inert or unsafe. SPRT streams point residuals in randomised order and rejects a hypothesis as soon as its log-likelihood ratio exceeds the Wald threshold, skipping full consensus scoring. Integration: - ransac/config.rs: add `RansacConfig::sprt: Option<SPRTConfig>` - ransac/driver.rs: add `run_with_rng` (seeded RNG); SPRT early-exit path in the driver loop (per-point residual streaming; cached residuals reused by the consensus step on passing hypotheses); best-inlier-count tracking - ransac/mod.rs: export `run_with_rng`; expose `sprt` as a public module - pnp/ransac.rs: SPRT precheck in `solve_pnp_ransac` (adaptive epsilon, cached squared-reprojection errors); O(k) `partial_shuffle_sample` for sampling - kornia-py/src/pnp.rs: `use_sprt`/`sprt_epsilon`/`sprt_delta` arguments; SPRT visit order driven by a second seeded StdRng for determinism - examples/pnp_demo: `--use-sprt` CLI flag - pixi.toml/pixi.lock: add numpy for the benchmark script - new benchmark script bench_pnp_sprt_cv2.py with results JSON, and kornia-py/tests/test_pnp_sprt.py binding tests - tests: SPRT math unit tests (threshold, LLR steps, early rejection, acceptance), driver integration tests (strategy toggle, best-model update, wrong-prior recovery, no-regression), PnP wrong-prior regression test, py binding tests (determinism with active SPRT, wrong-prior recovery) Correctness fixes: - sprt.rs: default `SPRTConfig` to the plain Wald threshold (t_M = t_m = 1). The py binding previously passed t_M = 200 / t_m = 1, giving A ~= 906 while the maximum reachable LLR over 600 points is ~= 385, so SPRT could never reject a hypothesis and SPRT-on was identical to SPRT-off - sprt.rs: align the time-aware threshold doc and test with the implemented multiplicative formula A = ln((1 - beta) / alpha) * (t_M - t_m) / t_m (the test asserted the power form and failed); remove dead code in `evaluate()` - driver.rs + pnp/ransac.rs: use the *observed* best-inlier ratio for the LLR step sizes once a model is accepted - previously clamped up to the prior, which rejects good hypotheses whenever the prior is optimistic; cap the pre-acceptance epsilon at min(prior, 0.3) - driver.rs + pnp/ransac.rs: grace period of max(n/4, 4*SAMPLE_SIZE) points before rejection is allowed prior to the first accepted model, so a wildly mismatched prior cannot starve the run of its first good hypothesis - driver.rs: per-hypothesis randomised visit order via rotation of a base permutation with a periodic re-shuffle every 16 hypotheses - O(1) per hypothesis instead of a full O(n) shuffle (~2.1 us at n = 600); deterministic under a seeded RNG - sprt.rs: `#[allow(non_snake_case)]` on the t_M/t_m fields (USAC paper notation) so clippy `-D warnings` passes Benchmark (600-pt scenes, 3 seeds): SPRT-on matches SPRT-off exactly on inliers/r_err/t_err and is faster (ap3p: 1.4-1.9x at 10-20% inliers, 1.06-1.09x at 30-50%); kornia+SPRT recovers more inliers than OpenCV at every ratio and is 4-11x faster than cv2.solvePnPRansac.
Fixes the pre-commit end-of-file-fixer hook failure in the CI lint pipeline.
Fixes the pre-commit trailing-whitespace hook failure in the CI lint pipeline.
AdarshGuptaa
marked this pull request as ready for review
August 12, 2026 23:55
Contributor
|
ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing |
The numpy top-level dependency in pixi.toml and the regenerated pixi.lock were pushed by mistake with the SPRT work; the benchmark gets numpy from the kornia-py dev extras anyway. Restores both files to their pre-SPRT state.
Contributor
Author
|
/review |
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.
📝 Description
This PR implements Wald's Sequential Probability Ratio Test (SPRT) into the RANSAC pipeline, inspired by OpenCV's
usac_framework. SPRT lets the RANSAC driver reject "bad" model hypotheses early: instead of scoring a hypothesis against all points, it evaluates points one at a time and bails out as soon as the running Log-Likelihood Ratio (LLR) exceeds a calculated decision threshold. A rejected hypothesis never pays for the full consensus pass, which speeds up the whole loop — especially on scenes with lots of outliers, where most hypotheses are garbage.SPRT is a pure speed layer: a rejected hypothesis is simply never scored. In the benchmark below, SPRT-on and SPRT-off produce bit-identical inliers,
r_err, andt_erron every scene — SPRT only makes the same decisions faster.The PR wires SPRT end-to-end (generic RANSAC driver,
solve_pnp_ransac, and the Python bindings as a toggle:use_sprt/sprt_epsilon/sprt_delta), and also fixes a set of issues that made the first wiring either inert or unsafe — most notably a decision threshold ~2.4× larger than the maximum LLR reachable on the benchmark scenes, which meant SPRT could never reject anything.Benchmark headline (600-pt synthetic scenes, 3 seeds): kornia+SPRT recovers equal or more inliers than OpenCV at every ratio (46 vs 4 at 10%, 120 vs 119, 180 vs 179, 300 vs 299) and is 3.8–5.4× faster than OpenCV's AP3P at 10–30% inlier ratios (and ~11× faster than OpenCV's EPnP/SQPNP); SPRT adds a further 6–90% speedup with bit-identical accuracy. OpenCV still wins on
t_err(0.007–0.012 vs 0.018–0.036) — that gap comes from the f32 terminal refit/LM polish in kornia's non-minimal path.Fixes/Relates to: #1073
🛠️ Changes Made
Core SPRT math —
crates/kornia-3d/src/ransac/sprt.rsSPRTConfigto store the SPRT hyperparameters (expected inlier ratioepsilon, Type-I errordelta, time termst_M/t_m).SPRTStateto track a single SPRT session (running LLR, points tested, decision threshold, rejected/accepted flags).update/update_with_steps), plusis_rejected()/is_accepted().evaluate()helper that streams a hypothesis' residuals through the test and stops at the first rejection.test_verify_threshold_calculation,test_calculate_threshold_time_aware,test_likelihood_ratio_inliers,test_likelihood_ratio_outliers,test_dynamic_epsilon_update,test_rejection_logic,test_sprt_full_acceptance,test_sprt_early_rejection,test_sprt_marginal_rejection,test_invalid_sprt_config_handling.RANSAC driver integration —
ransac/driver.rs,ransac/config.rs,ransac/mod.rsRansacConfig::sprt: Option<SPRTConfig>to toggle the feature.run_with_rng()— a seeded-RNG variant ofrun()so SPRT's visit order is reproducible;run()delegates with the thread RNG.run_with_rngre-exported,sprtexposed as a public module.PnP integration —
pnp/ransac.rssolve_pnp_ransac(Rust API) with adaptive epsilon and cached squared-reprojection errors.partial_shuffle_sample— O(k) minimal-sample drawing.Python bindings & demo
kornia-py/src/pnp.rs:use_sprt/sprt_epsilon/sprt_deltaarguments; SPRT visit order driven by a second seededStdRngfor determinism underseed=.examples/pnp_demo:--use-sprtCLI flag.kornia-py/tests/test_pnp_sprt.py: 12 tests (determinism with active SPRT, SPRT on/off quality equivalence, wrong-prior recovery, input validation, LO+SPRT, 50%-outlier scenes).Correctness fixes (why the first wiring was inert or unsafe)
t_M = 200, t_m = 1, making the decision threshold A ≈ 906 while the maximum reachable LLR on 600 points is ≈ 385 — SPRT could never reject, so SPRT-on was identical to SPRT-off. Defaults are now plain Wald (t_M = t_m = 1, A = ln((1−β)/α)).min(prior, 0.3)before it.max(n/4, 4·SAMPLE_SIZE)points before rejecting — a wildly mismatched prior can no longer starve the run of its first good hypothesis.evaluate();#[allow(non_snake_case)]on thet_M/t_mfields so clippy-D warningspasses.🧪 How Was This Tested?
pnp::ransacSPRT tests —cargo test -p kornia-3d(75 ransac/pnp tests pass). Python:test_pnp_sprt.py(12 tests) + homography-RANSAC tests intest_features.py— 16 pass.bench_pnp_sprt_cv2.pyfull run (4 ratios × 3 seeds × all method/SPRT/LO combinations) plus a 20-repetition timing study on a fixed scene (results below).🕵️ AI Usage Disclosure
Check one of the following:
🚦 Checklist
💭 Additional Context
Reading the table: kornia wins on inlier recall (equal or better at every ratio — 46 vs 4 inliers at 10%, 300 vs 299 at 50%) and rotation error (better at 50%, and via EPnP at 30%). Speed: kornia+SPRT is 3.8–5.4× faster than OpenCV's AP3P at 10–30% inlier ratios (~11× vs OpenCV's EPnP/SQPNP), with SPRT adding a further 6–90% on top at bit-identical accuracy. At 50% inliers OpenCV's AP3P is faster (2.2 ms vs 6.4 ms) because its iteration budget collapses as soon as a high-consensus model appears. OpenCV still wins on
t_err(0.007–0.012 vs 0.018–0.036) — that gap comes from kornia's f32 terminal refit/LM polish.Future work: a double-precision (f64) terminal refit + LM polish on the inlier set (mirroring OpenCV's f64 internals) to close the
t_errgap across all inlier ratios.Related: audit issue #1096 - estimation duplication & f32/f64 precision. The t_err gap measured here (0.018–0.036 m vs OpenCV's 0.007–0.012 m) is a concrete instance of the audit's "precision conversions embedded in hot estimation paths" impact item, and the duplicated pinhole-reprojection code in pnp/refine.rs it inventories. Closing the gap (f64 terminal refit) is planned as a follow-up; long-term it belongs under the shared factors/Atlas direction the audit proposes.