1use crate::{CameraModel, CameraModelError, DistortionModel, PinholeParams};
9use nalgebra::{DVector, SMatrix, Vector2, Vector3};
10
11#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct UcmCamera {
14 pub pinhole: PinholeParams,
15 pub distortion: DistortionModel,
16}
17
18impl UcmCamera {
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::UCM { alpha } => alpha,
53 _ => 0.0,
54 }
55 }
56
57 fn check_projection_condition(&self, z: f64, d: f64) -> bool {
61 let alpha = self.distortion_params();
62 let w = if alpha <= 0.5 {
63 alpha / (1.0 - alpha)
64 } else {
65 (1.0 - alpha) / alpha
66 };
67 z > -w * d
68 }
69
70 fn check_unprojection_condition(&self, r_squared: f64) -> bool {
73 let alpha = self.distortion_params();
74 if alpha > 0.5 {
75 let gamma = 1.0 - alpha;
76 r_squared <= gamma * gamma / (2.0 * alpha - 1.0)
77 } else {
78 true
79 }
80 }
81
82 pub fn linear_estimation(
86 &mut self,
87 points_3d: &nalgebra::Matrix3xX<f64>,
88 points_2d: &nalgebra::Matrix2xX<f64>,
89 ) -> Result<(), CameraModelError> {
90 if points_2d.ncols() != points_3d.ncols() {
91 return Err(CameraModelError::InvalidParams(
92 "Number of 2D and 3D points must match".to_string(),
93 ));
94 }
95
96 let num_points = points_2d.ncols();
97 let mut a = nalgebra::DMatrix::zeros(num_points * 2, 1);
98 let mut b = nalgebra::DVector::zeros(num_points * 2);
99
100 for i in 0..num_points {
101 let x = points_3d[(0, i)];
102 let y = points_3d[(1, i)];
103 let z = points_3d[(2, i)];
104 let u = points_2d[(0, i)];
105 let v = points_2d[(1, i)];
106
107 let d = (x * x + y * y + z * z).sqrt();
108 let u_cx = u - self.pinhole.cx;
109 let v_cy = v - self.pinhole.cy;
110
111 a[(i * 2, 0)] = u_cx * (d - z);
112 a[(i * 2 + 1, 0)] = v_cy * (d - z);
113
114 b[i * 2] = (self.pinhole.fx * x) - (u_cx * z);
115 b[i * 2 + 1] = (self.pinhole.fy * y) - (v_cy * z);
116 }
117
118 let svd = a.svd(true, true);
119 let alpha = match svd.solve(&b, 1e-10) {
120 Ok(sol) => sol[0],
121 Err(err_msg) => {
122 return Err(CameraModelError::NumericalError {
123 operation: "svd_solve".to_string(),
124 details: err_msg.to_string(),
125 });
126 }
127 };
128
129 self.distortion = DistortionModel::UCM { alpha };
130
131 self.validate_params()?;
132
133 Ok(())
134 }
135}
136
137impl From<&UcmCamera> for DVector<f64> {
139 fn from(camera: &UcmCamera) -> Self {
140 let alpha = camera.distortion_params();
141 DVector::from_vec(vec![
142 camera.pinhole.fx,
143 camera.pinhole.fy,
144 camera.pinhole.cx,
145 camera.pinhole.cy,
146 alpha,
147 ])
148 }
149}
150
151impl From<&UcmCamera> for [f64; 5] {
153 fn from(camera: &UcmCamera) -> Self {
154 let alpha = camera.distortion_params();
155 [
156 camera.pinhole.fx,
157 camera.pinhole.fy,
158 camera.pinhole.cx,
159 camera.pinhole.cy,
160 alpha,
161 ]
162 }
163}
164
165impl TryFrom<&[f64]> for UcmCamera {
168 type Error = CameraModelError;
169
170 fn try_from(params: &[f64]) -> Result<Self, Self::Error> {
171 if params.len() < 5 {
172 return Err(CameraModelError::InvalidParams(format!(
173 "UcmCamera requires at least 5 parameters, got {}",
174 params.len()
175 )));
176 }
177 Ok(Self {
178 pinhole: PinholeParams {
179 fx: params[0],
180 fy: params[1],
181 cx: params[2],
182 cy: params[3],
183 },
184 distortion: DistortionModel::UCM { alpha: params[4] },
185 })
186 }
187}
188
189impl From<[f64; 5]> for UcmCamera {
191 fn from(params: [f64; 5]) -> Self {
192 Self {
193 pinhole: PinholeParams {
194 fx: params[0],
195 fy: params[1],
196 cx: params[2],
197 cy: params[3],
198 },
199 distortion: DistortionModel::UCM { alpha: params[4] },
200 }
201 }
202}
203
204pub fn try_from_params(params: &[f64]) -> Result<UcmCamera, CameraModelError> {
208 let camera = UcmCamera::try_from(params)?;
209 camera.validate_params()?;
210 Ok(camera)
211}
212
213impl CameraModel for UcmCamera {
214 const INTRINSIC_DIM: usize = 5;
215 type IntrinsicJacobian = SMatrix<f64, 2, 5>;
216 type PointJacobian = SMatrix<f64, 2, 3>;
217
218 fn project(&self, p_cam: &Vector3<f64>) -> Result<Vector2<f64>, CameraModelError> {
222 let x = p_cam[0];
223 let y = p_cam[1];
224 let z = p_cam[2];
225
226 let d = (x * x + y * y + z * z).sqrt();
227 let alpha = self.distortion_params();
228 let denom = alpha * d + (1.0 - alpha) * z;
229
230 if !self.check_projection_condition(z, d) {
232 return Err(CameraModelError::PointBehindCamera {
233 z,
234 min_z: crate::GEOMETRIC_PRECISION,
235 });
236 }
237
238 if denom < crate::GEOMETRIC_PRECISION {
239 return Err(CameraModelError::DenominatorTooSmall {
240 denom,
241 threshold: crate::GEOMETRIC_PRECISION,
242 });
243 }
244
245 Ok(Vector2::new(
246 self.pinhole.fx * x / denom + self.pinhole.cx,
247 self.pinhole.fy * y / denom + self.pinhole.cy,
248 ))
249 }
250
251 fn unproject(&self, point_2d: &Vector2<f64>) -> Result<Vector3<f64>, CameraModelError> {
255 let u = point_2d.x;
256 let v = point_2d.y;
257 let alpha = self.distortion_params();
258 let gamma = 1.0 - alpha;
259 let xi = alpha / gamma;
260 let mx = (u - self.pinhole.cx) / self.pinhole.fx * gamma;
261 let my = (v - self.pinhole.cy) / self.pinhole.fy * gamma;
262
263 let r_squared = mx * mx + my * my;
264 if !self.check_unprojection_condition(r_squared) {
265 return Err(CameraModelError::PointOutsideImage { x: u, y: v });
266 }
267
268 let num = xi + (1.0 + (1.0 - xi * xi) * r_squared).sqrt();
271 let denom = 1.0 + r_squared;
272
273 if denom < crate::GEOMETRIC_PRECISION {
274 return Err(CameraModelError::PointOutsideImage { x: u, y: v });
275 }
276
277 let coeff = num / denom;
278
279 let point3d = Vector3::new(coeff * mx, coeff * my, coeff) - Vector3::new(0.0, 0.0, xi);
280
281 Ok(point3d.normalize())
282 }
283
284 fn jacobian_point(&self, p_cam: &Vector3<f64>) -> Self::PointJacobian {
287 let x = p_cam[0];
288 let y = p_cam[1];
289 let z = p_cam[2];
290
291 let rho = (x * x + y * y + z * z).sqrt();
292 let alpha = self.distortion_params();
293
294 let d_denom_dx = alpha * x / rho;
301 let d_denom_dy = alpha * y / rho;
302 let d_denom_dz = alpha * z / rho + (1.0 - alpha);
303
304 let denom = alpha * rho + (1.0 - alpha) * z;
305
306 let denom2 = denom * denom;
318
319 let mut jac = SMatrix::<f64, 2, 3>::zeros();
320
321 jac[(0, 0)] = self.pinhole.fx * (denom - x * d_denom_dx) / denom2;
322 jac[(0, 1)] = self.pinhole.fx * (-x * d_denom_dy) / denom2;
323 jac[(0, 2)] = self.pinhole.fx * (-x * d_denom_dz) / denom2;
324
325 jac[(1, 0)] = self.pinhole.fy * (-y * d_denom_dx) / denom2;
326 jac[(1, 1)] = self.pinhole.fy * (denom - y * d_denom_dy) / denom2;
327 jac[(1, 2)] = self.pinhole.fy * (-y * d_denom_dz) / denom2;
328
329 jac
330 }
331
332 fn jacobian_intrinsics(&self, p_cam: &Vector3<f64>) -> Self::IntrinsicJacobian {
335 let x = p_cam[0];
336 let y = p_cam[1];
337 let z = p_cam[2];
338
339 let rho = (x * x + y * y + z * z).sqrt();
340 let alpha = self.distortion_params();
341 let denom = alpha * rho + (1.0 - alpha) * z;
342
343 let x_norm = x / denom;
344 let y_norm = y / denom;
345
346 let u_cx = self.pinhole.fx * x_norm;
347 let v_cy = self.pinhole.fy * y_norm;
348
349 let mut jac = SMatrix::<f64, 2, 5>::zeros();
350
351 jac[(0, 0)] = x_norm;
352 jac[(1, 1)] = y_norm;
353 jac[(0, 2)] = 1.0;
354 jac[(1, 3)] = 1.0;
355
356 let d_denom_d_alpha = rho - z;
357 jac[(0, 4)] = -u_cx * d_denom_d_alpha / denom;
358 jac[(1, 4)] = -v_cy * d_denom_d_alpha / denom;
359
360 jac
361 }
362
363 fn validate_params(&self) -> Result<(), CameraModelError> {
375 self.pinhole.validate()?;
376 self.get_distortion().validate()
377 }
378
379 fn get_pinhole_params(&self) -> PinholeParams {
381 PinholeParams {
382 fx: self.pinhole.fx,
383 fy: self.pinhole.fy,
384 cx: self.pinhole.cx,
385 cy: self.pinhole.cy,
386 }
387 }
388
389 fn get_distortion(&self) -> DistortionModel {
391 self.distortion
392 }
393
394 fn get_model_name(&self) -> &'static str {
396 "ucm"
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403 use nalgebra::{Matrix2xX, Matrix3xX};
404
405 type TestResult = Result<(), Box<dyn std::error::Error>>;
406
407 #[test]
408 fn test_ucm_camera_creation() -> TestResult {
409 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
410 let distortion = DistortionModel::UCM { alpha: 0.5 };
411 let camera = UcmCamera::new(pinhole, distortion)?;
412
413 assert_eq!(camera.pinhole.fx, 300.0);
414 assert_eq!(camera.distortion_params(), 0.5);
415 Ok(())
416 }
417
418 #[test]
419 fn test_projection_at_optical_axis() -> TestResult {
420 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
421 let distortion = DistortionModel::UCM { alpha: 0.5 };
422 let camera = UcmCamera::new(pinhole, distortion)?;
423
424 let p_cam = Vector3::new(0.0, 0.0, 1.0);
425 let uv = camera.project(&p_cam)?;
426 assert!((uv.x - 320.0).abs() < crate::PROJECTION_TEST_TOLERANCE);
427 assert!((uv.y - 240.0).abs() < crate::PROJECTION_TEST_TOLERANCE);
428 Ok(())
429 }
430
431 #[test]
432 fn test_jacobian_point_numerical() -> TestResult {
433 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
434 let distortion = DistortionModel::UCM { alpha: 0.6 };
435 let camera = UcmCamera::new(pinhole, distortion)?;
436
437 let p_cam = Vector3::new(0.1, 0.2, 1.0);
438
439 let jac_analytical = camera.jacobian_point(&p_cam);
440 let eps = crate::NUMERICAL_DERIVATIVE_EPS;
441
442 for i in 0..3 {
443 let mut p_plus = p_cam;
444 let mut p_minus = p_cam;
445 p_plus[i] += eps;
446 p_minus[i] -= eps;
447
448 let uv_plus = camera.project(&p_plus)?;
449 let uv_minus = camera.project(&p_minus)?;
450 let num_jac = (uv_plus - uv_minus) / (2.0 * eps);
451
452 for r in 0..2 {
453 assert!(
454 jac_analytical[(r, i)].is_finite(),
455 "Jacobian [{r},{i}] is not finite"
456 );
457 let diff = (jac_analytical[(r, i)] - num_jac[r]).abs();
458 assert!(
459 diff < crate::JACOBIAN_TEST_TOLERANCE,
460 "Mismatch at ({}, {}): {} vs {}",
461 r,
462 i,
463 jac_analytical[(r, i)],
464 num_jac[r]
465 );
466 }
467 }
468 Ok(())
469 }
470
471 #[test]
472 fn test_jacobian_intrinsics_numerical() -> TestResult {
473 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
474 let distortion = DistortionModel::UCM { alpha: 0.6 };
475 let camera = UcmCamera::new(pinhole, distortion)?;
476
477 let p_cam = Vector3::new(0.1, 0.2, 1.0);
478
479 let jac_analytical = camera.jacobian_intrinsics(&p_cam);
480 let params: DVector<f64> = (&camera).into();
481 let eps = crate::NUMERICAL_DERIVATIVE_EPS;
482
483 for i in 0..5 {
484 let mut params_plus = params.clone();
485 let mut params_minus = params.clone();
486 params_plus[i] += eps;
487 params_minus[i] -= eps;
488
489 let cam_plus = UcmCamera::try_from(params_plus.as_slice())?;
490 let cam_minus = UcmCamera::try_from(params_minus.as_slice())?;
491
492 let uv_plus = cam_plus.project(&p_cam)?;
493 let uv_minus = cam_minus.project(&p_cam)?;
494 let num_jac = (uv_plus - uv_minus) / (2.0 * eps);
495
496 for r in 0..2 {
497 assert!(
498 jac_analytical[(r, i)].is_finite(),
499 "Jacobian [{r},{i}] is not finite"
500 );
501 let diff = (jac_analytical[(r, i)] - num_jac[r]).abs();
502 assert!(
503 diff < crate::JACOBIAN_TEST_TOLERANCE,
504 "Mismatch at ({}, {}): {} vs {}",
505 r,
506 i,
507 jac_analytical[(r, i)],
508 num_jac[r]
509 );
510 }
511 }
512 Ok(())
513 }
514
515 #[test]
516 fn test_ucm_from_into_traits() -> TestResult {
517 let pinhole = PinholeParams::new(400.0, 410.0, 320.0, 240.0)?;
518 let distortion = DistortionModel::UCM { alpha: 0.7 };
519 let camera = UcmCamera::new(pinhole, distortion)?;
520
521 let params: DVector<f64> = (&camera).into();
523 assert_eq!(params.len(), 5);
524 assert_eq!(params[0], 400.0);
525 assert_eq!(params[1], 410.0);
526 assert_eq!(params[2], 320.0);
527 assert_eq!(params[3], 240.0);
528 assert_eq!(params[4], 0.7);
529
530 let arr: [f64; 5] = (&camera).into();
532 assert_eq!(arr, [400.0, 410.0, 320.0, 240.0, 0.7]);
533
534 let params_slice = [450.0, 460.0, 330.0, 250.0, 0.8];
536 let camera2 = UcmCamera::try_from(¶ms_slice[..])?;
537 assert_eq!(camera2.pinhole.fx, 450.0);
538 assert_eq!(camera2.pinhole.fy, 460.0);
539 assert_eq!(camera2.pinhole.cx, 330.0);
540 assert_eq!(camera2.pinhole.cy, 250.0);
541 assert_eq!(camera2.distortion_params(), 0.8);
542
543 let camera3 = UcmCamera::from([500.0, 510.0, 340.0, 260.0, 0.9]);
545 assert_eq!(camera3.pinhole.fx, 500.0);
546 assert_eq!(camera3.pinhole.fy, 510.0);
547 assert_eq!(camera3.distortion_params(), 0.9);
548
549 Ok(())
550 }
551
552 #[test]
553 fn test_linear_estimation() -> TestResult {
554 let gt_pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
556 let gt_distortion = DistortionModel::UCM { alpha: 0.5 };
557 let gt_camera = UcmCamera::new(gt_pinhole, gt_distortion)?;
558
559 let n_points = 50;
561 let mut pts_3d = Matrix3xX::zeros(n_points);
562 let mut pts_2d = Matrix2xX::zeros(n_points);
563 let mut valid = 0;
564
565 for i in 0..n_points {
566 let angle = i as f64 * 2.0 * std::f64::consts::PI / n_points as f64;
567 let r = 0.1 + 0.3 * (i as f64 / n_points as f64);
568 let p3d = Vector3::new(r * angle.cos(), r * angle.sin(), 1.0);
569
570 if let Ok(p2d) = gt_camera.project(&p3d) {
571 pts_3d.set_column(valid, &p3d);
572 pts_2d.set_column(valid, &p2d);
573 valid += 1;
574 }
575 }
576 let pts_3d = pts_3d.columns(0, valid).into_owned();
577 let pts_2d = pts_2d.columns(0, valid).into_owned();
578
579 let init_pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
581 let init_distortion = DistortionModel::UCM { alpha: 0.0 };
582 let mut camera = UcmCamera::new(init_pinhole, init_distortion)?;
583
584 camera.linear_estimation(&pts_3d, &pts_2d)?;
585
586 for i in 0..valid {
588 let col = pts_3d.column(i);
589 let projected = camera.project(&Vector3::new(col[0], col[1], col[2]))?;
590 let err = ((projected.x - pts_2d[(0, i)]).powi(2)
591 + (projected.y - pts_2d[(1, i)]).powi(2))
592 .sqrt();
593 assert!(err < 1.0, "Reprojection error too large: {err}");
594 }
595
596 Ok(())
597 }
598
599 #[test]
600 fn test_project_unproject_round_trip() -> TestResult {
601 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
605 let distortion = DistortionModel::UCM { alpha: 0.6 };
606 let camera = UcmCamera::new(pinhole, distortion)?;
607
608 let test_points = [
612 Vector3::new(0.1, 0.2, 1.0),
613 Vector3::new(-0.3, 0.1, 2.0),
614 Vector3::new(0.05, -0.1, 0.5),
615 Vector3::new(0.6, 0.0, 0.8),
616 Vector3::new(0.4, -0.5, 0.7),
617 ];
618
619 for p_cam in &test_points {
620 let uv = camera.project(p_cam)?;
621 let ray = camera.unproject(&uv)?;
622 let dot = ray.dot(&p_cam.normalize());
623 assert!(
624 (dot - 1.0).abs() < 1e-8,
625 "Round-trip failed: dot={dot}, expected ~1.0 (p_cam = {p_cam:?})"
626 );
627 }
628
629 Ok(())
630 }
631
632 #[test]
633 fn test_project_returns_error_behind_camera() -> TestResult {
634 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
635 let distortion = DistortionModel::UCM { alpha: 0.5 };
636 let camera = UcmCamera::new(pinhole, distortion)?;
637 assert!(camera.project(&Vector3::new(0.0, 0.0, -1.0)).is_err());
638 Ok(())
639 }
640
641 #[test]
642 fn test_project_at_min_depth_boundary() -> TestResult {
643 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
644 let distortion = DistortionModel::UCM { alpha: 0.5 };
645 let camera = UcmCamera::new(pinhole, distortion)?;
646 let p_min = Vector3::new(0.0, 0.0, crate::MIN_DEPTH);
647 if let Ok(uv) = camera.project(&p_min) {
648 assert!(uv.x.is_finite() && uv.y.is_finite());
649 }
650 Ok(())
651 }
652
653 #[test]
654 fn test_projection_off_axis() -> TestResult {
655 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
656 let distortion = DistortionModel::UCM { alpha: 0.5 };
657 let camera = UcmCamera::new(pinhole, distortion)?;
658 let p_cam = Vector3::new(0.3, 0.0, 1.0);
659 let uv = camera.project(&p_cam)?;
660 assert!(
661 uv.x > 320.0,
662 "off-axis point should project right of principal point"
663 );
664 assert!(
665 (uv.y - 240.0).abs() < 1.0,
666 "y should be close to cy for horizontal offset"
667 );
668 Ok(())
669 }
670
671 #[test]
672 fn test_unproject_center_pixel() -> TestResult {
673 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
674 let distortion = DistortionModel::UCM { alpha: 0.5 };
675 let camera = UcmCamera::new(pinhole, distortion)?;
676 let uv = Vector2::new(320.0, 240.0);
677 let ray = camera.unproject(&uv)?;
678 assert!(ray.x.abs() < 1e-6, "x should be ~0, got {}", ray.x);
679 assert!(ray.y.abs() < 1e-6, "y should be ~0, got {}", ray.y);
680 assert!((ray.z - 1.0).abs() < 1e-6, "z should be ~1, got {}", ray.z);
681 Ok(())
682 }
683
684 #[test]
685 fn test_batch_projection_matches_individual() -> TestResult {
686 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
687 let distortion = DistortionModel::UCM { alpha: 0.5 };
688 let camera = UcmCamera::new(pinhole, distortion)?;
689 let pts = Matrix3xX::from_columns(&[
690 Vector3::new(0.0, 0.0, 1.0),
691 Vector3::new(0.3, 0.2, 1.5),
692 Vector3::new(-0.4, 0.1, 2.0),
693 ]);
694 let batch = camera.project_batch(&pts);
695 for i in 0..3 {
696 let col = pts.column(i);
697 let p = camera.project(&Vector3::new(col[0], col[1], col[2]))?;
698 assert!(
699 (batch[(0, i)] - p.x).abs() < 1e-10,
700 "batch u mismatch at col {i}"
701 );
702 assert!(
703 (batch[(1, i)] - p.y).abs() < 1e-10,
704 "batch v mismatch at col {i}"
705 );
706 }
707 Ok(())
708 }
709
710 #[test]
711 fn test_jacobian_dimensions() -> TestResult {
712 let pinhole = PinholeParams::new(300.0, 300.0, 320.0, 240.0)?;
713 let distortion = DistortionModel::UCM { alpha: 0.5 };
714 let camera = UcmCamera::new(pinhole, distortion)?;
715 let p_cam = Vector3::new(0.1, 0.2, 1.0);
716 let jac_point = camera.jacobian_point(&p_cam);
717 assert_eq!(jac_point.nrows(), 2);
718 assert_eq!(jac_point.ncols(), 3);
719 let jac_intr = camera.jacobian_intrinsics(&p_cam);
720 assert_eq!(jac_intr.nrows(), 2);
721 assert_eq!(jac_intr.ncols(), 5); Ok(())
723 }
724}