use kornia_algebra::{Mat3F64, Vec2F64, Vec3F64};
use super::triangulation::triangulate_point_linear;
pub fn correct_matches_sampson(f: &Mat3F64, x1: &Vec2F64, x2: &Vec2F64) -> (Vec2F64, Vec2F64) {
let x1h = Vec3F64::new(x1.x, x1.y, 1.0);
let x2h = Vec3F64::new(x2.x, x2.y, 1.0);
let l2 = *f * x1h; let l1 = f.transpose() * x2h; let err = x2h.dot(l2);
let denom = l2.x * l2.x + l2.y * l2.y + l1.x * l1.x + l1.y * l1.y;
if denom <= 1e-18 {
return (*x1, *x2);
}
let scale = err / denom;
(
Vec2F64::new(x1.x - scale * l1.x, x1.y - scale * l1.y),
Vec2F64::new(x2.x - scale * l2.x, x2.y - scale * l2.y),
)
}
pub fn triangulate_optimal_2view(
f: &Mat3F64,
x1: &Vec2F64,
x2: &Vec2F64,
r: &Mat3F64,
t: &Vec3F64,
) -> Option<Vec3F64> {
let (x1c, x2c) = correct_matches_sampson(f, x1, x2);
triangulate_point_linear(&x1c, &x2c, r, t)
}
pub fn triangulate_n_view_linear(pixels: &[Vec2F64], projections: &[[f64; 12]]) -> Option<Vec3F64> {
let n = pixels.len();
if n < 2 || n != projections.len() {
return None;
}
let mut a = faer::Mat::<f64>::zeros(2 * n, 4);
for (i, (px, p_row)) in pixels.iter().zip(projections.iter()).enumerate() {
for j in 0..4 {
let p0 = p_row[j];
let p1 = p_row[4 + j];
let p2 = p_row[8 + j];
a.write(2 * i, j, px.x * p2 - p0);
a.write(2 * i + 1, j, px.y * p2 - p1);
}
}
let svd = a.svd();
let v = svd.v();
let xh = v.col(3);
let w = xh[3];
if w.abs() < 1e-12 {
return None;
}
Some(Vec3F64::new(xh[0] / w, xh[1] / w, xh[2] / w))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sampson_correction_noop_on_exact_match() {
let f = synthetic_f();
let x1 = Vec2F64::new(100.0, 80.0);
let x1h = Vec3F64::new(x1.x, x1.y, 1.0);
let l = f * x1h;
let u = 120.0;
let v = -(l.x * u + l.z) / l.y;
let x2 = Vec2F64::new(u, v);
let (x1c, x2c) = correct_matches_sampson(&f, &x1, &x2);
let dx1 = (x1c.x - x1.x).hypot(x1c.y - x1.y);
let dx2 = (x2c.x - x2.x).hypot(x2c.y - x2.y);
assert!(dx1 < 1e-8, "x1 moved by {dx1} on exact-match input");
assert!(dx2 < 1e-8, "x2 moved by {dx2} on exact-match input");
}
#[test]
fn sampson_correction_reduces_residual() {
let f = synthetic_f();
let x1 = Vec2F64::new(100.0, 80.0);
let x1h = Vec3F64::new(x1.x, x1.y, 1.0);
let l = f * x1h;
let u = 120.0;
let v = -(l.x * u + l.z) / l.y;
let x2 = Vec2F64::new(u + 2.0, v - 1.5);
let before = sampson_dist(&f, &x1, &x2);
let (x1c, x2c) = correct_matches_sampson(&f, &x1, &x2);
let after = sampson_dist(&f, &x1c, &x2c);
assert!(
after < before * 1e-6,
"Sampson distance not driven near zero: before={before}, after={after}"
);
}
#[test]
fn n_view_dlt_recovers_point() {
let pt = Vec3F64::new(0.3, -0.2, 4.5);
let cams = synthetic_cameras_4();
let mut pixels = Vec::new();
let mut projs = Vec::new();
for cam in &cams {
let p = project(cam, &pt);
pixels.push(p);
projs.push(*cam);
}
let recovered = triangulate_n_view_linear(&pixels, &projs).expect("triangulation failed");
let err = (recovered.x - pt.x).powi(2)
+ (recovered.y - pt.y).powi(2)
+ (recovered.z - pt.z).powi(2);
assert!(err < 1e-8, "recovered point off by sqrt({err})");
}
#[test]
fn n_view_dlt_rejects_bad_input() {
let r = triangulate_n_view_linear(&[Vec2F64::new(0.0, 0.0)], &[]);
assert!(r.is_none());
}
fn synthetic_f() -> Mat3F64 {
let k = Mat3F64::from_cols(
Vec3F64::new(500.0, 0.0, 0.0),
Vec3F64::new(0.0, 500.0, 0.0),
Vec3F64::new(320.0, 240.0, 1.0),
);
let k_inv = k.inverse();
let angle = 0.1_f64;
let r = Mat3F64::from_cols(
Vec3F64::new(angle.cos(), 0.0, -angle.sin()),
Vec3F64::new(0.0, 1.0, 0.0),
Vec3F64::new(angle.sin(), 0.0, angle.cos()),
);
let t = Vec3F64::new(1.0, 0.0, 0.2);
let len = t.length();
let t = Vec3F64::new(t.x / len, t.y / len, t.z / len);
let tx = Mat3F64::from_cols(
Vec3F64::new(0.0, t.z, -t.y),
Vec3F64::new(-t.z, 0.0, t.x),
Vec3F64::new(t.y, -t.x, 0.0),
);
let e = tx * r;
k_inv.transpose() * e * k_inv
}
fn sampson_dist(f: &Mat3F64, x1: &Vec2F64, x2: &Vec2F64) -> f64 {
let x1h = Vec3F64::new(x1.x, x1.y, 1.0);
let x2h = Vec3F64::new(x2.x, x2.y, 1.0);
let fx1 = *f * x1h;
let ftx2 = f.transpose() * x2h;
let err = x2h.dot(fx1);
let denom = fx1.x * fx1.x + fx1.y * fx1.y + ftx2.x * ftx2.x + ftx2.y * ftx2.y;
if denom <= 1e-12 {
return err * err;
}
err * err / denom
}
fn synthetic_cameras_4() -> Vec<[f64; 12]> {
let mut out = Vec::new();
let baselines = [-0.4_f64, -0.1, 0.1, 0.5];
let fx = 600.0;
let fy = 600.0;
let cx = 320.0;
let cy = 240.0;
for &b in &baselines {
out.push([fx, 0.0, cx, -fx * b, 0.0, fy, cy, 0.0, 0.0, 0.0, 1.0, 0.0]);
}
out
}
fn project(cam: &[f64; 12], p: &Vec3F64) -> Vec2F64 {
let x = cam[0] * p.x + cam[1] * p.y + cam[2] * p.z + cam[3];
let y = cam[4] * p.x + cam[5] * p.y + cam[6] * p.z + cam[7];
let w = cam[8] * p.x + cam[9] * p.y + cam[10] * p.z + cam[11];
Vec2F64::new(x / w, y / w)
}
}