feat: add new model estimators (plane, PnP, radial homography) - #45
Draft
f-dy wants to merge 1 commit into
Draft
Conversation
f-dy
force-pushed
the
feature/new-estimators
branch
5 times, most recently
from
May 29, 2026 12:11
bb56ee9 to
cfa5848
Compare
f-dy
marked this pull request as ready for review
May 29, 2026 12:14
f-dy
force-pushed
the
feature/new-estimators
branch
from
May 29, 2026 15:22
cfa5848 to
804cef3
Compare
f-dy
marked this pull request as draft
May 29, 2026 15:23
f-dy
force-pushed
the
feature/new-estimators
branch
8 times, most recently
from
May 31, 2026 17:00
a6b7a47 to
d1a9854
Compare
f-dy
force-pushed
the
feature/new-estimators
branch
from
May 31, 2026 20:51
d1a9854 to
d5c1913
Compare
f-dy
added a commit
to f-dy/magsac
that referenced
this pull request
May 31, 2026
…g estimators The pybind find*_ functions left the MAGSAC reference threshold (interrupting_threshold, magsac.h) at its constructor default of 1.0 — a pixel-unit assumption. It counts 'confident' inliers (residual < interrupting_threshold), driving the adaptive iteration count N = log(1-conf) / log(1 - (inliers/N)^sample_size). 1.0 is not principled even for pixels (SIFT at coarse scales has inlier localization error of several pixels) and is unit-dependent for metric data (rigid transformation, line). It affects the RANSAC search budget — and thus, on hard problems, which model is found — not the per-hypothesis weight/loss. Derive it from the data: setReferenceThreshold(k * sigma_max) with k = getSigmaQuantile() = sqrt(chi2_0.99(DoF)). Using getSigmaQuantile() makes this forward-compatible with the per-estimator DoF fix. The normalized essential estimator now uses k * normalized_sigma_max instead of rescaling the arbitrary 1.0 default by the coordinate normalizer. Covers the pre-existing estimators (rigid, fundamental, essential, line, homography). The estimators added in danini#45 set this from the start. Affects only the iteration budget (runtime / hard-case search), not the per-hypothesis scoring. Should be validated on the paper's datasets (Adelaide/EVD/homogr) for failure-rate regressions before merging.
This was referenced May 31, 2026
f-dy
added a commit
to f-dy/magsac
that referenced
this pull request
May 31, 2026
Adds a 'seed' argument (default -1 = nondeterministic) to all 14 find* estimators. When seed >= 0 the run is reproducible: a shared helper (applyMagsacSeed) sets UniformRandomGenerator::setGlobalSeed(seed) for all URG-based samplers and std::srand(seed) for the std::rand()/Eigen::Random solver paths. Bumps the graph-cut-ransac submodule to include setGlobalSeed/clearGlobalSeed. Combines danini#48 (seed for the pre-existing estimators) and danini/graph-cut-ransac#51 (the seed mechanism), extended to the new estimators added in danini#45 (incl. findPlane3D).
f-dy
force-pushed
the
feature/new-estimators
branch
from
May 31, 2026 23:07
d5c1913 to
3172286
Compare
…, essential variants) Add Python bindings for models available in graph-cut-ransac: - findPlane3D (3D plane fitting) - findPnP (camera pose via P3P) - findRadialHomography (homography + radial distortion) - findHomographyAffine (homography from affine correspondences) - findFundamentalMatrixAffine (fundamental from affine/SIFT) - findEssentialMatrixPlanar (essential for planar motion) - findEssentialMatrixGravity (essential with gravity prior) Fix degrees of freedom for all MAGSAC estimators: - Fundamental/Essential: DoF=4 (Sampson distance, unchanged) - Homography/PnP/Radial/Affine: DoF=2 (2D transfer error) - Rigid transformation: DoF=3 (3D Euclidean distance) - Line2D/Plane3D: DoF=1 (scalar signed distance) The DoF fix uses: - Precomputed E₁(x) table for DoF=1 (gamma_values_dof1.cpp) - Closed-form erfc(√x) for DoF=2 - Closed-form exp(-x) for DoF=3 - Existing stored_gamma_values table for DoF=4 CMake detects if gamma_values_dof1.cpp is available from the graph-cut-ransac submodule; falls back to local copy if not.
f-dy
force-pushed
the
feature/new-estimators
branch
from
May 31, 2026 23:46
3172286 to
e6794fb
Compare
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.
New model estimators
Add Python bindings for models available in graph-cut-ransac but not previously exposed in pymagsac:
findPlane3DfindPnPfindRadialHomographyfindHomographyAffinefindFundamentalMatrixAffinefindEssentialMatrixPlanarfindEssentialMatrixGravityFix: correct degrees of freedom for all MAGSAC estimators
The MAGSAC scoring hardcoded DoF=4 for all models. This is only correct for Sampson distance (fundamental/essential). All other models had wrong chi² tail shape, leading to suboptimal inlier/outlier discrimination.
Implementation
The upper incomplete gamma Γ((DoF-1)/2, x) used in MAGSAC scoring is computed per-DoF:
gamma_values_dof1.cpp)√π · erfc(√x)— closed form viastd::erfce⁻ˣ— closed form viastd::expstored_gamma_values[]table (unchanged)The lower incomplete gamma γ((DoF+1)/2, x) used in MAGSAC++ scoring:
1 − e⁻ˣ— closed form(√π/2)·erf(√x) − √x·e⁻ˣ— closed form viastd::erf1 − (1+x)·e⁻ˣ— closed formstored_lower_incomplete_gamma_values[](unchanged)Dispatch is via
if constexprwith astatic_assertin theelsebranch — adding a model with unsupported DoF triggers a compile-time error.Backward compatibility
gamma_values_dof1.cpp; CMake detects if the submodule provides one).Files changed
src/pymagsac/include/estimators.h— new estimator wrappers + correct DoF/constants for all modelssrc/pymagsac/include/magsac.h— DoF-dispatched gamma lookups in IRLS and MAGSAC++ scoring; empty-vector guardsrc/pymagsac/include/gamma_values_dof1.cpp— precomputed E₁(x) table for DoF=1src/pymagsac/src/magsac_python.cpp— new bindings + fix memory leaks (stack-allocate MAGSAC objects)src/pymagsac/include/magsac_python.hpp— declarationssrc/pymagsac/src/bindings.cpp— Python bindingsCMakeLists.txt— detect gamma_values_dof1.cpp from submodule; exclude gamma tables from SRCS globstests/test_plane3d.py,tests/test_pnp.py, etc. — unit testsAdditional bugfixes
solver_linear_model.h: whenestimateModelNonminimalpassessample_=nullptr(meaning "use all normalized points") andsampleNumber_ == sampleSize(), the solver's fast-path dereferenced the null pointer. Fixed with&& sample_ != nullptrguard. (This guard is the standalone gcransac PR fix: guard LinearModelSolver minimal fast-path against a null sample graph-cut-ransac#52)magsac_python.cpp: all 13new MAGSAC<...>calls had no correspondingdelete. Replaced with stack allocation.tgamma(0)UB for DoF=1: guarded withif constexprlambda to guarantee the call is never emitted.&(sigma_inliers)[0]on potentially empty vectors replaced with.data()+ explicit empty check before solver calls.interrupting_threshold) defaults to1.0— a pixel-unit assumption that drives the adaptive iteration countN = log(1−conf) / log(1 − (inliers/N)^sample_size).1.0is unprincipled even for pixels (SIFT at coarse scales has inlier localization error of several pixels) and unit-dependent for metric data. This PR sets it from the data for every new estimator it introduces —setReferenceThreshold(estimator.getSigmaQuantile() * sigma_max)(= the statistical inlier thresholdτ = k·σ_max):findPlane3D,findPnP,findPnPAC,findPnPSIFT,findRadialHomography,findHomographyAffine,findFundamentalMatrixAffine,findEssentialMatrixPlanar,findEssentialMatrixGravity. Since these are new bindings there is no prior behavior to preserve. The same fix for the pre-existing estimators (rigid, fundamental, essential, line, homography) is a separate, independently-mergeable PR againstmaster(it changes existing behavior and needs failure-rate benchmarking). Affects only the RANSAC search budget, not the per-hypothesis weight/loss.findPlane3Dreturned no model (Bug A) or wandered (Bug C). Fixed by cappingtable[0] = E₁(step)(the first finite entry). Verified: plane fitting now recovers the truth to <0.001° / <0.1 mm, matching a reference numpy MAGSAC++.Known gamma-weight issues (NOT fixed here — flagged for maintainers)
A review against the MAGSAC++ paper (Barath et al., CVPR 2020, Eq. 2.1–2.2) found two
pre-existing inaccuracies in the IRLS weight path (
sigmaConsensusPlusPlus) thatalso affect graph-cut-ransac's
scoring_function.h. They are not fixed in this PRbecause they change results for all models (including n=4) and need accuracy
benchmarks the repo lacks. The MAGSAC++ scoring function (
getModelQualityPlusPlus)is correct and matches the paper exactly.
Γ(a, i/1000)(step 1/1000) but is indexed withprecision_of_stored_gammas = 1e4, so the IRLS evaluates the gamma at 10× the intended argument.σ_max = maximum_thresholddirectly, whereas the paper (and the scoring function) useσ_max = maximum_threshold / k.10/k²≈ 0.75 for n=4), which is why MAGSAC++ works well on real data despite both. Fixing B2 alone (e.g.precision_of_stored_gammas = 1e3) is harmful — it removes the cancellation and makes the gamma argument1/k²of correct (13× too small for n=4). B2 and B3 must be fixed together (useσ_max = threshold/k+ the fine table). See the maintainer notes for the full analysis and numerical verification.Reference threshold for the pre-existing estimators (separate PR)
The same
interrupting_threshold = 1.0default also affects the pre-existing estimators(rigid, fundamental, essential, line, homography). Fixing those changes behavior existing
users rely on, so it is a separate, independently-mergeable PR against
master(needs failure-rate benchmarking on Adelaide/EVD/homogr). This PR only sets the reference
threshold for the new estimators it introduces (see the bullet above), where there is no
prior behavior to preserve.