use nalgebra::{Matrix3, Point2, Vector3};
use projective_grid::estimate_homography as pg_estimate_homography;
#[derive(Debug, Clone, PartialEq)]
pub enum HomographyError {
TooFewPoints {
needed: usize,
got: usize,
},
NumericalFailure(String),
InsufficientInliers {
needed: usize,
found: usize,
},
}
impl std::fmt::Display for HomographyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TooFewPoints { needed, got } => {
write!(f, "too few points: need {}, got {}", needed, got)
}
Self::NumericalFailure(msg) => write!(f, "numerical failure: {}", msg),
Self::InsufficientInliers { needed, found } => {
write!(f, "insufficient inliers: need {}, found {}", needed, found)
}
}
}
}
impl std::error::Error for HomographyError {}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RansacStats {
pub n_candidates: usize,
pub n_inliers: usize,
pub threshold_px: f64,
pub mean_err_px: f64,
pub p95_err_px: f64,
}
pub fn homography_project(h: &Matrix3<f64>, x: f64, y: f64) -> [f64; 2] {
let p = h * Vector3::new(x, y, 1.0);
if p[2].abs() < 1e-15 {
return [f64::NAN, f64::NAN];
}
[p[0] / p[2], p[1] / p[2]]
}
pub fn homography_reprojection_error(h: &Matrix3<f64>, src: &[f64; 2], dst: &[f64; 2]) -> f64 {
let p = homography_project(h, src[0], src[1]);
let dx = p[0] - dst[0];
let dy = p[1] - dst[1];
(dx * dx + dy * dy).sqrt()
}
pub fn estimate_homography_dlt(
src: &[[f64; 2]],
dst: &[[f64; 2]],
) -> Result<Matrix3<f64>, HomographyError> {
let n = src.len();
if n < 4 || dst.len() < 4 {
return Err(HomographyError::TooFewPoints {
needed: 4,
got: n.min(dst.len()),
});
}
if src.len() != dst.len() {
return Err(HomographyError::NumericalFailure(
"src and dst must have the same length".into(),
));
}
let src_pts: Vec<Point2<f64>> = src.iter().map(|p| Point2::new(p[0], p[1])).collect();
let dst_pts: Vec<Point2<f64>> = dst.iter().map(|p| Point2::new(p[0], p[1])).collect();
pg_estimate_homography(&src_pts, &dst_pts)
.map(|h| h.h)
.ok_or_else(|| HomographyError::NumericalFailure("DLT estimation failed".into()))
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct RansacHomographyConfig {
pub max_iters: usize,
pub inlier_threshold: f64,
pub min_inliers: usize,
pub seed: u64,
}
impl Default for RansacHomographyConfig {
fn default() -> Self {
Self {
max_iters: 2000,
inlier_threshold: 5.0,
min_inliers: 6,
seed: 0,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RansacHomographyResult {
pub h: Matrix3<f64>,
pub inlier_mask: Vec<bool>,
pub n_inliers: usize,
pub errors: Vec<f64>,
}
fn sample_4_distinct(n: usize, rng: &mut rand::rngs::StdRng) -> [usize; 4] {
use rand::RngExt;
let mut indices = [0usize; 4];
for _ in 0..200 {
for idx in &mut indices {
*idx = rng.random_range(0..n);
}
let mut ok = true;
for i in 0..4 {
for j in (i + 1)..4 {
if indices[i] == indices[j] {
ok = false;
}
}
}
if ok {
return indices;
}
}
indices
}
pub fn fit_homography_ransac(
src: &[[f64; 2]],
dst: &[[f64; 2]],
config: &RansacHomographyConfig,
) -> Result<RansacHomographyResult, HomographyError> {
let n = src.len();
if n < 4 {
return Err(HomographyError::TooFewPoints { needed: 4, got: n });
}
use rand::SeedableRng;
let mut rng = rand::rngs::StdRng::seed_from_u64(config.seed);
let mut best_inliers = 0usize;
let mut best_mask: Vec<bool> = vec![false; n];
let mut best_h = Matrix3::identity();
let mut mask = vec![false; n];
let mut adaptive_limit = config.max_iters;
for iter in 0..config.max_iters {
if iter >= adaptive_limit {
break;
}
let indices = sample_4_distinct(n, &mut rng);
let s4: Vec<[f64; 2]> = indices.iter().map(|&i| src[i]).collect();
let d4: Vec<[f64; 2]> = indices.iter().map(|&i| dst[i]).collect();
let h = match estimate_homography_dlt(&s4, &d4) {
Ok(h) => h,
Err(_) => continue,
};
mask.fill(false);
let mut count = 0usize;
for i in 0..n {
let err = homography_reprojection_error(&h, &src[i], &dst[i]);
if err < config.inlier_threshold {
mask[i] = true;
count += 1;
}
}
if count > best_inliers {
best_inliers = count;
best_mask.copy_from_slice(&mask);
best_h = h;
let w = count as f64 / n as f64;
if w > 0.0 {
let p_fail = (1.0 - w.powi(4)).max(1e-15);
let needed = (1e-4_f64.ln() / p_fail.ln()).ceil() as usize;
adaptive_limit = needed.max(50).min(config.max_iters);
}
if count * 10 > n * 9 {
break;
}
}
}
if best_inliers < config.min_inliers {
return Err(HomographyError::InsufficientInliers {
needed: config.min_inliers,
found: best_inliers,
});
}
let inlier_src: Vec<[f64; 2]> = (0..n).filter(|&i| best_mask[i]).map(|i| src[i]).collect();
let inlier_dst: Vec<[f64; 2]> = (0..n).filter(|&i| best_mask[i]).map(|i| dst[i]).collect();
let h_refit = estimate_homography_dlt(&inlier_src, &inlier_dst).unwrap_or(best_h);
let mut final_mask = vec![false; n];
let mut errors = vec![0.0f64; n];
let mut final_inliers = 0usize;
for i in 0..n {
let err = homography_reprojection_error(&h_refit, &src[i], &dst[i]);
errors[i] = err;
if err < config.inlier_threshold {
final_mask[i] = true;
final_inliers += 1;
}
}
Ok(RansacHomographyResult {
h: h_refit,
inlier_mask: final_mask,
n_inliers: final_inliers,
errors,
})
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use rand::{RngExt, SeedableRng};
fn make_test_homography() -> Matrix3<f64> {
Matrix3::new(3.5, 0.1, 640.0, -0.05, 3.3, 480.0, 0.0001, -0.00005, 1.0)
}
#[test]
fn test_dlt_exact_4points() {
let h_true = make_test_homography();
let src = [[0.0, 0.0], [100.0, 0.0], [100.0, 100.0], [0.0, 100.0]];
let dst: Vec<[f64; 2]> = src
.iter()
.map(|s| homography_project(&h_true, s[0], s[1]))
.collect();
let h_est = estimate_homography_dlt(&src, &dst).unwrap();
for (s, d) in src.iter().zip(&dst) {
let err = homography_reprojection_error(&h_est, s, d);
assert!(err < 1e-6, "reprojection error too large: {}", err);
}
}
#[test]
fn test_dlt_overdetermined() {
let h_true = make_test_homography();
let mut src = Vec::new();
let mut dst = Vec::new();
for i in 0..5 {
for j in 0..5 {
let s = [i as f64 * 20.0, j as f64 * 20.0];
let d = homography_project(&h_true, s[0], s[1]);
src.push(s);
dst.push(d);
}
}
let h_est = estimate_homography_dlt(&src, &dst).unwrap();
for (s, d) in src.iter().zip(&dst) {
let err = homography_reprojection_error(&h_est, s, d);
assert!(err < 1e-6, "reprojection error: {}", err);
}
}
#[test]
fn test_ransac_with_outliers() {
let h_true = make_test_homography();
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let mut src = Vec::new();
let mut dst = Vec::new();
for i in 0..20 {
let s = [(i % 5) as f64 * 30.0, (i / 5) as f64 * 30.0];
let d = homography_project(&h_true, s[0], s[1]);
let d = [
d[0] + rng.random_range(-0.5..0.5),
d[1] + rng.random_range(-0.5..0.5),
];
src.push(s);
dst.push(d);
}
for _ in 0..8 {
let s = [rng.random_range(0.0..100.0), rng.random_range(0.0..100.0)];
let d = [rng.random_range(0.0..1280.0), rng.random_range(0.0..960.0)];
src.push(s);
dst.push(d);
}
let config = RansacHomographyConfig {
max_iters: 2000,
inlier_threshold: 3.0,
min_inliers: 6,
seed: 99,
};
let result = fit_homography_ransac(&src, &dst, &config).unwrap();
assert!(result.n_inliers >= 18, "only {} inliers", result.n_inliers);
for i in 0..20 {
let err = homography_reprojection_error(&result.h, &src[i], &dst[i]);
assert!(err < 5.0, "inlier {} has error {}", i, err);
}
}
#[test]
fn test_project_roundtrip() {
let h = make_test_homography();
let h_inv = h.try_inverse().unwrap();
let p = [50.0, 75.0];
let q = homography_project(&h, p[0], p[1]);
let p_back = homography_project(&h_inv, q[0], q[1]);
assert_relative_eq!(p[0], p_back[0], epsilon = 1e-8);
assert_relative_eq!(p[1], p_back[1], epsilon = 1e-8);
}
#[test]
fn test_too_few_points() {
let src = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]];
let dst = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]];
let result = estimate_homography_dlt(&src, &dst);
assert!(result.is_err());
}
}