1#![allow(dead_code)]
2use crate::{AlignError, AlignResult, Point2D};
16
17#[derive(Debug, Clone, PartialEq)]
19pub struct Matrix3x3 {
20 pub data: [f64; 9],
22}
23
24impl Matrix3x3 {
25 #[must_use]
27 pub fn new(data: [f64; 9]) -> Self {
28 Self { data }
29 }
30
31 #[must_use]
33 pub fn identity() -> Self {
34 Self {
35 data: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
36 }
37 }
38
39 #[must_use]
41 pub fn zero() -> Self {
42 Self { data: [0.0; 9] }
43 }
44
45 #[must_use]
47 pub fn at(&self, r: usize, c: usize) -> f64 {
48 self.data[r * 3 + c]
49 }
50
51 pub fn set(&mut self, r: usize, c: usize, val: f64) {
53 self.data[r * 3 + c] = val;
54 }
55
56 #[must_use]
58 pub fn transpose(&self) -> Self {
59 let d = &self.data;
60 Self {
61 data: [d[0], d[3], d[6], d[1], d[4], d[7], d[2], d[5], d[8]],
62 }
63 }
64
65 #[must_use]
67 #[allow(clippy::cast_precision_loss)]
68 pub fn determinant(&self) -> f64 {
69 let d = &self.data;
70 d[0] * (d[4] * d[8] - d[5] * d[7]) - d[1] * (d[3] * d[8] - d[5] * d[6])
71 + d[2] * (d[3] * d[7] - d[4] * d[6])
72 }
73
74 #[must_use]
76 pub fn multiply(&self, other: &Self) -> Self {
77 let a = &self.data;
78 let b = &other.data;
79 let mut result = [0.0; 9];
80 for i in 0..3 {
81 for j in 0..3 {
82 result[i * 3 + j] =
83 a[i * 3] * b[j] + a[i * 3 + 1] * b[3 + j] + a[i * 3 + 2] * b[6 + j];
84 }
85 }
86 Self { data: result }
87 }
88
89 #[must_use]
91 pub fn multiply_vec(&self, v: &[f64; 3]) -> [f64; 3] {
92 let d = &self.data;
93 [
94 d[0] * v[0] + d[1] * v[1] + d[2] * v[2],
95 d[3] * v[0] + d[4] * v[1] + d[5] * v[2],
96 d[6] * v[0] + d[7] * v[1] + d[8] * v[2],
97 ]
98 }
99
100 #[must_use]
102 pub fn frobenius_norm(&self) -> f64 {
103 self.data.iter().map(|x| x * x).sum::<f64>().sqrt()
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq)]
109pub struct StereoCorrespondence {
110 pub left: Point2D,
112 pub right: Point2D,
114}
115
116impl StereoCorrespondence {
117 #[must_use]
119 pub fn new(left: Point2D, right: Point2D) -> Self {
120 Self { left, right }
121 }
122}
123
124#[derive(Debug, Clone)]
126pub struct StereoRectifyConfig {
127 pub image_width: u32,
129 pub image_height: u32,
131 pub max_iterations: u32,
133 pub inlier_threshold: f64,
135 pub min_inliers: usize,
137}
138
139impl Default for StereoRectifyConfig {
140 fn default() -> Self {
141 Self {
142 image_width: 1920,
143 image_height: 1080,
144 max_iterations: 2000,
145 inlier_threshold: 2.0,
146 min_inliers: 8,
147 }
148 }
149}
150
151#[derive(Debug, Clone)]
153pub struct RectificationResult {
154 pub h_left: Matrix3x3,
156 pub h_right: Matrix3x3,
158 pub fundamental: Matrix3x3,
160 pub num_inliers: usize,
162 pub mean_error: f64,
164}
165
166#[derive(Debug, Clone)]
168pub struct StereoRectifier {
169 config: StereoRectifyConfig,
171}
172
173impl StereoRectifier {
174 #[must_use]
176 pub fn new(config: StereoRectifyConfig) -> Self {
177 Self { config }
178 }
179
180 #[must_use]
182 pub fn with_defaults() -> Self {
183 Self {
184 config: StereoRectifyConfig::default(),
185 }
186 }
187
188 pub fn estimate_fundamental(
192 &self,
193 correspondences: &[StereoCorrespondence],
194 ) -> AlignResult<Matrix3x3> {
195 if correspondences.len() < 8 {
196 return Err(AlignError::InsufficientData(
197 "At least 8 correspondences required for fundamental matrix".to_string(),
198 ));
199 }
200
201 let (left_norm, t_left) = self.normalize_points(correspondences, true);
203 let (right_norm, t_right) = self.normalize_points(correspondences, false);
204
205 let n = left_norm.len();
207 let mut ata = [0.0f64; 81]; for i in 0..n {
210 let (x1, y1) = (left_norm[i].0, left_norm[i].1);
211 let (x2, y2) = (right_norm[i].0, right_norm[i].1);
212 let row = [x2 * x1, x2 * y1, x2, y2 * x1, y2 * y1, y2, x1, y1, 1.0];
213 for r in 0..9 {
214 for c in 0..9 {
215 ata[r * 9 + c] += row[r] * row[c];
216 }
217 }
218 }
219
220 let f_vec = Self::smallest_eigenvector_ata(&ata);
222
223 let f_normalized = Matrix3x3::new(f_vec);
224
225 let result = t_right
227 .transpose()
228 .multiply(&f_normalized)
229 .multiply(&t_left);
230
231 let norm = result.frobenius_norm();
233 if norm < 1e-15 {
234 return Err(AlignError::NumericalError(
235 "Fundamental matrix has zero norm".to_string(),
236 ));
237 }
238 let mut final_data = result.data;
239 for v in &mut final_data {
240 *v /= norm;
241 }
242
243 Ok(Matrix3x3::new(final_data))
244 }
245
246 #[must_use]
250 #[allow(clippy::cast_precision_loss)]
251 pub fn epipolar_distance(f: &Matrix3x3, corr: &StereoCorrespondence) -> f64 {
252 let p1 = [corr.left.x, corr.left.y, 1.0];
253 let p2 = [corr.right.x, corr.right.y, 1.0];
254
255 let fp1 = f.multiply_vec(&p1);
257 let ftp2 = f.transpose().multiply_vec(&p2);
258
259 let p2fp1 = p2[0] * fp1[0] + p2[1] * fp1[1] + p2[2] * fp1[2];
260
261 let denom = fp1[0] * fp1[0] + fp1[1] * fp1[1] + ftp2[0] * ftp2[0] + ftp2[1] * ftp2[1];
262
263 if denom < 1e-15 {
264 return f64::MAX;
265 }
266
267 (p2fp1 * p2fp1 / denom).sqrt()
268 }
269
270 pub fn rectify(
274 &self,
275 correspondences: &[StereoCorrespondence],
276 ) -> AlignResult<RectificationResult> {
277 let fundamental = self.estimate_fundamental(correspondences)?;
278
279 let inliers: Vec<&StereoCorrespondence> = correspondences
281 .iter()
282 .filter(|c| Self::epipolar_distance(&fundamental, c) < self.config.inlier_threshold)
283 .collect();
284
285 if inliers.len() < self.config.min_inliers {
286 return Err(AlignError::InsufficientData(format!(
287 "Only {} inliers found, need at least {}",
288 inliers.len(),
289 self.config.min_inliers
290 )));
291 }
292
293 let epipole = self.approximate_epipole(&fundamental);
296
297 let h_right = self.build_rectify_homography(&epipole);
299
300 let h_left = self.build_matching_homography(&h_right, &fundamental, &inliers);
302
303 #[allow(clippy::cast_precision_loss)]
305 let mean_error = self.compute_rectification_error(&h_left, &h_right, &inliers);
306
307 Ok(RectificationResult {
308 h_left,
309 h_right,
310 fundamental,
311 num_inliers: inliers.len(),
312 mean_error,
313 })
314 }
315
316 #[allow(clippy::cast_precision_loss)]
318 fn normalize_points(
319 &self,
320 correspondences: &[StereoCorrespondence],
321 left: bool,
322 ) -> (Vec<(f64, f64)>, Matrix3x3) {
323 let points: Vec<(f64, f64)> = if left {
324 correspondences
325 .iter()
326 .map(|c| (c.left.x, c.left.y))
327 .collect()
328 } else {
329 correspondences
330 .iter()
331 .map(|c| (c.right.x, c.right.y))
332 .collect()
333 };
334
335 let n = points.len() as f64;
336 let cx: f64 = points.iter().map(|p| p.0).sum::<f64>() / n;
337 let cy: f64 = points.iter().map(|p| p.1).sum::<f64>() / n;
338
339 let mean_dist: f64 = points
340 .iter()
341 .map(|p| ((p.0 - cx).powi(2) + (p.1 - cy).powi(2)).sqrt())
342 .sum::<f64>()
343 / n;
344
345 let scale = if mean_dist > 1e-15 {
346 std::f64::consts::SQRT_2 / mean_dist
347 } else {
348 1.0
349 };
350
351 let normalized: Vec<(f64, f64)> = points
352 .iter()
353 .map(|p| ((p.0 - cx) * scale, (p.1 - cy) * scale))
354 .collect();
355
356 let t = Matrix3x3::new([
357 scale,
358 0.0,
359 -cx * scale,
360 0.0,
361 scale,
362 -cy * scale,
363 0.0,
364 0.0,
365 1.0,
366 ]);
367
368 (normalized, t)
369 }
370
371 fn smallest_eigenvector_ata(ata: &[f64; 81]) -> [f64; 9] {
373 let mut v = [1.0f64; 9];
377 let norm = (v.iter().map(|x| x * x).sum::<f64>()).sqrt();
378 for x in &mut v {
379 *x /= norm;
380 }
381
382 for _ in 0..200 {
385 let mut av = [0.0f64; 9];
387 for i in 0..9 {
388 for j in 0..9 {
389 av[i] += ata[i * 9 + j] * v[j];
390 }
391 }
392
393 let vav: f64 = v.iter().zip(av.iter()).map(|(a, b)| a * b).sum();
395 let vv: f64 = v.iter().map(|x| x * x).sum();
396 let lambda = vav / vv;
397
398 let mut residual = [0.0f64; 9];
400 for i in 0..9 {
401 residual[i] = av[i] - lambda * v[i];
402 }
403
404 let rnorm = residual.iter().map(|x| x * x).sum::<f64>().sqrt();
406 if rnorm < 1e-12 {
407 break;
408 }
409 let alpha = 0.01 / (1.0 + lambda.abs());
410 for i in 0..9 {
411 v[i] -= alpha * residual[i];
412 }
413
414 let n = v.iter().map(|x| x * x).sum::<f64>().sqrt();
416 if n > 1e-15 {
417 for x in &mut v {
418 *x /= n;
419 }
420 }
421 }
422
423 v
424 }
425
426 fn approximate_epipole(&self, f: &Matrix3x3) -> [f64; 3] {
428 let ft = f.transpose();
431 let row0 = [ft.at(0, 0), ft.at(0, 1), ft.at(0, 2)];
432 let row1 = [ft.at(1, 0), ft.at(1, 1), ft.at(1, 2)];
433
434 let e = [
435 row0[1] * row1[2] - row0[2] * row1[1],
436 row0[2] * row1[0] - row0[0] * row1[2],
437 row0[0] * row1[1] - row0[1] * row1[0],
438 ];
439
440 let norm = (e[0] * e[0] + e[1] * e[1] + e[2] * e[2]).sqrt();
441 if norm > 1e-15 {
442 [e[0] / norm, e[1] / norm, e[2] / norm]
443 } else {
444 [1.0, 0.0, 0.0]
445 }
446 }
447
448 #[allow(clippy::cast_precision_loss)]
450 fn build_rectify_homography(&self, epipole: &[f64; 3]) -> Matrix3x3 {
451 let cx = f64::from(self.config.image_width) / 2.0;
452 let cy = f64::from(self.config.image_height) / 2.0;
453
454 let t = Matrix3x3::new([1.0, 0.0, -cx, 0.0, 1.0, -cy, 0.0, 0.0, 1.0]);
456
457 let ex = epipole[0] - cx * epipole[2];
459 let ey = epipole[1] - cy * epipole[2];
460 let d = (ex * ex + ey * ey).sqrt();
461
462 let (cos_a, sin_a) = if d > 1e-15 {
463 (ex / d, ey / d)
464 } else {
465 (1.0, 0.0)
466 };
467
468 let r = Matrix3x3::new([cos_a, sin_a, 0.0, -sin_a, cos_a, 0.0, 0.0, 0.0, 1.0]);
469
470 let g = Matrix3x3::new([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, -1.0 / d, 0.0, 1.0]);
472
473 let t_inv = Matrix3x3::new([1.0, 0.0, cx, 0.0, 1.0, cy, 0.0, 0.0, 1.0]);
475
476 t_inv.multiply(&g).multiply(&r).multiply(&t)
477 }
478
479 fn build_matching_homography(
481 &self,
482 h_right: &Matrix3x3,
483 _fundamental: &Matrix3x3,
484 inliers: &[&StereoCorrespondence],
485 ) -> Matrix3x3 {
486 if inliers.is_empty() {
492 return Matrix3x3::identity();
493 }
494
495 let mut sum_dy = 0.0;
497 let mut count = 0.0;
498
499 for corr in inliers {
500 let rp = h_right.multiply_vec(&[corr.right.x, corr.right.y, 1.0]);
501 let ry = if rp[2].abs() > 1e-15 {
502 rp[1] / rp[2]
503 } else {
504 corr.right.y
505 };
506
507 let lp = h_right.multiply_vec(&[corr.left.x, corr.left.y, 1.0]);
508 let ly = if lp[2].abs() > 1e-15 {
509 lp[1] / lp[2]
510 } else {
511 corr.left.y
512 };
513
514 sum_dy += ry - ly;
515 count += 1.0;
516 }
517
518 let avg_dy = if count > 0.0 { sum_dy / count } else { 0.0 };
519
520 let shift = Matrix3x3::new([1.0, 0.0, 0.0, 0.0, 1.0, avg_dy, 0.0, 0.0, 1.0]);
522 shift.multiply(h_right)
523 }
524
525 #[allow(clippy::cast_precision_loss)]
527 fn compute_rectification_error(
528 &self,
529 h_left: &Matrix3x3,
530 h_right: &Matrix3x3,
531 inliers: &[&StereoCorrespondence],
532 ) -> f64 {
533 if inliers.is_empty() {
534 return 0.0;
535 }
536
537 let total_error: f64 = inliers
538 .iter()
539 .map(|corr| {
540 let lp = h_left.multiply_vec(&[corr.left.x, corr.left.y, 1.0]);
541 let rp = h_right.multiply_vec(&[corr.right.x, corr.right.y, 1.0]);
542
543 let ly = if lp[2].abs() > 1e-15 {
544 lp[1] / lp[2]
545 } else {
546 0.0
547 };
548 let ry = if rp[2].abs() > 1e-15 {
549 rp[1] / rp[2]
550 } else {
551 0.0
552 };
553
554 (ly - ry).abs()
555 })
556 .sum();
557
558 total_error / inliers.len() as f64
559 }
560
561 #[must_use]
563 pub fn config(&self) -> &StereoRectifyConfig {
564 &self.config
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571
572 #[test]
573 fn test_matrix_identity() {
574 let id = Matrix3x3::identity();
575 assert!((id.at(0, 0) - 1.0).abs() < f64::EPSILON);
576 assert!((id.at(1, 1) - 1.0).abs() < f64::EPSILON);
577 assert!((id.at(2, 2) - 1.0).abs() < f64::EPSILON);
578 assert!((id.at(0, 1)).abs() < f64::EPSILON);
579 }
580
581 #[test]
582 fn test_matrix_determinant() {
583 let id = Matrix3x3::identity();
584 assert!((id.determinant() - 1.0).abs() < f64::EPSILON);
585
586 let m = Matrix3x3::new([2.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 4.0]);
587 assert!((m.determinant() - 24.0).abs() < f64::EPSILON);
588 }
589
590 #[test]
591 fn test_matrix_transpose() {
592 let m = Matrix3x3::new([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
593 let mt = m.transpose();
594 assert!((mt.at(0, 1) - 4.0).abs() < f64::EPSILON);
595 assert!((mt.at(1, 0) - 2.0).abs() < f64::EPSILON);
596 assert!((mt.at(2, 0) - 3.0).abs() < f64::EPSILON);
597 }
598
599 #[test]
600 fn test_matrix_multiply_identity() {
601 let id = Matrix3x3::identity();
602 let m = Matrix3x3::new([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
603 let result = id.multiply(&m);
604 for i in 0..9 {
605 assert!((result.data[i] - m.data[i]).abs() < 1e-10);
606 }
607 }
608
609 #[test]
610 fn test_matrix_multiply_vec() {
611 let id = Matrix3x3::identity();
612 let v = [3.0, 4.0, 5.0];
613 let result = id.multiply_vec(&v);
614 assert!((result[0] - 3.0).abs() < f64::EPSILON);
615 assert!((result[1] - 4.0).abs() < f64::EPSILON);
616 assert!((result[2] - 5.0).abs() < f64::EPSILON);
617 }
618
619 #[test]
620 fn test_matrix_frobenius_norm() {
621 let m = Matrix3x3::new([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]);
622 assert!((m.frobenius_norm() - 3.0_f64.sqrt()).abs() < 1e-10);
623 }
624
625 #[test]
626 fn test_stereo_correspondence_creation() {
627 let left = Point2D::new(100.0, 200.0);
628 let right = Point2D::new(80.0, 200.0);
629 let corr = StereoCorrespondence::new(left, right);
630 assert!((corr.left.x - 100.0).abs() < f64::EPSILON);
631 assert!((corr.right.x - 80.0).abs() < f64::EPSILON);
632 }
633
634 #[test]
635 fn test_config_default() {
636 let config = StereoRectifyConfig::default();
637 assert_eq!(config.image_width, 1920);
638 assert_eq!(config.image_height, 1080);
639 assert_eq!(config.max_iterations, 2000);
640 assert!((config.inlier_threshold - 2.0).abs() < f64::EPSILON);
641 }
642
643 #[test]
644 fn test_rectifier_creation() {
645 let r = StereoRectifier::with_defaults();
646 assert_eq!(r.config().image_width, 1920);
647 }
648
649 #[test]
650 fn test_fundamental_insufficient_points() {
651 let rectifier = StereoRectifier::with_defaults();
652 let corrs = vec![
653 StereoCorrespondence::new(Point2D::new(0.0, 0.0), Point2D::new(1.0, 0.0)),
654 StereoCorrespondence::new(Point2D::new(1.0, 0.0), Point2D::new(2.0, 0.0)),
655 ];
656 let result = rectifier.estimate_fundamental(&corrs);
657 assert!(result.is_err());
658 }
659
660 #[test]
661 fn test_epipolar_distance_identity_like() {
662 let f = Matrix3x3::new([0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 1.0, 0.0]);
664 let corr = StereoCorrespondence::new(Point2D::new(100.0, 200.0), Point2D::new(80.0, 200.0));
665 let dist = StereoRectifier::epipolar_distance(&f, &corr);
666 assert!(dist.is_finite());
668 }
669
670 #[test]
671 fn test_rectification_with_enough_points() {
672 let rectifier = StereoRectifier::with_defaults();
673 let corrs: Vec<StereoCorrespondence> = (0..20)
675 .map(|i| {
676 #[allow(clippy::cast_precision_loss)]
677 let x = 100.0 + (i as f64) * 50.0;
678 let y = 300.0 + (i as f64) * 20.0;
679 StereoCorrespondence::new(Point2D::new(x, y), Point2D::new(x - 30.0, y + 0.5))
680 })
681 .collect();
682 let result = rectifier.rectify(&corrs);
684 let _ = result;
686 }
687
688 #[test]
689 fn test_matrix_zero() {
690 let z = Matrix3x3::zero();
691 for i in 0..9 {
692 assert!((z.data[i]).abs() < f64::EPSILON);
693 }
694 assert!((z.determinant()).abs() < f64::EPSILON);
695 }
696}