1use crate::{CameraModel, CameraModelError, DistortionModel, PinholeParams};
9use nalgebra::{DVector, SMatrix, Vector2, Vector3};
10use std::fmt;
11
12#[derive(Clone, Copy, PartialEq)]
14pub struct DoubleSphereCamera {
15 pub pinhole: PinholeParams,
16 pub distortion: DistortionModel,
17}
18
19impl DoubleSphereCamera {
20 pub fn new(
40 pinhole: PinholeParams,
41 distortion: DistortionModel,
42 ) -> Result<Self, CameraModelError> {
43 let model = Self {
44 pinhole,
45 distortion,
46 };
47 model.validate_params()?;
48 Ok(model)
49 }
50
51 fn distortion_params(&self) -> (f64, f64) {
54 match self.distortion {
55 DistortionModel::DoubleSphere { xi, alpha } => (xi, alpha),
56 _ => (0.0, 0.0),
57 }
58 }
59
60 fn cone_ratio(&self) -> f64 {
67 let (xi, alpha) = self.distortion_params();
68 let w1 = if alpha > 0.5 {
69 (1.0 - alpha) / alpha
70 } else {
71 alpha / (1.0 - alpha)
72 };
73 (w1 + xi) / (2.0 * w1 * xi + xi * xi + 1.0).sqrt()
74 }
75
76 fn check_projection_condition(&self, z: f64, d1: f64) -> Result<bool, CameraModelError> {
80 Ok(z > -self.cone_ratio() * d1)
81 }
82
83 fn check_unprojection_condition(&self, r_squared: f64) -> Result<bool, CameraModelError> {
86 let (_, alpha) = self.distortion_params();
87 if alpha > 0.5 && r_squared > 1.0 / (2.0 * alpha - 1.0) {
88 return Ok(false);
89 }
90 Ok(true)
91 }
92
93 pub fn linear_estimation(
97 &mut self,
98 points_3d: &nalgebra::Matrix3xX<f64>,
99 points_2d: &nalgebra::Matrix2xX<f64>,
100 ) -> Result<(), CameraModelError> {
101 if points_2d.ncols() != points_3d.ncols() {
102 return Err(CameraModelError::InvalidParams(
103 "Number of 2D and 3D points must match".to_string(),
104 ));
105 }
106
107 let num_points = points_2d.ncols();
108 let mut a = nalgebra::DMatrix::zeros(num_points * 2, 1);
109 let mut b = nalgebra::DVector::zeros(num_points * 2);
110
111 for i in 0..num_points {
112 let x = points_3d[(0, i)];
113 let y = points_3d[(1, i)];
114 let z = points_3d[(2, i)];
115 let u = points_2d[(0, i)];
116 let v = points_2d[(1, i)];
117
118 let d = (x * x + y * y + z * z).sqrt();
119 let u_cx = u - self.pinhole.cx;
120 let v_cy = v - self.pinhole.cy;
121
122 a[(i * 2, 0)] = u_cx * (d - z);
123 a[(i * 2 + 1, 0)] = v_cy * (d - z);
124
125 b[i * 2] = (self.pinhole.fx * x) - (u_cx * z);
126 b[i * 2 + 1] = (self.pinhole.fy * y) - (v_cy * z);
127 }
128
129 let svd = a.svd(true, true);
130 let alpha = match svd.solve(&b, 1e-10) {
131 Ok(sol) => sol[0],
132 Err(err_msg) => {
133 return Err(CameraModelError::NumericalError {
134 operation: "svd_solve".to_string(),
135 details: err_msg.to_string(),
136 });
137 }
138 };
139
140 self.distortion = DistortionModel::DoubleSphere { xi: 0.0, alpha };
141
142 self.validate_params()?;
143
144 Ok(())
145 }
146}
147
148impl fmt::Debug for DoubleSphereCamera {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 let (xi, alpha) = self.distortion_params();
152 write!(
153 f,
154 "DoubleSphere [fx: {} fy: {} cx: {} cy: {} alpha: {} xi: {}]",
155 self.pinhole.fx, self.pinhole.fy, self.pinhole.cx, self.pinhole.cy, alpha, xi
156 )
157 }
158}
159
160impl From<&DoubleSphereCamera> for DVector<f64> {
162 fn from(camera: &DoubleSphereCamera) -> Self {
163 let (xi, alpha) = camera.distortion_params();
164 DVector::from_vec(vec![
165 camera.pinhole.fx,
166 camera.pinhole.fy,
167 camera.pinhole.cx,
168 camera.pinhole.cy,
169 xi,
170 alpha,
171 ])
172 }
173}
174
175impl From<&DoubleSphereCamera> for [f64; 6] {
177 fn from(camera: &DoubleSphereCamera) -> Self {
178 let (xi, alpha) = camera.distortion_params();
179 [
180 camera.pinhole.fx,
181 camera.pinhole.fy,
182 camera.pinhole.cx,
183 camera.pinhole.cy,
184 xi,
185 alpha,
186 ]
187 }
188}
189
190impl TryFrom<&[f64]> for DoubleSphereCamera {
193 type Error = CameraModelError;
194
195 fn try_from(params: &[f64]) -> Result<Self, Self::Error> {
196 if params.len() < 6 {
197 return Err(CameraModelError::InvalidParams(format!(
198 "DoubleSphereCamera requires at least 6 parameters, got {}",
199 params.len()
200 )));
201 }
202 Ok(Self {
203 pinhole: PinholeParams {
204 fx: params[0],
205 fy: params[1],
206 cx: params[2],
207 cy: params[3],
208 },
209 distortion: DistortionModel::DoubleSphere {
210 xi: params[4],
211 alpha: params[5],
212 },
213 })
214 }
215}
216
217impl From<[f64; 6]> for DoubleSphereCamera {
219 fn from(params: [f64; 6]) -> Self {
220 Self {
221 pinhole: PinholeParams {
222 fx: params[0],
223 fy: params[1],
224 cx: params[2],
225 cy: params[3],
226 },
227 distortion: DistortionModel::DoubleSphere {
228 xi: params[4],
229 alpha: params[5],
230 },
231 }
232 }
233}
234
235pub fn try_from_params(params: &[f64]) -> Result<DoubleSphereCamera, CameraModelError> {
239 let camera = DoubleSphereCamera::try_from(params)?;
240 camera.validate_params()?;
241 Ok(camera)
242}
243
244impl CameraModel for DoubleSphereCamera {
245 const INTRINSIC_DIM: usize = 6;
246 type IntrinsicJacobian = SMatrix<f64, 2, 6>;
247 type PointJacobian = SMatrix<f64, 2, 3>;
248
249 fn project(&self, p_cam: &Vector3<f64>) -> Result<Vector2<f64>, CameraModelError> {
258 let x = p_cam[0];
259 let y = p_cam[1];
260 let z = p_cam[2];
261
262 let (xi, alpha) = self.distortion_params();
263 let r2 = x * x + y * y;
264 let d1 = (r2 + z * z).sqrt();
265
266 if !self.check_projection_condition(z, d1)? {
267 return Err(CameraModelError::ProjectionOutOfBounds);
268 }
269
270 let xi_d1_z = xi * d1 + z;
271 let d2 = (r2 + xi_d1_z * xi_d1_z).sqrt();
272 let denom = alpha * d2 + (1.0 - alpha) * xi_d1_z;
273
274 if denom < crate::GEOMETRIC_PRECISION {
275 return Err(CameraModelError::DenominatorTooSmall {
276 denom,
277 threshold: crate::GEOMETRIC_PRECISION,
278 });
279 }
280
281 Ok(Vector2::new(
282 self.pinhole.fx * x / denom + self.pinhole.cx,
283 self.pinhole.fy * y / denom + self.pinhole.cy,
284 ))
285 }
286
287 fn projection_deficit(&self, p_cam: &Vector3<f64>) -> (f64, Vector3<f64>) {
296 let w2 = self.cone_ratio();
297 let d1 = p_cam.norm();
298 let deficit = -(p_cam.z + w2 * d1);
299
300 let grad = if d1 > crate::GEOMETRIC_PRECISION {
303 Vector3::new(
304 -w2 * p_cam.x / d1,
305 -w2 * p_cam.y / d1,
306 -(1.0 + w2 * p_cam.z / d1),
307 )
308 } else {
309 Vector3::new(0.0, 0.0, -1.0)
310 };
311 (deficit, grad)
312 }
313
314 fn unproject(&self, point_2d: &Vector2<f64>) -> Result<Vector3<f64>, CameraModelError> {
318 let u = point_2d.x;
319 let v = point_2d.y;
320
321 let (xi, alpha) = self.distortion_params();
322 let mx = (u - self.pinhole.cx) / self.pinhole.fx;
323 let my = (v - self.pinhole.cy) / self.pinhole.fy;
324 let r2 = mx * mx + my * my;
325
326 if !self.check_unprojection_condition(r2)? {
327 return Err(CameraModelError::PointOutsideImage { x: u, y: v });
328 }
329
330 let mz_num = 1.0 - alpha * alpha * r2;
331 let mz_denom = alpha * (1.0 - (2.0 * alpha - 1.0) * r2).sqrt() + (1.0 - alpha);
332 let mz = mz_num / mz_denom;
333
334 let mz2 = mz * mz;
335
336 let num_term = mz * xi + (mz2 + (1.0 - xi * xi) * r2).sqrt();
337 let denom_term = mz2 + r2;
338
339 if denom_term < crate::GEOMETRIC_PRECISION {
340 return Err(CameraModelError::PointOutsideImage { x: u, y: v });
341 }
342
343 let k = num_term / denom_term;
344
345 let x = k * mx;
346 let y = k * my;
347 let z = k * mz - xi;
348
349 let norm = (x * x + y * y + z * z).sqrt();
351 Ok(Vector3::new(x / norm, y / norm, z / norm))
352 }
353
354 fn jacobian_point(&self, p_cam: &Vector3<f64>) -> Self::PointJacobian {
357 let x = p_cam[0];
358 let y = p_cam[1];
359 let z = p_cam[2];
360
361 let (xi, alpha) = self.distortion_params();
362 let r2 = x * x + y * y;
363 let d1 = (r2 + z * z).sqrt();
364 let xi_d1_z = xi * d1 + z;
365 let d2 = (r2 + xi_d1_z * xi_d1_z).sqrt();
366 let denom = alpha * d2 + (1.0 - alpha) * xi_d1_z;
367
368 let inv_d1 = 1.0 / d1;
370 let inv_d2 = 1.0 / d2;
371
372 let dd1_dx = x * inv_d1;
374 let dd1_dy = y * inv_d1;
375 let dd1_dz = z * inv_d1;
376
377 let d_xi_d1_z_dx = xi * dd1_dx;
379 let d_xi_d1_z_dy = xi * dd1_dy;
380 let d_xi_d1_z_dz = xi * dd1_dz + 1.0;
381
382 let dd2_dx = (x + xi_d1_z * d_xi_d1_z_dx) * inv_d2;
384 let dd2_dy = (y + xi_d1_z * d_xi_d1_z_dy) * inv_d2;
385 let dd2_dz = (xi_d1_z * d_xi_d1_z_dz) * inv_d2;
386
387 let ddenom_dx = alpha * dd2_dx + (1.0 - alpha) * d_xi_d1_z_dx;
389 let ddenom_dy = alpha * dd2_dy + (1.0 - alpha) * d_xi_d1_z_dy;
390 let ddenom_dz = alpha * dd2_dz + (1.0 - alpha) * d_xi_d1_z_dz;
391
392 let denom2 = denom * denom;
393
394 let du_dx = self.pinhole.fx * (denom - x * ddenom_dx) / denom2;
396 let du_dy = self.pinhole.fx * (-x * ddenom_dy) / denom2;
397 let du_dz = self.pinhole.fx * (-x * ddenom_dz) / denom2;
398
399 let dv_dx = self.pinhole.fy * (-y * ddenom_dx) / denom2;
400 let dv_dy = self.pinhole.fy * (denom - y * ddenom_dy) / denom2;
401 let dv_dz = self.pinhole.fy * (-y * ddenom_dz) / denom2;
402
403 SMatrix::<f64, 2, 3>::new(du_dx, du_dy, du_dz, dv_dx, dv_dy, dv_dz)
404 }
405
406 fn jacobian_intrinsics(&self, p_cam: &Vector3<f64>) -> Self::IntrinsicJacobian {
409 let x = p_cam[0];
410 let y = p_cam[1];
411 let z = p_cam[2];
412
413 let (xi, alpha) = self.distortion_params();
414 let r2 = x * x + y * y;
415 let d1 = (r2 + z * z).sqrt();
416 let xi_d1_z = xi * d1 + z;
417 let d2 = (r2 + xi_d1_z * xi_d1_z).sqrt();
418 let denom = alpha * d2 + (1.0 - alpha) * xi_d1_z;
419
420 let inv_denom = 1.0 / denom;
422 let inv_d2 = 1.0 / d2;
423
424 let x_norm = x * inv_denom;
425 let y_norm = y * inv_denom;
426
427 let d_xi_d1_z_dxi = d1;
432 let dd2_dxi = (xi_d1_z * d_xi_d1_z_dxi) * inv_d2;
433 let ddenom_dxi = alpha * dd2_dxi + (1.0 - alpha) * d_xi_d1_z_dxi;
434
435 let ddenom_dalpha = d2 - xi_d1_z;
436
437 let inv_denom2 = inv_denom * inv_denom;
438
439 let du_dxi = -self.pinhole.fx * x * ddenom_dxi * inv_denom2;
440 let dv_dxi = -self.pinhole.fy * y * ddenom_dxi * inv_denom2;
441
442 let du_dalpha = -self.pinhole.fx * x * ddenom_dalpha * inv_denom2;
443 let dv_dalpha = -self.pinhole.fy * y * ddenom_dalpha * inv_denom2;
444
445 SMatrix::<f64, 2, 6>::new(
446 x_norm, 0.0, 1.0, 0.0, du_dxi, du_dalpha, 0.0, y_norm, 0.0, 1.0, dv_dxi, dv_dalpha,
447 )
448 }
449
450 fn validate_params(&self) -> Result<(), CameraModelError> {
463 self.pinhole.validate()?;
464 self.get_distortion().validate()
465 }
466
467 fn get_pinhole_params(&self) -> PinholeParams {
469 PinholeParams {
470 fx: self.pinhole.fx,
471 fy: self.pinhole.fy,
472 cx: self.pinhole.cx,
473 cy: self.pinhole.cy,
474 }
475 }
476
477 fn get_distortion(&self) -> DistortionModel {
479 self.distortion
480 }
481
482 fn get_model_name(&self) -> &'static str {
484 "double_sphere"
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491 use nalgebra::{Matrix2xX, Matrix3xX};
492
493 type TestResult = Result<(), Box<dyn std::error::Error>>;
494
495 #[test]
496 fn test_double_sphere_camera_creation() -> TestResult {
497 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
498 let distortion = DistortionModel::DoubleSphere {
499 xi: -0.2,
500 alpha: 0.6,
501 };
502 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
503 assert_eq!(camera.pinhole.fx, 300.0);
504 let (xi, alpha) = camera.distortion_params();
505 assert_eq!(alpha, 0.6);
506 assert_eq!(xi, -0.2);
507
508 Ok(())
509 }
510
511 #[test]
512 fn test_projection_at_optical_axis() -> TestResult {
513 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
514 let distortion = DistortionModel::DoubleSphere {
515 xi: -0.2,
516 alpha: 0.6,
517 };
518 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
519 let p_cam = Vector3::new(0.0, 0.0, 1.0);
520 let uv = camera.project(&p_cam)?;
521
522 assert!((uv.x - 320.0).abs() < crate::PROJECTION_TEST_TOLERANCE);
523 assert!((uv.y - 240.0).abs() < crate::PROJECTION_TEST_TOLERANCE);
524
525 Ok(())
526 }
527
528 #[test]
529 fn test_jacobian_point_numerical() -> TestResult {
530 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
531 let distortion = DistortionModel::DoubleSphere {
532 xi: -0.2,
533 alpha: 0.6,
534 };
535 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
536 let p_cam = Vector3::new(0.1, 0.2, 1.0);
537
538 let jac_analytical = camera.jacobian_point(&p_cam);
539 let eps = crate::NUMERICAL_DERIVATIVE_EPS;
540
541 for i in 0..3 {
542 let mut p_plus = p_cam;
543 let mut p_minus = p_cam;
544 p_plus[i] += eps;
545 p_minus[i] -= eps;
546
547 let uv_plus = camera.project(&p_plus)?;
548 let uv_minus = camera.project(&p_minus)?;
549 let num_jac = (uv_plus - uv_minus) / (2.0 * eps);
550
551 for r in 0..2 {
552 assert!(
553 jac_analytical[(r, i)].is_finite(),
554 "Jacobian [{r},{i}] is not finite"
555 );
556 let diff = (jac_analytical[(r, i)] - num_jac[r]).abs();
557 assert!(
558 diff < crate::JACOBIAN_TEST_TOLERANCE,
559 "Mismatch at ({}, {})",
560 r,
561 i
562 );
563 }
564 }
565 Ok(())
566 }
567
568 #[test]
569 fn test_jacobian_intrinsics_numerical() -> TestResult {
570 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
571 let distortion = DistortionModel::DoubleSphere {
572 xi: -0.2,
573 alpha: 0.6,
574 };
575 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
576 let p_cam = Vector3::new(0.1, 0.2, 1.0);
577
578 let jac_analytical = camera.jacobian_intrinsics(&p_cam);
579 let params: DVector<f64> = (&camera).into();
580 let eps = crate::NUMERICAL_DERIVATIVE_EPS;
581
582 for i in 0..6 {
583 let mut params_plus = params.clone();
584 let mut params_minus = params.clone();
585 params_plus[i] += eps;
586 params_minus[i] -= eps;
587
588 let cam_plus = DoubleSphereCamera::try_from(params_plus.as_slice())?;
589 let cam_minus = DoubleSphereCamera::try_from(params_minus.as_slice())?;
590
591 let uv_plus = cam_plus.project(&p_cam)?;
592 let uv_minus = cam_minus.project(&p_cam)?;
593 let num_jac = (uv_plus - uv_minus) / (2.0 * eps);
594
595 for r in 0..2 {
596 assert!(
597 jac_analytical[(r, i)].is_finite(),
598 "Jacobian [{r},{i}] is not finite"
599 );
600 let diff = (jac_analytical[(r, i)] - num_jac[r]).abs();
601 assert!(
602 diff < crate::JACOBIAN_TEST_TOLERANCE,
603 "Mismatch at ({}, {})",
604 r,
605 i
606 );
607 }
608 }
609 Ok(())
610 }
611
612 #[test]
613 fn test_linear_estimation() -> TestResult {
614 let gt_pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
616 let gt_distortion = DistortionModel::DoubleSphere {
617 xi: 0.0,
618 alpha: 0.6,
619 };
620 let gt_camera = DoubleSphereCamera::new(gt_pinhole, gt_distortion)?;
621
622 let n_points = 50;
624 let mut pts_3d = Matrix3xX::zeros(n_points);
625 let mut pts_2d = Matrix2xX::zeros(n_points);
626 let mut valid = 0;
627
628 for i in 0..n_points {
629 let angle = i as f64 * 2.0 * std::f64::consts::PI / n_points as f64;
630 let r = 0.1 + 0.3 * (i as f64 / n_points as f64);
631 let p3d = Vector3::new(r * angle.cos(), r * angle.sin(), 1.0);
632
633 if let Ok(p2d) = gt_camera.project(&p3d) {
634 pts_3d.set_column(valid, &p3d);
635 pts_2d.set_column(valid, &p2d);
636 valid += 1;
637 }
638 }
639 let pts_3d = pts_3d.columns(0, valid).into_owned();
640 let pts_2d = pts_2d.columns(0, valid).into_owned();
641
642 let init_pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
644 let init_distortion = DistortionModel::DoubleSphere {
645 xi: 0.0,
646 alpha: 0.1,
647 };
648 let mut camera = DoubleSphereCamera::new(init_pinhole, init_distortion)?;
649
650 camera.linear_estimation(&pts_3d, &pts_2d)?;
651
652 for i in 0..valid {
654 let col = pts_3d.column(i);
655 let projected = camera.project(&Vector3::new(col[0], col[1], col[2]))?;
656 let err = ((projected.x - pts_2d[(0, i)]).powi(2)
657 + (projected.y - pts_2d[(1, i)]).powi(2))
658 .sqrt();
659 assert!(err < 1.0, "Reprojection error too large: {err}");
660 }
661
662 Ok(())
663 }
664
665 #[test]
666 fn test_project_unproject_round_trip() -> TestResult {
667 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
668 let distortion = DistortionModel::DoubleSphere {
669 xi: -0.2,
670 alpha: 0.6,
671 };
672 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
673
674 let test_points = [
675 Vector3::new(0.1, 0.2, 1.0),
676 Vector3::new(-0.3, 0.1, 2.0),
677 Vector3::new(0.05, -0.1, 0.5),
678 ];
679
680 for p_cam in &test_points {
681 let uv = camera.project(p_cam)?;
682 let ray = camera.unproject(&uv)?;
683 let dot = ray.dot(&p_cam.normalize());
684 assert!(
685 (dot - 1.0).abs() < 1e-6,
686 "Round-trip failed: dot={dot}, expected ~1.0"
687 );
688 }
689
690 Ok(())
691 }
692
693 #[test]
694 fn test_project_returns_error_behind_camera() -> TestResult {
695 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
696 let distortion = DistortionModel::DoubleSphere {
697 xi: -0.2,
698 alpha: 0.6,
699 };
700 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
701 assert!(camera.project(&Vector3::new(0.0, 0.0, -1.0)).is_err());
702 Ok(())
703 }
704
705 #[test]
706 fn test_project_at_min_depth_boundary() -> TestResult {
707 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
708 let distortion = DistortionModel::DoubleSphere {
709 xi: -0.2,
710 alpha: 0.6,
711 };
712 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
713 let p_min = Vector3::new(0.0, 0.0, crate::MIN_DEPTH);
714 if let Ok(uv) = camera.project(&p_min) {
715 assert!(uv.x.is_finite() && uv.y.is_finite());
716 }
717 Ok(())
718 }
719
720 #[test]
721 fn test_debug_format() -> TestResult {
722 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
723 let distortion = DistortionModel::DoubleSphere {
724 xi: -0.2,
725 alpha: 0.6,
726 };
727 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
728 let s = format!("{:?}", camera);
729 assert!(
730 s.contains("DoubleSphere"),
731 "Debug output should contain 'DoubleSphere', got: {s}"
732 );
733 assert!(
734 s.contains("300"),
735 "Debug output should contain focal length, got: {s}"
736 );
737 Ok(())
738 }
739
740 #[test]
741 fn test_from_camera_to_fixed_array() -> TestResult {
742 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
743 let distortion = DistortionModel::DoubleSphere {
744 xi: -0.2,
745 alpha: 0.6,
746 };
747 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
748 let arr: [f64; 6] = (&camera).into();
749 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(())
756 }
757
758 #[test]
759 fn test_from_fixed_array_to_camera() {
760 let arr = [300.0f64, 300.0, 320.0, 240.0, -0.2, 0.6];
761 let camera = DoubleSphereCamera::from(arr);
762 assert_eq!(camera.pinhole.fx, 300.0);
763 let (xi, alpha) = camera.distortion_params();
764 assert!((xi - (-0.2)).abs() < 1e-15);
765 assert!((alpha - 0.6).abs() < 1e-15);
766 }
767
768 #[test]
769 fn test_try_from_params_valid() -> TestResult {
770 let params = [300.0f64, 300.0, 320.0, 240.0, -0.2, 0.6];
771 let camera = try_from_params(¶ms)?;
772 assert_eq!(camera.pinhole.fx, 300.0);
773 Ok(())
774 }
775
776 #[test]
777 fn test_try_from_params_too_few() {
778 let params = [300.0f64, 300.0, 320.0];
779 let result = try_from_params(¶ms);
780 assert!(result.is_err(), "Should fail with fewer than 6 params");
781 }
782
783 #[test]
784 fn test_try_from_params_invalid_alpha() {
785 let params = [300.0f64, 300.0, 320.0, 240.0, 0.0, 0.0]; let result = try_from_params(¶ms);
787 assert!(result.is_err(), "Should fail with alpha = 0 (must be > 0)");
788 }
789
790 #[test]
791 fn test_get_pinhole_params() -> TestResult {
792 let pinhole = PinholeParams::new(300.0, 301.0, 320.0, 241.0)?;
793 let distortion = DistortionModel::DoubleSphere {
794 xi: -0.2,
795 alpha: 0.6,
796 };
797 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
798 let p = camera.get_pinhole_params();
799 assert_eq!(p.fx, 300.0);
800 assert_eq!(p.fy, 301.0);
801 assert_eq!(p.cx, 320.0);
802 assert_eq!(p.cy, 241.0);
803 Ok(())
804 }
805
806 #[test]
807 fn test_get_distortion() -> TestResult {
808 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
809 let distortion = DistortionModel::DoubleSphere {
810 xi: -0.2,
811 alpha: 0.6,
812 };
813 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
814 let d = camera.get_distortion();
815 assert_eq!(
816 d,
817 DistortionModel::DoubleSphere {
818 xi: -0.2,
819 alpha: 0.6
820 }
821 );
822 Ok(())
823 }
824
825 #[test]
826 fn test_get_model_name() -> TestResult {
827 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
828 let distortion = DistortionModel::DoubleSphere {
829 xi: -0.2,
830 alpha: 0.6,
831 };
832 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
833 assert_eq!(camera.get_model_name(), "double_sphere");
834 Ok(())
835 }
836
837 #[test]
838 fn test_validate_params_invalid_alpha_zero() -> TestResult {
839 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
840 let distortion = DistortionModel::DoubleSphere {
841 xi: 0.0,
842 alpha: 0.0,
843 };
844 let result = DoubleSphereCamera::new(pinhole, distortion);
845 assert!(result.is_err(), "alpha = 0 should be invalid");
846 Ok(())
847 }
848
849 #[test]
850 fn test_validate_params_invalid_xi_out_of_range() -> TestResult {
851 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
852 let distortion = DistortionModel::DoubleSphere {
853 xi: 2.0,
854 alpha: 0.6,
855 };
856 let result = DoubleSphereCamera::new(pinhole, distortion);
857 assert!(result.is_err(), "xi = 2.0 should be invalid");
858 Ok(())
859 }
860
861 #[test]
862 fn test_validate_params_invalid_focal_length() {
863 let pinhole = PinholeParams {
864 fx: -1.0,
865 fy: 300.0,
866 cx: 320.0,
867 cy: 240.0,
868 };
869 let distortion = DistortionModel::DoubleSphere {
870 xi: 0.0,
871 alpha: 0.6,
872 };
873 let result = DoubleSphereCamera::new(pinhole, distortion);
874 assert!(result.is_err(), "negative focal length should be invalid");
875 }
876
877 #[test]
878 fn test_unproject_outside_image_returns_error() -> TestResult {
879 let pinhole = PinholeParams::new(1.0, 1.0, 0.0, 0.0)?;
887 let distortion = DistortionModel::DoubleSphere {
888 xi: 0.3,
889 alpha: 0.9,
890 };
891 let camera = DoubleSphereCamera::new(pinhole, distortion)?;
892 let result = camera.unproject(&Vector2::new(2.0, 0.0));
893 assert!(
894 result.is_err(),
895 "Point with r² > 1/(2*alpha - 1) should return PointOutsideImage"
896 );
897 Ok(())
898 }
899}