Skip to content

feat(sprt): Implement SPRT for RANSAC: - #1098

Open
AdarshGuptaa wants to merge 6 commits into
kornia:mainfrom
AdarshGuptaa:feat-sprt
Open

feat(sprt): Implement SPRT for RANSAC:#1098
AdarshGuptaa wants to merge 6 commits into
kornia:mainfrom
AdarshGuptaa:feat-sprt

Conversation

@AdarshGuptaa

@AdarshGuptaa AdarshGuptaa commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

📝 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, and t_err on 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.rs

  • SPRTConfig to store the SPRT hyperparameters (expected inlier ratio epsilon, Type-I error delta, time terms t_M/t_m).
  • SPRTState to track a single SPRT session (running LLR, points tested, decision threshold, rejected/accepted flags).
  • Log-Likelihood Ratio update logic per inlier/outlier (update / update_with_steps), plus is_rejected() / is_accepted().
  • evaluate() helper that streams a hypothesis' residuals through the test and stops at the first rejection.
  • Unit tests for the core math: 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.rs

  • RansacConfig::sprt: Option<SPRTConfig> to toggle the feature.
  • SPRT early-exit path in the driver loop: per-point residual streaming, with cached residuals reused by the consensus step on passing hypotheses.
  • run_with_rng() — a seeded-RNG variant of run() so SPRT's visit order is reproducible; run() delegates with the thread RNG.
  • Exports: run_with_rng re-exported, sprt exposed as a public module.
  • Driver tests: strategy toggle, best-model update, seeded determinism with active SPRT, wrong-prior recovery, no-regression with a correct prior.

PnP integration — pnp/ransac.rs

  • SPRT precheck in solve_pnp_ransac (Rust API) with adaptive epsilon and cached squared-reprojection errors.
  • partial_shuffle_sample — O(k) minimal-sample drawing.
  • Tests: no-regression vs SPRT-off, wrong-prior recovery, invalid-config fallback, 50%-inlier accuracy, 100%-outlier termination, tiny-dataset fallback.

Python bindings & demo

  • kornia-py/src/pnp.rs: use_sprt / sprt_epsilon / sprt_delta arguments; SPRT visit order driven by a second seeded StdRng for determinism under seed=.
  • examples/pnp_demo: --use-sprt CLI 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)

  • Inert threshold: the py binding passed 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−β)/α)).
  • Wrong-prior starvation: the adaptive epsilon was clamped up to the caller's prior, so an optimistic prior would make good hypotheses look bad and get rejected; it now tracks the observed best-inlier ratio after the first accepted model, and is capped at min(prior, 0.3) before it.
  • Grace period: before the first accepted model, SPRT must evaluate at least max(n/4, 4·SAMPLE_SIZE) points before rejecting — a wildly mismatched prior can no longer starve the run of its first good hypothesis.
  • Performance: per-hypothesis visit order is now a random rotation of a base permutation (re-shuffled every 16 hypotheses) instead of a full O(n) shuffle (~2.1 µs at n = 600).
  • Housekeeping: time-aware threshold doc/test aligned with the implemented multiplicative formula (the test asserted the power form and failed); dead code removed in evaluate(); #[allow(non_snake_case)] on the t_M/t_m fields so clippy -D warnings passes.

🧪 How Was This Tested?

  • Unit Tests: Rust: sprt.rs math suite + driver tests (listed above) + pnp::ransac SPRT tests — cargo test -p kornia-3d (75 ransac/pnp tests pass). Python: test_pnp_sprt.py (12 tests) + homography-RANSAC tests in test_features.py — 16 pass.
  • Manual Verification: bench_pnp_sprt_cv2.py full run (4 ratios × 3 seeds × all method/SPRT/LO combinations) plus a 20-repetition timing study on a fixed scene (results below).
  • Performance/Edge Cases: wrong-prior recovery (0.9–0.95 priors on 20–30% scenes), 100%-outlier termination, tiny datasets, invalid configs falling back to non-SPRT, determinism under fixed seeds.

🕵️ AI Usage Disclosure

Check one of the following:

  • 🟢 No AI used.
  • 🟡 AI-assisted: I used AI for boilerplate/refactoring but have manually reviewed and tested every line.
  • 🔴 AI-generated: (Note: These PRs may be subject to stricter scrutiny or immediate closure if the logic is not explained).

🚦 Checklist

  • I am assigned to the linked issue (required before PR submission)
  • The linked issue has been approved by a maintainer
  • This PR strictly implements what the linked issue describes (no scope creep)
  • I have performed a self-review of my code (no "ghost" variables or hallucinations).
  • My code follows the existing style guidelines of this project.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • (Optional) I have attached screenshots/recordings for UI changes.

💭 Additional Context

======================================================================================
PnP RANSAC benchmark — synthetic 600-pt scenes, 0.5px inlier noise, 4px threshold, 3 seeds
======================================================================================

  outlier ratio = 10%   (total = 600)
  solver           inliers  recall   R_err°    t_err       ms
  ------------------------------------------------------------
  kornia ap3p           46   0.077   1.1422   0.2314     3.69
  kornia ap3p +SPRT     46   0.077   1.1422   0.2314     4.19
  kornia epnp            2   0.004 141.6877   3.2229    15.30
  kornia epnp +SPRT      2   0.004 141.6877   3.2229    16.99
  OpenCV ap3p            4   0.007 104.0351   4.7507    17.99
  OpenCV epnp      — failed —         OpenCV sqpnp — failed —

  outlier ratio = 20%   (total = 600)
  solver           inliers  recall   R_err°    t_err       ms
  ------------------------------------------------------------
  kornia ap3p          120   0.200   0.1593   0.0361     4.98
  kornia ap3p +SPRT    120   0.200   0.1593   0.0361     3.40   <- fastest overall
  kornia epnp           95   0.158   0.4107   0.0463    17.49
  kornia epnp +SPRT     95   0.158   0.4107   0.0463    16.51
  OpenCV ap3p          119   0.198   0.1337   0.0115    18.25
  OpenCV epnp           13   0.022  13.0842   1.8007    38.19
  OpenCV sqpnp          13   0.022  12.5548   1.5237    38.48

  outlier ratio = 30%   (total = 600)
  solver           inliers  recall   R_err°    t_err       ms
  ------------------------------------------------------------
  kornia ap3p          180   0.300   0.1205   0.0217     4.39
  kornia ap3p +SPRT    180   0.300   0.1205   0.0217     4.45
  kornia epnp          180   0.300   0.0767   0.0161    16.79   <- best R_err
  kornia epnp +SPRT    180   0.300   0.0767   0.0161    15.66
  OpenCV ap3p          179   0.298   0.0937   0.0058    17.11
  OpenCV epnp          179   0.298   0.0937   0.0058    38.64
  OpenCV sqpnp         179   0.298   0.0986   0.0043    38.44

  outlier ratio = 50%   (total = 600)
  solver           inliers  recall   R_err°    t_err       ms
  ------------------------------------------------------------
  kornia ap3p          300   0.500   0.0680   0.0182     6.68
  kornia ap3p +SPRT    300   0.500   0.0680   0.0182     6.39
  kornia epnp          300   0.500   0.0680   0.0182     7.90
  kornia epnp +SPRT    300   0.500   0.0680   0.0182     8.17
  OpenCV ap3p          299   0.498   0.0713   0.0066     2.21
  OpenCV epnp          299   0.498   0.0713   0.0066     8.85
  OpenCV sqpnp         299   0.498   0.0708   0.0027     8.54

======================================================================================
SPRT speed benefit (deterministic seed, mean of 20 runs): kornia ap3p
======================================================================================
   10% inliers:  5.36 ms -> +SPRT 2.82 ms   (1.90x)
   20% inliers:  4.97 ms -> +SPRT 3.44 ms   (1.44x)
   30% inliers:  4.38 ms -> +SPRT 4.00 ms   (1.09x)
   50% inliers:  6.46 ms -> +SPRT 6.11 ms   (1.06x)

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_err gap 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.

- 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`.
@github-actions

Copy link
Copy Markdown

⚠️ PR Validation Warnings

No linked issue found: This PR does not reference any issue. Please link to an issue using "Fixes #123" or "Closes #123" in the PR description.


Note: This PR can remain open, but please address these issues to ensure a smooth review process. For more information, see our Contributing Guide.

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.
- Fixed linting
- removed json benchmark file
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
AdarshGuptaa marked this pull request as ready for review August 12, 2026 23:55
@qodo-code-review

Copy link
Copy Markdown
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.
@AdarshGuptaa

Copy link
Copy Markdown
Contributor Author

/review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant