use super::polynomial::build_polynomial_system;
use crate::{
Error,
math::{mat3_from_svd_row, normalize_points_2d},
};
use nalgebra::{DMatrix, linalg::Schur};
use vision_calibration_core::{Mat3, Pt2, Real};
pub fn essential_5point(pts1: &[Pt2], pts2: &[Pt2]) -> Result<Vec<Mat3>, Error> {
if pts1.len() != pts2.len() || pts1.len() != 5 {
return Err(Error::invalid_input(format!(
"Point count mismatch: expected 5, pts1 has {}, pts2 has {}",
pts1.len(),
pts2.len()
)));
}
let (pts1_n, t1) = normalize_points_2d(pts1).ok_or(Error::Singular)?;
let (pts2_n, t2) = normalize_points_2d(pts2).ok_or(Error::Singular)?;
let mut a = DMatrix::<Real>::zeros(5, 9);
for (i, (p1, p2)) in pts1_n.iter().zip(pts2_n.iter()).enumerate() {
let x = p1.x;
let y = p1.y;
let xp = p2.x;
let yp = p2.y;
a[(i, 0)] = xp * x;
a[(i, 1)] = xp * y;
a[(i, 2)] = xp;
a[(i, 3)] = yp * x;
a[(i, 4)] = yp * y;
a[(i, 5)] = yp;
a[(i, 6)] = x;
a[(i, 7)] = y;
a[(i, 8)] = 1.0;
}
let mut a_work = a.clone();
if a_work.nrows() < a_work.ncols() {
let rows = a_work.nrows();
let cols = a_work.ncols();
let mut a_pad = DMatrix::<Real>::zeros(cols, cols);
a_pad.view_mut((0, 0), (rows, cols)).copy_from(&a_work);
a_work = a_pad;
}
let svd = a_work.svd(true, true);
let v_t = svd.v_t.ok_or(Error::Singular)?;
if v_t.nrows() < 4 {
return Err(Error::Singular);
}
let e1 = mat3_from_svd_row(&v_t, v_t.nrows() - 4);
let e2 = mat3_from_svd_row(&v_t, v_t.nrows() - 3);
let e3 = mat3_from_svd_row(&v_t, v_t.nrows() - 2);
let e4 = mat3_from_svd_row(&v_t, v_t.nrows() - 1);
let eqs = build_polynomial_system(&e1, &e2, &e3, &e4);
let mut m = DMatrix::<Real>::zeros(10, 20);
for (r, row) in eqs.iter().enumerate() {
for (c, &val) in row.iter().enumerate() {
m[(r, c)] = val;
}
}
let m1 = m.view((0, 0), (10, 10)).into_owned();
let m2 = m.view((0, 10), (10, 10)).into_owned();
let m1_lu = m1.lu();
let c = m1_lu.solve(&(-m2)).ok_or(Error::Singular)?;
let mut action = DMatrix::<Real>::zeros(10, 10);
let deg3_rows = [2, 4, 5, 7, 8, 9];
for (col, &row) in deg3_rows.iter().enumerate() {
for r in 0..10 {
action[(r, col)] = c[(row, r)];
}
}
action[(2, 6)] = 1.0;
action[(4, 7)] = 1.0;
action[(5, 8)] = 1.0;
action[(8, 9)] = 1.0;
let schur = Schur::new(action.clone());
let eigvals = schur.complex_eigenvalues();
let mut solutions = Vec::new();
for val in eigvals.iter() {
if val.im.abs() > 1e-8 {
continue;
}
let z = val.re;
let mut a_eval = action.clone();
for i in 0..10 {
a_eval[(i, i)] -= z;
}
let svd = a_eval.svd(true, true);
let v_t = svd.v_t.ok_or(Error::Singular)?;
let vec = v_t.row(v_t.nrows() - 1);
let v9 = vec[9];
if v9.abs() < 1e-12 {
continue;
}
let x = vec[6] / v9;
let y = vec[7] / v9;
let z_vec = vec[8] / v9;
let mut e = e1 * x + e2 * y + e3 * z_vec + e4;
e = t2.transpose() * e * t1;
solutions.push((z_vec, e));
}
if solutions.is_empty() {
return Err(Error::numerical("5-point polynomial solve failed"));
}
solutions.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
Ok(solutions.into_iter().map(|(_, e)| e).collect())
}
#[cfg(test)]
mod tests {
use super::*;
use nalgebra::Rotation3;
use vision_calibration_core::{Pt3, Vec3};
#[test]
fn essential_5point_fits_minimal_set() {
let rot = Rotation3::from_euler_angles(0.1, -0.05, 0.2);
let t = Vec3::new(0.1, 0.02, 0.03);
let world = vec![
Pt3::new(0.1, 0.2, 2.0),
Pt3::new(-0.2, 0.1, 2.5),
Pt3::new(0.3, -0.1, 3.0),
Pt3::new(-0.15, -0.2, 2.2),
Pt3::new(0.05, 0.3, 2.8),
];
let mut pts1 = Vec::new();
let mut pts2 = Vec::new();
for pw in world {
let pc1 = pw.coords;
let pc2 = rot * pw + t;
pts1.push(Pt2::new(pc1.x / pc1.z, pc1.y / pc1.z));
pts2.push(Pt2::new(pc2.x / pc2.z, pc2.y / pc2.z));
}
let sols = essential_5point(&pts1, &pts2).unwrap();
assert!(!sols.is_empty());
let mut best = f64::INFINITY;
for e in sols {
let mut err = 0.0;
for (p1, p2) in pts1.iter().zip(pts2.iter()) {
let x = nalgebra::Vector3::new(p1.x, p1.y, 1.0);
let xp = nalgebra::Vector3::new(p2.x, p2.y, 1.0);
let val = xp.transpose() * e * x;
err += val[0].abs();
}
best = best.min(err);
}
assert!(best < 1e-6, "5-point residual too large: {}", best);
}
}