use crate::error::{Error, Result};
use crate::geometry::Point;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Homography {
m: [f64; 9],
}
impl Homography {
pub fn identity() -> Self {
Homography {
m: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
}
}
pub fn from_matrix(m: [f64; 9]) -> Self {
Homography { m }
}
pub fn matrix(&self) -> [f64; 9] {
self.m
}
pub fn from_correspondences(src: [Point; 4], dst: [Point; 4]) -> Result<Self> {
let mut a = [[0.0f64; 8]; 8];
let mut b = [0.0f64; 8];
for i in 0..4 {
let sx = f64::from(src[i].x);
let sy = f64::from(src[i].y);
let dx = f64::from(dst[i].x);
let dy = f64::from(dst[i].y);
let r0 = 2 * i;
a[r0] = [sx, sy, 1.0, 0.0, 0.0, 0.0, -dx * sx, -dx * sy];
b[r0] = dx;
let r1 = 2 * i + 1;
a[r1] = [0.0, 0.0, 0.0, sx, sy, 1.0, -dy * sx, -dy * sy];
b[r1] = dy;
}
let h = solve8(&mut a, &mut b)
.ok_or_else(|| Error::invalid_parameter("degenerate homography correspondences"))?;
Ok(Homography {
m: [h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7], 1.0],
})
}
pub fn from_unit_square(dst: [Point; 4]) -> Result<Self> {
let src = [
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(1.0, 1.0),
Point::new(0.0, 1.0),
];
Self::from_correspondences(src, dst)
}
pub fn map(&self, p: Point) -> Point {
let (x, y) = (f64::from(p.x), f64::from(p.y));
let m = &self.m;
let xw = m[0] * x + m[1] * y + m[2];
let yw = m[3] * x + m[4] * y + m[5];
let w = m[6] * x + m[7] * y + m[8];
Point::new((xw / w) as f32, (yw / w) as f32)
}
pub fn map_f64(&self, x: f64, y: f64) -> (f64, f64) {
let m = &self.m;
let xw = m[0] * x + m[1] * y + m[2];
let yw = m[3] * x + m[4] * y + m[5];
let w = m[6] * x + m[7] * y + m[8];
(xw / w, yw / w)
}
pub fn inverse(&self) -> Result<Homography> {
let m = &self.m;
let c00 = m[4] * m[8] - m[5] * m[7];
let c01 = m[5] * m[6] - m[3] * m[8];
let c02 = m[3] * m[7] - m[4] * m[6];
let det = m[0] * c00 + m[1] * c01 + m[2] * c02;
if det.abs() < 1e-12 {
return Err(Error::invalid_parameter("singular homography"));
}
let inv_det = 1.0 / det;
let inv = [
c00 * inv_det,
(m[2] * m[7] - m[1] * m[8]) * inv_det,
(m[1] * m[5] - m[2] * m[4]) * inv_det,
c01 * inv_det,
(m[0] * m[8] - m[2] * m[6]) * inv_det,
(m[2] * m[3] - m[0] * m[5]) * inv_det,
c02 * inv_det,
(m[1] * m[6] - m[0] * m[7]) * inv_det,
(m[0] * m[4] - m[1] * m[3]) * inv_det,
];
Ok(Homography { m: inv })
}
}
fn solve8(a: &mut [[f64; 8]; 8], b: &mut [f64; 8]) -> Option<[f64; 8]> {
const N: usize = 8;
for col in 0..N {
let mut pivot = col;
let mut best = a[col][col].abs();
for (row, r) in a.iter().enumerate().skip(col + 1) {
let v = r[col].abs();
if v > best {
best = v;
pivot = row;
}
}
if best < 1e-12 {
return None;
}
a.swap(col, pivot);
b.swap(col, pivot);
let pivot_row = a[col];
let pivot_b = b[col];
for row in (col + 1)..N {
let factor = a[row][col] / pivot_row[col];
if factor != 0.0 {
for (k, ak) in a[row].iter_mut().enumerate().skip(col) {
*ak -= factor * pivot_row[k];
}
b[row] -= factor * pivot_b;
}
}
}
let mut x = [0.0f64; N];
for i in (0..N).rev() {
let mut acc = b[i];
for k in (i + 1)..N {
acc -= a[i][k] * x[k];
}
x[i] = acc / a[i][i];
}
Some(x)
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: Point, b: Point, eps: f32) -> bool {
(a.x - b.x).abs() < eps && (a.y - b.y).abs() < eps
}
#[test]
fn identity_maps_unchanged() {
let h = Homography::identity();
let p = Point::new(3.5, -2.0);
assert!(close(h.map(p), p, 1e-6));
}
#[test]
fn maps_unit_square_to_quad() {
let dst = [
Point::new(10.0, 20.0),
Point::new(110.0, 25.0),
Point::new(100.0, 130.0),
Point::new(15.0, 120.0),
];
let h = Homography::from_unit_square(dst).unwrap();
assert!(close(h.map(Point::new(0.0, 0.0)), dst[0], 1e-3));
assert!(close(h.map(Point::new(1.0, 0.0)), dst[1], 1e-3));
assert!(close(h.map(Point::new(1.0, 1.0)), dst[2], 1e-3));
assert!(close(h.map(Point::new(0.0, 1.0)), dst[3], 1e-3));
}
#[test]
fn inverse_round_trips() {
let dst = [
Point::new(12.0, 8.0),
Point::new(200.0, 30.0),
Point::new(190.0, 210.0),
Point::new(30.0, 180.0),
];
let h = Homography::from_unit_square(dst).unwrap();
let hi = h.inverse().unwrap();
for &(gx, gy) in &[(0.25f32, 0.75f32), (0.5, 0.5), (0.9, 0.1)] {
let g = Point::new(gx, gy);
let round = hi.map(h.map(g));
assert!(close(round, g, 1e-3), "round trip failed for {g:?}");
}
let img = Point::new(100.0, 90.0);
let round = h.map(hi.map(img));
assert!(close(round, img, 1e-3));
}
#[test]
fn perspective_is_genuinely_nonaffine() {
let dst = [
Point::new(0.0, 0.0),
Point::new(100.0, 0.0),
Point::new(70.0, 100.0),
Point::new(30.0, 100.0),
];
let h = Homography::from_unit_square(dst).unwrap();
let mid_bottom = h.map(Point::new(0.5, 1.0));
assert!((mid_bottom.x - 50.0).abs() < 1e-3);
let mid = h.map(Point::new(0.5, 0.5));
assert!((mid.x - 50.0).abs() < 1e-3);
assert!(mid.y > 50.0, "perspective should compress the far edge");
}
#[test]
fn degenerate_correspondences_error() {
let src = [
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(2.0, 0.0),
Point::new(0.0, 1.0),
];
let dst = [
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(2.0, 0.0),
Point::new(0.0, 1.0),
];
assert!(Homography::from_correspondences(src, dst).is_err());
}
#[test]
fn singular_matrix_inverse_errors() {
let h = Homography::from_matrix([1.0, 2.0, 3.0, 2.0, 4.0, 6.0, 1.0, 1.0, 1.0]);
assert!(h.inverse().is_err());
}
}