Scoring
The scoring core: the interface a problem implements to be scored, the scale-free marginal score and its sort-and-sweep, and the loss-width ("threshold-based") scorings it is compared against.
The scoreable-problem interface
A consumer makes its problem scoreable by subtyping AbstractScoreableProblem and implementing data_size, codimension, minimal_sample_size and VisualGeometryCore's constraint_with_jacobians; predictive scoring additionally needs solver_covariance, declared by provides_solver_covariance. The concrete problems (homography, fundamental and essential matrix, …) live in RobustVisualGeometry.
RansacScoringDoneRight.AbstractScoreableProblem — Type
AbstractScoreableProblemSupertype for problems that can be scored by the scale-free marginal scoring framework. Implementors provide data_size, codimension, minimal_sample_size, and constraint_with_jacobians; optionally solver_covariance with provides_solver_covariance (predictive scoring), and residual_context with whitened_residual (a Jacobian-free residual path).
RansacScoringDoneRight.data_size — Function
data_size(problem) -> IntTotal number of data points the problem scores over.
RansacScoringDoneRight.codimension — Function
codimension(problem) -> IntConstraint codimension d_g: the number of independent constraint equations a true datum satisfies (1 for F/E/line scalar residual, 2 for homography / PnP).
RansacScoringDoneRight.minimal_sample_size — Function
minimal_sample_size(problem) -> IntModel degrees of freedom m used in the residual DOF ν = (k − m)·d_g.
RansacScoringDoneRight.solver_covariance — Function
solver_covariance(problem, model, sample_indices) -> SMatrix or nothingModel parameter covariance Σ̃_θ propagated through the minimal solver, for predictive scoring (MarginalScoring{Predictive}). A provider returns nothing for a DEGENERATE minimal sample, one whose solver Jacobian does not exist, and predictive scoring then SKIPS that hypothesis like a failed solve.
A provider declares itself with provides_solver_covariance. A problem type that defines no provider cannot be scored predictively: the fallback method throws an ArgumentError naming the problem type. It returned nothing until 2026-09-22, which every predictive path read as "score this model certain", so a problem with no provider (RVG's AbsolutePoseProblem, HandEyeProblem) ran as model-certain scoring under a predictive name.
RansacScoringDoneRight.provides_solver_covariance — Function
provides_solver_covariance(::Type{<:AbstractScoreableProblem}) -> BoolWhether problem type P defines solver_covariance, so that it can be scored predictively. Default false; a provider declares provides_solver_covariance(::Type{<:P}) = true beside its method. MarginalScoring{Predictive}(problem, a) (alias PredictiveMarginalScoring) refuses a problem type that does not, at construction.
RansacScoringDoneRight.residuals! — Function
residuals!(r, problem, model)Fill r[1:data_size(problem)] with whitened residuals such that r[i]^2 is the Mahalanobis distance gᵢᵀ Σ̃_{gᵢ}⁻¹ gᵢ (so r[i]^2 ~ χ²(d_g) under H₀). It is whitened_residual at every point, SIGNED for a scalar constraint. The per-model work is residual_context, computed once; a problem overrides that hook and whitened_residual, not this loop.
RansacScoringDoneRight.whitened_residual — Function
whitened_residual(problem, ctx, i) -> RealThe whitened residual of data point i under model: the norm of L⁻¹gᵢ, where LLᵀ = Σ̃_{gᵢ} = ∂ₓgᵢ·Σ̃_x·∂ₓgᵢᵀ, so that its square is the Mahalanobis distance gᵢᵀ Σ̃_{gᵢ}⁻¹ gᵢ (~ χ²(d_g) under H₀). SIGNED for a scalar constraint (d_g = 1: gᵢ/√Σ̃_{gᵢ}), the non-negative norm for a vector one.
The default derives it from constraint_with_jacobians through VisualGeometryCore's whiten — the stack's one whitening path and its one Sampson degeneracy rule (±Inf for an unsatisfiable degenerate point, 0 for one that vanishes exactly). A problem overrides it when a cheaper Jacobian-free route to the SAME quantity exists; the override must keep the sign contract. residuals! is this per point, at ctx =residual_context(problem, model) — the model itself unless the problem overrides that hook, in which case it defines this function on its context type.
RansacScoringDoneRight.residual_context — Function
residual_context(problem, model) -> ctxWhat residuals! computes ONCE per model before its per-point loop, and hands to every whitened_residual(problem, ctx, i). The default is the model itself, so a problem that defines neither hook scores exactly as before.
A problem overrides it when part of the residual depends on the model alone and is costly: a relative-pose bundle's cheiral pose decomposition and inverse intrinsics, an essential matrix widened to Float64 once rather than per point. It then defines whitened_residual(problem, ctx::C, i) for its context type C.
The scale-free marginal score
MarginalScoring carries the configuration (outlier half-width, model degrees of freedom, scale prior); ScoringWorkspace carries the scratch buffers of the sort-and-sweep. JeffreysPrior, InverseGammaPrior and AbstractScalePrior are names of the one type IGPrior: the Jeffreys prior is its α₀ = β₀ = 0 limit.
RansacScoringDoneRight.MarginalScoring — Type
MarginalScoring{P, DG} <: GauScoringScale-free marginal score (Section 3, Eq. 12) — spec only.
This type carries only the immutable configuration of the scoring function (outlier half-width, model DOF, scale prior, profile-prior flag). All scratch buffers (perm, lgtable, penalties, sortkey, priortable) live on ScoringWorkspace which is stored on the RansacWorkspace. This parallels the LO split via `lows`.
Parameterized by:
P:Nothing(model-certain) orPredictive(model-uncertain)DG::Int: Codimension dg of the model manifold (compile-time constant)
The score marginalizes σ² under the Jeffreys prior π(σ²) ∝ 1/σ²:
S(θ, I) = log Γ((nᵢ·dg − n_θ)/2) − ((nᵢ·dg − n_θ)/2) log(π·RSS_I) − nₒ·dg·log(2h)where nᵢ = |I|, RSSI = Σᵢ∈I rᵢ², nₒ = N − nᵢ, nθ = m·dg (model parameters), h is the outlier half-width, and dg the codimension.
Per-point scores ‖rᵢ‖² = gᵢᵀ Σ̃{gᵢ}⁻¹ gᵢ, where rᵢ = Lᵢ⁻¹gᵢ is the shape-whitened residual. For the predictive variant, Σ̃{gᵢ} includes model estimation uncertainty.
Fields
outlier_halfwidth::Float64: the half-width of the uniform outlier support (the sweep chargeslog(2·outlier_halfwidth)per point and axis)model_dof::Int: Minimal sample size m = ⌈n_θ/dg⌉profile_prior::Bool: Whether the profiled-γ correction is used. Theprior_tableis built on the associatedScoringWorkspacewhen this istrue.scale_prior::IGPrior: Scale prior for σ² marginalization. Jeffreys isIGPrior(0.0, 0.0); weakly-informative IG isIGPrior(α₀, β₀). Use theJeffreysMarginal/InverseGammaMarginalconstructor shortcuts for self-documenting call sites.
Scratch buffers (perm, lg_table, scratch, penalties, sort_key, prior_table) live on ScoringWorkspace instead — allocated by alloc_scoring_workspace(problem, scoring) and stored on the RANSAC workspace.
RansacScoringDoneRight.JeffreysMarginal — Function
JeffreysMarginal(problem, outlier_halfwidth; kwargs...)
JeffreysMarginal(model_dof, outlier_halfwidth; kwargs...)
JeffreysMarginal(::Type{P}, outlier_halfwidth; kwargs...)Build a Jeffreys-prior MarginalScoring. Equivalent to calling the primary MarginalScoring constructor with JeffreysPrior() as the prior.
RansacScoringDoneRight.InverseGammaMarginal — Function
InverseGammaMarginal(problem, outlier_halfwidth; alpha0, beta0, kwargs...)
InverseGammaMarginal(model_dof, outlier_halfwidth; alpha0, beta0, kwargs...)
InverseGammaMarginal(::Type{P}, outlier_halfwidth; alpha0, beta0, kwargs...)Build an Inverse-Gamma-prior MarginalScoring. The prior hyperparameters alpha0, beta0 are required kwargs.
RansacScoringDoneRight.Predictive — Type
PredictiveType tag for the predictive variant of MarginalScoring.
MarginalScoring{Predictive} incorporates model estimation uncertainty (Σ̃θ from `solvercovariance, of a minimal-sample model or of a refit on its consensus set) into per-point scores.MarginalScoring{Nothing}` treats the model as exact (Σ_θ = 0).
See also: MarginalScoring, PredictiveMarginalScoring
RansacScoringDoneRight.PredictiveMarginalScoring — Type
PredictiveMarginalScoringAlias for MarginalScoring{Predictive}. Model-uncertain scoring that incorporates estimation uncertainty Σ̃_θ (Section 4, Eq. 20).
An OPTION, not a default (user decision, 2026-09-22; revisited after the release). Measured against model-certain scoring on the HEAD rerun — 320 tasks, thresholds tuned per scoring and per stage, paired seeds — it ties or loses on pooled ΔAUC@10°: H −0.003, pose −0.001, F +0.011 at selection but −0.029 after local optimization, E −0.010 at selection and +0.007 after it. Its one clear win is E after local optimization at N = 60 (+0.016), and it costs 13–82× per hypothesis at N = 60 and 68–565× at N = 250, with the kinetic _parametric_sweep!. The loss is largest where the predicted variance is most heterogeneous across inliers (sd log V: H 2.30, F 1.39, pose 1.29, E 0.97). The enumeration is EXACT, so this is a property of the FIRST-ORDER predictive model, not of the implementation; whether linearization is the cause is a post-release experiment (oracle covariance vs first-order). Tables: the certain-vs-predictive experiment (condensedtables.md, resultstable.txt, rhoNtable.txt, diagtable.txt, bench_cost.jls); RobustVisualGeometry's docs/src/guide/scoring.md says the same (RVG f4db00d).
RansacScoringDoneRight.IGPrior — Type
IGPrior(alpha0, beta0)Inverse-Gamma prior on σ²: π(σ²) ∝ (σ²)^{-(α₀+1)} exp(-β₀/σ²). Conjugate to the Gaussian inlier likelihood, so the marginal reduces to a closed-form sweep update.
The Jeffreys prior π(σ²) ∝ 1/σ² is the limiting α₀ = β₀ = 0 case. The marginal score formula _sweep_score(p, …) is algebraically identical at this limit:
lg_k − ν/2·log(2π) − ν/2·log(RSS/2) ≡ lg_k − ν/2·log(π·RSS)so a single struct + single dispatch handles both. Use JeffreysPrior() or IGPrior() for the parameter-free Jeffreys instance.
Fields
alpha0::Float64— shape hyperparameter (0 ⇒ Jeffreys)beta0::Float64— scale hyperparameter (0 ⇒ Jeffreys)
RansacScoringDoneRight.ScoringWorkspace — Type
ScoringWorkspacePre-allocated scratch buffers for scale-free marginal scoring (Algorithm 1).
Separated from MarginalScoring so that scoring carries only the immutable spec (model DOF, outlier half-width, codimension) while the mutable workspace lives on RansacWorkspace. Mirrors the spec/workspace split already in place for local-optimization via lo_ws.
Prior consistency
The lg_table is built at allocation time with a specific α₀ — once a ScoringWorkspace is constructed it is committed to that scale prior. The scale_prior field is the single source of truth for which prior the buffers are configured for; sweep!(sws, ...) reads it from here rather than accepting a runtime kwarg, which would risk pairing a Jeffreys lg_table with an IG score formula.
Fields
perm::Vector{Int}— sortperm buffer, mutated by everysweep!scratch::Vector{Int}— sortperm scratch (comparison-sort fallback paths)radix::RadixPermBuffers— scratch for the allocation-free stable radixsortpermtaken by the exact-size single-sort fast path (bitwise-identical permutation; see_radix_sortperm!)lg_table::Vector{Float64}— log Γ(k·dg/2 + α₀); depends on n, dg, and the scale priorpenalties::Vector{Float64}— per-point ℓᵢ = log|Ṽᵢ| (predictive scoring)sort_key::Vector{Float64}— the sweep ordering key: the per-point unit-scale Mahalanobisqin Predictive scoring (raw r² only in the model-certain fallback). The historical raw-r² key is retired — with heterogeneous penaltiessweep!follows the generalized sweep lemma's parametric ordering.prior_table::Vector{Float64}— profiled-γ correction; empty = flat priorscale_prior::IGPrior— the prior the lg_table was built for (α₀=β₀=0 ⇒ Jeffreys)
Build with alloc_scoring_workspace(problem, scoring) (defined in estimators/ransac/scoring.jl) or default_scoring_workspace(n; dg, scale_prior).
RansacScoringDoneRight.score_residuals! — Function
score_residuals!(sws, q, scoring, residuals, n; sample_idx = nothing) -> (S, k)Score from precomputed residuals (model-certain, isotropic noise).
Squares residuals[1:n] into q, pads q[n+1:end] with typemax, zeros the sws penalties, and sweeps for the optimal partition (overwriting the sws sweep buffers). The sample_idx points get q = 0.
Returns (S, k): sweep score and optimal inlier count.
RansacScoringDoneRight.score_sq_residuals! — Function
score_sq_residuals!(sws, q, scoring, problem, model, n) -> (S, k)Score a model by computing per-point squared whitened residuals and sweeping for the optimal partition (Algorithm 1, Eq. 12).
Fills q[1:n] with squared whitened residuals, q[n+1:end] with typemax, and overwrites the sws sweep buffers.
Returns (S, k): sweep score and optimal inlier count.
score_sq_residuals!(sws, q, scoring::PredictiveMarginalScoring, problem, model, sample_idx, n) -> (S, k)Predictive variant: compute model-uncertain scores qᵢ from solver Jacobian, then sort-and-sweep (Algorithm 1, Eq. 20).
Computes Σ̃θ from `solvercovariance(problem, model, sampleidx), then fillsq[i]with the predictive squared residual rᵢᵀ Ṽᵢ⁻¹ rᵢ. A problem with nosolvercovariance` throws (see its docstring).
Returns (S, k): sweep score and optimal inlier count. A DEGENERATE sample — the provider returns nothing — is no hypothesis: (-Inf, 0), as for a failed solve, with q and sws untouched. It is never scored model-certain. Otherwise it is the covariance-given method below at that Σ̃_θ.
score_sq_residuals!(sws, q, scoring::PredictiveMarginalScoring, problem, model, Σ̃_θ,
sample_idx, n; residuals = nothing) -> (S, k)Predictive scoring at a GIVEN unit-scale model covariance Σ̃_θ. The convention is multiplicative, Σ_θ = s²·Σ̃_θ, and Σ̃_θ is a minimal sample's solver_covariance, or a refit's on its consensus set, as an Uncertain model carries it. Fills q[i] with the predictive squared residual rᵢᵀ Ṽᵢ⁻¹ rᵢ and the sws penalties with log|Ṽᵢ|, then sort-and-sweeps (Algorithm 1, Eq. 20).
sample_idx (or nothing): the minimal sample's points, which satisfy their constraints exactly — their q and penalty are set to zero, as q is in score_residuals!. When residuals is supplied it receives each point's whitened_residual from the same constraint evaluation — SIGNED for a scalar constraint, exactly what residuals! writes.
Returns (S, k): sweep score and optimal inlier count.
The Gaussian-uniform mixture and its decision boundary
RansacScoringDoneRight.GauInlier — Type
GauInlier(s2; nu = Inf, gamma = 1/2, d_g)Everything the mixture needs EXCEPT the outlier half-width — the half both directions of the boundary share, so neither has to take loose scalars.
s2inlier variance sigma^2nueffective dof:Infis the Gaussian inlier, a finite value the scale-marginalised (Student-t) one. The Gaussian IS the nu -> Inf limit, and the two closed forms below agree there.gammathe label priorPr(z_i = 1). The paper adopts 1/2 and shows in B.1 that it is the effective prior the score's own optimality condition produces; a general gamma addslog(gamma/(1-gamma)), which is exactly the term that "biases the inlier/outlier boundary".d_gthe codimension, REQUIRED. It enters the outlier density as the EXPONENT(2·outlier_halfwidth)^-d_g, so no value of it is a safe default.
RansacScoringDoneRight.decision_threshold — Function
decision_threshold(m::GauInlier, outlier_halfwidth) -> Float64The residual at which the MAP rule flips between inlier and outlier: z* = 1 iff gamma*f_in > (1-gamma)*f_out holds exactly when norm(r) is below it.
Returns 0.0 when the outlier component already explains every residual at least as well (the mixture admits nothing), rather than a negative root.
VisualGeometryCore.decision_width — Method
decision_width(m::GauInlier, decision_threshold) -> outlier_halfwidthThe mixture's method of VisualGeometryCore's decision_width, and the inverse of decision_threshold: the per-axis outlier half-width that puts the MAP boundary at decision_threshold. A HALF-WIDTH and not a radius — the outlier support is a box, a product of intervals, never a ball, which is why the codimension enters as the exponent.
Loss-width scorings
The baselines of the paper (RANSAC, MSAC, GaU/MLESAC, MAGSAC++) and three robust costs, each a singleton type carrying its codimension and scored through one per-residual scalar, residual_quality.
RansacScoringDoneRight.LossWidthScoring — Type
LossWidthScoring{DG}The loss-width-parameterised scorings of both branches at codimension DG — the paper's "threshold-based family". A Union, not a supertype: "takes a loss width" cuts across GauScoring and LossScoring, so it cannot be a supertype of both without implying a mixture underneath the losses, which is exactly what the retired ThresholdFamily got wrong.
This is the type the loss-width sweep entry points accept. Widening them to AbstractScoring would readmit MarginalScoring, which has no loss width and would fail several frames down inside GPU staging instead of at the call site.
RansacScoringDoneRight.RansacScoring — Type
RansacScoring{DG}()RANSAC inlier counting at codimension DG: a model's score is the number of residuals below the loss width (see residual_quality).
Build it with RansacScoring() (codimension 1), RansacScoring(dg), RansacScoring(problem) or loss_width_scoring(:ransac, dg); the other six loss-width scorings take the same constructors.
RansacScoringDoneRight.MsacScoring — Type
MsacScoring{DG}()MSAC at codimension DG: the truncated quadratic 1 − ρ(d², w²)/w² at loss width w, summed over the residuals (see residual_quality). Symbol name :msac.
RansacScoringDoneRight.MlesacScoring — Type
MlesacScoring{DG}()MLESAC (GaU) at codimension DG: the Gaussian-uniform log-density at a fixed scale, normalised to 1 at d = 0 (see residual_quality). Symbol name :gau.
RansacScoringDoneRight.MagsacScoring — Type
MagsacScoring{DG}()MAGSAC++ at codimension DG: the σ-marginalised quality, zero from the cutoff on; its loss width is that cutoff, _MAGSAC_SCALE_MULTIPLE·σ_max (see residual_quality). Symbol name :magsac.
RansacScoringDoneRight.WelschCostScoring — Type
WelschCostScoring{DG}()The Welsch cost as a loss-width score at codimension DG: 1 − ρ/w² with VisualGeometryCore's Welsch ρ, no mixture underneath. Symbol name :welsch.
RansacScoringDoneRight.SmoothTLSScoring — Type
SmoothTLSScoring{DG}()The smooth truncated-least-squares cost as a loss-width score at codimension DG: 1 − ρ/w² with VisualGeometryCore's smooth-TLS ρ. Symbol name :smoothtls.
RansacScoringDoneRight.GemanScoring — Type
GemanScoring{DG}()The Geman–McClure cost as a loss-width score at codimension DG: 1 − ρ/w² with VisualGeometryCore's Geman–McClure ρ. Symbol name :geman.
RansacScoringDoneRight.loss_width_scoring — Function
loss_width_scoring(method::Symbol[, dg | problem]) -> LossWidthScoringThe LossWidthScoring member a Symbol names — :ransac, :msac, :gau (MLESAC), :magsac, :welsch, :smoothtls, :geman — at codimension dg, or at codimension(problem). Bare, it is codimension 1.
Supply the codimension whenever the problem has one other than 1: the Symbol vocabulary must not be a back door that silently re-defaults DG.
It returns a member of the LossWidthScoring union, and is named for it.
RansacScoringDoneRight.residual_quality — Function
residual_quality(scoring::LossWidthScoring, d2::Float64, loss_width::Float64) -> Float64The score contribution of ONE residual with squared value d2 under the losswidth family scoring at `losswidth(for MAGSAC++ its cutoff,MAGSACSCALEMULTIPLE·σmax`). A model's score is the sum over its residuals, and higher is better:
- RANSAC: the inlier indicator
d² < loss_width²; - MSAC and the Welsch / smooth-TLS / Geman-McClure costs:
1 − ρ(d², loss_width²)/loss_width²; - MLESAC: the Gaussian-uniform log-density, normalised to 1 at
d = 0; - MAGSAC++: the σ-marginalised quality, zero from
max_loss_widthon.
It is the one exact per-residual scalar of the loss-width families: the KernelAbstractions Exact and LogBinned grids (grid_evaluate) and the CPU per-pair best_pool_model all evaluate it. Plain Float64 functions, callable inside a kernel. A non-finite d2 is an outlier and is skipped by every caller; it is not an input.