1use crate::{AlignError, AlignResult, Point2D};
12use nalgebra::{Matrix3, Vector2};
13
14#[derive(Debug, Clone)]
16pub struct CameraIntrinsics {
17 pub fx: f64,
19 pub fy: f64,
21 pub cx: f64,
23 pub cy: f64,
25}
26
27impl CameraIntrinsics {
28 #[must_use]
30 pub fn new(fx: f64, fy: f64, cx: f64, cy: f64) -> Self {
31 Self { fx, fy, cx, cy }
32 }
33
34 #[must_use]
36 pub fn from_image_size(width: usize, height: usize, fov_degrees: f64) -> Self {
37 let fov_rad = fov_degrees.to_radians();
38 let fx = (width as f64 / 2.0) / (fov_rad / 2.0).tan();
39 let fy = fx; let cx = width as f64 / 2.0;
41 let cy = height as f64 / 2.0;
42
43 Self { fx, fy, cx, cy }
44 }
45
46 #[must_use]
48 pub fn to_matrix(&self) -> Matrix3<f64> {
49 Matrix3::new(self.fx, 0.0, self.cx, 0.0, self.fy, self.cy, 0.0, 0.0, 1.0)
50 }
51
52 #[must_use]
54 pub fn pixel_to_normalized(&self, pixel: &Point2D) -> Vector2<f64> {
55 Vector2::new((pixel.x - self.cx) / self.fx, (pixel.y - self.cy) / self.fy)
56 }
57
58 #[must_use]
60 pub fn normalized_to_pixel(&self, normalized: &Vector2<f64>) -> Point2D {
61 Point2D::new(
62 normalized[0] * self.fx + self.cx,
63 normalized[1] * self.fy + self.cy,
64 )
65 }
66}
67
68#[derive(Debug, Clone)]
70pub struct BrownConradyDistortion {
71 pub radial: [f64; 3],
73 pub tangential: [f64; 2],
75}
76
77impl Default for BrownConradyDistortion {
78 fn default() -> Self {
79 Self {
80 radial: [0.0, 0.0, 0.0],
81 tangential: [0.0, 0.0],
82 }
83 }
84}
85
86impl BrownConradyDistortion {
87 #[must_use]
89 pub fn new(k1: f64, k2: f64, k3: f64, p1: f64, p2: f64) -> Self {
90 Self {
91 radial: [k1, k2, k3],
92 tangential: [p1, p2],
93 }
94 }
95
96 #[must_use]
98 pub fn distort(&self, point: &Vector2<f64>) -> Vector2<f64> {
99 let x = point[0];
100 let y = point[1];
101 let r2 = x * x + y * y;
102 let r4 = r2 * r2;
103 let r6 = r4 * r2;
104
105 let k1 = self.radial[0];
106 let k2 = self.radial[1];
107 let k3 = self.radial[2];
108 let p1 = self.tangential[0];
109 let p2 = self.tangential[1];
110
111 let radial_distortion = 1.0 + k1 * r2 + k2 * r4 + k3 * r6;
113
114 let x_tangential = 2.0 * p1 * x * y + p2 * (r2 + 2.0 * x * x);
116 let y_tangential = p1 * (r2 + 2.0 * y * y) + 2.0 * p2 * x * y;
117
118 Vector2::new(
119 x * radial_distortion + x_tangential,
120 y * radial_distortion + y_tangential,
121 )
122 }
123
124 #[must_use]
126 pub fn undistort(&self, distorted: &Vector2<f64>) -> Vector2<f64> {
127 let mut undistorted = *distorted;
128
129 for _ in 0..10 {
131 let distorted_estimate = self.distort(&undistorted);
132 let error = distorted - distorted_estimate;
133
134 undistorted += error;
135
136 if error.norm() < 1e-8 {
138 break;
139 }
140 }
141
142 undistorted
143 }
144}
145
146#[derive(Debug, Clone)]
148pub struct FisheyeDistortion {
149 pub coefficients: [f64; 4],
151}
152
153impl Default for FisheyeDistortion {
154 fn default() -> Self {
155 Self {
156 coefficients: [0.0, 0.0, 0.0, 0.0],
157 }
158 }
159}
160
161impl FisheyeDistortion {
162 #[must_use]
164 pub fn new(k1: f64, k2: f64, k3: f64, k4: f64) -> Self {
165 Self {
166 coefficients: [k1, k2, k3, k4],
167 }
168 }
169
170 #[must_use]
172 pub fn distort(&self, point: &Vector2<f64>) -> Vector2<f64> {
173 let x = point[0];
174 let y = point[1];
175 let r = (x * x + y * y).sqrt();
176
177 if r < 1e-10 {
178 return *point;
179 }
180
181 let theta = r.atan();
182 let theta2 = theta * theta;
183 let theta4 = theta2 * theta2;
184 let theta6 = theta4 * theta2;
185 let theta8 = theta6 * theta2;
186
187 let k1 = self.coefficients[0];
188 let k2 = self.coefficients[1];
189 let k3 = self.coefficients[2];
190 let k4 = self.coefficients[3];
191
192 let theta_d = theta * (1.0 + k1 * theta2 + k2 * theta4 + k3 * theta6 + k4 * theta8);
193
194 let scale = theta_d / r;
195
196 Vector2::new(x * scale, y * scale)
197 }
198
199 #[must_use]
201 pub fn undistort(&self, distorted: &Vector2<f64>) -> Vector2<f64> {
202 let x = distorted[0];
203 let y = distorted[1];
204 let r = (x * x + y * y).sqrt();
205
206 if r < 1e-10 {
207 return *distorted;
208 }
209
210 let mut theta = r;
212 for _ in 0..10 {
213 let theta2 = theta * theta;
214 let theta4 = theta2 * theta2;
215 let theta6 = theta4 * theta2;
216 let theta8 = theta6 * theta2;
217
218 let k1 = self.coefficients[0];
219 let k2 = self.coefficients[1];
220 let k3 = self.coefficients[2];
221 let k4 = self.coefficients[3];
222
223 let theta_d = theta * (1.0 + k1 * theta2 + k2 * theta4 + k3 * theta6 + k4 * theta8);
224 let error = theta_d - r;
225
226 if error.abs() < 1e-8 {
227 break;
228 }
229
230 let derivative =
232 1.0 + 3.0 * k1 * theta2 + 5.0 * k2 * theta4 + 7.0 * k3 * theta6 + 9.0 * k4 * theta8;
233
234 theta -= error / derivative;
235 }
236
237 let scale = theta.tan() / r;
238 Vector2::new(x * scale, y * scale)
239 }
240}
241
242pub struct CameraModel {
244 pub intrinsics: CameraIntrinsics,
246 pub distortion: DistortionModel,
248}
249
250#[derive(Debug, Clone)]
252pub enum DistortionModel {
253 None,
255 BrownConrady(BrownConradyDistortion),
257 Fisheye(FisheyeDistortion),
259}
260
261impl CameraModel {
262 #[must_use]
264 pub fn new(intrinsics: CameraIntrinsics, distortion: DistortionModel) -> Self {
265 Self {
266 intrinsics,
267 distortion,
268 }
269 }
270
271 #[must_use]
273 pub fn project(&self, point_3d: &nalgebra::Vector3<f64>) -> Point2D {
274 let normalized = Vector2::new(point_3d[0] / point_3d[2], point_3d[1] / point_3d[2]);
276
277 let distorted = match &self.distortion {
279 DistortionModel::None => normalized,
280 DistortionModel::BrownConrady(d) => d.distort(&normalized),
281 DistortionModel::Fisheye(d) => d.distort(&normalized),
282 };
283
284 self.intrinsics.normalized_to_pixel(&distorted)
286 }
287
288 #[must_use]
290 pub fn unproject(&self, pixel: &Point2D) -> Vector2<f64> {
291 let distorted = self.intrinsics.pixel_to_normalized(pixel);
293
294 match &self.distortion {
296 DistortionModel::None => distorted,
297 DistortionModel::BrownConrady(d) => d.undistort(&distorted),
298 DistortionModel::Fisheye(d) => d.undistort(&distorted),
299 }
300 }
301}
302
303pub struct ImageUndistorter {
305 pub camera: CameraModel,
307 pub width: usize,
309 pub height: usize,
311 map_x: Vec<f32>,
313 map_y: Vec<f32>,
315}
316
317impl ImageUndistorter {
318 #[must_use]
320 pub fn new(camera: CameraModel, width: usize, height: usize) -> Self {
321 let mut map_x = vec![0.0; width * height];
322 let mut map_y = vec![0.0; width * height];
323
324 for y in 0..height {
326 for x in 0..width {
327 let pixel = Point2D::new(x as f64, y as f64);
328 let undistorted = camera.unproject(&pixel);
329 let source = camera.intrinsics.normalized_to_pixel(&undistorted);
330
331 let idx = y * width + x;
332 map_x[idx] = source.x as f32;
333 map_y[idx] = source.y as f32;
334 }
335 }
336
337 Self {
338 camera,
339 width,
340 height,
341 map_x,
342 map_y,
343 }
344 }
345
346 pub fn undistort(&self, input: &[u8], channels: usize) -> AlignResult<Vec<u8>> {
351 let expected_size = self.width * self.height * channels;
352 if input.len() != expected_size {
353 return Err(AlignError::InvalidConfig(format!(
354 "Input size {} doesn't match expected {}",
355 input.len(),
356 expected_size
357 )));
358 }
359
360 let mut output = vec![0u8; expected_size];
361
362 for y in 0..self.height {
363 for x in 0..self.width {
364 let idx = y * self.width + x;
365 let src_x = self.map_x[idx];
366 let src_y = self.map_y[idx];
367
368 let x0 = src_x.floor() as isize;
370 let y0 = src_y.floor() as isize;
371 let x1 = x0 + 1;
372 let y1 = y0 + 1;
373
374 let dx = src_x - x0 as f32;
375 let dy = src_y - y0 as f32;
376
377 if x0 >= 0 && x1 < self.width as isize && y0 >= 0 && y1 < self.height as isize {
379 for c in 0..channels {
380 let i00 = ((y0 as usize) * self.width + (x0 as usize)) * channels + c;
381 let i10 = ((y0 as usize) * self.width + (x1 as usize)) * channels + c;
382 let i01 = ((y1 as usize) * self.width + (x0 as usize)) * channels + c;
383 let i11 = ((y1 as usize) * self.width + (x1 as usize)) * channels + c;
384
385 let v00 = f32::from(input[i00]);
386 let v10 = f32::from(input[i10]);
387 let v01 = f32::from(input[i01]);
388 let v11 = f32::from(input[i11]);
389
390 let v0 = v00 * (1.0 - dx) + v10 * dx;
391 let v1 = v01 * (1.0 - dx) + v11 * dx;
392 let v = v0 * (1.0 - dy) + v1 * dy;
393
394 output[idx * channels + c] = v.round() as u8;
395 }
396 }
397 }
398 }
399
400 Ok(output)
401 }
402}
403
404#[derive(Debug, Clone)]
414pub struct FisheyeEquidistant {
415 pub scale: f64,
417}
418
419impl FisheyeEquidistant {
420 #[must_use]
422 pub fn new(scale: f64) -> Self {
423 Self { scale }
424 }
425
426 #[must_use]
430 pub fn distort(&self, point: &Vector2<f64>) -> Vector2<f64> {
431 let x = point[0];
432 let y = point[1];
433 let r = (x * x + y * y).sqrt();
434
435 if r < 1e-10 {
436 return *point;
437 }
438
439 let theta = r.atan();
440 let r_d = self.scale * theta;
441 let scale = r_d / r;
442
443 Vector2::new(x * scale, y * scale)
444 }
445
446 #[must_use]
450 pub fn undistort(&self, distorted: &Vector2<f64>) -> Vector2<f64> {
451 let x = distorted[0];
452 let y = distorted[1];
453 let r_d = (x * x + y * y).sqrt();
454
455 if r_d < 1e-10 {
456 return *distorted;
457 }
458
459 let theta = r_d / self.scale;
461 let r = theta.tan();
462 let scale = r / r_d;
463
464 Vector2::new(x * scale, y * scale)
465 }
466}
467
468#[derive(Debug, Clone)]
478pub struct StereographicProjection {
479 pub scale: f64,
481}
482
483impl StereographicProjection {
484 #[must_use]
486 pub fn new(scale: f64) -> Self {
487 Self { scale }
488 }
489
490 #[must_use]
492 pub fn distort(&self, point: &Vector2<f64>) -> Vector2<f64> {
493 let x = point[0];
494 let y = point[1];
495 let r = (x * x + y * y).sqrt();
496
497 if r < 1e-10 {
498 return *point;
499 }
500
501 let theta = r.atan();
502 let r_d = 2.0 * self.scale * (theta / 2.0).tan();
503 let scale = r_d / r;
504
505 Vector2::new(x * scale, y * scale)
506 }
507
508 #[must_use]
510 pub fn undistort(&self, distorted: &Vector2<f64>) -> Vector2<f64> {
511 let x = distorted[0];
512 let y = distorted[1];
513 let r_d = (x * x + y * y).sqrt();
514
515 if r_d < 1e-10 {
516 return *distorted;
517 }
518
519 let theta = 2.0 * (r_d / (2.0 * self.scale)).atan();
521 let r = theta.tan();
522 let scale = r / r_d;
523
524 Vector2::new(x * scale, y * scale)
525 }
526}
527
528#[derive(Debug, Clone)]
534pub enum ExtendedDistortionModel {
535 None,
537 BrownConrady(BrownConradyDistortion),
539 Fisheye(FisheyeDistortion),
541 FisheyeEquidistant(FisheyeEquidistant),
543 Stereographic(StereographicProjection),
545}
546
547impl ExtendedDistortionModel {
548 #[must_use]
550 pub fn distort(&self, point: &Vector2<f64>) -> Vector2<f64> {
551 match self {
552 Self::None => *point,
553 Self::BrownConrady(m) => m.distort(point),
554 Self::Fisheye(m) => m.distort(point),
555 Self::FisheyeEquidistant(m) => m.distort(point),
556 Self::Stereographic(m) => m.distort(point),
557 }
558 }
559
560 #[must_use]
562 pub fn undistort(&self, point: &Vector2<f64>) -> Vector2<f64> {
563 match self {
564 Self::None => *point,
565 Self::BrownConrady(m) => m.undistort(point),
566 Self::Fisheye(m) => m.undistort(point),
567 Self::FisheyeEquidistant(m) => m.undistort(point),
568 Self::Stereographic(m) => m.undistort(point),
569 }
570 }
571}
572
573pub struct CameraCalibrator {
575 pub board_width: usize,
577 pub board_height: usize,
579 pub square_size: f64,
581}
582
583impl CameraCalibrator {
584 #[must_use]
586 pub fn new(board_width: usize, board_height: usize, square_size: f64) -> Self {
587 Self {
588 board_width,
589 board_height,
590 square_size,
591 }
592 }
593
594 #[allow(dead_code)]
599 pub fn calibrate(
600 &self,
601 image_points: &[Vec<Point2D>],
602 image_width: usize,
603 image_height: usize,
604 ) -> AlignResult<CameraModel> {
605 if image_points.is_empty() {
606 return Err(AlignError::InsufficientData(
607 "Need at least one image for calibration".to_string(),
608 ));
609 }
610
611 let _object_points = self.generate_object_points();
613
614 let intrinsics = CameraIntrinsics::from_image_size(image_width, image_height, 60.0);
616
617 Ok(CameraModel::new(
620 intrinsics,
621 DistortionModel::BrownConrady(BrownConradyDistortion::default()),
622 ))
623 }
624
625 fn generate_object_points(&self) -> Vec<nalgebra::Vector3<f64>> {
627 let mut points = Vec::new();
628 for y in 0..self.board_height {
629 for x in 0..self.board_width {
630 points.push(nalgebra::Vector3::new(
631 x as f64 * self.square_size,
632 y as f64 * self.square_size,
633 0.0,
634 ));
635 }
636 }
637 points
638 }
639}
640
641#[cfg(test)]
642mod tests {
643 use super::*;
644
645 #[test]
648 fn test_fisheye_equidistant_identity_at_origin() {
649 let model = FisheyeEquidistant::new(1.0);
650 let origin = Vector2::new(0.0, 0.0);
651 let distorted = model.distort(&origin);
652 assert!((distorted[0]).abs() < 1e-10);
653 assert!((distorted[1]).abs() < 1e-10);
654 }
655
656 #[test]
657 fn test_fisheye_equidistant_roundtrip() {
658 let model = FisheyeEquidistant::new(1.0);
659 let point = Vector2::new(0.4, 0.3);
660 let distorted = model.distort(&point);
661 let recovered = model.undistort(&distorted);
662 assert!((recovered[0] - point[0]).abs() < 1e-8);
663 assert!((recovered[1] - point[1]).abs() < 1e-8);
664 }
665
666 #[test]
667 fn test_fisheye_equidistant_scale_effect() {
668 let m1 = FisheyeEquidistant::new(1.0);
670 let m2 = FisheyeEquidistant::new(2.0);
671 let point = Vector2::new(0.5, 0.5);
672 let d1 = m1.distort(&point);
673 let d2 = m2.distort(&point);
674 let r1 = (d1[0] * d1[0] + d1[1] * d1[1]).sqrt();
675 let r2 = (d2[0] * d2[0] + d2[1] * d2[1]).sqrt();
676 assert!(
677 r2 > r1,
678 "Larger scale should produce larger distorted radius"
679 );
680 }
681
682 #[test]
683 fn test_stereographic_identity_at_origin() {
684 let model = StereographicProjection::new(1.0);
685 let origin = Vector2::new(0.0, 0.0);
686 let distorted = model.distort(&origin);
687 assert!(distorted[0].abs() < 1e-10);
688 assert!(distorted[1].abs() < 1e-10);
689 }
690
691 #[test]
692 fn test_stereographic_roundtrip() {
693 let model = StereographicProjection::new(1.0);
694 let point = Vector2::new(0.3, 0.4);
695 let distorted = model.distort(&point);
696 let recovered = model.undistort(&distorted);
697 assert!((recovered[0] - point[0]).abs() < 1e-8);
698 assert!((recovered[1] - point[1]).abs() < 1e-8);
699 }
700
701 #[test]
702 fn test_extended_distortion_model_none() {
703 let model = ExtendedDistortionModel::None;
704 let p = Vector2::new(0.5, 0.5);
705 assert_eq!(model.distort(&p), p);
706 assert_eq!(model.undistort(&p), p);
707 }
708
709 #[test]
710 fn test_extended_distortion_brown_conrady() {
711 let bc = BrownConradyDistortion::new(0.1, 0.01, 0.0, 0.0, 0.0);
712 let model = ExtendedDistortionModel::BrownConrady(bc.clone());
713 let p = Vector2::new(0.3, 0.3);
714 assert_eq!(model.distort(&p), bc.distort(&p));
715 }
716
717 #[test]
718 fn test_extended_distortion_equidistant() {
719 let eq = FisheyeEquidistant::new(1.0);
720 let model = ExtendedDistortionModel::FisheyeEquidistant(eq.clone());
721 let p = Vector2::new(0.2, 0.2);
722 let d1 = model.distort(&p);
723 let d2 = eq.distort(&p);
724 assert!((d1[0] - d2[0]).abs() < 1e-12);
725 }
726
727 #[test]
728 fn test_extended_distortion_stereographic() {
729 let sg = StereographicProjection::new(1.0);
730 let model = ExtendedDistortionModel::Stereographic(sg.clone());
731 let p = Vector2::new(0.2, 0.3);
732 let d1 = model.distort(&p);
733 let d2 = sg.distort(&p);
734 assert!((d1[0] - d2[0]).abs() < 1e-12);
735 }
736
737 #[test]
738 fn test_stereographic_preserves_direction() {
739 let model = StereographicProjection::new(1.0);
740 let point = Vector2::new(1.0, 0.0);
741 let distorted = model.distort(&point);
742 assert!(distorted[1].abs() < 1e-12);
744 assert!(distorted[0] > 0.0);
745 }
746
747 #[test]
750 fn test_camera_intrinsics() {
751 let intrinsics = CameraIntrinsics::new(1000.0, 1000.0, 640.0, 480.0);
752 assert_eq!(intrinsics.fx, 1000.0);
753 assert_eq!(intrinsics.fy, 1000.0);
754 assert_eq!(intrinsics.cx, 640.0);
755 assert_eq!(intrinsics.cy, 480.0);
756 }
757
758 #[test]
759 fn test_pixel_to_normalized() {
760 let intrinsics = CameraIntrinsics::new(1000.0, 1000.0, 640.0, 480.0);
761 let pixel = Point2D::new(640.0, 480.0);
762 let normalized = intrinsics.pixel_to_normalized(&pixel);
763 assert!((normalized[0] - 0.0).abs() < 1e-10);
764 assert!((normalized[1] - 0.0).abs() < 1e-10);
765 }
766
767 #[test]
768 fn test_brown_conrady_no_distortion() {
769 let distortion = BrownConradyDistortion::default();
770 let point = Vector2::new(0.5, 0.5);
771 let distorted = distortion.distort(&point);
772 assert!((distorted[0] - 0.5).abs() < 1e-10);
773 assert!((distorted[1] - 0.5).abs() < 1e-10);
774 }
775
776 #[test]
777 fn test_brown_conrady_roundtrip() {
778 let distortion = BrownConradyDistortion::new(0.1, 0.01, 0.001, 0.001, 0.001);
779 let original = Vector2::new(0.5, 0.5);
780 let distorted = distortion.distort(&original);
781 let undistorted = distortion.undistort(&distorted);
782 assert!((undistorted[0] - original[0]).abs() < 1e-6);
783 assert!((undistorted[1] - original[1]).abs() < 1e-6);
784 }
785
786 #[test]
787 fn test_fisheye_no_distortion() {
788 let distortion = FisheyeDistortion::default();
792 let point = Vector2::new(0.1, 0.1);
793 let distorted = distortion.distort(&point);
794 assert!((distorted[0] - 0.1).abs() < 0.01);
796 assert!((distorted[1] - 0.1).abs() < 0.01);
797 }
798
799 #[test]
800 fn test_camera_model() {
801 let intrinsics = CameraIntrinsics::new(1000.0, 1000.0, 640.0, 480.0);
802 let model = CameraModel::new(intrinsics, DistortionModel::None);
803
804 let point_3d = nalgebra::Vector3::new(1.0, 1.0, 2.0);
805 let pixel = model.project(&point_3d);
806
807 assert!((pixel.x - 1140.0).abs() < 1e-10);
809 assert!((pixel.y - 980.0).abs() < 1e-10);
810 }
811
812 #[test]
813 fn test_image_undistorter_creation() {
814 let intrinsics = CameraIntrinsics::new(1000.0, 1000.0, 640.0, 480.0);
815 let model = CameraModel::new(intrinsics, DistortionModel::None);
816 let undistorter = ImageUndistorter::new(model, 1280, 960);
817
818 assert_eq!(undistorter.width, 1280);
819 assert_eq!(undistorter.height, 960);
820 assert_eq!(undistorter.map_x.len(), 1280 * 960);
821 assert_eq!(undistorter.map_y.len(), 1280 * 960);
822 }
823}