1use crate::float::{lit, Float};
8use nalgebra::{Matrix3, Point2, SMatrix, SVector, Vector3};
9
10#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct Homography<F: Float = f32> {
15 pub h: Matrix3<F>,
18}
19
20#[non_exhaustive]
37#[derive(Clone, Copy, Debug)]
38pub struct HomographyQuality<F: Float = f32> {
39 pub max_singular_value: F,
41 pub min_singular_value: F,
44 pub condition: F,
49 pub determinant: F,
52}
53
54impl<F: Float> HomographyQuality<F> {
55 pub fn from_homography(h: &Homography<F>) -> Self {
57 let svd = h.h.svd(false, false);
58 let mut s_max = F::zero();
59 let mut s_min = F::max_value().unwrap_or_else(|| lit(1e30));
60 for k in 0..3 {
61 let s = svd.singular_values[k];
62 if s > s_max {
63 s_max = s;
64 }
65 if s < s_min {
66 s_min = s;
67 }
68 }
69 let condition = if s_min > F::default_epsilon() {
70 s_max / s_min
71 } else {
72 F::max_value().unwrap_or_else(|| lit(1e30))
73 };
74 let determinant = h.h.determinant();
75 Self {
76 max_singular_value: s_max,
77 min_singular_value: s_min,
78 condition,
79 determinant,
80 }
81 }
82
83 pub fn is_ill_conditioned(&self, min_singular_value_threshold: F) -> bool {
91 self.min_singular_value < min_singular_value_threshold
92 }
93}
94
95impl<F: Float> Homography<F> {
96 pub fn new(h: Matrix3<F>) -> Self {
99 Self { h }
100 }
101
102 pub fn from_array(rows: [[F; 3]; 3]) -> Self {
104 Self::new(Matrix3::from_row_slice(&[
105 rows[0][0], rows[0][1], rows[0][2], rows[1][0], rows[1][1], rows[1][2], rows[2][0],
106 rows[2][1], rows[2][2],
107 ]))
108 }
109
110 pub fn to_array(&self) -> [[F; 3]; 3] {
112 [
113 [self.h[(0, 0)], self.h[(0, 1)], self.h[(0, 2)]],
114 [self.h[(1, 0)], self.h[(1, 1)], self.h[(1, 2)]],
115 [self.h[(2, 0)], self.h[(2, 1)], self.h[(2, 2)]],
116 ]
117 }
118
119 pub fn zero() -> Self {
122 Self {
123 h: Matrix3::zeros(),
124 }
125 }
126
127 #[inline]
129 pub fn apply(&self, p: Point2<F>) -> Point2<F> {
130 let v = self.h * Vector3::new(p.x, p.y, F::one());
131 let w = v[2];
132 Point2::new(v[0] / w, v[1] / w)
133 }
134
135 pub fn inverse(&self) -> Option<Self> {
137 self.h.try_inverse().map(Self::new)
138 }
139}
140
141fn hartley_normalization<F: Float>(cx: F, cy: F, mean_dist: F) -> Matrix3<F> {
144 let s = if mean_dist > lit(1e-12) {
145 lit::<F>(2.0).sqrt() / mean_dist
146 } else {
147 F::one()
148 };
149
150 Matrix3::new(
151 s,
152 F::zero(),
153 -s * cx,
154 F::zero(),
155 s,
156 -s * cy,
157 F::zero(),
158 F::zero(),
159 F::one(),
160 )
161}
162
163fn normalize_points<F: Float>(pts: &[Point2<F>]) -> (Vec<Point2<F>>, Matrix3<F>) {
164 let n: F = lit(pts.len() as f32);
165 let mut cx = F::zero();
166 let mut cy = F::zero();
167 for p in pts {
168 cx += p.x;
169 cy += p.y;
170 }
171 cx /= n;
172 cy /= n;
173
174 let mut mean_dist = F::zero();
175 for p in pts {
176 let dx = p.x - cx;
177 let dy = p.y - cy;
178 mean_dist += (dx * dx + dy * dy).sqrt();
179 }
180 mean_dist /= n;
181
182 let t = hartley_normalization(cx, cy, mean_dist);
183
184 let mut out = Vec::with_capacity(pts.len());
185 for p in pts {
186 let v = t * Vector3::new(p.x, p.y, F::one());
187 out.push(Point2::new(v[0], v[1]));
188 }
189 (out, t)
190}
191
192fn normalize_points4<F: Float>(pts: &[Point2<F>; 4]) -> ([Point2<F>; 4], Matrix3<F>) {
193 let n: F = lit(4.0);
194 let mut cx = F::zero();
195 let mut cy = F::zero();
196 for p in pts {
197 cx += p.x;
198 cy += p.y;
199 }
200 cx /= n;
201 cy /= n;
202
203 let mut mean_dist = F::zero();
204 for p in pts {
205 let dx = p.x - cx;
206 let dy = p.y - cy;
207 mean_dist += (dx * dx + dy * dy).sqrt();
208 }
209 mean_dist /= n;
210
211 let t = hartley_normalization(cx, cy, mean_dist);
212
213 let mut out = [Point2::new(F::zero(), F::zero()); 4];
214 for (i, p) in pts.iter().enumerate() {
215 let v = t * Vector3::new(p.x, p.y, F::one());
216 out[i] = Point2::new(v[0], v[1]);
217 }
218
219 (out, t)
220}
221
222fn normalize_homography<F: Float>(h: Matrix3<F>) -> Option<Matrix3<F>> {
223 let s = h[(2, 2)];
224 if s.abs() < lit(1e-12) {
225 return None;
226 }
227 Some(h / s)
228}
229
230fn denormalize_homography<F: Float>(
231 hn: Matrix3<F>,
232 t_src: Matrix3<F>,
233 t_dst: Matrix3<F>,
234) -> Option<Matrix3<F>> {
235 let t_dst_inv = t_dst.try_inverse()?;
236 Some(t_dst_inv * hn * t_src)
237}
238
239pub fn estimate_homography_with_quality<F: Float>(
247 src_pts: &[Point2<F>],
248 dst_pts: &[Point2<F>],
249) -> Option<(Homography<F>, HomographyQuality<F>)> {
250 let h = estimate_homography(src_pts, dst_pts)?;
251 let q = HomographyQuality::from_homography(&h);
252 Some((h, q))
253}
254
255pub fn homography_from_4pt_with_quality<F: Float>(
257 src: &[Point2<F>; 4],
258 dst: &[Point2<F>; 4],
259) -> Option<(Homography<F>, HomographyQuality<F>)> {
260 let h = homography_from_4pt(src, dst)?;
261 let q = HomographyQuality::from_homography(&h);
262 Some((h, q))
263}
264
265pub fn estimate_homography<F: Float>(
269 src_pts: &[Point2<F>],
270 dst_pts: &[Point2<F>],
271) -> Option<Homography<F>> {
272 if src_pts.len() != dst_pts.len() || src_pts.len() < 4 {
273 return None;
274 }
275
276 if src_pts.len() == 4 {
277 let src: &[Point2<F>; 4] = src_pts.try_into().ok()?;
278 let dst: &[Point2<F>; 4] = dst_pts.try_into().ok()?;
279 return homography_from_4pt(src, dst);
280 }
281
282 let (r, tr) = normalize_points(src_pts);
283 let (im, ti) = normalize_points(dst_pts);
284
285 let n = src_pts.len();
286 let zero = F::zero();
287 let neg_one = -F::one();
288
289 let mut m: SMatrix<F, 9, 9> = SMatrix::zeros();
296 for k in 0..n {
297 let x = r[k].x;
298 let y = r[k].y;
299 let u = im[k].x;
300 let v = im[k].y;
301
302 let row1 = SVector::<F, 9>::from_column_slice(&[
303 -x,
304 -y,
305 neg_one,
306 zero,
307 zero,
308 zero,
309 u * x,
310 u * y,
311 u,
312 ]);
313 let row2 = SVector::<F, 9>::from_column_slice(&[
314 zero,
315 zero,
316 zero,
317 -x,
318 -y,
319 neg_one,
320 v * x,
321 v * y,
322 v,
323 ]);
324 m += row1 * row1.transpose();
325 m += row2 * row2.transpose();
326 }
327
328 let eig = m.symmetric_eigen();
334 let mut min_idx = 0usize;
335 let mut min_val = eig.eigenvalues[0];
336 for k in 1..9 {
337 if eig.eigenvalues[k] < min_val {
338 min_val = eig.eigenvalues[k];
339 min_idx = k;
340 }
341 }
342 let h = eig.eigenvectors.column(min_idx);
343
344 let hn = Matrix3::<F>::from_row_slice(&[h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7], h[8]]);
345
346 let h_den = denormalize_homography(hn, tr, ti)?;
347 let h_den = normalize_homography(h_den)?;
348
349 Some(Homography::new(h_den))
350}
351
352pub fn homography_from_4pt<F: Float>(
356 src: &[Point2<F>; 4],
357 dst: &[Point2<F>; 4],
358) -> Option<Homography<F>> {
359 let (src_n, t_src) = normalize_points4(src);
360 let (dst_n, t_dst) = normalize_points4(dst);
361
362 let mut a = SMatrix::<F, 8, 8>::zeros();
363 let mut b = SVector::<F, 8>::zeros();
364
365 for k in 0..4 {
366 let x = src_n[k].x;
367 let y = src_n[k].y;
368 let u = dst_n[k].x;
369 let v = dst_n[k].y;
370
371 let r0 = 2 * k;
372 a[(r0, 0)] = x;
373 a[(r0, 1)] = y;
374 a[(r0, 2)] = F::one();
375 a[(r0, 6)] = -u * x;
376 a[(r0, 7)] = -u * y;
377 b[r0] = u;
378
379 let r1 = 2 * k + 1;
380 a[(r1, 3)] = x;
381 a[(r1, 4)] = y;
382 a[(r1, 5)] = F::one();
383 a[(r1, 6)] = -v * x;
384 a[(r1, 7)] = -v * y;
385 b[r1] = v;
386 }
387
388 let x = a.lu().solve(&b)?;
389
390 let hn = Matrix3::<F>::new(
391 x[0],
392 x[1],
393 x[2], x[3],
395 x[4],
396 x[5], x[6],
398 x[7],
399 F::one(),
400 );
401
402 let h_den = denormalize_homography(hn, t_src, t_dst)?;
403 let h_den = normalize_homography(h_den)?;
404
405 Some(Homography::new(h_den))
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411
412 fn assert_close(a: Point2<f32>, b: Point2<f32>, tol: f32) {
413 let dx = (a.x - b.x).abs();
414 let dy = (a.y - b.y).abs();
415 assert!(
416 dx < tol && dy < tol,
417 "expected ({:.6},{:.6}) ~ ({:.6},{:.6}) within {}",
418 a.x,
419 a.y,
420 b.x,
421 b.y,
422 tol
423 );
424 }
425
426 #[test]
427 fn inverse_round_trips_points() {
428 let h = Homography::new(Matrix3::new(
429 1.2, 0.1, 5.0, -0.05, 0.9, 3.0, 0.001, 0.0005, 1.0,
432 ));
433 let inv = h.inverse().expect("invertible");
434
435 for p in [
436 Point2::new(0.0_f32, 0.0),
437 Point2::new(50.0_f32, -20.0),
438 Point2::new(320.0_f32, 200.0),
439 ] {
440 let q = h.apply(p);
441 let back = inv.apply(q);
442 assert_close(back, p, 1e-3);
443 }
444 }
445
446 #[test]
447 fn four_point_specialization_recovers_h() {
448 let ground_truth = Homography::new(Matrix3::new(
449 0.8, 0.05, 120.0, -0.02, 1.1, 80.0, 0.0009, -0.0004, 1.0,
452 ));
453
454 let rect = [
455 Point2::new(0.0_f32, 0.0),
456 Point2::new(180.0_f32, 0.0),
457 Point2::new(180.0_f32, 130.0),
458 Point2::new(0.0_f32, 130.0),
459 ];
460 let dst = rect.map(|p| ground_truth.apply(p));
461
462 let recovered = homography_from_4pt(&rect, &dst).expect("recoverable");
463
464 for p in [
465 Point2::new(0.0_f32, 0.0),
466 Point2::new(60.0, 40.0),
467 Point2::new(150.0, 120.0),
468 ] {
469 assert_close(recovered.apply(p), ground_truth.apply(p), 1e-3);
470 }
471 }
472
473 #[test]
474 fn dlt_handles_overdetermined_case() {
475 let ground_truth = Homography::new(Matrix3::new(
476 1.0, 0.2, 12.0, -0.1, 0.9, 6.0, 0.0006, 0.0004, 1.0,
479 ));
480
481 let rect: Vec<Point2<f32>> = (0..3)
482 .flat_map(|y| (0..3).map(move |x| Point2::new(x as f32 * 40.0, y as f32 * 50.0)))
483 .collect();
484 let img: Vec<Point2<f32>> = rect.iter().map(|&p| ground_truth.apply(p)).collect();
485
486 let estimated = estimate_homography(&rect, &img).expect("estimate");
487 for p in [
488 Point2::new(0.0_f32, 0.0),
489 Point2::new(60.0, 40.0),
490 Point2::new(80.0, 90.0),
491 Point2::new(80.0, 100.0),
492 ] {
493 assert_close(estimated.apply(p), ground_truth.apply(p), 1e-3);
494 }
495 }
496
497 #[test]
498 fn mismatched_input_lengths_fail() {
499 let rect = [Point2::new(0.0_f32, 0.0); 4];
500 let img = [Point2::new(1.0_f32, 1.0); 3];
501 assert!(estimate_homography(&rect, &img).is_none());
502 }
503
504 #[test]
505 fn quality_reports_finite_metrics_for_clean_homography() {
506 let rect = [
507 Point2::new(0.0_f32, 0.0),
508 Point2::new(100.0, 0.0),
509 Point2::new(100.0, 100.0),
510 Point2::new(0.0, 100.0),
511 ];
512 let dst = [
514 Point2::new(50.0, 50.0),
515 Point2::new(150.0, 60.0),
516 Point2::new(140.0, 160.0),
517 Point2::new(40.0, 150.0),
518 ];
519 let (_, q) = homography_from_4pt_with_quality(&rect, &dst).expect("h");
520 assert!(q.max_singular_value.is_finite() && q.max_singular_value > 0.0);
522 assert!(q.min_singular_value > 0.0);
523 assert!(q.condition.is_finite());
524 assert!(q.determinant.abs() > 1e-3);
525 assert!(
529 q.min_singular_value > 1e-2,
530 "min_sv {} unexpectedly tiny on a clean fit",
531 q.min_singular_value
532 );
533 }
534
535 #[test]
536 fn quality_min_sv_separates_clean_from_degenerate() {
537 let rect = [
542 Point2::new(0.0_f32, 0.0),
543 Point2::new(1.0, 0.0),
544 Point2::new(1.0, 1.0),
545 Point2::new(0.0, 1.0),
546 ];
547 let clean_dst = [
548 Point2::new(0.0_f32, 0.0),
549 Point2::new(2.0, 0.0),
550 Point2::new(2.0, 2.0),
551 Point2::new(0.0, 2.0),
552 ];
553 let degen_dst = [
554 Point2::new(0.0_f32, 0.0),
555 Point2::new(1.0, 0.0),
556 Point2::new(1.0, 1e-6),
557 Point2::new(0.0, 1e-6),
558 ];
559 let (_, q_clean) = homography_from_4pt_with_quality(&rect, &clean_dst).expect("clean");
560 let (_, q_degen) = homography_from_4pt_with_quality(&rect, °en_dst).expect("degen");
561
562 assert!(
563 q_clean.min_singular_value > q_degen.min_singular_value * 100.0,
564 "clean min_sv {} must be much larger than degenerate {}",
565 q_clean.min_singular_value,
566 q_degen.min_singular_value
567 );
568 let recip_clean = q_clean.min_singular_value / q_clean.max_singular_value;
570 let recip_degen = q_degen.min_singular_value / q_degen.max_singular_value;
571 assert!(
572 recip_clean > 0.1,
573 "clean recip_cond {recip_clean} too small"
574 );
575 assert!(
576 recip_degen < 1e-3,
577 "degenerate recip_cond {recip_degen} too large"
578 );
579 }
580
581 #[test]
582 fn is_ill_conditioned_threshold_works() {
583 let rect = [
584 Point2::new(0.0_f32, 0.0),
585 Point2::new(1.0, 0.0),
586 Point2::new(1.0, 1.0),
587 Point2::new(0.0, 1.0),
588 ];
589 let degen_dst = [
590 Point2::new(0.0_f32, 0.0),
591 Point2::new(1.0, 0.0),
592 Point2::new(1.0, 1e-6),
593 Point2::new(0.0, 1e-6),
594 ];
595 let (_, q) = homography_from_4pt_with_quality(&rect, °en_dst).expect("h");
596 assert!(q.is_ill_conditioned(1e-3));
597 assert!(!q.is_ill_conditioned(1e-12));
598 }
599
600 #[test]
601 fn estimate_with_quality_matches_direct_call() {
602 let ground_truth: Homography<f32> = Homography::new(Matrix3::new(
603 1.0, 0.2, 12.0, -0.1, 0.9, 6.0, 0.0006, 0.0004, 1.0,
606 ));
607 let rect: Vec<Point2<f32>> = (0..3)
608 .flat_map(|y| (0..3).map(move |x| Point2::new(x as f32 * 40.0, y as f32 * 50.0)))
609 .collect();
610 let img: Vec<Point2<f32>> = rect.iter().map(|&p| ground_truth.apply(p)).collect();
611
612 let h = estimate_homography(&rect, &img).expect("h");
613 let (h_with_q, _) = estimate_homography_with_quality(&rect, &img).expect("h+q");
614 for r in 0..3 {
615 for c in 0..3 {
616 assert!((h.h[(r, c)] - h_with_q.h[(r, c)]).abs() < 1e-6);
617 }
618 }
619 }
620
621 #[test]
622 fn f64_round_trip() {
623 let h: Homography<f64> = Homography::new(Matrix3::new(
624 1.2, 0.1, 5.0, -0.05, 0.9, 3.0, 0.001, 0.0005, 1.0,
627 ));
628 let inv = h.inverse().expect("invertible");
629
630 for p in [
631 Point2::new(0.0_f64, 0.0),
632 Point2::new(50.0_f64, -20.0),
633 Point2::new(320.0_f64, 200.0),
634 ] {
635 let q = h.apply(p);
636 let back = inv.apply(q);
637 assert!((back.x - p.x).abs() < 1e-10);
638 assert!((back.y - p.y).abs() < 1e-10);
639 }
640 }
641
642 #[test]
643 fn f64_estimate_homography() {
644 let ground_truth: Homography<f64> = Homography::new(Matrix3::new(
645 1.0, 0.2, 12.0, -0.1, 0.9, 6.0, 0.0006, 0.0004, 1.0,
648 ));
649
650 let rect: Vec<Point2<f64>> = (0..3)
651 .flat_map(|y| (0..3).map(move |x| Point2::new(x as f64 * 40.0, y as f64 * 50.0)))
652 .collect();
653 let img: Vec<Point2<f64>> = rect.iter().map(|&p| ground_truth.apply(p)).collect();
654
655 let estimated = estimate_homography(&rect, &img).expect("estimate");
656 for p in [
657 Point2::new(0.0_f64, 0.0),
658 Point2::new(60.0, 40.0),
659 Point2::new(80.0, 90.0),
660 ] {
661 let a = estimated.apply(p);
662 let b = ground_truth.apply(p);
663 assert!((a.x - b.x).abs() < 1e-8);
664 assert!((a.y - b.y).abs() < 1e-8);
665 }
666 }
667
668 fn dlt_via_svd_reference(
674 src_pts: &[Point2<f32>],
675 dst_pts: &[Point2<f32>],
676 ) -> Option<Homography<f32>> {
677 if src_pts.len() != dst_pts.len() || src_pts.len() < 4 {
678 return None;
679 }
680 let (r, tr) = normalize_points(src_pts);
681 let (im, ti) = normalize_points(dst_pts);
682
683 let n = src_pts.len();
684 let rows = 2 * n;
685 let mut a = nalgebra::DMatrix::<f32>::zeros(rows, 9);
686 for k in 0..n {
687 let x = r[k].x;
688 let y = r[k].y;
689 let u = im[k].x;
690 let v = im[k].y;
691 a[(2 * k, 0)] = -x;
692 a[(2 * k, 1)] = -y;
693 a[(2 * k, 2)] = -1.0;
694 a[(2 * k, 6)] = u * x;
695 a[(2 * k, 7)] = u * y;
696 a[(2 * k, 8)] = u;
697
698 a[(2 * k + 1, 3)] = -x;
699 a[(2 * k + 1, 4)] = -y;
700 a[(2 * k + 1, 5)] = -1.0;
701 a[(2 * k + 1, 6)] = v * x;
702 a[(2 * k + 1, 7)] = v * y;
703 a[(2 * k + 1, 8)] = v;
704 }
705 let svd = a.svd(true, true);
706 let vt = svd.v_t?;
707 let last = vt.nrows().checked_sub(1)?;
708 let h = vt.row(last);
709 let hn =
710 Matrix3::<f32>::from_row_slice(&[h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7], h[8]]);
711 let h_den = denormalize_homography(hn, tr, ti)?;
712 let h_den = normalize_homography(h_den)?;
713 Some(Homography::new(h_den))
714 }
715
716 struct XorShift32(u32);
720 impl XorShift32 {
721 fn new(seed: u32) -> Self {
722 Self(seed.max(1))
723 }
724 fn next_u32(&mut self) -> u32 {
725 let mut x = self.0;
726 x ^= x << 13;
727 x ^= x >> 17;
728 x ^= x << 5;
729 self.0 = x;
730 x
731 }
732 fn unit(&mut self) -> f32 {
734 (self.next_u32() as f32 / u32::MAX as f32) * 2.0 - 1.0
735 }
736 }
737
738 #[test]
739 fn dlt_matches_old_svd_path_on_random_battery() {
740 let mut rng = XorShift32::new(42);
758
759 let mut max_fwd_err_new = 0.0f32;
760 let mut max_fwd_err_ref = 0.0f32;
761 let mut max_pair_err = 0.0f32;
762 let mut sample_count = 0usize;
763
764 for _ in 0..1000 {
765 let gt = Homography::new(Matrix3::new(
767 1.0 + 0.5 * rng.unit(),
768 0.2 * rng.unit(),
769 50.0 * rng.unit(),
770 0.2 * rng.unit(),
771 1.0 + 0.5 * rng.unit(),
772 50.0 * rng.unit(),
773 0.001 * rng.unit(),
774 0.001 * rng.unit(),
775 1.0,
776 ));
777 let src: Vec<Point2<f32>> = (0..12)
779 .map(|_| Point2::new(100.0 * rng.unit(), 100.0 * rng.unit()))
780 .collect();
781 let dst: Vec<Point2<f32>> = src.iter().map(|&p| gt.apply(p)).collect();
782
783 let Some(new_h) = estimate_homography(&src, &dst) else {
784 continue;
785 };
786 let Some(ref_h) = dlt_via_svd_reference(&src, &dst) else {
787 continue;
788 };
789
790 for &p in &src {
792 let new_p = new_h.apply(p);
793 let ref_p = ref_h.apply(p);
794 let gt_p = gt.apply(p);
795 let new_err = ((new_p.x - gt_p.x).powi(2) + (new_p.y - gt_p.y).powi(2)).sqrt();
796 let ref_err = ((ref_p.x - gt_p.x).powi(2) + (ref_p.y - gt_p.y).powi(2)).sqrt();
797 let pair_err = ((new_p.x - ref_p.x).powi(2) + (new_p.y - ref_p.y).powi(2)).sqrt();
798 if new_err > max_fwd_err_new {
799 max_fwd_err_new = new_err;
800 }
801 if ref_err > max_fwd_err_ref {
802 max_fwd_err_ref = ref_err;
803 }
804 if pair_err > max_pair_err {
805 max_pair_err = pair_err;
806 }
807 }
808
809 sample_count += 1;
810 }
811
812 assert!(
813 sample_count > 900,
814 "expected most random samples to be valid, got {sample_count}"
815 );
816 assert!(
819 max_fwd_err_new < 1e-2,
820 "new path max forward error {max_fwd_err_new} px exceeds 1e-2"
821 );
822 assert!(
823 max_fwd_err_ref < 1e-2,
824 "reference SVD max forward error {max_fwd_err_ref} px exceeds 1e-2"
825 );
826 assert!(
829 max_pair_err < 1e-2,
830 "new vs reference max pixel divergence {max_pair_err} px exceeds 1e-2"
831 );
832 }
833}