use kornia_algebra::{Mat3F64, Vec2F64, Vec3F64};
use crate::pose::{homography_4pt2d, homography_dlt};
use crate::ransac::{Estimator, Match2d2d};
#[derive(Debug, Clone, Copy, Default)]
pub struct HomographyEstimator;
impl Estimator for HomographyEstimator {
type Model = Mat3F64;
type Sample = Match2d2d;
const SAMPLE_SIZE: usize = 4;
fn fit(&self, samples: &[Self::Sample], out: &mut Vec<Self::Model>) {
if samples.len() < Self::SAMPLE_SIZE {
return;
}
let x1 = [
[samples[0].x1.x, samples[0].x1.y],
[samples[1].x1.x, samples[1].x1.y],
[samples[2].x1.x, samples[2].x1.y],
[samples[3].x1.x, samples[3].x1.y],
];
let x2 = [
[samples[0].x2.x, samples[0].x2.y],
[samples[1].x2.x, samples[1].x2.y],
[samples[2].x2.x, samples[2].x2.y],
[samples[3].x2.x, samples[3].x2.y],
];
let mut h = [[0.0f64; 3]; 3];
if homography_4pt2d(&x1, &x2, &mut h).is_ok() {
out.push(Mat3F64::from_cols(
Vec3F64::new(h[0][0], h[1][0], h[2][0]),
Vec3F64::new(h[0][1], h[1][1], h[2][1]),
Vec3F64::new(h[0][2], h[1][2], h[2][2]),
));
}
}
#[inline]
fn residual(&self, model: &Self::Model, sample: &Self::Sample) -> f64 {
let x1h = Vec3F64::new(sample.x1.x, sample.x1.y, 1.0);
let mapped = *model * x1h;
if mapped.z.abs() < 1e-12 {
return f64::INFINITY;
}
let dx = mapped.x / mapped.z - sample.x2.x;
let dy = mapped.y / mapped.z - sample.x2.y;
dx * dx + dy * dy
}
fn refit(&self, inliers: &[Self::Sample], out: &mut Vec<Self::Model>) {
let n = inliers.len();
if n < Self::SAMPLE_SIZE {
return;
}
if n == Self::SAMPLE_SIZE {
self.fit(inliers, out);
return;
}
let x1: Vec<Vec2F64> = inliers.iter().map(|s| s.x1).collect();
let x2: Vec<Vec2F64> = inliers.iter().map(|s| s.x2).collect();
if let Ok(h) = homography_dlt(&x1, &x2) {
out.push(h);
}
}
fn residual_batch(&self, model: &Self::Model, samples: &[Self::Sample], out: &mut [f64]) {
debug_assert_eq!(out.len(), samples.len());
let h = pack_h(model);
#[cfg(target_arch = "aarch64")]
unsafe {
let idx = transfer_error_batch_neon(h, samples, out);
transfer_error_batch_scalar_tail(h, samples, out, idx);
return;
}
#[cfg(target_arch = "x86_64")]
if kornia_imgproc::simd::cpu_features().has_avx2 {
unsafe {
let idx = transfer_error_batch_avx2(h, samples, out);
transfer_error_batch_scalar_tail(h, samples, out, idx);
}
return;
}
#[allow(unreachable_code)]
transfer_error_batch_scalar(h, samples, out);
}
}
type HPacked = (f64, f64, f64, f64, f64, f64, f64, f64, f64);
#[inline(always)]
fn pack_h(model: &Mat3F64) -> HPacked {
(
model.x_axis.x,
model.y_axis.x,
model.z_axis.x,
model.x_axis.y,
model.y_axis.y,
model.z_axis.y,
model.x_axis.z,
model.y_axis.z,
model.z_axis.z,
)
}
#[inline]
fn transfer_error_batch_scalar(h: HPacked, samples: &[Match2d2d], out: &mut [f64]) {
let (h00, h01, h02, h10, h11, h12, h20, h21, h22) = h;
for (i, s) in samples.iter().enumerate() {
let (x, y) = (s.x1.x, s.x1.y);
let mx = h00 * x + h01 * y + h02;
let my = h10 * x + h11 * y + h12;
let mz = h20 * x + h21 * y + h22;
if mz.abs() < 1e-12 {
out[i] = f64::INFINITY;
continue;
}
let inv_z = 1.0 / mz;
let dx = mx * inv_z - s.x2.x;
let dy = my * inv_z - s.x2.y;
out[i] = dx * dx + dy * dy;
}
}
#[inline]
fn transfer_error_batch_scalar_tail(
h: HPacked,
samples: &[Match2d2d],
out: &mut [f64],
start: usize,
) {
if start >= samples.len() {
return;
}
transfer_error_batch_scalar(h, &samples[start..], &mut out[start..]);
}
#[cfg(target_arch = "aarch64")]
#[target_feature(enable = "neon")]
#[inline]
unsafe fn transfer_error_batch_neon(h: HPacked, samples: &[Match2d2d], out: &mut [f64]) -> usize {
use std::arch::aarch64::*;
let (h00, h01, h02, h10, h11, h12, h20, h21, h22) = h;
let h00v = vdupq_n_f64(h00);
let h01v = vdupq_n_f64(h01);
let h02v = vdupq_n_f64(h02);
let h10v = vdupq_n_f64(h10);
let h11v = vdupq_n_f64(h11);
let h12v = vdupq_n_f64(h12);
let h20v = vdupq_n_f64(h20);
let h21v = vdupq_n_f64(h21);
let h22v = vdupq_n_f64(h22);
let eps = vdupq_n_f64(1e-12);
let inf = vdupq_n_f64(f64::INFINITY);
let n = samples.len();
let mut idx = 0usize;
while idx + 2 <= n {
let base = samples.as_ptr().add(idx) as *const f64;
let lanes = vld4q_f64(base);
let x1 = lanes.0;
let y1 = lanes.1;
let x2 = lanes.2;
let y2 = lanes.3;
let mx = vfmaq_f64(vfmaq_f64(h02v, x1, h00v), y1, h01v);
let my = vfmaq_f64(vfmaq_f64(h12v, x1, h10v), y1, h11v);
let mz = vfmaq_f64(vfmaq_f64(h22v, x1, h20v), y1, h21v);
let abs_mz = vabsq_f64(mz);
let mz_ok = vcgtq_f64(abs_mz, eps);
let safe_mz = vbslq_f64(mz_ok, mz, vdupq_n_f64(1.0));
let inv_z = vdivq_f64(vdupq_n_f64(1.0), safe_mz);
let dx = vsubq_f64(vmulq_f64(mx, inv_z), x2);
let dy = vsubq_f64(vmulq_f64(my, inv_z), y2);
let dd = vfmaq_f64(vmulq_f64(dx, dx), dy, dy);
let result = vbslq_f64(mz_ok, dd, inf);
vst1q_f64(out.as_mut_ptr().add(idx), result);
idx += 2;
}
idx
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2,fma")]
#[inline]
unsafe fn transfer_error_batch_avx2(h: HPacked, samples: &[Match2d2d], out: &mut [f64]) -> usize {
use std::arch::x86_64::*;
let (h00, h01, h02, h10, h11, h12, h20, h21, h22) = h;
let h00v = _mm256_set1_pd(h00);
let h01v = _mm256_set1_pd(h01);
let h02v = _mm256_set1_pd(h02);
let h10v = _mm256_set1_pd(h10);
let h11v = _mm256_set1_pd(h11);
let h12v = _mm256_set1_pd(h12);
let h20v = _mm256_set1_pd(h20);
let h21v = _mm256_set1_pd(h21);
let h22v = _mm256_set1_pd(h22);
let one = _mm256_set1_pd(1.0);
let eps = _mm256_set1_pd(1e-12);
let inf = _mm256_set1_pd(f64::INFINITY);
let abs_mask = _mm256_castsi256_pd(_mm256_set1_epi64x(0x7fff_ffff_ffff_ffffi64));
let n = samples.len();
let mut idx = 0usize;
while idx + 4 <= n {
let base = samples.as_ptr().add(idx) as *const f64;
let a = _mm256_loadu_pd(base);
let b = _mm256_loadu_pd(base.add(4));
let c = _mm256_loadu_pd(base.add(8));
let d = _mm256_loadu_pd(base.add(12));
let t0 = _mm256_unpacklo_pd(a, b);
let t1 = _mm256_unpackhi_pd(a, b);
let t2 = _mm256_unpacklo_pd(c, d);
let t3 = _mm256_unpackhi_pd(c, d);
let x1 = _mm256_permute2f128_pd::<0x20>(t0, t2);
let y1 = _mm256_permute2f128_pd::<0x20>(t1, t3);
let x2 = _mm256_permute2f128_pd::<0x31>(t0, t2);
let y2 = _mm256_permute2f128_pd::<0x31>(t1, t3);
let mx = _mm256_fmadd_pd(x1, h00v, _mm256_fmadd_pd(y1, h01v, h02v));
let my = _mm256_fmadd_pd(x1, h10v, _mm256_fmadd_pd(y1, h11v, h12v));
let mz = _mm256_fmadd_pd(x1, h20v, _mm256_fmadd_pd(y1, h21v, h22v));
let abs_mz = _mm256_and_pd(mz, abs_mask);
let mz_ok = _mm256_cmp_pd::<_CMP_GT_OQ>(abs_mz, eps);
let safe_mz = _mm256_blendv_pd(one, mz, mz_ok);
let inv_z = _mm256_div_pd(one, safe_mz);
let dx = _mm256_sub_pd(_mm256_mul_pd(mx, inv_z), x2);
let dy = _mm256_sub_pd(_mm256_mul_pd(my, inv_z), y2);
let dd = _mm256_fmadd_pd(dy, dy, _mm256_mul_pd(dx, dx));
let result = _mm256_blendv_pd(inf, dd, mz_ok);
_mm256_storeu_pd(out.as_mut_ptr().add(idx), result);
idx += 4;
}
idx
}
#[cfg(test)]
mod tests {
use super::*;
use kornia_algebra::Vec2F64;
#[test]
fn fits_and_scores_clean_correspondences() {
let h_true = Mat3F64::from_cols(
Vec3F64::new(1.2, 0.05, 0.0),
Vec3F64::new(0.03, 0.95, 0.0),
Vec3F64::new(7.0, -3.0, 1.0),
);
let pts = [
Vec2F64::new(10.0, 20.0),
Vec2F64::new(100.0, 30.0),
Vec2F64::new(80.0, 200.0),
Vec2F64::new(20.0, 180.0),
];
let matches: Vec<Match2d2d> = pts
.iter()
.map(|p| {
let mapped = h_true * Vec3F64::new(p.x, p.y, 1.0);
let mp = Vec2F64::new(mapped.x / mapped.z, mapped.y / mapped.z);
Match2d2d::new(*p, mp)
})
.collect();
let est = HomographyEstimator;
let mut models = Vec::new();
est.fit(&matches, &mut models);
assert_eq!(models.len(), 1);
let h = models[0];
for m in &matches {
let r = est.residual(&h, m);
assert!(r < 1e-10, "transfer error too large: {r}");
}
}
#[test]
fn batch_dispatcher_matches_scalar_residual() {
let h_true = Mat3F64::from_cols(
Vec3F64::new(1.2, 0.05, 0.0),
Vec3F64::new(0.03, 0.95, 0.0),
Vec3F64::new(7.0, -3.0, 1.0),
);
let pts = [
Vec2F64::new(10.0, 20.0),
Vec2F64::new(100.0, 30.0),
Vec2F64::new(80.0, 200.0),
Vec2F64::new(20.0, 180.0),
Vec2F64::new(-50.0, 40.0),
Vec2F64::new(300.0, -200.0),
Vec2F64::new(0.0, 0.0),
];
let matches: Vec<Match2d2d> = pts
.iter()
.map(|p| Match2d2d::new(*p, Vec2F64::new(0.0, 0.0)))
.collect();
assert_eq!(matches.len() % 2, 1, "odd N to exercise scalar tail");
let est = HomographyEstimator;
let mut batched = vec![0.0f64; matches.len()];
est.residual_batch(&h_true, &matches, &mut batched);
for (i, m) in matches.iter().enumerate() {
let scalar = est.residual(&h_true, m);
if scalar.is_finite() {
assert!(
(batched[i] - scalar).abs() < 1e-12 * scalar.max(1.0).abs(),
"lane {i}: batched={} scalar={}",
batched[i],
scalar
);
} else {
assert!(
!batched[i].is_finite(),
"lane {i}: scalar=∞ but batched={}",
batched[i]
);
}
}
}
#[test]
fn under_min_samples_yields_no_model() {
let est = HomographyEstimator;
let mut models = Vec::new();
est.fit(&[], &mut models);
assert!(models.is_empty());
}
}