use crate::error::{Result, VisionError};
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct Homography {
pub data: [[f64; 3]; 3],
}
impl Homography {
pub fn identity() -> Self {
Self {
data: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
}
}
pub fn project(&self, x: f64, y: f64) -> Option<(f64, f64)> {
let h = &self.data;
let w = h[2][0] * x + h[2][1] * y + h[2][2];
if w.abs() < 1e-10 {
return None;
}
let px = (h[0][0] * x + h[0][1] * y + h[0][2]) / w;
let py = (h[1][0] * x + h[1][1] * y + h[1][2]) / w;
Some((px, py))
}
pub fn inverse(&self) -> Option<Homography> {
let h = &self.data;
let a00 = h[1][1] * h[2][2] - h[1][2] * h[2][1];
let a01 = -(h[0][1] * h[2][2] - h[0][2] * h[2][1]);
let a02 = h[0][1] * h[1][2] - h[0][2] * h[1][1];
let det = h[0][0] * a00
+ h[0][1] * (-(h[1][0] * h[2][2] - h[1][2] * h[2][0]))
+ h[0][2] * (h[1][0] * h[2][1] - h[1][1] * h[2][0]);
if det.abs() < 1e-10 {
return None;
}
let inv_det = 1.0 / det;
let a10 = -(h[1][0] * h[2][2] - h[1][2] * h[2][0]);
let a11 = h[0][0] * h[2][2] - h[0][2] * h[2][0];
let a12 = -(h[0][0] * h[1][2] - h[0][2] * h[1][0]);
let a20 = h[1][0] * h[2][1] - h[1][1] * h[2][0];
let a21 = -(h[0][0] * h[2][1] - h[0][1] * h[2][0]);
let a22 = h[0][0] * h[1][1] - h[0][1] * h[1][0];
Some(Homography {
data: [
[a00 * inv_det, a01 * inv_det, a02 * inv_det],
[a10 * inv_det, a11 * inv_det, a12 * inv_det],
[a20 * inv_det, a21 * inv_det, a22 * inv_det],
],
})
}
}
#[derive(Debug, Clone)]
pub struct RansacHomographyResult {
pub homography: Homography,
pub inlier_mask: Vec<bool>,
pub num_inliers: usize,
pub iterations: usize,
pub mean_error: f64,
}
#[derive(Debug, Clone)]
pub struct RansacConfig {
pub max_iterations: usize,
pub threshold: f64,
pub confidence: f64,
pub min_inliers: usize,
pub lm_iterations: usize,
pub seed: Option<u64>,
}
impl Default for RansacConfig {
fn default() -> Self {
Self {
max_iterations: 2000,
threshold: 3.0,
confidence: 0.995,
min_inliers: 4,
lm_iterations: 20,
seed: None,
}
}
}
pub fn estimate_homography_ransac(
src_pts: &[(f64, f64)],
dst_pts: &[(f64, f64)],
config: &RansacConfig,
) -> Result<RansacHomographyResult> {
let n = src_pts.len();
if n != dst_pts.len() {
return Err(VisionError::InvalidParameter(
"src_pts and dst_pts must have the same length".to_string(),
));
}
if n < 4 {
return Err(VisionError::InvalidParameter(
"At least 4 point correspondences are required".to_string(),
));
}
let mut rng_state = config.seed.unwrap_or(0xDEAD_BEEF_1234_5678u64);
let mut best_inliers: Vec<bool> = vec![false; n];
let mut best_count = 0usize;
let mut best_h = Homography::identity();
let mut actual_iters = 0usize;
let mut dynamic_max = config.max_iterations;
for iter in 0..config.max_iterations {
if iter >= dynamic_max {
break;
}
actual_iters += 1;
let mut idx: Vec<usize> = (0..n).collect();
for i in 0..4 {
rng_state = lcg_next(rng_state);
let j = i + (rng_state as usize % (n - i));
idx.swap(i, j);
}
let sample = &idx[..4];
let s_src: Vec<(f64, f64)> = sample.iter().map(|&i| src_pts[i]).collect();
let s_dst: Vec<(f64, f64)> = sample.iter().map(|&i| dst_pts[i]).collect();
let h = match dlt_homography(&s_src, &s_dst) {
Ok(h) => h,
Err(_) => continue,
};
let (inliers, count) = count_inliers(&h, src_pts, dst_pts, config.threshold);
if count > best_count {
best_count = count;
best_inliers = inliers;
best_h = h;
if best_count >= config.min_inliers && n > 0 {
let epsilon = 1.0 - best_count as f64 / n as f64;
let denom = (1.0 - epsilon.powi(4)).max(f64::EPSILON).ln().abs();
let new_max = ((1.0 - config.confidence).ln().abs() / denom).ceil() as usize;
dynamic_max = new_max.min(config.max_iterations);
}
if best_count == n {
break; }
}
}
if best_count < config.min_inliers {
return Err(VisionError::OperationError(format!(
"RANSAC failed: only {best_count} inliers (required ≥ {})",
config.min_inliers
)));
}
let in_src: Vec<(f64, f64)> = src_pts
.iter()
.zip(best_inliers.iter())
.filter_map(|(&s, &ok)| if ok { Some(s) } else { None })
.collect();
let in_dst: Vec<(f64, f64)> = dst_pts
.iter()
.zip(best_inliers.iter())
.filter_map(|(&d, &ok)| if ok { Some(d) } else { None })
.collect();
let refined_h = dlt_homography(&in_src, &in_dst).unwrap_or(best_h);
let final_h = if config.lm_iterations > 0 {
lm_refine(&refined_h, &in_src, &in_dst, config.lm_iterations)?
} else {
refined_h
};
let (final_inliers, final_count) = count_inliers(&final_h, src_pts, dst_pts, config.threshold);
let mean_error = compute_mean_error(&final_h, src_pts, dst_pts, &final_inliers);
Ok(RansacHomographyResult {
homography: final_h,
inlier_mask: final_inliers,
num_inliers: final_count,
iterations: actual_iters,
mean_error,
})
}
pub fn dlt_homography(src_pts: &[(f64, f64)], dst_pts: &[(f64, f64)]) -> Result<Homography> {
let n = src_pts.len();
if n < 4 {
return Err(VisionError::InvalidParameter(
"DLT requires ≥ 4 point correspondences".to_string(),
));
}
let (src_norm, t_src) = normalize_points(src_pts);
let (dst_norm, t_dst) = normalize_points(dst_pts);
let mut a = vec![[0.0f64; 9]; 2 * n];
for (i, ((x, y), (xp, yp))) in src_norm.iter().zip(dst_norm.iter()).enumerate() {
a[2 * i] = [-x, -y, -1.0, 0.0, 0.0, 0.0, xp * x, xp * y, *xp];
a[2 * i + 1] = [0.0, 0.0, 0.0, -x, -y, -1.0, yp * x, yp * y, *yp];
}
let h_vec = smallest_singular_vector(&a, 9)?;
let h_norm = Homography {
data: [
[h_vec[0], h_vec[1], h_vec[2]],
[h_vec[3], h_vec[4], h_vec[5]],
[h_vec[6], h_vec[7], h_vec[8]],
],
};
let h = denormalize_homography(&h_norm, &t_src, &t_dst)?;
Ok(h)
}
fn normalize_points(pts: &[(f64, f64)]) -> (Vec<(f64, f64)>, [[f64; 3]; 3]) {
let n = pts.len() as f64;
let cx = pts.iter().map(|p| p.0).sum::<f64>() / n;
let cy = pts.iter().map(|p| p.1).sum::<f64>() / n;
let mean_dist = pts
.iter()
.map(|p| ((p.0 - cx).powi(2) + (p.1 - cy).powi(2)).sqrt())
.sum::<f64>()
/ n;
let scale = if mean_dist > 1e-10 {
std::f64::consts::SQRT_2 / mean_dist
} else {
1.0
};
let normed: Vec<(f64, f64)> = pts
.iter()
.map(|p| ((p.0 - cx) * scale, (p.1 - cy) * scale))
.collect();
let t = [
[scale, 0.0, -scale * cx],
[0.0, scale, -scale * cy],
[0.0, 0.0, 1.0],
];
(normed, t)
}
fn denormalize_homography(
h_norm: &Homography,
t_src: &[[f64; 3]; 3],
t_dst: &[[f64; 3]; 3],
) -> Result<Homography> {
let s = t_dst[0][0];
if s.abs() < 1e-10 {
return Err(VisionError::OperationError(
"Degenerate normalisation matrix".to_string(),
));
}
let inv_s = 1.0 / s;
let t_dst_inv = [
[inv_s, 0.0, -t_dst[0][2] * inv_s],
[0.0, inv_s, -t_dst[1][2] * inv_s],
[0.0, 0.0, 1.0],
];
let tmp = mat3x3_mul(&h_norm.data, t_src);
let h_data = mat3x3_mul(&t_dst_inv, &tmp);
Ok(Homography { data: h_data })
}
fn count_inliers(
h: &Homography,
src: &[(f64, f64)],
dst: &[(f64, f64)],
threshold: f64,
) -> (Vec<bool>, usize) {
let h_inv = h.inverse();
let thresh2 = threshold * threshold;
let mut mask = vec![false; src.len()];
let mut count = 0usize;
for (i, (s, d)) in src.iter().zip(dst.iter()).enumerate() {
let fwd_err = if let Some((px, py)) = h.project(s.0, s.1) {
(px - d.0).powi(2) + (py - d.1).powi(2)
} else {
f64::MAX
};
let bwd_err = if let Some(ref hi) = h_inv {
if let Some((px, py)) = hi.project(d.0, d.1) {
(px - s.0).powi(2) + (py - s.1).powi(2)
} else {
f64::MAX
}
} else {
f64::MAX
};
let sym_err = (fwd_err + bwd_err) / 2.0;
if sym_err < thresh2 {
mask[i] = true;
count += 1;
}
}
(mask, count)
}
fn compute_mean_error(
h: &Homography,
src: &[(f64, f64)],
dst: &[(f64, f64)],
inliers: &[bool],
) -> f64 {
let mut total = 0.0;
let mut count = 0usize;
let h_inv = h.inverse();
for ((&s, &d), &ok) in src.iter().zip(dst.iter()).zip(inliers.iter()) {
if !ok {
continue;
}
let fwd = h
.project(s.0, s.1)
.map(|(px, py)| ((px - d.0).powi(2) + (py - d.1).powi(2)).sqrt())
.unwrap_or(f64::MAX);
let bwd = h_inv
.as_ref()
.and_then(|hi| hi.project(d.0, d.1))
.map(|(px, py)| ((px - s.0).powi(2) + (py - s.1).powi(2)).sqrt())
.unwrap_or(f64::MAX);
total += (fwd + bwd) / 2.0;
count += 1;
}
if count == 0 {
f64::MAX
} else {
total / count as f64
}
}
fn lm_refine(
h0: &Homography,
src: &[(f64, f64)],
dst: &[(f64, f64)],
max_iter: usize,
) -> Result<Homography> {
let mut h = h0.clone();
let mut mu = 1e-3; let nu = 2.0f64;
let n = src.len();
for _iter in 0..max_iter {
let (r, j) = residuals_and_jacobian(&h, src, dst);
let cost: f64 = r.iter().map(|ri| ri * ri).sum();
let g = mat_vec_t_mul(&j, &r, 8);
let jtj = mat_t_mat_mul(&j, 8);
let mut a_diag = jtj.clone();
#[allow(clippy::needless_range_loop)]
for k in 0..8 {
a_diag[k][k] += mu;
}
let neg_g: Vec<f64> = g.iter().map(|v| -v).collect();
let delta = solve_8x8(&a_diag, &neg_g);
let h_new = apply_delta(&h, &delta);
let (r_new, _) = residuals_and_jacobian(&h_new, src, dst);
let cost_new: f64 = r_new.iter().map(|ri| ri * ri).sum();
let predicted_reduction: f64 = delta
.iter()
.zip(g.iter())
.map(|(d, gi)| d * gi)
.sum::<f64>()
.abs()
+ 1e-20;
let gain = (cost - cost_new) / predicted_reduction;
if cost_new < cost {
h = h_new;
mu /= nu;
} else {
mu *= nu;
}
if gain > 0.0 && cost_new < 1e-10 {
break;
}
}
Ok(h)
}
fn residuals_and_jacobian(
h: &Homography,
src: &[(f64, f64)],
dst: &[(f64, f64)],
) -> (Vec<f64>, Vec<[f64; 8]>) {
let hd = &h.data;
let n = src.len();
let mut residuals = Vec::with_capacity(2 * n);
let mut jacobian: Vec<[f64; 8]> = Vec::with_capacity(2 * n);
for (&(x, y), &(xp, yp)) in src.iter().zip(dst.iter()) {
let w = hd[2][0] * x + hd[2][1] * y + hd[2][2];
if w.abs() < 1e-10 {
residuals.push(0.0);
residuals.push(0.0);
jacobian.push([0.0; 8]);
jacobian.push([0.0; 8]);
continue;
}
let inv_w = 1.0 / w;
let px = (hd[0][0] * x + hd[0][1] * y + hd[0][2]) * inv_w;
let py = (hd[1][0] * x + hd[1][1] * y + hd[1][2]) * inv_w;
residuals.push(px - xp);
residuals.push(py - yp);
let mut jx = [0.0f64; 8];
jx[0] = x * inv_w;
jx[1] = y * inv_w;
jx[2] = inv_w;
jx[6] = -px * x * inv_w;
jx[7] = -px * y * inv_w;
let mut jy = [0.0f64; 8];
jy[3] = x * inv_w;
jy[4] = y * inv_w;
jy[5] = inv_w;
jy[6] = -py * x * inv_w;
jy[7] = -py * y * inv_w;
jacobian.push(jx);
jacobian.push(jy);
}
(residuals, jacobian)
}
fn apply_delta(h: &Homography, delta: &[f64]) -> Homography {
let hd = &h.data;
let params = [
hd[0][0] + delta.first().copied().unwrap_or(0.0),
hd[0][1] + delta.get(1).copied().unwrap_or(0.0),
hd[0][2] + delta.get(2).copied().unwrap_or(0.0),
hd[1][0] + delta.get(3).copied().unwrap_or(0.0),
hd[1][1] + delta.get(4).copied().unwrap_or(0.0),
hd[1][2] + delta.get(5).copied().unwrap_or(0.0),
hd[2][0] + delta.get(6).copied().unwrap_or(0.0),
hd[2][1] + delta.get(7).copied().unwrap_or(0.0),
hd[2][2], ];
Homography {
data: [
[params[0], params[1], params[2]],
[params[3], params[4], params[5]],
[params[6], params[7], params[8]],
],
}
}
fn mat3x3_mul(a: &[[f64; 3]; 3], b: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
let mut c = [[0.0f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
for k in 0..3 {
c[i][j] += a[i][k] * b[k][j];
}
}
}
c
}
fn smallest_singular_vector(a: &[[f64; 9]], cols: usize) -> Result<Vec<f64>> {
let rows = a.len();
if rows < cols - 1 {
return Err(VisionError::OperationError(
"Underdetermined system for DLT".to_string(),
));
}
let mut ata = vec![vec![0.0f64; cols]; cols];
for row in a {
for j in 0..cols {
for k in 0..cols {
ata[j][k] += row[j] * row[k];
}
}
}
let n8 = cols - 1;
let mut mat: Vec<Vec<f64>> = (0..n8).map(|i| ata[i][0..n8].to_vec()).collect();
let mut rhs: Vec<f64> = (0..n8).map(|i| -ata[i][cols - 1]).collect();
#[allow(clippy::needless_range_loop)]
for col in 0..n8 {
let mut max_row = col;
let mut max_val = mat[col][col].abs();
for row in (col + 1)..n8 {
if mat[row][col].abs() > max_val {
max_val = mat[row][col].abs();
max_row = row;
}
}
mat.swap(col, max_row);
rhs.swap(col, max_row);
let pivot = mat[col][col];
if pivot.abs() < 1e-10 {
let mut h = vec![0.0f64; cols];
h[cols - 1] = 1.0;
return Ok(h);
}
for row in (col + 1)..n8 {
let factor = mat[row][col] / pivot;
for k in col..n8 {
let sub = mat[col][k] * factor;
mat[row][k] -= sub;
}
rhs[row] -= rhs[col] * factor;
}
}
let mut x = vec![0.0f64; n8];
for row in (0..n8).rev() {
let mut sum = rhs[row];
for k in (row + 1)..n8 {
sum -= mat[row][k] * x[k];
}
if mat[row][row].abs() > 1e-10 {
x[row] = sum / mat[row][row];
}
}
let mut h = x;
h.push(1.0_f64);
let norm: f64 = h.iter().map(|v| v * v).sum::<f64>().sqrt();
if norm < 1e-14 {
return Err(VisionError::OperationError(
"Null-space computation failed: degenerate input".to_string(),
));
}
for v in h.iter_mut() {
*v /= norm;
}
let max_abs = h.iter().cloned().map(f64::abs).fold(0.0_f64, f64::max);
let sign = h
.iter()
.find(|&&v| v.abs() >= max_abs - 1e-14)
.map_or(1.0, |&v| if v < 0.0 { -1.0 } else { 1.0 });
for v in h.iter_mut() {
*v *= sign;
}
Ok(h)
}
fn mat_vec_t_mul(j: &[[f64; 8]], r: &[f64], n_params: usize) -> Vec<f64> {
let mut g = vec![0.0f64; n_params];
for (ji, ri) in j.iter().zip(r.iter()) {
for k in 0..n_params {
g[k] += ji[k] * ri;
}
}
g
}
fn mat_t_mat_mul(j: &[[f64; 8]], n_params: usize) -> Vec<Vec<f64>> {
let mut h = vec![vec![0.0f64; n_params]; n_params];
for ji in j.iter() {
for k in 0..n_params {
for l in 0..n_params {
h[k][l] += ji[k] * ji[l];
}
}
}
h
}
fn solve_8x8(a: &[Vec<f64>], b: &[f64]) -> Vec<f64> {
let n = 8usize;
let mut mat: Vec<Vec<f64>> = a.to_vec();
let mut rhs: Vec<f64> = b.to_vec();
#[allow(clippy::needless_range_loop)]
for col in 0..n {
let mut max_row = col;
let mut max_val = mat[col][col].abs();
for row in (col + 1)..n {
if mat[row][col].abs() > max_val {
max_val = mat[row][col].abs();
max_row = row;
}
}
mat.swap(col, max_row);
rhs.swap(col, max_row);
let pivot = mat[col][col];
if pivot.abs() < 1e-12 {
continue;
}
for row in (col + 1)..n {
let factor = mat[row][col] / pivot;
for k in col..n {
let sub = mat[col][k] * factor;
mat[row][k] -= sub;
}
rhs[row] -= rhs[col] * factor;
}
}
let mut x = vec![0.0f64; n];
for row in (0..n).rev() {
let mut sum = rhs[row];
for k in (row + 1)..n {
sum -= mat[row][k] * x[k];
}
if mat[row][row].abs() > 1e-12 {
x[row] = sum / mat[row][row];
}
}
x
}
fn lcg_next(state: u64) -> u64 {
state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407)
}
#[cfg(test)]
mod tests {
use super::*;
fn apply_known_homography(pts: &[(f64, f64)], h: &[[f64; 3]; 3]) -> Vec<(f64, f64)> {
pts.iter()
.map(|&(x, y)| {
let w = h[2][0] * x + h[2][1] * y + h[2][2];
let px = (h[0][0] * x + h[0][1] * y + h[0][2]) / w;
let py = (h[1][0] * x + h[1][1] * y + h[1][2]) / w;
(px, py)
})
.collect()
}
fn sample_points(n: usize) -> Vec<(f64, f64)> {
(0..n)
.map(|i| {
let t = i as f64 * 37.0;
(50.0 + (t.sin() * 120.0), 50.0 + (t.cos() * 80.0))
})
.collect()
}
#[test]
fn test_identity_homography_dlt() {
let src = sample_points(8);
let dst = src.clone();
let h = dlt_homography(&src, &dst)
.expect("dlt_homography should succeed with 8+ point correspondences");
for (s, d) in src.iter().zip(dst.iter()) {
let (px, py) = h
.project(s.0, s.1)
.expect("homography project should succeed for valid point");
assert!((px - d.0).abs() < 0.5, "px={px} d.0={}", d.0);
assert!((py - d.1).abs() < 0.5, "py={py} d.1={}", d.1);
}
}
#[test]
fn test_dlt_known_homography() {
let h_true = [[1.1, 0.05, 10.0], [-0.03, 1.2, 5.0], [0.0002, 0.0001, 1.0]];
let src = sample_points(12);
let dst = apply_known_homography(&src, &h_true);
let h = dlt_homography(&src, &dst)
.expect("dlt_homography should succeed with known correspondences");
for (s, d) in src.iter().zip(dst.iter()) {
let (px, py) = h
.project(s.0, s.1)
.expect("homography project should succeed for valid point");
let err = ((px - d.0).powi(2) + (py - d.1).powi(2)).sqrt();
assert!(err < 1.0, "Reprojection error too large: {err}");
}
}
#[test]
fn test_ransac_with_outliers() {
let h_true = [[1.2, 0.1, 15.0], [0.05, 0.9, -10.0], [0.0003, -0.0002, 1.0]];
let mut src = sample_points(30);
let mut dst = apply_known_homography(&src, &h_true);
let outlier_indices = [2usize, 7, 11, 18, 23, 28];
for &i in &outlier_indices {
dst[i].0 += 100.0 * ((i as f64).sin());
dst[i].1 -= 80.0 * ((i as f64).cos());
}
let config = RansacConfig {
max_iterations: 500,
threshold: 5.0,
confidence: 0.99,
min_inliers: 10,
lm_iterations: 10,
seed: Some(42),
};
let result = estimate_homography_ransac(&src, &dst, &config)
.expect("RANSAC homography should succeed with sufficient inliers");
let outlier_accepted = outlier_indices
.iter()
.filter(|&&i| result.inlier_mask[i])
.count();
assert!(
outlier_accepted <= 2,
"Too many outliers accepted: {outlier_accepted}"
);
assert!(
result.num_inliers >= 20,
"Too few inliers: {}",
result.num_inliers
);
}
#[test]
fn test_homography_inverse() {
let h_data = [[1.1, 0.05, 10.0], [-0.03, 1.2, 5.0], [0.0002, 0.0001, 1.0]];
let h = Homography { data: h_data };
let hi = h.inverse().expect("Should have an inverse");
let prod = crate::features::homography_ransac::mat3x3_mul(&h.data, &hi.data);
for (i, row) in prod.iter().enumerate() {
for (j, &val) in row.iter().enumerate() {
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
(val - expected).abs() < 1e-8,
"H·H^-1 [{i}][{j}] = {} expected {expected}",
val
);
}
}
}
#[test]
fn test_too_few_points() {
let src = vec![(0.0, 0.0), (1.0, 0.0), (0.5, 1.0)];
let dst = vec![(0.1, 0.1), (1.1, 0.1), (0.6, 1.1)];
assert!(dlt_homography(&src, &dst).is_err());
}
#[test]
fn test_homography_project_infinity() {
let h = Homography {
data: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]],
};
let res = h.project(0.0, 5.0);
assert!(res.is_none() || res.is_some()); }
}