#![allow(clippy::needless_range_loop)]
use kornia_algebra::{Mat3F64, Vec2F64, Vec3F64};
#[inline(always)]
fn row20_add_assign(dst: &mut [f64; 20], src: &[f64; 20]) {
#[cfg(target_arch = "aarch64")]
unsafe {
use std::arch::aarch64::*;
let dp = dst.as_mut_ptr();
let sp = src.as_ptr();
let mut k = 0usize;
while k < 20 {
let a = vld1q_f64(dp.add(k));
let b = vld1q_f64(sp.add(k));
vst1q_f64(dp.add(k), vaddq_f64(a, b));
k += 2;
}
}
#[cfg(target_arch = "x86_64")]
{
if kornia_imgproc::simd::cpu_features().has_avx2 {
unsafe { row20_add_assign_avx2(dst, src) };
return;
}
}
#[cfg(not(target_arch = "aarch64"))]
{
for k in 0..20 {
dst[k] += src[k];
}
}
}
#[inline(always)]
fn row20_sub_assign(dst: &mut [f64; 20], src: &[f64; 20]) {
#[cfg(target_arch = "aarch64")]
unsafe {
use std::arch::aarch64::*;
let dp = dst.as_mut_ptr();
let sp = src.as_ptr();
let mut k = 0usize;
while k < 20 {
let a = vld1q_f64(dp.add(k));
let b = vld1q_f64(sp.add(k));
vst1q_f64(dp.add(k), vsubq_f64(a, b));
k += 2;
}
}
#[cfg(target_arch = "x86_64")]
{
if kornia_imgproc::simd::cpu_features().has_avx2 {
unsafe { row20_sub_assign_avx2(dst, src) };
return;
}
}
#[cfg(not(target_arch = "aarch64"))]
{
for k in 0..20 {
dst[k] -= src[k];
}
}
}
#[inline(always)]
fn row20_scale(src: &[f64; 20], s: f64) -> [f64; 20] {
let mut out = [0.0f64; 20];
#[cfg(target_arch = "aarch64")]
unsafe {
use std::arch::aarch64::*;
let op = out.as_mut_ptr();
let sp = src.as_ptr();
let ss = vdupq_n_f64(s);
let mut k = 0usize;
while k < 20 {
let a = vld1q_f64(sp.add(k));
vst1q_f64(op.add(k), vmulq_f64(a, ss));
k += 2;
}
}
#[cfg(target_arch = "x86_64")]
{
if kornia_imgproc::simd::cpu_features().has_avx2 {
unsafe { row20_scale_avx2(&mut out, src, s) };
return out;
}
}
#[cfg(not(target_arch = "aarch64"))]
{
for k in 0..20 {
out[k] = src[k] * s;
}
}
out
}
#[inline(always)]
fn row20_fma_sub(dst: &mut [f64; 20], src: &[f64; 20], factor: f64) {
#[cfg(target_arch = "aarch64")]
unsafe {
use std::arch::aarch64::*;
let dp = dst.as_mut_ptr();
let sp = src.as_ptr();
let neg_f = vdupq_n_f64(-factor);
let mut k = 0usize;
while k < 20 {
let d = vld1q_f64(dp.add(k));
let s = vld1q_f64(sp.add(k));
vst1q_f64(dp.add(k), vfmaq_f64(d, s, neg_f));
k += 2;
}
}
#[cfg(target_arch = "x86_64")]
{
if kornia_imgproc::simd::cpu_features().has_avx2 {
unsafe { row20_fma_sub_avx2(dst, src, factor) };
return;
}
}
#[cfg(not(target_arch = "aarch64"))]
{
for k in 0..20 {
dst[k] -= factor * src[k];
}
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn row20_add_assign_avx2(dst: &mut [f64; 20], src: &[f64; 20]) {
use std::arch::x86_64::*;
let dp = dst.as_mut_ptr();
let sp = src.as_ptr();
let mut k = 0usize;
while k < 20 {
let a = _mm256_loadu_pd(dp.add(k));
let b = _mm256_loadu_pd(sp.add(k));
_mm256_storeu_pd(dp.add(k), _mm256_add_pd(a, b));
k += 4;
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn row20_sub_assign_avx2(dst: &mut [f64; 20], src: &[f64; 20]) {
use std::arch::x86_64::*;
let dp = dst.as_mut_ptr();
let sp = src.as_ptr();
let mut k = 0usize;
while k < 20 {
let a = _mm256_loadu_pd(dp.add(k));
let b = _mm256_loadu_pd(sp.add(k));
_mm256_storeu_pd(dp.add(k), _mm256_sub_pd(a, b));
k += 4;
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn row20_scale_avx2(out: &mut [f64; 20], src: &[f64; 20], s: f64) {
use std::arch::x86_64::*;
let op = out.as_mut_ptr();
let sp = src.as_ptr();
let ss = _mm256_set1_pd(s);
let mut k = 0usize;
while k < 20 {
let a = _mm256_loadu_pd(sp.add(k));
_mm256_storeu_pd(op.add(k), _mm256_mul_pd(a, ss));
k += 4;
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2,fma")]
unsafe fn row20_fma_sub_avx2(dst: &mut [f64; 20], src: &[f64; 20], factor: f64) {
use std::arch::x86_64::*;
let dp = dst.as_mut_ptr();
let sp = src.as_ptr();
let fv = _mm256_set1_pd(factor);
let mut k = 0usize;
while k < 20 {
let d = _mm256_loadu_pd(dp.add(k));
let s = _mm256_loadu_pd(sp.add(k));
_mm256_storeu_pd(dp.add(k), _mm256_fnmadd_pd(fv, s, d));
k += 4;
}
}
#[inline(always)]
fn givens_row_pair_10(
h: &mut [[f64; 10]; 10],
row_k: usize,
start: usize,
end: usize,
c: f64,
s: f64,
) {
debug_assert!(row_k + 1 < 10 && end <= 10 && start <= end);
let (top, bot) = {
let (hi, lo) = h.split_at_mut(row_k + 1);
(&mut hi[row_k], &mut lo[0])
};
#[cfg(target_arch = "aarch64")]
unsafe {
use std::arch::aarch64::*;
let tp = top.as_mut_ptr();
let bp = bot.as_mut_ptr();
let cv = vdupq_n_f64(c);
let sv = vdupq_n_f64(s);
let neg_sv = vdupq_n_f64(-s);
let mut j = start;
while j + 2 <= end {
let x = vld1q_f64(tp.add(j));
let y = vld1q_f64(bp.add(j));
let nx = vfmaq_f64(vmulq_f64(x, cv), y, sv);
let ny = vfmaq_f64(vmulq_f64(y, cv), x, neg_sv);
vst1q_f64(tp.add(j), nx);
vst1q_f64(bp.add(j), ny);
j += 2;
}
while j < end {
let x = *tp.add(j);
let y = *bp.add(j);
*tp.add(j) = c * x + s * y;
*bp.add(j) = -s * x + c * y;
j += 1;
}
}
#[cfg(target_arch = "x86_64")]
{
if kornia_imgproc::simd::cpu_features().has_avx2 {
unsafe { givens_row_pair_10_avx2(top, bot, start, end, c, s) };
return;
}
}
#[cfg(not(target_arch = "aarch64"))]
{
for j in start..end {
let x = top[j];
let y = bot[j];
top[j] = c * x + s * y;
bot[j] = -s * x + c * y;
}
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2,fma")]
unsafe fn givens_row_pair_10_avx2(
top: &mut [f64],
bot: &mut [f64],
start: usize,
end: usize,
c: f64,
s: f64,
) {
use std::arch::x86_64::*;
let tp = top.as_mut_ptr();
let bp = bot.as_mut_ptr();
let cv = _mm256_set1_pd(c);
let sv = _mm256_set1_pd(s);
let neg_sv = _mm256_set1_pd(-s);
let mut j = start;
while j + 4 <= end {
let x = _mm256_loadu_pd(tp.add(j));
let y = _mm256_loadu_pd(bp.add(j));
let nx = _mm256_fmadd_pd(y, sv, _mm256_mul_pd(x, cv));
let ny = _mm256_fmadd_pd(x, neg_sv, _mm256_mul_pd(y, cv));
_mm256_storeu_pd(tp.add(j), nx);
_mm256_storeu_pd(bp.add(j), ny);
j += 4;
}
while j < end {
let x = *tp.add(j);
let y = *bp.add(j);
*tp.add(j) = c * x + s * y;
*bp.add(j) = -s * x + c * y;
j += 1;
}
}
const DEGREES: [[u8; 3]; 20] = [
[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1], [2, 0, 0], [1, 1, 0], [1, 0, 1], [0, 2, 0], [0, 1, 1], [0, 0, 2], [3, 0, 0], [2, 1, 0], [2, 0, 1], [1, 2, 0], [1, 1, 1], [1, 0, 2], [0, 3, 0], [0, 2, 1], [0, 1, 2], [0, 0, 3], ];
#[derive(Clone, Copy, Debug, Default)]
struct Poly3D {
c: [f64; 20],
}
impl Poly3D {
const ZERO: Poly3D = Poly3D { c: [0.0; 20] };
fn linear(a: f64, b: f64, c: f64, d: f64) -> Self {
let mut p = Self::ZERO;
p.c[0] = d;
p.c[1] = a;
p.c[2] = b;
p.c[3] = c;
p
}
#[inline(always)]
fn add_assign(&mut self, other: &Poly3D) {
row20_add_assign(&mut self.c, &other.c);
}
#[inline(always)]
fn sub_assign(&mut self, other: &Poly3D) {
row20_sub_assign(&mut self.c, &other.c);
}
#[inline(always)]
fn scale(&self, s: f64) -> Poly3D {
Poly3D {
c: row20_scale(&self.c, s),
}
}
fn mul(&self, other: &Poly3D) -> Poly3D {
let mut out = Poly3D::ZERO;
for i in 0..20 {
let a = self.c[i];
if a == 0.0 {
continue;
}
let di = DEGREES[i];
for j in 0..20 {
let b = other.c[j];
if b == 0.0 {
continue;
}
let dj = DEGREES[j];
let d = [di[0] + dj[0], di[1] + dj[1], di[2] + dj[2]];
if d[0] + d[1] + d[2] > 3 {
continue;
}
let mut k = 0;
while k < 20 && DEGREES[k] != d {
k += 1;
}
if k < 20 {
out.c[k] += a * b;
}
}
}
out
}
}
fn null_space_5x9(x1: &[Vec2F64; 5], x2: &[Vec2F64; 5]) -> Option<[[f64; 9]; 4]> {
let mut mat = faer::Mat::<f64>::zeros(5, 9);
for i in 0..5 {
let (u1, v1) = (x1[i].x, x1[i].y);
let (u2, v2) = (x2[i].x, x2[i].y);
mat.write(i, 0, u1 * u2);
mat.write(i, 1, v1 * u2);
mat.write(i, 2, u2);
mat.write(i, 3, u1 * v2);
mat.write(i, 4, v1 * v2);
mat.write(i, 5, v2);
mat.write(i, 6, u1);
mat.write(i, 7, v1);
mat.write(i, 8, 1.0);
}
let svd = mat.svd();
let v = svd.v();
let s = svd.s_diagonal();
let s4 = s[4].abs();
let s0 = s[0].abs().max(1e-30);
if s4 / s0 < 1e-8 {
return None;
}
let mut basis = [[0.0f64; 9]; 4];
for (k, col) in (5..9).enumerate() {
for row in 0..9 {
basis[k][row] = v[(row, col)];
}
}
Some(basis)
}
#[inline]
fn vec9_to_mat3(e: &[f64; 9]) -> Mat3F64 {
Mat3F64::from_cols(
Vec3F64::new(e[0], e[3], e[6]),
Vec3F64::new(e[1], e[4], e[7]),
Vec3F64::new(e[2], e[5], e[8]),
)
}
fn build_constraint_matrix(null_basis: &[[f64; 9]; 4]) -> [[f64; 20]; 10] {
let mut e_poly = [[Poly3D::ZERO; 3]; 3];
for i in 0..3 {
for j in 0..3 {
let idx = 3 * i + j;
e_poly[i][j] = Poly3D::linear(
null_basis[0][idx],
null_basis[1][idx],
null_basis[2][idx],
null_basis[3][idx],
);
}
}
let mut eet = [[Poly3D::ZERO; 3]; 3];
for i in 0..3 {
for j in 0..3 {
let mut s = Poly3D::ZERO;
for k in 0..3 {
let prod = e_poly[i][k].mul(&e_poly[j][k]);
s.add_assign(&prod);
}
eet[i][j] = s;
}
}
let mut trace = Poly3D::ZERO;
for i in 0..3 {
trace.add_assign(&eet[i][i]);
}
let half_trace = trace.scale(0.5);
let mut b = [[Poly3D::ZERO; 3]; 3];
for i in 0..3 {
for j in 0..3 {
let mut s = Poly3D::ZERO;
for k in 0..3 {
let mut m = eet[i][k];
if i == k {
m.sub_assign(&half_trace);
}
let prod = m.mul(&e_poly[k][j]);
s.add_assign(&prod);
}
b[i][j] = s;
}
}
let m01 = e_poly[1][1].mul(&e_poly[2][2]);
let m02 = e_poly[1][2].mul(&e_poly[2][1]);
let mut minor0 = m01;
minor0.sub_assign(&m02);
let m11 = e_poly[1][0].mul(&e_poly[2][2]);
let m12 = e_poly[1][2].mul(&e_poly[2][0]);
let mut minor1 = m11;
minor1.sub_assign(&m12);
let m21 = e_poly[1][0].mul(&e_poly[2][1]);
let m22 = e_poly[1][1].mul(&e_poly[2][0]);
let mut minor2 = m21;
minor2.sub_assign(&m22);
let t0 = e_poly[0][0].mul(&minor0);
let t1 = e_poly[0][1].mul(&minor1);
let t2 = e_poly[0][2].mul(&minor2);
let mut det = t0;
det.sub_assign(&t1);
det.add_assign(&t2);
let mut mat = [[0.0f64; 20]; 10];
for k in 0..20 {
mat[0][k] = det.c[k];
}
for i in 0..3 {
for j in 0..3 {
let row = 1 + 3 * i + j;
for k in 0..20 {
mat[row][k] = b[i][j].c[k];
}
}
}
mat
}
fn gauss_jordan_eliminate_deg3(m: &mut [[f64; 20]; 10]) -> bool {
for i in 0..10 {
let col = 10 + i;
let mut piv = i;
let mut piv_abs = m[i][col].abs();
for r in (i + 1)..10 {
let a = m[r][col].abs();
if a > piv_abs {
piv_abs = a;
piv = r;
}
}
if piv_abs < 1e-12 {
return false;
}
if piv != i {
m.swap(i, piv);
}
let inv = 1.0 / m[i][col];
m[i] = row20_scale(&m[i], inv);
let pivot = m[i];
for r in 0..10 {
if r == i {
continue;
}
let factor = m[r][col];
if factor == 0.0 {
continue;
}
row20_fma_sub(&mut m[r], &pivot, factor);
}
}
true
}
fn assemble_e(null_basis: &[[f64; 9]; 4], x: f64, y: f64, z: f64) -> Mat3F64 {
let mut e = [0.0f64; 9];
for k in 0..9 {
e[k] =
x * null_basis[0][k] + y * null_basis[1][k] + z * null_basis[2][k] + null_basis[3][k];
}
vec9_to_mat3(&e)
}
fn build_action_matrix(cm_reduced: &[[f64; 20]; 10]) -> [[f64; 10]; 10] {
let mut mz = [[0.0f64; 10]; 10];
mz[0][3] = 1.0; mz[1][6] = 1.0; mz[2][8] = 1.0; mz[3][9] = 1.0; let reduction_row: [usize; 6] = [2, 4, 5, 7, 8, 9];
for (k, &r) in reduction_row.iter().enumerate() {
let row = 4 + k;
for j in 0..10 {
mz[row][j] = -cm_reduced[r][j];
}
}
mz
}
fn hessenberg_reduce_10(a: &mut [[f64; 10]; 10]) {
for k in 0..8 {
let mut sigma = 0.0f64;
for i in (k + 2)..10 {
sigma += a[i][k] * a[i][k];
}
if sigma < 1e-28 {
continue;
}
let alpha = a[k + 1][k];
let mu = (alpha * alpha + sigma).sqrt();
let d = if alpha >= 0.0 { alpha + mu } else { alpha - mu };
let mut v = [0.0f64; 10];
v[k + 1] = 1.0;
let inv_d = 1.0 / d;
for i in (k + 2)..10 {
v[i] = a[i][k] * inv_d;
}
let beta = 2.0 * d * d / (d * d + sigma);
for j in k..10 {
let mut dot = a[k + 1][j];
for i in (k + 2)..10 {
dot += v[i] * a[i][j];
}
dot *= beta;
a[k + 1][j] -= dot;
for i in (k + 2)..10 {
a[i][j] -= dot * v[i];
}
}
for i in 0..10 {
let mut dot = a[i][k + 1];
for j in (k + 2)..10 {
dot += v[j] * a[i][j];
}
dot *= beta;
a[i][k + 1] -= dot;
for j in (k + 2)..10 {
a[i][j] -= dot * v[j];
}
}
}
}
fn eigs_2x2(a: f64, b: f64, c: f64, d: f64) -> ((f64, f64), (f64, f64)) {
let tr = a + d;
let det = a * d - b * c;
let disc = tr * tr / 4.0 - det;
if disc >= 0.0 {
let s = disc.sqrt();
((tr / 2.0 + s, 0.0), (tr / 2.0 - s, 0.0))
} else {
let s = (-disc).sqrt();
((tr / 2.0, s), (tr / 2.0, -s))
}
}
fn qr_step_shifted(h: &mut [[f64; 10]; 10], n: usize) {
if n < 2 {
return;
}
let hnn = h[n - 1][n - 1];
let p = (h[n - 2][n - 2] - hnn) * 0.5;
let q = h[n - 1][n - 2] * h[n - 2][n - 1];
let disc = p * p + q;
let shift = if disc >= 0.0 {
let r = disc.sqrt();
let denom = if p >= 0.0 { p + r } else { p - r };
if denom.abs() < 1e-300 {
hnn
} else {
hnn - q / denom
}
} else {
hnn - p
};
for i in 0..n {
h[i][i] -= shift;
}
let mut cs = [(0.0f64, 0.0f64); 9];
for k in 0..(n - 1) {
let a = h[k][k];
let b = h[k + 1][k];
let r = (a * a + b * b).sqrt();
let (c, s) = if r < 1e-300 {
(1.0, 0.0)
} else {
(a / r, b / r)
};
cs[k] = (c, s);
givens_row_pair_10(h, k, k, n, c, s);
}
for k in 0..(n - 1) {
let (c, s) = cs[k];
let lim = (k + 2).min(n);
for i in 0..lim {
let x = h[i][k];
let y = h[i][k + 1];
h[i][k] = c * x + s * y;
h[i][k + 1] = -s * x + c * y;
}
}
for i in 0..n {
h[i][i] += shift;
}
}
fn eigenvalues_10(a: &[[f64; 10]; 10]) -> [(f64, f64); 10] {
let mut h = *a;
hessenberg_reduce_10(&mut h);
let mut eigs = [(0.0f64, 0.0f64); 10];
let mut found = 0;
let mut n = 10usize; let mut iters = 0usize;
const MAX_ITERS: usize = 500;
while n > 0 && iters < MAX_ITERS {
iters += 1;
let mut split = 0usize;
for k in (1..n).rev() {
let sub = h[k][k - 1].abs();
let diag = h[k - 1][k - 1].abs() + h[k][k].abs();
if sub <= 1e-13 * diag.max(1e-300) {
split = k;
break;
}
}
if split == n - 1 {
eigs[found] = (h[n - 1][n - 1], 0.0);
found += 1;
n -= 1;
continue;
}
if split == n - 2 {
let (e1, e2) = eigs_2x2(
h[n - 2][n - 2],
h[n - 2][n - 1],
h[n - 1][n - 2],
h[n - 1][n - 1],
);
eigs[found] = e1;
found += 1;
eigs[found] = e2;
found += 1;
n -= 2;
continue;
}
qr_step_shifted(&mut h, n);
}
while n > 0 && found < 10 {
if n == 1 {
eigs[found] = (h[0][0], 0.0);
found += 1;
n -= 1;
} else {
let (e1, e2) = eigs_2x2(
h[n - 2][n - 2],
h[n - 2][n - 1],
h[n - 1][n - 2],
h[n - 1][n - 1],
);
eigs[found] = e1;
found += 1;
if found < 10 {
eigs[found] = e2;
found += 1;
}
n = n.saturating_sub(2);
}
}
eigs
}
fn real_eigenvalues_10(a: &[[f64; 10]; 10]) -> Vec<f64> {
let all = eigenvalues_10(a);
let mut out = Vec::new();
for (re, im) in all.iter() {
if im.abs() < 1e-8 * (1.0 + re.abs()) {
out.push(*re);
}
}
out
}
fn solve_9x9(mut a: [[f64; 9]; 9], mut b: [f64; 9]) -> Option<[f64; 9]> {
for k in 0..9 {
let mut piv = k;
let mut piv_abs = a[k][k].abs();
for r in (k + 1)..9 {
let v = a[r][k].abs();
if v > piv_abs {
piv_abs = v;
piv = r;
}
}
if piv_abs < 1e-12 {
return None;
}
if piv != k {
a.swap(k, piv);
b.swap(k, piv);
}
let inv = 1.0 / a[k][k];
for r in (k + 1)..9 {
let f = a[r][k] * inv;
if f == 0.0 {
continue;
}
for c in k..9 {
a[r][c] -= f * a[k][c];
}
b[r] -= f * b[k];
}
}
let mut x = [0.0f64; 9];
for k in (0..9).rev() {
let mut s = b[k];
for c in (k + 1)..9 {
s -= a[k][c] * x[c];
}
x[k] = s / a[k][k];
}
Some(x)
}
fn back_substitute(mz: &[[f64; 10]; 10], z: f64) -> Option<(f64, f64)> {
let mut a = [[0.0f64; 9]; 9];
let mut b = [0.0f64; 9];
for r in 1..10 {
for c in 1..10 {
let v = if r == c { mz[r][c] - z } else { mz[r][c] };
a[r - 1][c - 1] = v;
}
b[r - 1] = -mz[r][0];
}
let v = solve_9x9(a, b)?;
Some((v[0], v[1]))
}
pub fn essential_5pt(x1_norm: &[Vec2F64; 5], x2_norm: &[Vec2F64; 5]) -> Vec<Mat3F64> {
let basis = match null_space_5x9(x1_norm, x2_norm) {
Some(b) => b,
None => return Vec::new(),
};
let mut cm = build_constraint_matrix(&basis);
if !gauss_jordan_eliminate_deg3(&mut cm) {
return Vec::new();
}
let mz = build_action_matrix(&cm);
let real_zs = real_eigenvalues_10(&mz);
let mut out = Vec::with_capacity(real_zs.len());
for z in real_zs {
if let Some((x, y)) = back_substitute(&mz, z) {
out.push(assemble_e(&basis, x, y, z));
}
}
out
}
#[cfg(test)]
pub(crate) fn __test_null_space_5x9(x1: &[Vec2F64; 5], x2: &[Vec2F64; 5]) -> Option<[[f64; 9]; 4]> {
null_space_5x9(x1, x2)
}
#[cfg(test)]
pub(crate) fn __test_build_constraint_matrix(null_basis: &[[f64; 9]; 4]) -> [[f64; 20]; 10] {
build_constraint_matrix(null_basis)
}
#[cfg(test)]
pub(crate) fn __test_gauss_jordan_eliminate_deg3(m: &mut [[f64; 20]; 10]) -> bool {
gauss_jordan_eliminate_deg3(m)
}
#[cfg(test)]
mod tests {
use super::*;
fn skew(t: Vec3F64) -> Mat3F64 {
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),
)
}
fn project_normalized(p: Vec3F64) -> Vec2F64 {
Vec2F64::new(p.x / p.z, p.y / p.z)
}
fn synthetic_sample(r: Mat3F64, t: Vec3F64) -> ([Vec2F64; 5], [Vec2F64; 5]) {
let pts = [
Vec3F64::new(-0.5, -0.3, 4.0),
Vec3F64::new(0.4, -0.2, 3.5),
Vec3F64::new(-0.3, 0.5, 5.0),
Vec3F64::new(0.6, 0.4, 4.5),
Vec3F64::new(-0.1, -0.6, 3.0),
];
let mut x1 = [Vec2F64::ZERO; 5];
let mut x2 = [Vec2F64::ZERO; 5];
for (i, p) in pts.iter().enumerate() {
x1[i] = project_normalized(*p);
x2[i] = project_normalized(r * *p + t);
}
(x1, x2)
}
#[test]
fn test_null_space_5x9_kills_input_rows() {
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 (x1, x2) = synthetic_sample(r, t);
let basis = __test_null_space_5x9(&x1, &x2).expect("null-space must exist");
for (k, vec9) in basis.iter().enumerate() {
let e = vec9_to_mat3(vec9);
for i in 0..5 {
let x1h = Vec3F64::new(x1[i].x, x1[i].y, 1.0);
let x2h = Vec3F64::new(x2[i].x, x2[i].y, 1.0);
let r = x2h.dot(e * x1h);
assert!(
r.abs() < 1e-8,
"basis vec {k} fails epipolar on pt {i}: |x2ᵀ E x1| = {r:.3e}"
);
}
}
}
#[test]
fn test_null_space_5x9_contains_ground_truth_e() {
let angle = 0.15_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(0.8, 0.1, 0.3).normalize();
let (x1, x2) = synthetic_sample(r, t);
let basis = __test_null_space_5x9(&x1, &x2).expect("null-space must exist");
let e_true = skew(t) * r;
let cols = e_true.to_cols_array();
let e_vec = [
cols[0], cols[3], cols[6], cols[1], cols[4], cols[7], cols[2], cols[5], cols[8],
];
let mut coeffs = [0.0; 4];
for k in 0..4 {
for row in 0..9 {
coeffs[k] += basis[k][row] * e_vec[row];
}
}
let mut recon = [0.0; 9];
for k in 0..4 {
for row in 0..9 {
recon[row] += coeffs[k] * basis[k][row];
}
}
let mut residual = 0.0;
for row in 0..9 {
residual += (recon[row] - e_vec[row]).powi(2);
}
residual = residual.sqrt();
let e_norm: f64 = e_vec.iter().map(|v| v * v).sum::<f64>().sqrt();
assert!(
residual < 1e-8 * e_norm.max(1e-12),
"E_true should lie in null-space span; residual = {residual:.3e}, ‖E‖ = {e_norm:.3e}"
);
}
#[test]
fn test_constraint_matrix_vanishes_on_true_e() {
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(0.9, 0.05, 0.4).normalize();
let (x1, x2) = synthetic_sample(r, t);
let basis = __test_null_space_5x9(&x1, &x2).expect("null-space must exist");
let e_true = skew(t) * r;
let cols = e_true.to_cols_array();
let e_vec = [
cols[0], cols[3], cols[6], cols[1], cols[4], cols[7], cols[2], cols[5], cols[8],
];
let mut coeffs = [0.0; 4];
for k in 0..4 {
for row in 0..9 {
coeffs[k] += basis[k][row] * e_vec[row];
}
}
assert!(
coeffs[3].abs() > 1e-6,
"test setup: w coefficient is zero, pick a different E"
);
let inv_w = 1.0 / coeffs[3];
let x = coeffs[0] * inv_w;
let y = coeffs[1] * inv_w;
let z = coeffs[2] * inv_w;
let cm = __test_build_constraint_matrix(&basis);
let mut monomials = [0.0f64; 20];
monomials[0] = 1.0;
monomials[1] = x;
monomials[2] = y;
monomials[3] = z;
monomials[4] = x * x;
monomials[5] = x * y;
monomials[6] = x * z;
monomials[7] = y * y;
monomials[8] = y * z;
monomials[9] = z * z;
monomials[10] = x * x * x;
monomials[11] = x * x * y;
monomials[12] = x * x * z;
monomials[13] = x * y * y;
monomials[14] = x * y * z;
monomials[15] = x * z * z;
monomials[16] = y * y * y;
monomials[17] = y * y * z;
monomials[18] = y * z * z;
monomials[19] = z * z * z;
for row_idx in 0..10 {
let mut s = 0.0;
for k in 0..20 {
s += cm[row_idx][k] * monomials[k];
}
let scale = coeffs[3].abs().max(1.0).powi(3);
assert!(
s.abs() < 1e-6 * scale,
"constraint row {row_idx} doesn't vanish at E_true: residual = {s:.3e}"
);
}
}
#[test]
fn test_gauss_jordan_produces_identity_on_right_block() {
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(0.9, 0.05, 0.4).normalize();
let (x1, x2) = synthetic_sample(r, t);
let basis = __test_null_space_5x9(&x1, &x2).expect("null-space must exist");
let mut cm = __test_build_constraint_matrix(&basis);
let ok = __test_gauss_jordan_eliminate_deg3(&mut cm);
assert!(
ok,
"Gauss-Jordan should not fail on a non-degenerate sample"
);
for i in 0..10 {
for j in 0..10 {
let expected = if i == j { 1.0 } else { 0.0 };
let val = cm[i][10 + j];
assert!(
(val - expected).abs() < 1e-9,
"GJ right block [{i}][{j}] = {val} (expected {expected})"
);
}
}
}
#[test]
fn test_essential_5pt_recovers_true_e() {
let angle = 0.15_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(0.8, 0.1, 0.3).normalize();
let (x1, x2) = synthetic_sample(r, t);
let candidates = essential_5pt(&x1, &x2);
assert!(
!candidates.is_empty(),
"solver must return ≥1 candidate on a non-degenerate sample"
);
let e_true = skew(t) * r;
let flat_true = e_true.to_cols_array();
let norm_true: f64 = flat_true.iter().map(|v| v * v).sum::<f64>().sqrt();
let e_true_unit: [f64; 9] = core::array::from_fn(|k| flat_true[k] / norm_true);
let mut best_err = f64::INFINITY;
for cand in &candidates {
let flat = cand.to_cols_array();
let norm: f64 = flat.iter().map(|v| v * v).sum::<f64>().sqrt();
if norm < 1e-12 {
continue;
}
let mut err_pos = 0.0f64;
let mut err_neg = 0.0f64;
for k in 0..9 {
let u = flat[k] / norm;
err_pos += (u - e_true_unit[k]).powi(2);
err_neg += (u + e_true_unit[k]).powi(2);
}
let err = err_pos.min(err_neg).sqrt();
if err < best_err {
best_err = err;
}
}
assert!(
best_err < 1e-4,
"no candidate close to E_true: best Frobenius dist (unit-norm, ±sign) = {best_err:.3e}"
);
}
#[test]
fn test_essential_5pt_candidates_satisfy_epipolar() {
let angle = 0.12_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(0.7, -0.2, 0.4).normalize();
let (x1, x2) = synthetic_sample(r, t);
let candidates = essential_5pt(&x1, &x2);
assert!(!candidates.is_empty());
for (idx, e) in candidates.iter().enumerate() {
for i in 0..5 {
let x1h = Vec3F64::new(x1[i].x, x1[i].y, 1.0);
let x2h = Vec3F64::new(x2[i].x, x2[i].y, 1.0);
let r_val = x2h.dot(*e * x1h);
assert!(
r_val.abs() < 1e-6,
"candidate {idx} fails epipolar on pt {i}: |x2ᵀ E x1| = {r_val:.3e}"
);
}
}
}
}