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