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