1use crate::{CameraModel, CameraModelError, DistortionModel, PinholeParams};
41use nalgebra::{DVector, SMatrix, Vector2, Vector3};
42
43#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct UcmCamera {
46 pub pinhole: PinholeParams,
47 pub distortion: DistortionModel,
48}
49
50impl UcmCamera {
51 pub fn new(
66 pinhole: PinholeParams,
67 distortion: DistortionModel,
68 ) -> Result<Self, CameraModelError> {
69 let camera = Self {
70 pinhole,
71 distortion,
72 };
73 camera.validate_params()?;
74 Ok(camera)
75 }
76
77 fn distortion_params(&self) -> f64 {
84 match self.distortion {
85 DistortionModel::UCM { alpha } => alpha,
86 _ => 0.0,
87 }
88 }
89
90 fn check_projection_condition(&self, z: f64, d: f64) -> bool {
101 let alpha = self.distortion_params();
102 let w = if alpha <= 0.5 {
103 alpha / (1.0 - alpha)
104 } else {
105 (1.0 - alpha) / alpha
106 };
107 z > -w * d
108 }
109
110 fn check_unprojection_condition(&self, r_squared: f64) -> bool {
120 let alpha = self.distortion_params();
121 if alpha > 0.5 {
122 let gamma = 1.0 - alpha;
123 r_squared <= gamma * gamma / (2.0 * alpha - 1.0)
124 } else {
125 true
126 }
127 }
128
129 pub fn linear_estimation(
144 &mut self,
145 points_3d: &nalgebra::Matrix3xX<f64>,
146 points_2d: &nalgebra::Matrix2xX<f64>,
147 ) -> Result<(), CameraModelError> {
148 if points_2d.ncols() != points_3d.ncols() {
150 return Err(CameraModelError::InvalidParams(
151 "Number of 2D and 3D points must match".to_string(),
152 ));
153 }
154
155 let num_points = points_2d.ncols();
157 let mut a = nalgebra::DMatrix::zeros(num_points * 2, 1);
158 let mut b = nalgebra::DVector::zeros(num_points * 2);
159
160 for i in 0..num_points {
161 let x = points_3d[(0, i)];
162 let y = points_3d[(1, i)];
163 let z = points_3d[(2, i)];
164 let u = points_2d[(0, i)];
165 let v = points_2d[(1, i)];
166
167 let d = (x * x + y * y + z * z).sqrt();
168 let u_cx = u - self.pinhole.cx;
169 let v_cy = v - self.pinhole.cy;
170
171 a[(i * 2, 0)] = u_cx * (d - z);
172 a[(i * 2 + 1, 0)] = v_cy * (d - z);
173
174 b[i * 2] = (self.pinhole.fx * x) - (u_cx * z);
175 b[i * 2 + 1] = (self.pinhole.fy * y) - (v_cy * z);
176 }
177
178 let svd = a.svd(true, true);
180 let alpha = match svd.solve(&b, 1e-10) {
181 Ok(sol) => sol[0],
182 Err(err_msg) => {
183 return Err(CameraModelError::NumericalError {
184 operation: "svd_solve".to_string(),
185 details: err_msg.to_string(),
186 });
187 }
188 };
189
190 self.distortion = DistortionModel::UCM { alpha };
191
192 self.validate_params()?;
193
194 Ok(())
195 }
196}
197
198impl From<&UcmCamera> for DVector<f64> {
204 fn from(camera: &UcmCamera) -> Self {
205 let alpha = camera.distortion_params();
206 DVector::from_vec(vec![
207 camera.pinhole.fx,
208 camera.pinhole.fy,
209 camera.pinhole.cx,
210 camera.pinhole.cy,
211 alpha,
212 ])
213 }
214}
215
216impl From<&UcmCamera> for [f64; 5] {
222 fn from(camera: &UcmCamera) -> Self {
223 let alpha = camera.distortion_params();
224 [
225 camera.pinhole.fx,
226 camera.pinhole.fy,
227 camera.pinhole.cx,
228 camera.pinhole.cy,
229 alpha,
230 ]
231 }
232}
233
234impl TryFrom<&[f64]> for UcmCamera {
244 type Error = CameraModelError;
245
246 fn try_from(params: &[f64]) -> Result<Self, Self::Error> {
247 if params.len() < 5 {
248 return Err(CameraModelError::InvalidParams(format!(
249 "UcmCamera requires at least 5 parameters, got {}",
250 params.len()
251 )));
252 }
253 Ok(Self {
254 pinhole: PinholeParams {
255 fx: params[0],
256 fy: params[1],
257 cx: params[2],
258 cy: params[3],
259 },
260 distortion: DistortionModel::UCM { alpha: params[4] },
261 })
262 }
263}
264
265impl From<[f64; 5]> for UcmCamera {
271 fn from(params: [f64; 5]) -> Self {
272 Self {
273 pinhole: PinholeParams {
274 fx: params[0],
275 fy: params[1],
276 cx: params[2],
277 cy: params[3],
278 },
279 distortion: DistortionModel::UCM { alpha: params[4] },
280 }
281 }
282}
283
284pub fn try_from_params(params: &[f64]) -> Result<UcmCamera, CameraModelError> {
294 let camera = UcmCamera::try_from(params)?;
295 camera.validate_params()?;
296 Ok(camera)
297}
298
299impl CameraModel for UcmCamera {
300 const INTRINSIC_DIM: usize = 5;
301 type IntrinsicJacobian = SMatrix<f64, 2, 5>;
302 type PointJacobian = SMatrix<f64, 2, 3>;
303
304 fn project(&self, p_cam: &Vector3<f64>) -> Result<Vector2<f64>, CameraModelError> {
327 let x = p_cam[0];
328 let y = p_cam[1];
329 let z = p_cam[2];
330
331 let d = (x * x + y * y + z * z).sqrt();
332 let alpha = self.distortion_params();
333 let denom = alpha * d + (1.0 - alpha) * z;
334
335 if !self.check_projection_condition(z, d) {
337 return Err(CameraModelError::PointBehindCamera {
338 z,
339 min_z: crate::GEOMETRIC_PRECISION,
340 });
341 }
342
343 if denom < crate::GEOMETRIC_PRECISION {
344 return Err(CameraModelError::DenominatorTooSmall {
345 denom,
346 threshold: crate::GEOMETRIC_PRECISION,
347 });
348 }
349
350 Ok(Vector2::new(
351 self.pinhole.fx * x / denom + self.pinhole.cx,
352 self.pinhole.fy * y / denom + self.pinhole.cy,
353 ))
354 }
355
356 fn unproject(&self, point_2d: &Vector2<f64>) -> Result<Vector3<f64>, CameraModelError> {
374 let u = point_2d.x;
375 let v = point_2d.y;
376 let alpha = self.distortion_params();
377 let gamma = 1.0 - alpha;
378 let xi = alpha / gamma;
379 let mx = (u - self.pinhole.cx) / self.pinhole.fx * gamma;
380 let my = (v - self.pinhole.cy) / self.pinhole.fy * gamma;
381
382 let r_squared = mx * mx + my * my;
383
384 if !self.check_unprojection_condition(r_squared) {
386 return Err(CameraModelError::PointOutsideImage { x: u, y: v });
387 }
388
389 let num = xi + (1.0 + (1.0 - xi * xi) * r_squared).sqrt();
390 let denom = 1.0 - r_squared;
391
392 if denom < crate::GEOMETRIC_PRECISION {
393 return Err(CameraModelError::PointOutsideImage { x: u, y: v });
394 }
395
396 let coeff = num / denom;
397
398 let point3d = Vector3::new(coeff * mx, coeff * my, coeff) - Vector3::new(0.0, 0.0, xi);
399
400 Ok(point3d.normalize())
401 }
402
403 fn jacobian_point(&self, p_cam: &Vector3<f64>) -> Self::PointJacobian {
459 let x = p_cam[0];
460 let y = p_cam[1];
461 let z = p_cam[2];
462
463 let rho = (x * x + y * y + z * z).sqrt();
464 let alpha = self.distortion_params();
465
466 let d_denom_dx = alpha * x / rho;
473 let d_denom_dy = alpha * y / rho;
474 let d_denom_dz = alpha * z / rho + (1.0 - alpha);
475
476 let denom = alpha * rho + (1.0 - alpha) * z;
477
478 let denom2 = denom * denom;
490
491 let mut jac = SMatrix::<f64, 2, 3>::zeros();
492
493 jac[(0, 0)] = self.pinhole.fx * (denom - x * d_denom_dx) / denom2;
494 jac[(0, 1)] = self.pinhole.fx * (-x * d_denom_dy) / denom2;
495 jac[(0, 2)] = self.pinhole.fx * (-x * d_denom_dz) / denom2;
496
497 jac[(1, 0)] = self.pinhole.fy * (-y * d_denom_dx) / denom2;
498 jac[(1, 1)] = self.pinhole.fy * (denom - y * d_denom_dy) / denom2;
499 jac[(1, 2)] = self.pinhole.fy * (-y * d_denom_dz) / denom2;
500
501 jac
502 }
503
504 fn jacobian_intrinsics(&self, p_cam: &Vector3<f64>) -> Self::IntrinsicJacobian {
550 let x = p_cam[0];
551 let y = p_cam[1];
552 let z = p_cam[2];
553
554 let rho = (x * x + y * y + z * z).sqrt();
555 let alpha = self.distortion_params();
556 let denom = alpha * rho + (1.0 - alpha) * z;
557
558 let x_norm = x / denom;
559 let y_norm = y / denom;
560
561 let u_cx = self.pinhole.fx * x_norm;
562 let v_cy = self.pinhole.fy * y_norm;
563
564 let mut jac = SMatrix::<f64, 2, 5>::zeros();
565
566 jac[(0, 0)] = x_norm;
568
569 jac[(1, 1)] = y_norm;
571
572 jac[(0, 2)] = 1.0;
574
575 jac[(1, 3)] = 1.0;
577
578 let d_denom_d_alpha = rho - z;
580
581 jac[(0, 4)] = -u_cx * d_denom_d_alpha / denom;
583 jac[(1, 4)] = -v_cy * d_denom_d_alpha / denom;
584
585 jac
586 }
587
588 fn validate_params(&self) -> Result<(), CameraModelError> {
601 self.pinhole.validate()?;
602 self.get_distortion().validate()
603 }
604
605 fn get_pinhole_params(&self) -> PinholeParams {
611 PinholeParams {
612 fx: self.pinhole.fx,
613 fy: self.pinhole.fy,
614 cx: self.pinhole.cx,
615 cy: self.pinhole.cy,
616 }
617 }
618
619 fn get_distortion(&self) -> DistortionModel {
625 self.distortion
626 }
627
628 fn get_model_name(&self) -> &'static str {
634 "ucm"
635 }
636}
637
638#[cfg(test)]
643mod tests {
644 use super::*;
645 use nalgebra::{Matrix2xX, Matrix3xX};
646
647 type TestResult = Result<(), Box<dyn std::error::Error>>;
648
649 #[test]
650 fn test_ucm_camera_creation() -> TestResult {
651 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
652 let distortion = DistortionModel::UCM { alpha: 0.5 };
653 let camera = UcmCamera::new(pinhole, distortion)?;
654
655 assert_eq!(camera.pinhole.fx, 300.0);
656 assert_eq!(camera.distortion_params(), 0.5);
657 Ok(())
658 }
659
660 #[test]
661 fn test_projection_at_optical_axis() -> TestResult {
662 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
663 let distortion = DistortionModel::UCM { alpha: 0.5 };
664 let camera = UcmCamera::new(pinhole, distortion)?;
665
666 let p_cam = Vector3::new(0.0, 0.0, 1.0);
667 let uv = camera.project(&p_cam)?;
668 assert!((uv.x - 320.0).abs() < crate::PROJECTION_TEST_TOLERANCE);
669 assert!((uv.y - 240.0).abs() < crate::PROJECTION_TEST_TOLERANCE);
670 Ok(())
671 }
672
673 #[test]
674 fn test_jacobian_point_numerical() -> TestResult {
675 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
676 let distortion = DistortionModel::UCM { alpha: 0.6 };
677 let camera = UcmCamera::new(pinhole, distortion)?;
678
679 let p_cam = Vector3::new(0.1, 0.2, 1.0);
680
681 let jac_analytical = camera.jacobian_point(&p_cam);
682 let eps = crate::NUMERICAL_DERIVATIVE_EPS;
683
684 for i in 0..3 {
685 let mut p_plus = p_cam;
686 let mut p_minus = p_cam;
687 p_plus[i] += eps;
688 p_minus[i] -= eps;
689
690 let uv_plus = camera.project(&p_plus)?;
691 let uv_minus = camera.project(&p_minus)?;
692 let num_jac = (uv_plus - uv_minus) / (2.0 * eps);
693
694 for r in 0..2 {
695 assert!(
696 jac_analytical[(r, i)].is_finite(),
697 "Jacobian [{r},{i}] is not finite"
698 );
699 let diff = (jac_analytical[(r, i)] - num_jac[r]).abs();
700 assert!(
701 diff < crate::JACOBIAN_TEST_TOLERANCE,
702 "Mismatch at ({}, {}): {} vs {}",
703 r,
704 i,
705 jac_analytical[(r, i)],
706 num_jac[r]
707 );
708 }
709 }
710 Ok(())
711 }
712
713 #[test]
714 fn test_jacobian_intrinsics_numerical() -> TestResult {
715 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
716 let distortion = DistortionModel::UCM { alpha: 0.6 };
717 let camera = UcmCamera::new(pinhole, distortion)?;
718
719 let p_cam = Vector3::new(0.1, 0.2, 1.0);
720
721 let jac_analytical = camera.jacobian_intrinsics(&p_cam);
722 let params: DVector<f64> = (&camera).into();
723 let eps = crate::NUMERICAL_DERIVATIVE_EPS;
724
725 for i in 0..5 {
726 let mut params_plus = params.clone();
727 let mut params_minus = params.clone();
728 params_plus[i] += eps;
729 params_minus[i] -= eps;
730
731 let cam_plus = UcmCamera::try_from(params_plus.as_slice())?;
732 let cam_minus = UcmCamera::try_from(params_minus.as_slice())?;
733
734 let uv_plus = cam_plus.project(&p_cam)?;
735 let uv_minus = cam_minus.project(&p_cam)?;
736 let num_jac = (uv_plus - uv_minus) / (2.0 * eps);
737
738 for r in 0..2 {
739 assert!(
740 jac_analytical[(r, i)].is_finite(),
741 "Jacobian [{r},{i}] is not finite"
742 );
743 let diff = (jac_analytical[(r, i)] - num_jac[r]).abs();
744 assert!(
745 diff < crate::JACOBIAN_TEST_TOLERANCE,
746 "Mismatch at ({}, {}): {} vs {}",
747 r,
748 i,
749 jac_analytical[(r, i)],
750 num_jac[r]
751 );
752 }
753 }
754 Ok(())
755 }
756
757 #[test]
758 fn test_ucm_from_into_traits() -> TestResult {
759 let pinhole = PinholeParams::new(400.0, 410.0, 320.0, 240.0)?;
760 let distortion = DistortionModel::UCM { alpha: 0.7 };
761 let camera = UcmCamera::new(pinhole, distortion)?;
762
763 let params: DVector<f64> = (&camera).into();
765 assert_eq!(params.len(), 5);
766 assert_eq!(params[0], 400.0);
767 assert_eq!(params[1], 410.0);
768 assert_eq!(params[2], 320.0);
769 assert_eq!(params[3], 240.0);
770 assert_eq!(params[4], 0.7);
771
772 let arr: [f64; 5] = (&camera).into();
774 assert_eq!(arr, [400.0, 410.0, 320.0, 240.0, 0.7]);
775
776 let params_slice = [450.0, 460.0, 330.0, 250.0, 0.8];
778 let camera2 = UcmCamera::try_from(¶ms_slice[..])?;
779 assert_eq!(camera2.pinhole.fx, 450.0);
780 assert_eq!(camera2.pinhole.fy, 460.0);
781 assert_eq!(camera2.pinhole.cx, 330.0);
782 assert_eq!(camera2.pinhole.cy, 250.0);
783 assert_eq!(camera2.distortion_params(), 0.8);
784
785 let camera3 = UcmCamera::from([500.0, 510.0, 340.0, 260.0, 0.9]);
787 assert_eq!(camera3.pinhole.fx, 500.0);
788 assert_eq!(camera3.pinhole.fy, 510.0);
789 assert_eq!(camera3.distortion_params(), 0.9);
790
791 Ok(())
792 }
793
794 #[test]
795 fn test_linear_estimation() -> TestResult {
796 let gt_pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
798 let gt_distortion = DistortionModel::UCM { alpha: 0.5 };
799 let gt_camera = UcmCamera::new(gt_pinhole, gt_distortion)?;
800
801 let n_points = 50;
803 let mut pts_3d = Matrix3xX::zeros(n_points);
804 let mut pts_2d = Matrix2xX::zeros(n_points);
805 let mut valid = 0;
806
807 for i in 0..n_points {
808 let angle = i as f64 * 2.0 * std::f64::consts::PI / n_points as f64;
809 let r = 0.1 + 0.3 * (i as f64 / n_points as f64);
810 let p3d = Vector3::new(r * angle.cos(), r * angle.sin(), 1.0);
811
812 if let Ok(p2d) = gt_camera.project(&p3d) {
813 pts_3d.set_column(valid, &p3d);
814 pts_2d.set_column(valid, &p2d);
815 valid += 1;
816 }
817 }
818 let pts_3d = pts_3d.columns(0, valid).into_owned();
819 let pts_2d = pts_2d.columns(0, valid).into_owned();
820
821 let init_pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
823 let init_distortion = DistortionModel::UCM { alpha: 0.0 };
824 let mut camera = UcmCamera::new(init_pinhole, init_distortion)?;
825
826 camera.linear_estimation(&pts_3d, &pts_2d)?;
827
828 for i in 0..valid {
830 let col = pts_3d.column(i);
831 let projected = camera.project(&Vector3::new(col[0], col[1], col[2]))?;
832 let err = ((projected.x - pts_2d[(0, i)]).powi(2)
833 + (projected.y - pts_2d[(1, i)]).powi(2))
834 .sqrt();
835 assert!(err < 1.0, "Reprojection error too large: {err}");
836 }
837
838 Ok(())
839 }
840
841 #[test]
842 fn test_project_unproject_round_trip() -> TestResult {
843 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
844 let distortion = DistortionModel::UCM { alpha: 0.5 };
845 let camera = UcmCamera::new(pinhole, distortion)?;
846
847 let test_points = [
848 Vector3::new(0.1, 0.2, 1.0),
849 Vector3::new(-0.3, 0.1, 2.0),
850 Vector3::new(0.05, -0.1, 0.5),
851 ];
852
853 for p_cam in &test_points {
854 let uv = camera.project(p_cam)?;
855 let ray = camera.unproject(&uv)?;
856 let dot = ray.dot(&p_cam.normalize());
857 assert!(
858 (dot - 1.0).abs() < 1e-4,
859 "Round-trip failed: dot={dot}, expected ~1.0"
860 );
861 }
862
863 Ok(())
864 }
865
866 #[test]
867 fn test_project_returns_error_behind_camera() -> TestResult {
868 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
869 let distortion = DistortionModel::UCM { alpha: 0.5 };
870 let camera = UcmCamera::new(pinhole, distortion)?;
871 assert!(camera.project(&Vector3::new(0.0, 0.0, -1.0)).is_err());
872 Ok(())
873 }
874
875 #[test]
876 fn test_project_at_min_depth_boundary() -> TestResult {
877 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
878 let distortion = DistortionModel::UCM { alpha: 0.5 };
879 let camera = UcmCamera::new(pinhole, distortion)?;
880 let p_min = Vector3::new(0.0, 0.0, crate::MIN_DEPTH);
881 if let Ok(uv) = camera.project(&p_min) {
882 assert!(uv.x.is_finite() && uv.y.is_finite());
883 }
884 Ok(())
885 }
886
887 #[test]
888 fn test_projection_off_axis() -> TestResult {
889 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
890 let distortion = DistortionModel::UCM { alpha: 0.5 };
891 let camera = UcmCamera::new(pinhole, distortion)?;
892 let p_cam = Vector3::new(0.3, 0.0, 1.0);
893 let uv = camera.project(&p_cam)?;
894 assert!(
895 uv.x > 320.0,
896 "off-axis point should project right of principal point"
897 );
898 assert!(
899 (uv.y - 240.0).abs() < 1.0,
900 "y should be close to cy for horizontal offset"
901 );
902 Ok(())
903 }
904
905 #[test]
906 fn test_unproject_center_pixel() -> TestResult {
907 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
908 let distortion = DistortionModel::UCM { alpha: 0.5 };
909 let camera = UcmCamera::new(pinhole, distortion)?;
910 let uv = Vector2::new(320.0, 240.0);
911 let ray = camera.unproject(&uv)?;
912 assert!(ray.x.abs() < 1e-6, "x should be ~0, got {}", ray.x);
913 assert!(ray.y.abs() < 1e-6, "y should be ~0, got {}", ray.y);
914 assert!((ray.z - 1.0).abs() < 1e-6, "z should be ~1, got {}", ray.z);
915 Ok(())
916 }
917
918 #[test]
919 fn test_batch_projection_matches_individual() -> TestResult {
920 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
921 let distortion = DistortionModel::UCM { alpha: 0.5 };
922 let camera = UcmCamera::new(pinhole, distortion)?;
923 let pts = Matrix3xX::from_columns(&[
924 Vector3::new(0.0, 0.0, 1.0),
925 Vector3::new(0.3, 0.2, 1.5),
926 Vector3::new(-0.4, 0.1, 2.0),
927 ]);
928 let batch = camera.project_batch(&pts);
929 for i in 0..3 {
930 let col = pts.column(i);
931 let p = camera.project(&Vector3::new(col[0], col[1], col[2]))?;
932 assert!(
933 (batch[(0, i)] - p.x).abs() < 1e-10,
934 "batch u mismatch at col {i}"
935 );
936 assert!(
937 (batch[(1, i)] - p.y).abs() < 1e-10,
938 "batch v mismatch at col {i}"
939 );
940 }
941 Ok(())
942 }
943
944 #[test]
945 fn test_jacobian_dimensions() -> TestResult {
946 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
947 let distortion = DistortionModel::UCM { alpha: 0.5 };
948 let camera = UcmCamera::new(pinhole, distortion)?;
949 let p_cam = Vector3::new(0.1, 0.2, 1.0);
950 let jac_point = camera.jacobian_point(&p_cam);
951 assert_eq!(jac_point.nrows(), 2);
952 assert_eq!(jac_point.ncols(), 3);
953 let jac_intr = camera.jacobian_intrinsics(&p_cam);
954 assert_eq!(jac_intr.nrows(), 2);
955 assert_eq!(jac_intr.ncols(), 5); Ok(())
957 }
958}