1use crate::{CameraModel, CameraModelError, DistortionModel, PinholeParams};
9use nalgebra::{DVector, SMatrix, Vector2, Vector3};
10
11#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct FovCamera {
14 pub pinhole: PinholeParams,
15 pub distortion: DistortionModel,
16}
17
18impl FovCamera {
19 pub fn new(
38 pinhole: PinholeParams,
39 distortion: DistortionModel,
40 ) -> Result<Self, CameraModelError> {
41 let camera = Self {
42 pinhole,
43 distortion,
44 };
45 camera.validate_params()?;
46 Ok(camera)
47 }
48
49 fn distortion_params(&self) -> f64 {
51 match self.distortion {
52 DistortionModel::FOV { w } => w,
53 _ => 0.0,
54 }
55 }
56
57 pub fn linear_estimation(
61 &mut self,
62 points_3d: &nalgebra::Matrix3xX<f64>,
63 points_2d: &nalgebra::Matrix2xX<f64>,
64 ) -> Result<(), CameraModelError> {
65 if points_2d.ncols() != points_3d.ncols() {
66 return Err(CameraModelError::InvalidParams(
67 "Number of 2D and 3D points must match".to_string(),
68 ));
69 }
70
71 let num_points = points_2d.ncols();
72
73 if num_points < 2 {
74 return Err(CameraModelError::InvalidParams(
75 "Need at least 2 point correspondences for linear estimation".to_string(),
76 ));
77 }
78
79 let mut best_w = 1.0;
80 let mut best_error = f64::INFINITY;
81
82 for w_test in (10..300).map(|i| i as f64 / 100.0) {
83 let mut error_sum = 0.0;
84 let mut valid_count = 0;
85
86 for i in 0..num_points {
87 let x = points_3d[(0, i)];
88 let y = points_3d[(1, i)];
89 let z = points_3d[(2, i)];
90 let u_observed = points_2d[(0, i)];
91 let v_observed = points_2d[(1, i)];
92
93 let r2 = x * x + y * y;
94 let r = r2.sqrt();
95
96 let tan_w_half = (w_test / 2.0).tan();
97 let atan_wrd = (2.0 * tan_w_half * r).atan2(z);
98
99 let eps_sqrt = f64::EPSILON.sqrt();
100 let rd = if r2 < eps_sqrt {
101 2.0 * tan_w_half / w_test
102 } else {
103 atan_wrd / (r * w_test)
104 };
105
106 let mx = x * rd;
107 let my = y * rd;
108
109 let u_predicted = self.pinhole.fx * mx + self.pinhole.cx;
110 let v_predicted = self.pinhole.fy * my + self.pinhole.cy;
111
112 let error = ((u_predicted - u_observed).powi(2)
113 + (v_predicted - v_observed).powi(2))
114 .sqrt();
115
116 if error.is_finite() {
117 error_sum += error;
118 valid_count += 1;
119 }
120 }
121
122 if valid_count > 0 {
123 let avg_error = error_sum / valid_count as f64;
124 if avg_error < best_error {
125 best_error = avg_error;
126 best_w = w_test;
127 }
128 }
129 }
130
131 self.distortion = DistortionModel::FOV { w: best_w };
132
133 self.validate_params()?;
134
135 Ok(())
136 }
137}
138
139impl From<&FovCamera> for DVector<f64> {
141 fn from(camera: &FovCamera) -> Self {
142 let w = camera.distortion_params();
143 DVector::from_vec(vec![
144 camera.pinhole.fx,
145 camera.pinhole.fy,
146 camera.pinhole.cx,
147 camera.pinhole.cy,
148 w,
149 ])
150 }
151}
152
153impl From<&FovCamera> for [f64; 5] {
155 fn from(camera: &FovCamera) -> Self {
156 let w = camera.distortion_params();
157 [
158 camera.pinhole.fx,
159 camera.pinhole.fy,
160 camera.pinhole.cx,
161 camera.pinhole.cy,
162 w,
163 ]
164 }
165}
166
167impl TryFrom<&[f64]> for FovCamera {
170 type Error = CameraModelError;
171
172 fn try_from(params: &[f64]) -> Result<Self, Self::Error> {
173 if params.len() < 5 {
174 return Err(CameraModelError::InvalidParams(format!(
175 "FovCamera requires at least 5 parameters, got {}",
176 params.len()
177 )));
178 }
179 Ok(Self {
180 pinhole: PinholeParams {
181 fx: params[0],
182 fy: params[1],
183 cx: params[2],
184 cy: params[3],
185 },
186 distortion: DistortionModel::FOV { w: params[4] },
187 })
188 }
189}
190
191impl From<[f64; 5]> for FovCamera {
193 fn from(params: [f64; 5]) -> Self {
194 Self {
195 pinhole: PinholeParams {
196 fx: params[0],
197 fy: params[1],
198 cx: params[2],
199 cy: params[3],
200 },
201 distortion: DistortionModel::FOV { w: params[4] },
202 }
203 }
204}
205
206pub fn try_from_params(params: &[f64]) -> Result<FovCamera, CameraModelError> {
210 let camera = FovCamera::try_from(params)?;
211 camera.validate_params()?;
212 Ok(camera)
213}
214
215impl CameraModel for FovCamera {
216 const INTRINSIC_DIM: usize = 5;
217 type IntrinsicJacobian = SMatrix<f64, 2, 5>;
218 type PointJacobian = SMatrix<f64, 2, 3>;
219
220 fn project(&self, p_cam: &Vector3<f64>) -> Result<Vector2<f64>, CameraModelError> {
226 let x = p_cam[0];
227 let y = p_cam[1];
228 let z = p_cam[2];
229
230 if z < crate::GEOMETRIC_PRECISION {
231 return Err(CameraModelError::ProjectionOutOfBounds);
232 }
233
234 let r = (x * x + y * y).sqrt();
235 let w = self.distortion_params();
236 let tan_w_2 = (w / 2.0).tan();
237 let mul2tanwby2 = tan_w_2 * 2.0;
238
239 let rd = if r > crate::GEOMETRIC_PRECISION {
240 let atan_wrd = (mul2tanwby2 * r / z).atan();
241 atan_wrd / (r * w)
242 } else {
243 mul2tanwby2 / w
244 };
245
246 let mx = x * rd;
247 let my = y * rd;
248
249 Ok(Vector2::new(
250 self.pinhole.fx * mx + self.pinhole.cx,
251 self.pinhole.fy * my + self.pinhole.cy,
252 ))
253 }
254
255 fn unproject(&self, point_2d: &Vector2<f64>) -> Result<Vector3<f64>, CameraModelError> {
258 let u = point_2d.x;
259 let v = point_2d.y;
260
261 let w = self.distortion_params();
262 let tan_w_2 = (w / 2.0).tan();
263 let mul2tanwby2 = tan_w_2 * 2.0;
264
265 let mx = (u - self.pinhole.cx) / self.pinhole.fx;
266 let my = (v - self.pinhole.cy) / self.pinhole.fy;
267
268 let r2 = mx * mx + my * my;
269 let rd = r2.sqrt();
270
271 if rd < crate::GEOMETRIC_PRECISION {
272 return Ok(Vector3::new(0.0, 0.0, 1.0));
273 }
274
275 let ru = (rd * w).tan() / mul2tanwby2;
276
277 let norm_factor = (1.0 + ru * ru).sqrt();
278 let x = mx * ru / (rd * norm_factor);
279 let y = my * ru / (rd * norm_factor);
280 let z = 1.0 / norm_factor;
281
282 Ok(Vector3::new(x, y, z))
283 }
284
285 fn jacobian_point(&self, p_cam: &Vector3<f64>) -> Self::PointJacobian {
288 let x = p_cam[0];
289 let y = p_cam[1];
290 let z = p_cam[2];
291
292 let r = (x * x + y * y).sqrt();
293 let w = self.distortion_params();
294 let tan_w_2 = (w / 2.0).tan();
295 let mul2tanwby2 = tan_w_2 * 2.0;
296
297 if r < crate::GEOMETRIC_PRECISION {
298 let rd = mul2tanwby2 / w;
299 return SMatrix::<f64, 2, 3>::new(
300 self.pinhole.fx * rd,
301 0.0,
302 0.0,
303 0.0,
304 self.pinhole.fy * rd,
305 0.0,
306 );
307 }
308
309 let atan_wrd = (mul2tanwby2 * r / z).atan();
310 let rd = atan_wrd / (r * w);
311
312 let datan_dr = mul2tanwby2 * z / (z * z + mul2tanwby2 * mul2tanwby2 * r * r);
314 let datan_dz = -mul2tanwby2 * r / (z * z + mul2tanwby2 * mul2tanwby2 * r * r);
315
316 let drd_dr = (datan_dr * r - atan_wrd) / (r * r * w);
317 let drd_dz = datan_dz / (r * w);
318
319 let dr_dx = x / r;
320 let dr_dy = y / r;
321
322 let dmx_dx = rd + x * drd_dr * dr_dx;
323 let dmx_dy = x * drd_dr * dr_dy;
324 let dmx_dz = x * drd_dz;
325
326 let dmy_dx = y * drd_dr * dr_dx;
327 let dmy_dy = rd + y * drd_dr * dr_dy;
328 let dmy_dz = y * drd_dz;
329
330 SMatrix::<f64, 2, 3>::new(
331 self.pinhole.fx * dmx_dx,
332 self.pinhole.fx * dmx_dy,
333 self.pinhole.fx * dmx_dz,
334 self.pinhole.fy * dmy_dx,
335 self.pinhole.fy * dmy_dy,
336 self.pinhole.fy * dmy_dz,
337 )
338 }
339
340 fn jacobian_intrinsics(&self, p_cam: &Vector3<f64>) -> Self::IntrinsicJacobian {
343 let x = p_cam[0];
344 let y = p_cam[1];
345 let z = p_cam[2];
346
347 let r = (x * x + y * y).sqrt();
348 let w = self.distortion_params();
349 let tan_w_2 = (w / 2.0).tan();
350 let mul2tanwby2 = tan_w_2 * 2.0;
351
352 let rd = if r > crate::GEOMETRIC_PRECISION {
353 let atan_wrd = (mul2tanwby2 * r / z).atan();
354 atan_wrd / (r * w)
355 } else {
356 mul2tanwby2 / w
357 };
358
359 let mx = x * rd;
360 let my = y * rd;
361
362 let drd_dw = if r > crate::GEOMETRIC_PRECISION {
367 let tan_w_2 = (w / 2.0).tan();
368 let alpha = 2.0 * tan_w_2 * r / z;
369 let atan_alpha = alpha.atan();
370
371 let sec2_w_2 = 1.0 + tan_w_2 * tan_w_2;
373 let dalpha_dw = sec2_w_2 * r / z;
374
375 let datan_dw = dalpha_dw / (1.0 + alpha * alpha);
377 (datan_dw * r * w - atan_alpha * r) / (r * r * w * w)
378 } else {
379 let tan_w_2 = (w / 2.0).tan();
380 let sec2_w_2 = 1.0 + tan_w_2 * tan_w_2;
381 (sec2_w_2 * w - 2.0 * tan_w_2) / (w * w)
385 };
386
387 let du_dw = self.pinhole.fx * x * drd_dw;
388 let dv_dw = self.pinhole.fy * y * drd_dw;
389
390 SMatrix::<f64, 2, 5>::new(mx, 0.0, 1.0, 0.0, du_dw, 0.0, my, 0.0, 1.0, dv_dw)
391 }
392
393 fn validate_params(&self) -> Result<(), CameraModelError> {
405 self.pinhole.validate()?;
406 self.get_distortion().validate()
407 }
408
409 fn get_pinhole_params(&self) -> PinholeParams {
411 PinholeParams {
412 fx: self.pinhole.fx,
413 fy: self.pinhole.fy,
414 cx: self.pinhole.cx,
415 cy: self.pinhole.cy,
416 }
417 }
418
419 fn get_distortion(&self) -> DistortionModel {
421 self.distortion
422 }
423
424 fn get_model_name(&self) -> &'static str {
426 "fov"
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433 use nalgebra::{Matrix2xX, Matrix3xX};
434
435 type TestResult = Result<(), Box<dyn std::error::Error>>;
436
437 #[test]
438 fn test_fov_camera_creation() -> TestResult {
439 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
440 let distortion = DistortionModel::FOV { w: 1.5 };
441 let camera = FovCamera::new(pinhole, distortion)?;
442
443 assert_eq!(camera.pinhole.fx, 300.0);
444 assert_eq!(camera.distortion_params(), 1.5);
445 Ok(())
446 }
447
448 #[test]
449 fn test_projection_at_optical_axis() -> TestResult {
450 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
451 let distortion = DistortionModel::FOV { w: 1.5 };
452 let camera = FovCamera::new(pinhole, distortion)?;
453
454 let p_cam = Vector3::new(0.0, 0.0, 1.0);
455 let uv = camera.project(&p_cam)?;
456
457 assert!((uv.x - 320.0).abs() < 1e-4);
458 assert!((uv.y - 240.0).abs() < 1e-4);
459
460 Ok(())
461 }
462
463 #[test]
464 fn test_jacobian_point_numerical() -> TestResult {
465 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
466 let distortion = DistortionModel::FOV { w: 1.5 };
467 let camera = FovCamera::new(pinhole, distortion)?;
468
469 let p_cam = Vector3::new(0.1, 0.2, 1.0);
470
471 let jac_analytical = camera.jacobian_point(&p_cam);
472 let eps = crate::NUMERICAL_DERIVATIVE_EPS;
473
474 for i in 0..3 {
475 let mut p_plus = p_cam;
476 let mut p_minus = p_cam;
477 p_plus[i] += eps;
478 p_minus[i] -= eps;
479
480 let uv_plus = camera.project(&p_plus)?;
481 let uv_minus = camera.project(&p_minus)?;
482 let num_jac = (uv_plus - uv_minus) / (2.0 * eps);
483
484 for r in 0..2 {
485 assert!(
486 jac_analytical[(r, i)].is_finite(),
487 "Jacobian [{r},{i}] is not finite"
488 );
489 let diff = (jac_analytical[(r, i)] - num_jac[r]).abs();
490 assert!(
491 diff < crate::JACOBIAN_TEST_TOLERANCE,
492 "Mismatch at ({}, {})",
493 r,
494 i
495 );
496 }
497 }
498 Ok(())
499 }
500
501 #[test]
502 fn test_jacobian_intrinsics_numerical() -> TestResult {
503 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
504 let distortion = DistortionModel::FOV { w: 1.5 };
505 let camera = FovCamera::new(pinhole, distortion)?;
506
507 let p_cam = Vector3::new(0.1, 0.2, 1.0);
508
509 let jac_analytical = camera.jacobian_intrinsics(&p_cam);
510 let params: DVector<f64> = (&camera).into();
511 let eps = crate::NUMERICAL_DERIVATIVE_EPS;
512
513 for i in 0..5 {
514 let mut params_plus = params.clone();
515 let mut params_minus = params.clone();
516 params_plus[i] += eps;
517 params_minus[i] -= eps;
518
519 let cam_plus = FovCamera::try_from(params_plus.as_slice())?;
520 let cam_minus = FovCamera::try_from(params_minus.as_slice())?;
521
522 let uv_plus = cam_plus.project(&p_cam)?;
523 let uv_minus = cam_minus.project(&p_cam)?;
524 let num_jac = (uv_plus - uv_minus) / (2.0 * eps);
525
526 for r in 0..2 {
527 assert!(
528 jac_analytical[(r, i)].is_finite(),
529 "Jacobian [{r},{i}] is not finite"
530 );
531 let diff = (jac_analytical[(r, i)] - num_jac[r]).abs();
532 assert!(diff < 1e-4, "Mismatch at ({}, {})", r, i);
533 }
534 }
535 Ok(())
536 }
537
538 #[test]
539 fn test_fov_from_into_traits() -> TestResult {
540 let pinhole = PinholeParams::new(400.0, 410.0, 320.0, 240.0)?;
541 let distortion = DistortionModel::FOV { w: 1.8 };
542 let camera = FovCamera::new(pinhole, distortion)?;
543
544 let params: DVector<f64> = (&camera).into();
546 assert_eq!(params.len(), 5);
547 assert_eq!(params[0], 400.0);
548 assert_eq!(params[1], 410.0);
549 assert_eq!(params[2], 320.0);
550 assert_eq!(params[3], 240.0);
551 assert_eq!(params[4], 1.8);
552
553 let arr: [f64; 5] = (&camera).into();
555 assert_eq!(arr, [400.0, 410.0, 320.0, 240.0, 1.8]);
556
557 let params_slice = [450.0, 460.0, 330.0, 250.0, 2.0];
559 let camera2 = FovCamera::try_from(¶ms_slice[..])?;
560 assert_eq!(camera2.pinhole.fx, 450.0);
561 assert_eq!(camera2.pinhole.fy, 460.0);
562 assert_eq!(camera2.pinhole.cx, 330.0);
563 assert_eq!(camera2.pinhole.cy, 250.0);
564 assert_eq!(camera2.distortion_params(), 2.0);
565
566 let camera3 = FovCamera::from([500.0, 510.0, 340.0, 260.0, 2.5]);
568 assert_eq!(camera3.pinhole.fx, 500.0);
569 assert_eq!(camera3.pinhole.fy, 510.0);
570 assert_eq!(camera3.distortion_params(), 2.5);
571
572 Ok(())
573 }
574
575 #[test]
576 fn test_linear_estimation() -> TestResult {
577 let gt_pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
579 let gt_distortion = DistortionModel::FOV { w: 1.0 };
580 let gt_camera = FovCamera::new(gt_pinhole, gt_distortion)?;
581
582 let n_points = 50;
584 let mut pts_3d = Matrix3xX::zeros(n_points);
585 let mut pts_2d = Matrix2xX::zeros(n_points);
586 let mut valid = 0;
587
588 for i in 0..n_points {
589 let angle = i as f64 * 2.0 * std::f64::consts::PI / n_points as f64;
590 let r = 0.1 + 0.3 * (i as f64 / n_points as f64);
591 let p3d = Vector3::new(r * angle.cos(), r * angle.sin(), 1.0);
592
593 if let Ok(p2d) = gt_camera.project(&p3d) {
594 pts_3d.set_column(valid, &p3d);
595 pts_2d.set_column(valid, &p2d);
596 valid += 1;
597 }
598 }
599 let pts_3d = pts_3d.columns(0, valid).into_owned();
600 let pts_2d = pts_2d.columns(0, valid).into_owned();
601
602 let init_pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
604 let init_distortion = DistortionModel::FOV { w: 0.5 };
605 let mut camera = FovCamera::new(init_pinhole, init_distortion)?;
606
607 camera.linear_estimation(&pts_3d, &pts_2d)?;
608
609 for i in 0..valid {
611 let col = pts_3d.column(i);
612 let projected = camera.project(&Vector3::new(col[0], col[1], col[2]))?;
613 let err = ((projected.x - pts_2d[(0, i)]).powi(2)
614 + (projected.y - pts_2d[(1, i)]).powi(2))
615 .sqrt();
616 assert!(err < 5.0, "Reprojection error too large: {err}");
617 }
618
619 Ok(())
620 }
621
622 #[test]
623 fn test_project_unproject_round_trip() -> TestResult {
624 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
625 let distortion = DistortionModel::FOV { w: 1.5 };
626 let camera = FovCamera::new(pinhole, distortion)?;
627
628 let test_points = [
629 Vector3::new(0.1, 0.2, 1.0),
630 Vector3::new(-0.3, 0.1, 2.0),
631 Vector3::new(0.05, -0.1, 0.5),
632 ];
633
634 for p_cam in &test_points {
635 let uv = camera.project(p_cam)?;
636 let ray = camera.unproject(&uv)?;
637 let dot = ray.dot(&p_cam.normalize());
638 assert!(
639 (dot - 1.0).abs() < 1e-6,
640 "Round-trip failed: dot={dot}, expected ~1.0"
641 );
642 }
643
644 Ok(())
645 }
646
647 #[test]
648 fn test_project_returns_error_behind_camera() -> TestResult {
649 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
650 let distortion = DistortionModel::FOV { w: 1.5 };
651 let camera = FovCamera::new(pinhole, distortion)?;
652 assert!(camera.project(&Vector3::new(0.0, 0.0, -1.0)).is_err());
653 Ok(())
654 }
655
656 #[test]
657 fn test_project_at_min_depth_boundary() -> TestResult {
658 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
659 let distortion = DistortionModel::FOV { w: 1.5 };
660 let camera = FovCamera::new(pinhole, distortion)?;
661 let p_min = Vector3::new(0.0, 0.0, crate::MIN_DEPTH);
662 if let Ok(uv) = camera.project(&p_min) {
663 assert!(uv.x.is_finite() && uv.y.is_finite());
664 }
665 Ok(())
666 }
667
668 #[test]
669 fn test_projection_off_axis() -> TestResult {
670 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
671 let distortion = DistortionModel::FOV { w: 1.5 };
672 let camera = FovCamera::new(pinhole, distortion)?;
673 let p_cam = Vector3::new(0.3, 0.0, 1.0);
674 let uv = camera.project(&p_cam)?;
675 assert!(
676 uv.x > 320.0,
677 "off-axis point should project right of principal point"
678 );
679 assert!(
680 (uv.y - 240.0).abs() < 1.0,
681 "y should be close to cy for horizontal offset"
682 );
683 Ok(())
684 }
685
686 #[test]
687 fn test_unproject_center_pixel() -> TestResult {
688 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
689 let distortion = DistortionModel::FOV { w: 1.5 };
690 let camera = FovCamera::new(pinhole, distortion)?;
691 let uv = Vector2::new(320.0, 240.0);
692 let ray = camera.unproject(&uv)?;
693 assert!(ray.x.abs() < 1e-6, "x should be ~0, got {}", ray.x);
694 assert!(ray.y.abs() < 1e-6, "y should be ~0, got {}", ray.y);
695 assert!((ray.z - 1.0).abs() < 1e-6, "z should be ~1, got {}", ray.z);
696 Ok(())
697 }
698
699 #[test]
700 fn test_batch_projection_matches_individual() -> TestResult {
701 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
702 let distortion = DistortionModel::FOV { w: 1.5 };
703 let camera = FovCamera::new(pinhole, distortion)?;
704 let pts = Matrix3xX::from_columns(&[
705 Vector3::new(0.0, 0.0, 1.0),
706 Vector3::new(0.3, 0.2, 1.5),
707 Vector3::new(-0.4, 0.1, 2.0),
708 ]);
709 let batch = camera.project_batch(&pts);
710 for i in 0..3 {
711 let col = pts.column(i);
712 let p = camera.project(&Vector3::new(col[0], col[1], col[2]))?;
713 assert!(
714 (batch[(0, i)] - p.x).abs() < 1e-10,
715 "batch u mismatch at col {i}"
716 );
717 assert!(
718 (batch[(1, i)] - p.y).abs() < 1e-10,
719 "batch v mismatch at col {i}"
720 );
721 }
722 Ok(())
723 }
724
725 #[test]
726 fn test_jacobian_dimensions() -> TestResult {
727 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
728 let distortion = DistortionModel::FOV { w: 1.5 };
729 let camera = FovCamera::new(pinhole, distortion)?;
730 let p_cam = Vector3::new(0.1, 0.2, 1.0);
731 let jac_point = camera.jacobian_point(&p_cam);
732 assert_eq!(jac_point.nrows(), 2);
733 assert_eq!(jac_point.ncols(), 3);
734 let jac_intr = camera.jacobian_intrinsics(&p_cam);
735 assert_eq!(jac_intr.nrows(), 2);
736 assert_eq!(jac_intr.ncols(), 5); Ok(())
738 }
739}