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
AbstractScoreableProblem

Supertype 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).

source
RansacScoringDoneRight.codimension — Function
codimension(problem) -> Int

Constraint codimension d_g: the number of independent constraint equations a true datum satisfies (1 for F/E/line scalar residual, 2 for homography / PnP).

source
RansacScoringDoneRight.solver_covariance — Function
solver_covariance(problem, model, sample_indices) -> SMatrix or nothing

Model 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.

source
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.

source
RansacScoringDoneRight.whitened_residual — Function
whitened_residual(problem, ctx, i) -> Real

The 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.

source
RansacScoringDoneRight.residual_context — Function
residual_context(problem, model) -> ctx

What 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.

source

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} <: GauScoring

Scale-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) or Predictive (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 charges log(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. The prior_table is built on the associated ScoringWorkspace when this is true.
  • scale_prior::IGPrior: Scale prior for σ² marginalization. Jeffreys is IGPrior(0.0, 0.0); weakly-informative IG is IGPrior(α₀, β₀). Use the JeffreysMarginal / InverseGammaMarginal constructor 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.

source
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.

source
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.

source
RansacScoringDoneRight.Predictive — Type
Predictive

Type 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

source
RansacScoringDoneRight.PredictiveMarginalScoring — Type
PredictiveMarginalScoring

Alias 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).

source
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)
source
RansacScoringDoneRight.ScoringWorkspace — Type
ScoringWorkspace

Pre-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 every sweep!
  • scratch::Vector{Int} — sortperm scratch (comparison-sort fallback paths)
  • radix::RadixPermBuffers — scratch for the allocation-free stable radix sortperm taken 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 prior
  • penalties::Vector{Float64} — per-point ℓᵢ = log|Ṽᵢ| (predictive scoring)
  • sort_key::Vector{Float64} — the sweep ordering key: the per-point unit-scale Mahalanobis q in Predictive scoring (raw r² only in the model-certain fallback). The historical raw-r² key is retired — with heterogeneous penalties sweep! follows the generalized sweep lemma's parametric ordering.
  • prior_table::Vector{Float64} — profiled-γ correction; empty = flat prior
  • scale_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).

source
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.

source
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.

source
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 Σ̃_θ.

source
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.

source

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.

  • s2 inlier variance sigma^2
  • nu effective dof: Inf is 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.
  • gamma the label prior Pr(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 adds log(gamma/(1-gamma)), which is exactly the term that "biases the inlier/outlier boundary".
  • d_g the codimension, REQUIRED. It enters the outlier density as the EXPONENT (2·outlier_halfwidth)^-d_g, so no value of it is a safe default.
source
RansacScoringDoneRight.decision_threshold — Function
decision_threshold(m::GauInlier, outlier_halfwidth) -> Float64

The 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.

source
VisualGeometryCore.decision_width — Method
decision_width(m::GauInlier, decision_threshold) -> outlier_halfwidth

The 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.

source

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.

source
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.

source
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.

source
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.

source
RansacScoringDoneRight.loss_width_scoring — Function
loss_width_scoring(method::Symbol[, dg | problem]) -> LossWidthScoring

The 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.

source
RansacScoringDoneRight.residual_quality — Function
residual_quality(scoring::LossWidthScoring, d2::Float64, loss_width::Float64) -> Float64

The 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_width on.

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.

source