1use crate::{CONVERGENCE_THRESHOLD, CameraModel, CameraModelError, GEOMETRIC_PRECISION, MIN_DEPTH};
75use crate::{DistortionModel, PinholeParams};
76use nalgebra::{DVector, Matrix3xX, SMatrix, Vector2, Vector3};
77
78#[derive(Clone, Copy, PartialEq)]
83pub struct FThetaCamera {
84 pub cx: f64,
86 pub cy: f64,
88 pub distortion: DistortionModel,
90}
91
92impl std::fmt::Debug for FThetaCamera {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 let (k1, k2, k3, k4) = self.distortion_params();
95 f.debug_struct("FThetaCamera")
96 .field("cx", &self.cx)
97 .field("cy", &self.cy)
98 .field("k1", &k1)
99 .field("k2", &k2)
100 .field("k3", &k3)
101 .field("k4", &k4)
102 .finish()
103 }
104}
105
106impl FThetaCamera {
107 pub fn new(cx: f64, cy: f64, distortion: DistortionModel) -> Result<Self, CameraModelError> {
114 let cam = Self { cx, cy, distortion };
115 cam.validate_params()?;
116 Ok(cam)
117 }
118
119 pub fn try_from_params(params: &[f64]) -> Result<Self, CameraModelError> {
121 Self::try_from(params)
122 }
123
124 #[inline]
131 pub fn distortion_params(&self) -> (f64, f64, f64, f64) {
132 match self.distortion {
133 DistortionModel::FTheta { k1, k2, k3, k4 } => (k1, k2, k3, k4),
134 _ => unreachable!("FThetaCamera always has FTheta distortion"),
135 }
136 }
137
138 #[inline]
140 fn poly_forward(&self, theta: f64) -> f64 {
141 let (k1, k2, k3, k4) = self.distortion_params();
142 theta * (k1 + theta * (k2 + theta * (k3 + theta * k4)))
143 }
144
145 #[inline]
147 fn poly_forward_deriv(&self, theta: f64) -> f64 {
148 let (k1, k2, k3, k4) = self.distortion_params();
149 k1 + theta * (2.0 * k2 + theta * (3.0 * k3 + theta * 4.0 * k4))
150 }
151
152 pub fn linear_estimation(
166 &self,
167 points_3d: &Matrix3xX<f64>,
168 points_2d: &nalgebra::Matrix2xX<f64>,
169 ) -> Result<Self, CameraModelError> {
170 let n = points_3d.ncols();
171 if n < 4 {
172 return Err(CameraModelError::InvalidParams(
173 "linear_estimation requires at least 4 correspondences".to_string(),
174 ));
175 }
176
177 let mut a = nalgebra::DMatrix::<f64>::zeros(n, 4);
178 let mut b = nalgebra::DVector::<f64>::zeros(n);
179
180 for i in 0..n {
181 let x = points_3d[(0, i)];
182 let y = points_3d[(1, i)];
183 let z = points_3d[(2, i)];
184 let d = (x * x + y * y + z * z).sqrt();
185 if d < GEOMETRIC_PRECISION {
186 continue;
187 }
188 let theta = (z / d).clamp(-1.0, 1.0).acos();
189
190 let u = points_2d[(0, i)];
191 let v = points_2d[(1, i)];
192 let dx = u - self.cx;
193 let dy = v - self.cy;
194 let r = (dx * dx + dy * dy).sqrt();
195
196 let t2 = theta * theta;
197 let t3 = t2 * theta;
198 let t4 = t3 * theta;
199 a[(i, 0)] = theta;
200 a[(i, 1)] = t2;
201 a[(i, 2)] = t3;
202 a[(i, 3)] = t4;
203 b[i] = r;
204 }
205
206 let svd = a.svd(true, true);
207 let coeffs = svd
208 .solve(&b, GEOMETRIC_PRECISION)
209 .map_err(|e| CameraModelError::InvalidParams(format!("SVD solve failed: {e}")))?;
210
211 let distortion = DistortionModel::FTheta {
212 k1: coeffs[0],
213 k2: coeffs[1],
214 k3: coeffs[2],
215 k4: coeffs[3],
216 };
217 FThetaCamera::new(self.cx, self.cy, distortion)
218 }
219}
220
221impl CameraModel for FThetaCamera {
224 const INTRINSIC_DIM: usize = 6;
225
226 type IntrinsicJacobian = SMatrix<f64, 2, 6>;
227 type PointJacobian = SMatrix<f64, 2, 3>;
228
229 fn project(&self, p_cam: &Vector3<f64>) -> Result<Vector2<f64>, CameraModelError> {
230 let (x, y, z) = (p_cam.x, p_cam.y, p_cam.z);
231
232 if z < MIN_DEPTH {
233 return Err(CameraModelError::PointBehindCamera {
234 z,
235 min_z: MIN_DEPTH,
236 });
237 }
238
239 let d = (x * x + y * y + z * z).sqrt();
240 let theta = (z / d).clamp(-1.0, 1.0).acos();
241 let f_theta = self.poly_forward(theta);
242 let r_p = (x * x + y * y).sqrt();
243
244 if r_p < GEOMETRIC_PRECISION {
245 return Ok(Vector2::new(self.cx, self.cy));
246 }
247
248 let inv_rp = 1.0 / r_p;
249 Ok(Vector2::new(
250 self.cx + f_theta * x * inv_rp,
251 self.cy + f_theta * y * inv_rp,
252 ))
253 }
254
255 fn unproject(&self, point_2d: &Vector2<f64>) -> Result<Vector3<f64>, CameraModelError> {
256 let dx = point_2d.x - self.cx;
257 let dy = point_2d.y - self.cy;
258 let r_d = (dx * dx + dy * dy).sqrt();
259
260 if r_d < GEOMETRIC_PRECISION {
261 return Ok(Vector3::new(0.0, 0.0, 1.0));
262 }
263
264 let (k1, ..) = self.distortion_params();
265
266 let mut theta = r_d / k1;
268 for _ in 0..100 {
269 let f_val = self.poly_forward(theta);
270 let f_deriv = self.poly_forward_deriv(theta);
271 if f_deriv.abs() < 1e-12 {
272 break;
273 }
274 let delta = (f_val - r_d) / f_deriv;
275 theta -= delta;
276 if delta.abs() < CONVERGENCE_THRESHOLD {
277 break;
278 }
279 }
280
281 if !theta.is_finite() || theta < 0.0 {
282 return Err(CameraModelError::NumericalError {
283 operation: "ftheta_unproject".to_string(),
284 details: format!("Newton-Raphson diverged, theta={theta}"),
285 });
286 }
287
288 let sin_theta = theta.sin();
289 let cos_theta = theta.cos();
290 let inv_rd = 1.0 / r_d;
291 let ray = Vector3::new(sin_theta * dx * inv_rd, sin_theta * dy * inv_rd, cos_theta);
292 Ok(ray.normalize())
293 }
294
295 fn jacobian_point(&self, p_cam: &Vector3<f64>) -> Self::PointJacobian {
296 let (x, y, z) = (p_cam.x, p_cam.y, p_cam.z);
297 let r_p2 = x * x + y * y;
298 let d2 = r_p2 + z * z;
299 let d = d2.sqrt();
300 let r_p = r_p2.sqrt();
301
302 let mut j = SMatrix::<f64, 2, 3>::zeros();
303
304 if r_p < GEOMETRIC_PRECISION {
305 let k1 = self.distortion_params().0;
307 j[(0, 0)] = k1 / z;
308 j[(1, 1)] = k1 / z;
309 return j;
310 }
311
312 let theta = (z / d).clamp(-1.0, 1.0).acos();
313 let f_val = self.poly_forward(theta);
314 let f_prime = self.poly_forward_deriv(theta);
315
316 let a = f_prime * z / (r_p2 * d2);
318 let b = f_val / (r_p2 * r_p);
319
320 j[(0, 0)] = a * x * x + b * y * y;
321 j[(0, 1)] = (a - b) * x * y;
322 j[(0, 2)] = -f_prime * x / d2;
323 j[(1, 0)] = j[(0, 1)];
324 j[(1, 1)] = a * y * y + b * x * x;
325 j[(1, 2)] = -f_prime * y / d2;
326 j
327 }
328
329 fn jacobian_intrinsics(&self, p_cam: &Vector3<f64>) -> Self::IntrinsicJacobian {
330 let (x, y, z) = (p_cam.x, p_cam.y, p_cam.z);
331 let r_p2 = x * x + y * y;
332 let r_p = r_p2.sqrt();
333 let d = (r_p2 + z * z).sqrt();
334
335 let mut j = SMatrix::<f64, 2, 6>::zeros();
336 j[(0, 0)] = 1.0;
338 j[(1, 1)] = 1.0;
339
340 if r_p < GEOMETRIC_PRECISION {
341 return j;
343 }
344
345 let theta = (z / d).clamp(-1.0, 1.0).acos();
346 let cos_phi = x / r_p;
347 let sin_phi = y / r_p;
348
349 let mut theta_pow = theta;
350 for col in 2..6 {
351 j[(0, col)] = theta_pow * cos_phi;
352 j[(1, col)] = theta_pow * sin_phi;
353 theta_pow *= theta;
354 }
355 j
356 }
357
358 fn validate_params(&self) -> Result<(), CameraModelError> {
359 if !self.cx.is_finite() || !self.cy.is_finite() {
360 return Err(CameraModelError::PrincipalPointNotFinite {
361 cx: self.cx,
362 cy: self.cy,
363 });
364 }
365 match self.distortion {
366 DistortionModel::FTheta { .. } => self.distortion.validate(),
367 _ => Err(CameraModelError::InvalidParams(
368 "FThetaCamera requires DistortionModel::FTheta".to_string(),
369 )),
370 }
371 }
372
373 fn get_pinhole_params(&self) -> PinholeParams {
374 let (k1, ..) = self.distortion_params();
375 PinholeParams {
376 fx: k1,
377 fy: k1,
378 cx: self.cx,
379 cy: self.cy,
380 }
381 }
382
383 fn get_distortion(&self) -> DistortionModel {
384 self.distortion
385 }
386
387 fn get_model_name(&self) -> &'static str {
388 "ftheta"
389 }
390}
391
392impl From<&FThetaCamera> for [f64; 6] {
396 fn from(cam: &FThetaCamera) -> Self {
397 let (k1, k2, k3, k4) = cam.distortion_params();
398 [cam.cx, cam.cy, k1, k2, k3, k4]
399 }
400}
401
402impl From<[f64; 6]> for FThetaCamera {
403 fn from(p: [f64; 6]) -> Self {
404 Self {
405 cx: p[0],
406 cy: p[1],
407 distortion: DistortionModel::FTheta {
408 k1: p[2],
409 k2: p[3],
410 k3: p[4],
411 k4: p[5],
412 },
413 }
414 }
415}
416
417impl From<&FThetaCamera> for DVector<f64> {
418 fn from(cam: &FThetaCamera) -> Self {
419 let arr: [f64; 6] = cam.into();
420 DVector::from_row_slice(&arr)
421 }
422}
423
424impl TryFrom<&[f64]> for FThetaCamera {
425 type Error = CameraModelError;
426
427 fn try_from(params: &[f64]) -> Result<Self, Self::Error> {
428 if params.len() != 6 {
429 return Err(CameraModelError::InvalidParams(format!(
430 "FThetaCamera requires 6 parameters, got {}",
431 params.len()
432 )));
433 }
434 let cam = FThetaCamera::from([
435 params[0], params[1], params[2], params[3], params[4], params[5],
436 ]);
437 cam.validate_params()?;
438 Ok(cam)
439 }
440}
441
442#[cfg(test)]
445#[allow(clippy::unwrap_used)]
446mod tests {
447 use super::*;
448 use crate::{JACOBIAN_TEST_TOLERANCE, NUMERICAL_DERIVATIVE_EPS, PROJECTION_TEST_TOLERANCE};
449 use nalgebra::{Matrix2xX, Matrix3xX};
450
451 type TestResult = Result<(), Box<dyn std::error::Error>>;
452
453 fn make_camera() -> FThetaCamera {
455 FThetaCamera::from([320.0, 240.0, 500.0, -10.0, 2.0, -0.1])
456 }
457
458 fn make_linear_camera() -> FThetaCamera {
460 FThetaCamera::from([320.0, 240.0, 500.0, 0.0, 0.0, 0.0])
461 }
462
463 #[test]
466 fn test_creation_and_validate() -> TestResult {
467 let cam = FThetaCamera::new(
468 320.0,
469 240.0,
470 DistortionModel::FTheta {
471 k1: 500.0,
472 k2: -10.0,
473 k3: 2.0,
474 k4: -0.1,
475 },
476 )?;
477 assert_eq!(cam.cx, 320.0);
478 assert_eq!(cam.cy, 240.0);
479 assert!(cam.validate_params().is_ok());
480 Ok(())
481 }
482
483 #[test]
484 fn test_validate_invalid_k1_zero() {
485 let cam = FThetaCamera {
486 cx: 320.0,
487 cy: 240.0,
488 distortion: DistortionModel::FTheta {
489 k1: 0.0,
490 k2: 0.0,
491 k3: 0.0,
492 k4: 0.0,
493 },
494 };
495 assert!(cam.validate_params().is_err());
496 }
497
498 #[test]
499 fn test_validate_invalid_k1_negative() {
500 let cam = FThetaCamera {
501 cx: 320.0,
502 cy: 240.0,
503 distortion: DistortionModel::FTheta {
504 k1: -1.0,
505 k2: 0.0,
506 k3: 0.0,
507 k4: 0.0,
508 },
509 };
510 assert!(cam.validate_params().is_err());
511 }
512
513 #[test]
514 fn test_validate_nan_coefficient() {
515 let cam = FThetaCamera {
516 cx: 320.0,
517 cy: 240.0,
518 distortion: DistortionModel::FTheta {
519 k1: 500.0,
520 k2: f64::NAN,
521 k3: 0.0,
522 k4: 0.0,
523 },
524 };
525 assert!(cam.validate_params().is_err());
526 }
527
528 #[test]
529 fn test_validate_wrong_distortion_type() {
530 let cam = FThetaCamera {
531 cx: 320.0,
532 cy: 240.0,
533 distortion: DistortionModel::None,
534 };
535 assert!(cam.validate_params().is_err());
536 }
537
538 #[test]
539 fn test_get_model_name() {
540 assert_eq!(make_camera().get_model_name(), "ftheta");
541 }
542
543 #[test]
544 fn test_debug_format() {
545 let cam = make_camera();
546 let s = format!("{cam:?}");
547 assert!(s.contains("FThetaCamera"));
548 assert!(s.contains("cx"));
549 assert!(s.contains("k1"));
550 }
551
552 #[test]
555 fn test_project_optical_axis() -> TestResult {
556 let cam = make_camera();
557 let p = Vector3::new(0.0, 0.0, 1.0);
558 let px = cam.project(&p)?;
559 assert!((px.x - cam.cx).abs() < PROJECTION_TEST_TOLERANCE);
560 assert!((px.y - cam.cy).abs() < PROJECTION_TEST_TOLERANCE);
561 Ok(())
562 }
563
564 #[test]
565 fn test_project_off_axis_linear_model() -> TestResult {
566 let cam = make_linear_camera();
568 let p = Vector3::new(1.0, 0.0, 1.0);
569 let theta = (1.0_f64 / 2.0_f64.sqrt()).acos(); let expected_u = 320.0 + 500.0 * theta;
571 let px = cam.project(&p)?;
572 assert!(
573 (px.x - expected_u).abs() < 1e-8,
574 "u={} expected={expected_u}",
575 px.x
576 );
577 assert!((px.y - 240.0).abs() < 1e-8);
578 Ok(())
579 }
580
581 #[test]
582 fn test_project_with_higher_order_terms() -> TestResult {
583 let linear = make_linear_camera();
584 let distorted = make_camera();
585 let p = Vector3::new(0.5, 0.3, 1.0);
586 let px_lin = linear.project(&p)?;
587 let px_dist = distorted.project(&p)?;
588 let r_lin = ((px_lin.x - 320.0).powi(2) + (px_lin.y - 240.0).powi(2)).sqrt();
590 let r_dist = ((px_dist.x - 320.0).powi(2) + (px_dist.y - 240.0).powi(2)).sqrt();
591 assert!(r_lin > 0.0 && r_dist > 0.0);
592 Ok(())
593 }
594
595 #[test]
596 fn test_project_behind_camera_error() {
597 let cam = make_camera();
598 let p = Vector3::new(0.0, 0.0, -1.0);
599 assert!(matches!(
600 cam.project(&p),
601 Err(CameraModelError::PointBehindCamera { .. })
602 ));
603 }
604
605 #[test]
608 fn test_unproject_center_pixel() -> TestResult {
609 let cam = make_camera();
610 let px = Vector2::new(cam.cx, cam.cy);
611 let ray = cam.unproject(&px)?;
612 assert!((ray.x).abs() < PROJECTION_TEST_TOLERANCE);
613 assert!((ray.y).abs() < PROJECTION_TEST_TOLERANCE);
614 assert!((ray.z - 1.0).abs() < PROJECTION_TEST_TOLERANCE);
615 Ok(())
616 }
617
618 #[test]
619 fn test_unproject_off_axis_linear() -> TestResult {
620 let cam = make_linear_camera();
622 let r = 100.0_f64;
623 let px = Vector2::new(cam.cx + r, cam.cy);
624 let ray = cam.unproject(&px)?;
625 let theta_expected = r / 500.0;
626 assert!((ray.x - theta_expected.sin()).abs() < 1e-6);
627 assert!((ray.z - theta_expected.cos()).abs() < 1e-6);
628 Ok(())
629 }
630
631 #[test]
634 fn test_round_trip_optical_axis() -> TestResult {
635 let cam = make_camera();
636 let p = Vector3::new(0.0, 0.0, 2.0);
637 let px = cam.project(&p)?;
638 let ray = cam.unproject(&px)?;
639 let p_norm = p.normalize();
640 assert!((ray.x - p_norm.x).abs() < 1e-10);
641 assert!((ray.y - p_norm.y).abs() < 1e-10);
642 assert!((ray.z - p_norm.z).abs() < 1e-10);
643 Ok(())
644 }
645
646 #[test]
647 fn test_round_trip_project_unproject() -> TestResult {
648 let cam = make_camera();
649 let points = [
650 Vector3::new(0.3, 0.2, 1.0),
651 Vector3::new(-0.5, 0.1, 2.0),
652 Vector3::new(0.1, -0.4, 1.5),
653 Vector3::new(0.0, 0.6, 1.0),
654 ];
655 for p in &points {
656 let px = cam.project(p)?;
657 let ray = cam.unproject(&px)?;
658 let p_norm = p.normalize();
659 let dot = ray.dot(&p_norm);
660 assert!(
661 (dot - 1.0).abs() < 1e-8,
662 "round-trip failed for {p:?}: dot={dot}"
663 );
664 }
665 Ok(())
666 }
667
668 #[test]
669 fn test_round_trip_with_distortion() -> TestResult {
670 let cam = make_camera(); let p = Vector3::new(0.4, -0.3, 1.2);
672 let px = cam.project(&p)?;
673 let ray = cam.unproject(&px)?;
674 let dot = ray.dot(&p.normalize());
675 assert!((dot - 1.0).abs() < 1e-8);
676 Ok(())
677 }
678
679 #[test]
682 fn test_jacobian_point_dimensions() {
683 let cam = make_camera();
684 let p = Vector3::new(0.3, 0.2, 1.0);
685 let j = cam.jacobian_point(&p);
686 assert_eq!(j.nrows(), 2);
687 assert_eq!(j.ncols(), 3);
688 }
689
690 #[test]
691 fn test_jacobian_point_optical_axis() {
692 let cam = make_camera();
693 let z = 2.0_f64;
694 let p = Vector3::new(0.0, 0.0, z);
695 let j = cam.jacobian_point(&p);
696 let k1 = cam.distortion_params().0;
697 let expected = k1 / z;
698 assert!((j[(0, 0)] - expected).abs() < 1e-10, "J[0,0]={}", j[(0, 0)]);
699 assert!((j[(1, 1)] - expected).abs() < 1e-10, "J[1,1]={}", j[(1, 1)]);
700 assert!(j[(0, 1)].abs() < 1e-10);
701 assert!(j[(0, 2)].abs() < 1e-10);
702 assert!(j[(1, 0)].abs() < 1e-10);
703 assert!(j[(1, 2)].abs() < 1e-10);
704 }
705
706 #[test]
707 fn test_jacobian_point_numerical() -> TestResult {
708 let cam = make_camera();
709 let p = Vector3::new(0.3, 0.2, 1.5);
710 let j_analytical = cam.jacobian_point(&p);
711 let px0 = cam.project(&p)?;
712
713 for col in 0..3 {
714 let mut p_plus = p;
715 p_plus[col] += NUMERICAL_DERIVATIVE_EPS;
716 let px_plus = cam.project(&p_plus)?;
717 let num_du = (px_plus.x - px0.x) / NUMERICAL_DERIVATIVE_EPS;
718 let num_dv = (px_plus.y - px0.y) / NUMERICAL_DERIVATIVE_EPS;
719
720 assert!(
721 (j_analytical[(0, col)] - num_du).abs() < JACOBIAN_TEST_TOLERANCE,
722 "J[0,{col}]: analytical={} numerical={num_du}",
723 j_analytical[(0, col)]
724 );
725 assert!(
726 (j_analytical[(1, col)] - num_dv).abs() < JACOBIAN_TEST_TOLERANCE,
727 "J[1,{col}]: analytical={} numerical={num_dv}",
728 j_analytical[(1, col)]
729 );
730 }
731 Ok(())
732 }
733
734 #[test]
737 fn test_jacobian_intrinsics_dimensions() {
738 let cam = make_camera();
739 let p = Vector3::new(0.3, 0.2, 1.0);
740 let j = cam.jacobian_intrinsics(&p);
741 assert_eq!(j.nrows(), 2);
742 assert_eq!(j.ncols(), 6);
743 }
744
745 #[test]
746 fn test_jacobian_intrinsics_optical_axis() {
747 let cam = make_camera();
748 let p = Vector3::new(0.0, 0.0, 1.0);
749 let j = cam.jacobian_intrinsics(&p);
750 assert!((j[(0, 0)] - 1.0).abs() < 1e-12); assert!((j[(1, 1)] - 1.0).abs() < 1e-12); for col in 2..6 {
754 assert!(j[(0, col)].abs() < 1e-12, "J[0,{col}]={}", j[(0, col)]);
755 assert!(j[(1, col)].abs() < 1e-12, "J[1,{col}]={}", j[(1, col)]);
756 }
757 }
758
759 #[test]
760 fn test_jacobian_intrinsics_numerical() -> TestResult {
761 let cam = make_camera();
762 let p = Vector3::new(0.3, 0.2, 1.5);
763 let j_analytical = cam.jacobian_intrinsics(&p);
764 let px0 = cam.project(&p)?;
765
766 let params0: [f64; 6] = (&cam).into();
768 for col in 0..6 {
769 let mut params_plus = params0;
770 params_plus[col] += NUMERICAL_DERIVATIVE_EPS;
771 let cam_plus = FThetaCamera::from(params_plus);
772 let px_plus = cam_plus.project(&p)?;
773 let num_du = (px_plus.x - px0.x) / NUMERICAL_DERIVATIVE_EPS;
774 let num_dv = (px_plus.y - px0.y) / NUMERICAL_DERIVATIVE_EPS;
775
776 assert!(
777 (j_analytical[(0, col)] - num_du).abs() < JACOBIAN_TEST_TOLERANCE,
778 "J_intr[0,{col}]: analytical={} numerical={num_du}",
779 j_analytical[(0, col)]
780 );
781 assert!(
782 (j_analytical[(1, col)] - num_dv).abs() < JACOBIAN_TEST_TOLERANCE,
783 "J_intr[1,{col}]: analytical={} numerical={num_dv}",
784 j_analytical[(1, col)]
785 );
786 }
787 Ok(())
788 }
789
790 #[test]
793 fn test_from_array_roundtrip() {
794 let arr = [320.0_f64, 240.0, 500.0, -10.0, 2.0, -0.1];
795 let cam = FThetaCamera::from(arr);
796 let arr2: [f64; 6] = (&cam).into();
797 for (a, b) in arr.iter().zip(arr2.iter()) {
798 assert!((a - b).abs() < 1e-15);
799 }
800 }
801
802 #[test]
803 fn test_try_from_slice_correct_length() -> TestResult {
804 let params = [320.0_f64, 240.0, 500.0, -10.0, 2.0, -0.1];
805 let cam = FThetaCamera::try_from(params.as_slice())?;
806 assert_eq!(cam.cx, 320.0);
807 Ok(())
808 }
809
810 #[test]
811 fn test_try_from_slice_wrong_length() {
812 let params = [320.0_f64, 240.0, 500.0];
813 assert!(FThetaCamera::try_from(params.as_slice()).is_err());
814 }
815
816 #[test]
817 fn test_dvector_conversion() {
818 let cam = make_camera();
819 let v: DVector<f64> = (&cam).into();
820 assert_eq!(v.len(), 6);
821 assert!((v[0] - cam.cx).abs() < 1e-15);
822 let (k1, k2, k3, k4) = cam.distortion_params();
823 assert!((v[2] - k1).abs() < 1e-15);
824 assert!((v[3] - k2).abs() < 1e-15);
825 assert!((v[4] - k3).abs() < 1e-15);
826 assert!((v[5] - k4).abs() < 1e-15);
827 }
828
829 #[test]
830 fn test_get_pinhole_params() {
831 let cam = make_camera();
832 let pp = cam.get_pinhole_params();
833 let (k1, ..) = cam.distortion_params();
834 assert!((pp.fx - k1).abs() < 1e-15);
835 assert!((pp.fy - k1).abs() < 1e-15);
836 assert!((pp.cx - cam.cx).abs() < 1e-15);
837 assert!((pp.cy - cam.cy).abs() < 1e-15);
838 }
839
840 #[test]
843 fn test_linear_estimation() -> TestResult {
844 let gt_cam = make_camera();
845 let n = 50_usize;
846 let mut pts3d = Matrix3xX::zeros(n);
847 let mut pts2d = Matrix2xX::zeros(n);
848
849 for i in 0..n {
851 let theta = std::f64::consts::FRAC_PI_3 * (i as f64) / (n as f64);
852 let phi = 2.0 * std::f64::consts::PI * (i as f64 * 0.618_033_988); let x = theta.sin() * phi.cos();
854 let y = theta.sin() * phi.sin();
855 let z = theta.cos();
856 pts3d[(0, i)] = x;
857 pts3d[(1, i)] = y;
858 pts3d[(2, i)] = z;
859 let px = gt_cam.project(&Vector3::new(x, y, z))?;
860 pts2d[(0, i)] = px.x;
861 pts2d[(1, i)] = px.y;
862 }
863
864 let seed = FThetaCamera::from([gt_cam.cx, gt_cam.cy, 500.0, 0.0, 0.0, 0.0]);
865 let estimated = seed.linear_estimation(&pts3d, &pts2d)?;
866
867 let mut max_err = 0.0_f64;
869 for i in 0..n {
870 let p = Vector3::new(pts3d[(0, i)], pts3d[(1, i)], pts3d[(2, i)]);
871 let px_est = estimated.project(&p)?;
872 let err =
873 ((px_est.x - pts2d[(0, i)]).powi(2) + (px_est.y - pts2d[(1, i)]).powi(2)).sqrt();
874 max_err = max_err.max(err);
875 }
876 assert!(max_err < 1.0, "max reprojection error={max_err:.3} px");
877 Ok(())
878 }
879
880 #[test]
883 fn test_project_batch_sentinel() {
884 let cam = make_camera();
885 let pts = Matrix3xX::from_columns(&[
886 Vector3::new(0.1, 0.2, 1.0),
887 Vector3::new(0.0, 0.0, -1.0), ]);
889 let result = cam.project_batch(&pts);
890 assert!(result[(0, 0)].is_finite());
891 assert!((result[(0, 1)] - 1e6).abs() < 1.0);
892 assert!((result[(1, 1)] - 1e6).abs() < 1.0);
893 }
894}