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