1use crate::{CameraModel, CameraModelError, DistortionModel, PinholeParams};
44use nalgebra::{DVector, SMatrix, Vector2, Vector3};
45use std::fmt;
46
47#[derive(Clone, Copy, PartialEq)]
49pub struct DoubleSphereCamera {
50 pub pinhole: PinholeParams,
51 pub distortion: DistortionModel,
52}
53
54impl DoubleSphereCamera {
55 pub fn new(
83 pinhole: PinholeParams,
84 distortion: DistortionModel,
85 ) -> Result<Self, CameraModelError> {
86 let model = Self {
87 pinhole,
88 distortion,
89 };
90 model.validate_params()?;
91 Ok(model)
92 }
93
94 fn distortion_params(&self) -> (f64, f64) {
102 match self.distortion {
103 DistortionModel::DoubleSphere { xi, alpha } => (xi, alpha),
104 _ => (0.0, 0.0),
105 }
106 }
107
108 fn check_projection_condition(&self, z: f64, d1: f64) -> Result<bool, CameraModelError> {
119 let (xi, alpha) = self.distortion_params();
120 let w1 = if alpha > 0.5 {
121 (1.0 - alpha) / alpha
122 } else {
123 alpha / (1.0 - alpha)
124 };
125 let w2 = (w1 + xi) / (2.0 * w1 * xi + xi * xi + 1.0).sqrt();
126 Ok(z > -w2 * d1)
127 }
128
129 fn check_unprojection_condition(&self, r_squared: f64) -> Result<bool, CameraModelError> {
139 let (alpha, _) = self.distortion_params();
140 if alpha > 0.5 && r_squared > 1.0 / (2.0 * alpha - 1.0) {
141 return Ok(false);
142 }
143 Ok(true)
144 }
145
146 pub fn linear_estimation(
161 &mut self,
162 points_3d: &nalgebra::Matrix3xX<f64>,
163 points_2d: &nalgebra::Matrix2xX<f64>,
164 ) -> Result<(), CameraModelError> {
165 if points_2d.ncols() != points_3d.ncols() {
166 return Err(CameraModelError::InvalidParams(
167 "Number of 2D and 3D points must match".to_string(),
168 ));
169 }
170
171 let num_points = points_2d.ncols();
172 let mut a = nalgebra::DMatrix::zeros(num_points * 2, 1);
173 let mut b = nalgebra::DVector::zeros(num_points * 2);
174
175 for i in 0..num_points {
176 let x = points_3d[(0, i)];
177 let y = points_3d[(1, i)];
178 let z = points_3d[(2, i)];
179 let u = points_2d[(0, i)];
180 let v = points_2d[(1, i)];
181
182 let d = (x * x + y * y + z * z).sqrt();
183 let u_cx = u - self.pinhole.cx;
184 let v_cy = v - self.pinhole.cy;
185
186 a[(i * 2, 0)] = u_cx * (d - z);
187 a[(i * 2 + 1, 0)] = v_cy * (d - z);
188
189 b[i * 2] = (self.pinhole.fx * x) - (u_cx * z);
190 b[i * 2 + 1] = (self.pinhole.fy * y) - (v_cy * z);
191 }
192
193 let svd = a.svd(true, true);
194 let alpha = match svd.solve(&b, 1e-10) {
195 Ok(sol) => sol[0],
196 Err(err_msg) => {
197 return Err(CameraModelError::NumericalError {
198 operation: "svd_solve".to_string(),
199 details: err_msg.to_string(),
200 });
201 }
202 };
203
204 self.distortion = DistortionModel::DoubleSphere { xi: 0.0, alpha };
206
207 self.validate_params()?;
209
210 Ok(())
211 }
212}
213
214impl fmt::Debug for DoubleSphereCamera {
216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217 let (xi, alpha) = self.distortion_params();
218 write!(
219 f,
220 "DoubleSphere [fx: {} fy: {} cx: {} cy: {} alpha: {} xi: {}]",
221 self.pinhole.fx, self.pinhole.fy, self.pinhole.cx, self.pinhole.cy, alpha, xi
222 )
223 }
224}
225
226impl From<&DoubleSphereCamera> for DVector<f64> {
230 fn from(camera: &DoubleSphereCamera) -> Self {
231 let (xi, alpha) = camera.distortion_params();
232 DVector::from_vec(vec![
233 camera.pinhole.fx,
234 camera.pinhole.fy,
235 camera.pinhole.cx,
236 camera.pinhole.cy,
237 xi,
238 alpha,
239 ])
240 }
241}
242
243impl From<&DoubleSphereCamera> for [f64; 6] {
247 fn from(camera: &DoubleSphereCamera) -> Self {
248 let (xi, alpha) = camera.distortion_params();
249 [
250 camera.pinhole.fx,
251 camera.pinhole.fy,
252 camera.pinhole.cx,
253 camera.pinhole.cy,
254 xi,
255 alpha,
256 ]
257 }
258}
259
260impl TryFrom<&[f64]> for DoubleSphereCamera {
270 type Error = CameraModelError;
271
272 fn try_from(params: &[f64]) -> Result<Self, Self::Error> {
273 if params.len() < 6 {
274 return Err(CameraModelError::InvalidParams(format!(
275 "DoubleSphereCamera requires at least 6 parameters, got {}",
276 params.len()
277 )));
278 }
279 Ok(Self {
280 pinhole: PinholeParams {
281 fx: params[0],
282 fy: params[1],
283 cx: params[2],
284 cy: params[3],
285 },
286 distortion: DistortionModel::DoubleSphere {
287 xi: params[4],
288 alpha: params[5],
289 },
290 })
291 }
292}
293
294impl From<[f64; 6]> for DoubleSphereCamera {
300 fn from(params: [f64; 6]) -> Self {
301 Self {
302 pinhole: PinholeParams {
303 fx: params[0],
304 fy: params[1],
305 cx: params[2],
306 cy: params[3],
307 },
308 distortion: DistortionModel::DoubleSphere {
309 xi: params[4],
310 alpha: params[5],
311 },
312 }
313 }
314}
315
316pub fn try_from_params(params: &[f64]) -> Result<DoubleSphereCamera, CameraModelError> {
326 let camera = DoubleSphereCamera::try_from(params)?;
327 camera.validate_params()?;
328 Ok(camera)
329}
330
331impl CameraModel for DoubleSphereCamera {
332 const INTRINSIC_DIM: usize = 6;
333 type IntrinsicJacobian = SMatrix<f64, 2, 6>;
334 type PointJacobian = SMatrix<f64, 2, 3>;
335
336 fn project(&self, p_cam: &Vector3<f64>) -> Result<Vector2<f64>, CameraModelError> {
362 let x = p_cam[0];
363 let y = p_cam[1];
364 let z = p_cam[2];
365
366 let (xi, alpha) = self.distortion_params();
367 let r2 = x * x + y * y;
368 let d1 = (r2 + z * z).sqrt();
369
370 if !self.check_projection_condition(z, d1)? {
372 return Err(CameraModelError::ProjectionOutOfBounds);
373 }
374
375 let xi_d1_z = xi * d1 + z;
376 let d2 = (r2 + xi_d1_z * xi_d1_z).sqrt();
377 let denom = alpha * d2 + (1.0 - alpha) * xi_d1_z;
378
379 if denom < crate::GEOMETRIC_PRECISION {
380 return Err(CameraModelError::DenominatorTooSmall {
381 denom,
382 threshold: crate::GEOMETRIC_PRECISION,
383 });
384 }
385
386 Ok(Vector2::new(
387 self.pinhole.fx * x / denom + self.pinhole.cx,
388 self.pinhole.fy * y / denom + self.pinhole.cy,
389 ))
390 }
391
392 fn unproject(&self, point_2d: &Vector2<f64>) -> Result<Vector3<f64>, CameraModelError> {
410 let u = point_2d.x;
411 let v = point_2d.y;
412
413 let (xi, alpha) = self.distortion_params();
414 let mx = (u - self.pinhole.cx) / self.pinhole.fx;
415 let my = (v - self.pinhole.cy) / self.pinhole.fy;
416 let r2 = mx * mx + my * my;
417
418 if !self.check_unprojection_condition(r2)? {
419 return Err(CameraModelError::PointOutsideImage { x: u, y: v });
420 }
421
422 let mz_num = 1.0 - alpha * alpha * r2;
423 let mz_denom = alpha * (1.0 - (2.0 * alpha - 1.0) * r2).sqrt() + (1.0 - alpha);
424 let mz = mz_num / mz_denom;
425
426 let mz2 = mz * mz;
427
428 let num_term = mz * xi + (mz2 + (1.0 - xi * xi) * r2).sqrt();
429 let denom_term = mz2 + r2;
430
431 if denom_term < crate::GEOMETRIC_PRECISION {
432 return Err(CameraModelError::PointOutsideImage { x: u, y: v });
433 }
434
435 let k = num_term / denom_term;
436
437 let x = k * mx;
438 let y = k * my;
439 let z = k * mz - xi;
440
441 let norm = (x * x + y * y + z * z).sqrt();
443 Ok(Vector3::new(x / norm, y / norm, z / norm))
444 }
445
446 fn jacobian_point(&self, p_cam: &Vector3<f64>) -> Self::PointJacobian {
533 let x = p_cam[0];
534 let y = p_cam[1];
535 let z = p_cam[2];
536
537 let (xi, alpha) = self.distortion_params();
538 let r2 = x * x + y * y;
539 let d1 = (r2 + z * z).sqrt();
540 let xi_d1_z = xi * d1 + z;
541 let d2 = (r2 + xi_d1_z * xi_d1_z).sqrt();
542 let denom = alpha * d2 + (1.0 - alpha) * xi_d1_z;
543
544 let inv_d1 = 1.0 / d1;
546 let inv_d2 = 1.0 / d2;
547
548 let dd1_dx = x * inv_d1;
550 let dd1_dy = y * inv_d1;
551 let dd1_dz = z * inv_d1;
552
553 let d_xi_d1_z_dx = xi * dd1_dx;
555 let d_xi_d1_z_dy = xi * dd1_dy;
556 let d_xi_d1_z_dz = xi * dd1_dz + 1.0;
557
558 let dd2_dx = (x + xi_d1_z * d_xi_d1_z_dx) * inv_d2;
560 let dd2_dy = (y + xi_d1_z * d_xi_d1_z_dy) * inv_d2;
561 let dd2_dz = (xi_d1_z * d_xi_d1_z_dz) * inv_d2;
562
563 let ddenom_dx = alpha * dd2_dx + (1.0 - alpha) * d_xi_d1_z_dx;
565 let ddenom_dy = alpha * dd2_dy + (1.0 - alpha) * d_xi_d1_z_dy;
566 let ddenom_dz = alpha * dd2_dz + (1.0 - alpha) * d_xi_d1_z_dz;
567
568 let denom2 = denom * denom;
569
570 let du_dx = self.pinhole.fx * (denom - x * ddenom_dx) / denom2;
572 let du_dy = self.pinhole.fx * (-x * ddenom_dy) / denom2;
573 let du_dz = self.pinhole.fx * (-x * ddenom_dz) / denom2;
574
575 let dv_dx = self.pinhole.fy * (-y * ddenom_dx) / denom2;
576 let dv_dy = self.pinhole.fy * (denom - y * ddenom_dy) / denom2;
577 let dv_dz = self.pinhole.fy * (-y * ddenom_dz) / denom2;
578
579 SMatrix::<f64, 2, 3>::new(du_dx, du_dy, du_dz, dv_dx, dv_dy, dv_dz)
580 }
581
582 fn jacobian_intrinsics(&self, p_cam: &Vector3<f64>) -> Self::IntrinsicJacobian {
692 let x = p_cam[0];
693 let y = p_cam[1];
694 let z = p_cam[2];
695
696 let (xi, alpha) = self.distortion_params();
697 let r2 = x * x + y * y;
698 let d1 = (r2 + z * z).sqrt();
699 let xi_d1_z = xi * d1 + z;
700 let d2 = (r2 + xi_d1_z * xi_d1_z).sqrt();
701 let denom = alpha * d2 + (1.0 - alpha) * xi_d1_z;
702
703 let inv_denom = 1.0 / denom;
705 let inv_d2 = 1.0 / d2;
706
707 let x_norm = x * inv_denom;
708 let y_norm = y * inv_denom;
709
710 let d_xi_d1_z_dxi = d1;
715 let dd2_dxi = (xi_d1_z * d_xi_d1_z_dxi) * inv_d2;
716 let ddenom_dxi = alpha * dd2_dxi + (1.0 - alpha) * d_xi_d1_z_dxi;
717
718 let ddenom_dalpha = d2 - xi_d1_z;
719
720 let inv_denom2 = inv_denom * inv_denom;
721
722 let du_dxi = -self.pinhole.fx * x * ddenom_dxi * inv_denom2;
723 let dv_dxi = -self.pinhole.fy * y * ddenom_dxi * inv_denom2;
724
725 let du_dalpha = -self.pinhole.fx * x * ddenom_dalpha * inv_denom2;
726 let dv_dalpha = -self.pinhole.fy * y * ddenom_dalpha * inv_denom2;
727
728 SMatrix::<f64, 2, 6>::new(
729 x_norm, 0.0, 1.0, 0.0, du_dxi, du_dalpha, 0.0, y_norm, 0.0, 1.0, dv_dxi, dv_dalpha,
730 )
731 }
732
733 fn validate_params(&self) -> Result<(), CameraModelError> {
747 self.pinhole.validate()?;
748 self.get_distortion().validate()
749 }
750
751 fn get_pinhole_params(&self) -> PinholeParams {
757 PinholeParams {
758 fx: self.pinhole.fx,
759 fy: self.pinhole.fy,
760 cx: self.pinhole.cx,
761 cy: self.pinhole.cy,
762 }
763 }
764
765 fn get_distortion(&self) -> DistortionModel {
771 self.distortion
772 }
773
774 fn get_model_name(&self) -> &'static str {
780 "double_sphere"
781 }
782}
783
784#[cfg(test)]
785mod tests {
786 use super::*;
787 use nalgebra::{Matrix2xX, Matrix3xX};
788
789 type TestResult = Result<(), Box<dyn std::error::Error>>;
790
791 #[test]
792 fn test_double_sphere_camera_creation() -> TestResult {
793 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
794 let distortion = DistortionModel::DoubleSphere {
795 xi: -0.2,
796 alpha: 0.6,
797 };
798 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
799 assert_eq!(camera.pinhole.fx, 300.0);
800 let (xi, alpha) = camera.distortion_params();
801 assert_eq!(alpha, 0.6);
802 assert_eq!(xi, -0.2);
803
804 Ok(())
805 }
806
807 #[test]
808 fn test_projection_at_optical_axis() -> TestResult {
809 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
810 let distortion = DistortionModel::DoubleSphere {
811 xi: -0.2,
812 alpha: 0.6,
813 };
814 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
815 let p_cam = Vector3::new(0.0, 0.0, 1.0);
816 let uv = camera.project(&p_cam)?;
817
818 assert!((uv.x - 320.0).abs() < crate::PROJECTION_TEST_TOLERANCE);
819 assert!((uv.y - 240.0).abs() < crate::PROJECTION_TEST_TOLERANCE);
820
821 Ok(())
822 }
823
824 #[test]
825 fn test_jacobian_point_numerical() -> TestResult {
826 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
827 let distortion = DistortionModel::DoubleSphere {
828 xi: -0.2,
829 alpha: 0.6,
830 };
831 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
832 let p_cam = Vector3::new(0.1, 0.2, 1.0);
833
834 let jac_analytical = camera.jacobian_point(&p_cam);
835 let eps = crate::NUMERICAL_DERIVATIVE_EPS;
836
837 for i in 0..3 {
838 let mut p_plus = p_cam;
839 let mut p_minus = p_cam;
840 p_plus[i] += eps;
841 p_minus[i] -= eps;
842
843 let uv_plus = camera.project(&p_plus)?;
844 let uv_minus = camera.project(&p_minus)?;
845 let num_jac = (uv_plus - uv_minus) / (2.0 * eps);
846
847 for r in 0..2 {
848 assert!(
849 jac_analytical[(r, i)].is_finite(),
850 "Jacobian [{r},{i}] is not finite"
851 );
852 let diff = (jac_analytical[(r, i)] - num_jac[r]).abs();
853 assert!(
854 diff < crate::JACOBIAN_TEST_TOLERANCE,
855 "Mismatch at ({}, {})",
856 r,
857 i
858 );
859 }
860 }
861 Ok(())
862 }
863
864 #[test]
865 fn test_jacobian_intrinsics_numerical() -> TestResult {
866 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
867 let distortion = DistortionModel::DoubleSphere {
868 xi: -0.2,
869 alpha: 0.6,
870 };
871 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
872 let p_cam = Vector3::new(0.1, 0.2, 1.0);
873
874 let jac_analytical = camera.jacobian_intrinsics(&p_cam);
875 let params: DVector<f64> = (&camera).into();
876 let eps = crate::NUMERICAL_DERIVATIVE_EPS;
877
878 for i in 0..6 {
879 let mut params_plus = params.clone();
880 let mut params_minus = params.clone();
881 params_plus[i] += eps;
882 params_minus[i] -= eps;
883
884 let cam_plus = DoubleSphereCamera::try_from(params_plus.as_slice())?;
885 let cam_minus = DoubleSphereCamera::try_from(params_minus.as_slice())?;
886
887 let uv_plus = cam_plus.project(&p_cam)?;
888 let uv_minus = cam_minus.project(&p_cam)?;
889 let num_jac = (uv_plus - uv_minus) / (2.0 * eps);
890
891 for r in 0..2 {
892 assert!(
893 jac_analytical[(r, i)].is_finite(),
894 "Jacobian [{r},{i}] is not finite"
895 );
896 let diff = (jac_analytical[(r, i)] - num_jac[r]).abs();
897 assert!(
898 diff < crate::JACOBIAN_TEST_TOLERANCE,
899 "Mismatch at ({}, {})",
900 r,
901 i
902 );
903 }
904 }
905 Ok(())
906 }
907
908 #[test]
909 fn test_linear_estimation() -> TestResult {
910 let gt_pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
912 let gt_distortion = DistortionModel::DoubleSphere {
913 xi: 0.0,
914 alpha: 0.6,
915 };
916 let gt_camera = DoubleSphereCamera::new(gt_pinhole, gt_distortion)?;
917
918 let n_points = 50;
920 let mut pts_3d = Matrix3xX::zeros(n_points);
921 let mut pts_2d = Matrix2xX::zeros(n_points);
922 let mut valid = 0;
923
924 for i in 0..n_points {
925 let angle = i as f64 * 2.0 * std::f64::consts::PI / n_points as f64;
926 let r = 0.1 + 0.3 * (i as f64 / n_points as f64);
927 let p3d = Vector3::new(r * angle.cos(), r * angle.sin(), 1.0);
928
929 if let Ok(p2d) = gt_camera.project(&p3d) {
930 pts_3d.set_column(valid, &p3d);
931 pts_2d.set_column(valid, &p2d);
932 valid += 1;
933 }
934 }
935 let pts_3d = pts_3d.columns(0, valid).into_owned();
936 let pts_2d = pts_2d.columns(0, valid).into_owned();
937
938 let init_pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
940 let init_distortion = DistortionModel::DoubleSphere {
941 xi: 0.0,
942 alpha: 0.1,
943 };
944 let mut camera = DoubleSphereCamera::new(init_pinhole, init_distortion)?;
945
946 camera.linear_estimation(&pts_3d, &pts_2d)?;
947
948 for i in 0..valid {
950 let col = pts_3d.column(i);
951 let projected = camera.project(&Vector3::new(col[0], col[1], col[2]))?;
952 let err = ((projected.x - pts_2d[(0, i)]).powi(2)
953 + (projected.y - pts_2d[(1, i)]).powi(2))
954 .sqrt();
955 assert!(err < 1.0, "Reprojection error too large: {err}");
956 }
957
958 Ok(())
959 }
960
961 #[test]
962 fn test_project_unproject_round_trip() -> TestResult {
963 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
964 let distortion = DistortionModel::DoubleSphere {
965 xi: -0.2,
966 alpha: 0.6,
967 };
968 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
969
970 let test_points = [
971 Vector3::new(0.1, 0.2, 1.0),
972 Vector3::new(-0.3, 0.1, 2.0),
973 Vector3::new(0.05, -0.1, 0.5),
974 ];
975
976 for p_cam in &test_points {
977 let uv = camera.project(p_cam)?;
978 let ray = camera.unproject(&uv)?;
979 let dot = ray.dot(&p_cam.normalize());
980 assert!(
981 (dot - 1.0).abs() < 1e-6,
982 "Round-trip failed: dot={dot}, expected ~1.0"
983 );
984 }
985
986 Ok(())
987 }
988
989 #[test]
990 fn test_project_returns_error_behind_camera() -> TestResult {
991 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
992 let distortion = DistortionModel::DoubleSphere {
993 xi: -0.2,
994 alpha: 0.6,
995 };
996 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
997 assert!(camera.project(&Vector3::new(0.0, 0.0, -1.0)).is_err());
998 Ok(())
999 }
1000
1001 #[test]
1002 fn test_project_at_min_depth_boundary() -> TestResult {
1003 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
1004 let distortion = DistortionModel::DoubleSphere {
1005 xi: -0.2,
1006 alpha: 0.6,
1007 };
1008 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
1009 let p_min = Vector3::new(0.0, 0.0, crate::MIN_DEPTH);
1010 if let Ok(uv) = camera.project(&p_min) {
1011 assert!(uv.x.is_finite() && uv.y.is_finite());
1012 }
1013 Ok(())
1014 }
1015
1016 #[test]
1017 fn test_debug_format() -> TestResult {
1018 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
1019 let distortion = DistortionModel::DoubleSphere {
1020 xi: -0.2,
1021 alpha: 0.6,
1022 };
1023 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
1024 let s = format!("{:?}", camera);
1025 assert!(
1026 s.contains("DoubleSphere"),
1027 "Debug output should contain 'DoubleSphere', got: {s}"
1028 );
1029 assert!(
1030 s.contains("300"),
1031 "Debug output should contain focal length, got: {s}"
1032 );
1033 Ok(())
1034 }
1035
1036 #[test]
1037 fn test_from_camera_to_fixed_array() -> TestResult {
1038 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
1039 let distortion = DistortionModel::DoubleSphere {
1040 xi: -0.2,
1041 alpha: 0.6,
1042 };
1043 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
1044 let arr: [f64; 6] = (&camera).into();
1045 assert_eq!(arr[0], 300.0); assert_eq!(arr[1], 300.0); assert_eq!(arr[2], 320.0); assert_eq!(arr[3], 240.0); assert!((arr[4] - (-0.2)).abs() < 1e-15); assert!((arr[5] - 0.6).abs() < 1e-15); Ok(())
1052 }
1053
1054 #[test]
1055 fn test_from_fixed_array_to_camera() {
1056 let arr = [300.0f64, 300.0, 320.0, 240.0, -0.2, 0.6];
1057 let camera = DoubleSphereCamera::from(arr);
1058 assert_eq!(camera.pinhole.fx, 300.0);
1059 let (xi, alpha) = camera.distortion_params();
1060 assert!((xi - (-0.2)).abs() < 1e-15);
1061 assert!((alpha - 0.6).abs() < 1e-15);
1062 }
1063
1064 #[test]
1065 fn test_try_from_params_valid() -> TestResult {
1066 let params = [300.0f64, 300.0, 320.0, 240.0, -0.2, 0.6];
1067 let camera = try_from_params(¶ms)?;
1068 assert_eq!(camera.pinhole.fx, 300.0);
1069 Ok(())
1070 }
1071
1072 #[test]
1073 fn test_try_from_params_too_few() {
1074 let params = [300.0f64, 300.0, 320.0];
1075 let result = try_from_params(¶ms);
1076 assert!(result.is_err(), "Should fail with fewer than 6 params");
1077 }
1078
1079 #[test]
1080 fn test_try_from_params_invalid_alpha() {
1081 let params = [300.0f64, 300.0, 320.0, 240.0, 0.0, 0.0]; let result = try_from_params(¶ms);
1083 assert!(result.is_err(), "Should fail with alpha = 0 (must be > 0)");
1084 }
1085
1086 #[test]
1087 fn test_get_pinhole_params() -> TestResult {
1088 let pinhole = PinholeParams::new(300.0, 301.0, 320.0, 241.0)?;
1089 let distortion = DistortionModel::DoubleSphere {
1090 xi: -0.2,
1091 alpha: 0.6,
1092 };
1093 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
1094 let p = camera.get_pinhole_params();
1095 assert_eq!(p.fx, 300.0);
1096 assert_eq!(p.fy, 301.0);
1097 assert_eq!(p.cx, 320.0);
1098 assert_eq!(p.cy, 241.0);
1099 Ok(())
1100 }
1101
1102 #[test]
1103 fn test_get_distortion() -> TestResult {
1104 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
1105 let distortion = DistortionModel::DoubleSphere {
1106 xi: -0.2,
1107 alpha: 0.6,
1108 };
1109 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
1110 let d = camera.get_distortion();
1111 assert_eq!(
1112 d,
1113 DistortionModel::DoubleSphere {
1114 xi: -0.2,
1115 alpha: 0.6
1116 }
1117 );
1118 Ok(())
1119 }
1120
1121 #[test]
1122 fn test_get_model_name() -> TestResult {
1123 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
1124 let distortion = DistortionModel::DoubleSphere {
1125 xi: -0.2,
1126 alpha: 0.6,
1127 };
1128 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
1129 assert_eq!(camera.get_model_name(), "double_sphere");
1130 Ok(())
1131 }
1132
1133 #[test]
1134 fn test_validate_params_invalid_alpha_zero() -> TestResult {
1135 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
1136 let distortion = DistortionModel::DoubleSphere {
1137 xi: 0.0,
1138 alpha: 0.0,
1139 };
1140 let result = DoubleSphereCamera::new(pinhole, distortion);
1141 assert!(result.is_err(), "alpha = 0 should be invalid");
1142 Ok(())
1143 }
1144
1145 #[test]
1146 fn test_validate_params_invalid_xi_out_of_range() -> TestResult {
1147 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
1148 let distortion = DistortionModel::DoubleSphere {
1149 xi: 2.0,
1150 alpha: 0.6,
1151 };
1152 let result = DoubleSphereCamera::new(pinhole, distortion);
1153 assert!(result.is_err(), "xi = 2.0 should be invalid");
1154 Ok(())
1155 }
1156
1157 #[test]
1158 fn test_validate_params_invalid_focal_length() {
1159 let pinhole = PinholeParams {
1160 fx: -1.0,
1161 fy: 300.0,
1162 cx: 320.0,
1163 cy: 240.0,
1164 };
1165 let distortion = DistortionModel::DoubleSphere {
1166 xi: 0.0,
1167 alpha: 0.6,
1168 };
1169 let result = DoubleSphereCamera::new(pinhole, distortion);
1170 assert!(result.is_err(), "negative focal length should be invalid");
1171 }
1172
1173 #[test]
1174 fn test_unproject_outside_image_returns_error() -> TestResult {
1175 let pinhole = PinholeParams::new(1.0, 1.0, 0.0, 0.0)?;
1181 let distortion = DistortionModel::DoubleSphere {
1182 xi: 0.6,
1183 alpha: 0.9,
1184 };
1185 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
1186 let result = camera.unproject(&Vector2::new(3.0, 0.0));
1187 assert!(
1188 result.is_err(),
1189 "Point with r² > threshold should return PointOutsideImage"
1190 );
1191 Ok(())
1192 }
1193}