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