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 From<&[f64]> for UcmCamera {
244 fn from(params: &[f64]) -> Self {
245 assert!(
246 params.len() >= 5,
247 "UcmCamera requires at least 5 parameters, got {}",
248 params.len()
249 );
250 Self {
251 pinhole: PinholeParams {
252 fx: params[0],
253 fy: params[1],
254 cx: params[2],
255 cy: params[3],
256 },
257 distortion: DistortionModel::UCM { alpha: params[4] },
258 }
259 }
260}
261
262impl From<[f64; 5]> for UcmCamera {
268 fn from(params: [f64; 5]) -> Self {
269 Self {
270 pinhole: PinholeParams {
271 fx: params[0],
272 fy: params[1],
273 cx: params[2],
274 cy: params[3],
275 },
276 distortion: DistortionModel::UCM { alpha: params[4] },
277 }
278 }
279}
280
281pub fn try_from_params(params: &[f64]) -> Result<UcmCamera, CameraModelError> {
291 if params.len() < 5 {
292 return Err(CameraModelError::InvalidParams(format!(
293 "UcmCamera requires at least 5 parameters, got {}",
294 params.len()
295 )));
296 }
297 let camera = UcmCamera::from(params);
298 camera.validate_params()?;
299 Ok(camera)
300}
301
302impl CameraModel for UcmCamera {
303 const INTRINSIC_DIM: usize = 5;
304 type IntrinsicJacobian = SMatrix<f64, 2, 5>;
305 type PointJacobian = SMatrix<f64, 2, 3>;
306
307 fn project(&self, p_cam: &Vector3<f64>) -> Result<Vector2<f64>, CameraModelError> {
330 let x = p_cam[0];
331 let y = p_cam[1];
332 let z = p_cam[2];
333
334 let d = (x * x + y * y + z * z).sqrt();
335 let alpha = self.distortion_params();
336 let denom = alpha * d + (1.0 - alpha) * z;
337
338 if !self.check_projection_condition(z, d) {
340 return Err(CameraModelError::PointBehindCamera {
341 z,
342 min_z: crate::GEOMETRIC_PRECISION,
343 });
344 }
345
346 if denom < crate::GEOMETRIC_PRECISION {
347 return Err(CameraModelError::DenominatorTooSmall {
348 denom,
349 threshold: crate::GEOMETRIC_PRECISION,
350 });
351 }
352
353 Ok(Vector2::new(
354 self.pinhole.fx * x / denom + self.pinhole.cx,
355 self.pinhole.fy * y / denom + self.pinhole.cy,
356 ))
357 }
358
359 fn unproject(&self, point_2d: &Vector2<f64>) -> Result<Vector3<f64>, CameraModelError> {
377 let u = point_2d.x;
378 let v = point_2d.y;
379 let alpha = self.distortion_params();
380 let gamma = 1.0 - alpha;
381 let xi = alpha / gamma;
382 let mx = (u - self.pinhole.cx) / self.pinhole.fx * gamma;
383 let my = (v - self.pinhole.cy) / self.pinhole.fy * gamma;
384
385 let r_squared = mx * mx + my * my;
386
387 if !self.check_unprojection_condition(r_squared) {
389 return Err(CameraModelError::PointOutsideImage { x: u, y: v });
390 }
391
392 let num = xi + (1.0 + (1.0 - xi * xi) * r_squared).sqrt();
393 let denom = 1.0 - r_squared;
394
395 if denom < crate::GEOMETRIC_PRECISION {
396 return Err(CameraModelError::PointOutsideImage { x: u, y: v });
397 }
398
399 let coeff = num / denom;
400
401 let point3d = Vector3::new(coeff * mx, coeff * my, coeff) - Vector3::new(0.0, 0.0, xi);
402
403 Ok(point3d.normalize())
404 }
405
406 fn jacobian_point(&self, p_cam: &Vector3<f64>) -> Self::PointJacobian {
462 let x = p_cam[0];
463 let y = p_cam[1];
464 let z = p_cam[2];
465
466 let rho = (x * x + y * y + z * z).sqrt();
467 let alpha = self.distortion_params();
468
469 let d_denom_dx = alpha * x / rho;
476 let d_denom_dy = alpha * y / rho;
477 let d_denom_dz = alpha * z / rho + (1.0 - alpha);
478
479 let denom = alpha * rho + (1.0 - alpha) * z;
480
481 let denom2 = denom * denom;
493
494 let mut jac = SMatrix::<f64, 2, 3>::zeros();
495
496 jac[(0, 0)] = self.pinhole.fx * (denom - x * d_denom_dx) / denom2;
497 jac[(0, 1)] = self.pinhole.fx * (-x * d_denom_dy) / denom2;
498 jac[(0, 2)] = self.pinhole.fx * (-x * d_denom_dz) / denom2;
499
500 jac[(1, 0)] = self.pinhole.fy * (-y * d_denom_dx) / denom2;
501 jac[(1, 1)] = self.pinhole.fy * (denom - y * d_denom_dy) / denom2;
502 jac[(1, 2)] = self.pinhole.fy * (-y * d_denom_dz) / denom2;
503
504 jac
505 }
506
507 fn jacobian_intrinsics(&self, p_cam: &Vector3<f64>) -> Self::IntrinsicJacobian {
553 let x = p_cam[0];
554 let y = p_cam[1];
555 let z = p_cam[2];
556
557 let rho = (x * x + y * y + z * z).sqrt();
558 let alpha = self.distortion_params();
559 let denom = alpha * rho + (1.0 - alpha) * z;
560
561 let x_norm = x / denom;
562 let y_norm = y / denom;
563
564 let u_cx = self.pinhole.fx * x_norm;
565 let v_cy = self.pinhole.fy * y_norm;
566
567 let mut jac = SMatrix::<f64, 2, 5>::zeros();
568
569 jac[(0, 0)] = x_norm;
571
572 jac[(1, 1)] = y_norm;
574
575 jac[(0, 2)] = 1.0;
577
578 jac[(1, 3)] = 1.0;
580
581 let d_denom_d_alpha = rho - z;
583
584 jac[(0, 4)] = -u_cx * d_denom_d_alpha / denom;
586 jac[(1, 4)] = -v_cy * d_denom_d_alpha / denom;
587
588 jac
589 }
590
591 fn validate_params(&self) -> Result<(), CameraModelError> {
604 self.pinhole.validate()?;
605 self.get_distortion().validate()
606 }
607
608 fn get_pinhole_params(&self) -> PinholeParams {
614 PinholeParams {
615 fx: self.pinhole.fx,
616 fy: self.pinhole.fy,
617 cx: self.pinhole.cx,
618 cy: self.pinhole.cy,
619 }
620 }
621
622 fn get_distortion(&self) -> DistortionModel {
628 self.distortion
629 }
630
631 fn get_model_name(&self) -> &'static str {
637 "ucm"
638 }
639}
640
641#[cfg(test)]
646mod tests {
647 use super::*;
648 use nalgebra::{Matrix2xX, Matrix3xX};
649
650 type TestResult = Result<(), Box<dyn std::error::Error>>;
651
652 #[test]
653 fn test_ucm_camera_creation() -> TestResult {
654 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
655 let distortion = DistortionModel::UCM { alpha: 0.5 };
656 let camera = UcmCamera::new(pinhole, distortion)?;
657
658 assert_eq!(camera.pinhole.fx, 300.0);
659 assert_eq!(camera.distortion_params(), 0.5);
660 Ok(())
661 }
662
663 #[test]
664 fn test_projection_at_optical_axis() -> TestResult {
665 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
666 let distortion = DistortionModel::UCM { alpha: 0.5 };
667 let camera = UcmCamera::new(pinhole, distortion)?;
668
669 let p_cam = Vector3::new(0.0, 0.0, 1.0);
670 let uv = camera.project(&p_cam)?;
671 assert!((uv.x - 320.0).abs() < crate::PROJECTION_TEST_TOLERANCE);
672 assert!((uv.y - 240.0).abs() < crate::PROJECTION_TEST_TOLERANCE);
673 Ok(())
674 }
675
676 #[test]
677 fn test_jacobian_point_numerical() -> TestResult {
678 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
679 let distortion = DistortionModel::UCM { alpha: 0.6 };
680 let camera = UcmCamera::new(pinhole, distortion)?;
681
682 let p_cam = Vector3::new(0.1, 0.2, 1.0);
683
684 let jac_analytical = camera.jacobian_point(&p_cam);
685 let eps = crate::NUMERICAL_DERIVATIVE_EPS;
686
687 for i in 0..3 {
688 let mut p_plus = p_cam;
689 let mut p_minus = p_cam;
690 p_plus[i] += eps;
691 p_minus[i] -= eps;
692
693 let uv_plus = camera.project(&p_plus)?;
694 let uv_minus = camera.project(&p_minus)?;
695 let num_jac = (uv_plus - uv_minus) / (2.0 * eps);
696
697 for r in 0..2 {
698 assert!(
699 jac_analytical[(r, i)].is_finite(),
700 "Jacobian [{r},{i}] is not finite"
701 );
702 let diff = (jac_analytical[(r, i)] - num_jac[r]).abs();
703 assert!(
704 diff < crate::JACOBIAN_TEST_TOLERANCE,
705 "Mismatch at ({}, {}): {} vs {}",
706 r,
707 i,
708 jac_analytical[(r, i)],
709 num_jac[r]
710 );
711 }
712 }
713 Ok(())
714 }
715
716 #[test]
717 fn test_jacobian_intrinsics_numerical() -> TestResult {
718 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
719 let distortion = DistortionModel::UCM { alpha: 0.6 };
720 let camera = UcmCamera::new(pinhole, distortion)?;
721
722 let p_cam = Vector3::new(0.1, 0.2, 1.0);
723
724 let jac_analytical = camera.jacobian_intrinsics(&p_cam);
725 let params: DVector<f64> = (&camera).into();
726 let eps = crate::NUMERICAL_DERIVATIVE_EPS;
727
728 for i in 0..5 {
729 let mut params_plus = params.clone();
730 let mut params_minus = params.clone();
731 params_plus[i] += eps;
732 params_minus[i] -= eps;
733
734 let cam_plus = UcmCamera::from(params_plus.as_slice());
735 let cam_minus = UcmCamera::from(params_minus.as_slice());
736
737 let uv_plus = cam_plus.project(&p_cam)?;
738 let uv_minus = cam_minus.project(&p_cam)?;
739 let num_jac = (uv_plus - uv_minus) / (2.0 * eps);
740
741 for r in 0..2 {
742 assert!(
743 jac_analytical[(r, i)].is_finite(),
744 "Jacobian [{r},{i}] is not finite"
745 );
746 let diff = (jac_analytical[(r, i)] - num_jac[r]).abs();
747 assert!(
748 diff < crate::JACOBIAN_TEST_TOLERANCE,
749 "Mismatch at ({}, {}): {} vs {}",
750 r,
751 i,
752 jac_analytical[(r, i)],
753 num_jac[r]
754 );
755 }
756 }
757 Ok(())
758 }
759
760 #[test]
761 fn test_ucm_from_into_traits() -> TestResult {
762 let pinhole = PinholeParams::new(400.0, 410.0, 320.0, 240.0)?;
763 let distortion = DistortionModel::UCM { alpha: 0.7 };
764 let camera = UcmCamera::new(pinhole, distortion)?;
765
766 let params: DVector<f64> = (&camera).into();
768 assert_eq!(params.len(), 5);
769 assert_eq!(params[0], 400.0);
770 assert_eq!(params[1], 410.0);
771 assert_eq!(params[2], 320.0);
772 assert_eq!(params[3], 240.0);
773 assert_eq!(params[4], 0.7);
774
775 let arr: [f64; 5] = (&camera).into();
777 assert_eq!(arr, [400.0, 410.0, 320.0, 240.0, 0.7]);
778
779 let params_slice = [450.0, 460.0, 330.0, 250.0, 0.8];
781 let camera2 = UcmCamera::from(¶ms_slice[..]);
782 assert_eq!(camera2.pinhole.fx, 450.0);
783 assert_eq!(camera2.pinhole.fy, 460.0);
784 assert_eq!(camera2.pinhole.cx, 330.0);
785 assert_eq!(camera2.pinhole.cy, 250.0);
786 assert_eq!(camera2.distortion_params(), 0.8);
787
788 let camera3 = UcmCamera::from([500.0, 510.0, 340.0, 260.0, 0.9]);
790 assert_eq!(camera3.pinhole.fx, 500.0);
791 assert_eq!(camera3.pinhole.fy, 510.0);
792 assert_eq!(camera3.distortion_params(), 0.9);
793
794 Ok(())
795 }
796
797 #[test]
798 fn test_linear_estimation() -> TestResult {
799 let gt_pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
801 let gt_distortion = DistortionModel::UCM { alpha: 0.5 };
802 let gt_camera = UcmCamera::new(gt_pinhole, gt_distortion)?;
803
804 let n_points = 50;
806 let mut pts_3d = Matrix3xX::zeros(n_points);
807 let mut pts_2d = Matrix2xX::zeros(n_points);
808 let mut valid = 0;
809
810 for i in 0..n_points {
811 let angle = i as f64 * 2.0 * std::f64::consts::PI / n_points as f64;
812 let r = 0.1 + 0.3 * (i as f64 / n_points as f64);
813 let p3d = Vector3::new(r * angle.cos(), r * angle.sin(), 1.0);
814
815 if let Ok(p2d) = gt_camera.project(&p3d) {
816 pts_3d.set_column(valid, &p3d);
817 pts_2d.set_column(valid, &p2d);
818 valid += 1;
819 }
820 }
821 let pts_3d = pts_3d.columns(0, valid).into_owned();
822 let pts_2d = pts_2d.columns(0, valid).into_owned();
823
824 let init_pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
826 let init_distortion = DistortionModel::UCM { alpha: 0.0 };
827 let mut camera = UcmCamera::new(init_pinhole, init_distortion)?;
828
829 camera.linear_estimation(&pts_3d, &pts_2d)?;
830
831 for i in 0..valid {
833 let p3d = pts_3d.column(i).into_owned();
834 let projected = camera.project(&Vector3::new(p3d.x, p3d.y, p3d.z))?;
835 let err = ((projected.x - pts_2d[(0, i)]).powi(2)
836 + (projected.y - pts_2d[(1, i)]).powi(2))
837 .sqrt();
838 assert!(err < 1.0, "Reprojection error too large: {err}");
839 }
840
841 Ok(())
842 }
843
844 #[test]
845 fn test_project_unproject_round_trip() -> TestResult {
846 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
847 let distortion = DistortionModel::UCM { alpha: 0.5 };
848 let camera = UcmCamera::new(pinhole, distortion)?;
849
850 let test_points = [
851 Vector3::new(0.1, 0.2, 1.0),
852 Vector3::new(-0.3, 0.1, 2.0),
853 Vector3::new(0.05, -0.1, 0.5),
854 ];
855
856 for p_cam in &test_points {
857 let uv = camera.project(p_cam)?;
858 let ray = camera.unproject(&uv)?;
859 let dot = ray.dot(&p_cam.normalize());
860 assert!(
861 (dot - 1.0).abs() < 1e-4,
862 "Round-trip failed: dot={dot}, expected ~1.0"
863 );
864 }
865
866 Ok(())
867 }
868
869 #[test]
870 fn test_project_returns_error_behind_camera() -> TestResult {
871 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
872 let distortion = DistortionModel::UCM { alpha: 0.5 };
873 let camera = UcmCamera::new(pinhole, distortion)?;
874 assert!(camera.project(&Vector3::new(0.0, 0.0, -1.0)).is_err());
875 Ok(())
876 }
877
878 #[test]
879 fn test_project_at_min_depth_boundary() -> TestResult {
880 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
881 let distortion = DistortionModel::UCM { alpha: 0.5 };
882 let camera = UcmCamera::new(pinhole, distortion)?;
883 let p_min = Vector3::new(0.0, 0.0, crate::MIN_DEPTH);
884 if let Ok(uv) = camera.project(&p_min) {
885 assert!(uv.x.is_finite() && uv.y.is_finite());
886 }
887 Ok(())
888 }
889}