1use crate::{CameraModel, CameraModelError, DistortionModel, PinholeParams, skew_symmetric};
11use apex_manifolds::LieGroup;
12use apex_manifolds::se3::SE3;
13use nalgebra::{DVector, SMatrix, Vector2, Vector3};
14
15#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct BALPinholeCameraStrict {
23 pub f: f64,
25 pub distortion: DistortionModel,
26}
27
28impl BALPinholeCameraStrict {
29 pub fn new(
50 pinhole: PinholeParams,
51 distortion: DistortionModel,
52 ) -> Result<Self, CameraModelError> {
53 if (pinhole.fx - pinhole.fy).abs() > 1e-10 {
54 return Err(CameraModelError::InvalidParams(
55 "BALPinholeCameraStrict requires fx = fy (single focal length)".to_string(),
56 ));
57 }
58 if pinhole.cx.abs() > 1e-10 || pinhole.cy.abs() > 1e-10 {
59 return Err(CameraModelError::InvalidParams(
60 "BALPinholeCameraStrict requires cx = cy = 0 (no principal point offset)"
61 .to_string(),
62 ));
63 }
64
65 let camera = Self {
66 f: pinhole.fx,
67 distortion,
68 };
69 camera.validate_params()?;
70 Ok(camera)
71 }
72
73 pub fn new_no_distortion(f: f64) -> Result<Self, CameraModelError> {
79 let pinhole = PinholeParams::new(f, f, 0.0, 0.0)?;
80 let distortion = DistortionModel::Radial { k1: 0.0, k2: 0.0 };
81 Self::new(pinhole, distortion)
82 }
83
84 fn distortion_params(&self) -> (f64, f64) {
86 match self.distortion {
87 DistortionModel::Radial { k1, k2 } => (k1, k2),
88 _ => (0.0, 0.0),
89 }
90 }
91
92 fn check_projection_condition(&self, z: f64) -> bool {
94 z < -crate::MIN_DEPTH
95 }
96}
97
98impl From<&BALPinholeCameraStrict> for DVector<f64> {
100 fn from(camera: &BALPinholeCameraStrict) -> Self {
101 let (k1, k2) = camera.distortion_params();
102 DVector::from_vec(vec![camera.f, k1, k2])
103 }
104}
105
106impl From<&BALPinholeCameraStrict> for [f64; 3] {
108 fn from(camera: &BALPinholeCameraStrict) -> Self {
109 let (k1, k2) = camera.distortion_params();
110 [camera.f, k1, k2]
111 }
112}
113
114impl TryFrom<&[f64]> for BALPinholeCameraStrict {
116 type Error = CameraModelError;
117
118 fn try_from(params: &[f64]) -> Result<Self, Self::Error> {
119 if params.len() < 3 {
120 return Err(CameraModelError::InvalidParams(format!(
121 "BALPinholeCameraStrict requires at least 3 parameters, got {}",
122 params.len()
123 )));
124 }
125 Ok(Self {
126 f: params[0],
127 distortion: DistortionModel::Radial {
128 k1: params[1],
129 k2: params[2],
130 },
131 })
132 }
133}
134
135impl From<[f64; 3]> for BALPinholeCameraStrict {
137 fn from(params: [f64; 3]) -> Self {
138 Self {
139 f: params[0],
140 distortion: DistortionModel::Radial {
141 k1: params[1],
142 k2: params[2],
143 },
144 }
145 }
146}
147
148pub fn try_from_params(params: &[f64]) -> Result<BALPinholeCameraStrict, CameraModelError> {
155 let camera = BALPinholeCameraStrict::try_from(params)?;
156 camera.validate_params()?;
157 Ok(camera)
158}
159
160impl CameraModel for BALPinholeCameraStrict {
161 const INTRINSIC_DIM: usize = 3; type IntrinsicJacobian = SMatrix<f64, 2, 3>;
163 type PointJacobian = SMatrix<f64, 2, 3>;
164
165 fn project(&self, p_cam: &Vector3<f64>) -> Result<Vector2<f64>, CameraModelError> {
172 if !self.check_projection_condition(p_cam.z) {
173 return Err(CameraModelError::ProjectionOutOfBounds);
174 }
175 let inv_neg_z = -1.0 / p_cam.z;
176 let x_n = p_cam.x * inv_neg_z;
177 let y_n = p_cam.y * inv_neg_z;
178
179 let (k1, k2) = self.distortion_params();
180 let r2 = x_n * x_n + y_n * y_n;
181 let r4 = r2 * r2;
182 let distortion = 1.0 + k1 * r2 + k2 * r4;
183
184 let x_d = x_n * distortion;
185 let y_d = y_n * distortion;
186
187 Ok(Vector2::new(self.f * x_d, self.f * y_d))
188 }
189
190 fn projection_deficit(&self, p_cam: &Vector3<f64>) -> (f64, Vector3<f64>) {
199 (p_cam.z + crate::MIN_DEPTH, Vector3::new(0.0, 0.0, 1.0))
200 }
201
202 fn jacobian_point(&self, p_cam: &Vector3<f64>) -> Self::PointJacobian {
208 let inv_neg_z = -1.0 / p_cam.z;
209 let x_n = p_cam.x * inv_neg_z;
210 let y_n = p_cam.y * inv_neg_z;
211
212 let (k1, k2) = self.distortion_params();
213 let r2 = x_n * x_n + y_n * y_n;
214 let r4 = r2 * r2;
215 let distortion = 1.0 + k1 * r2 + k2 * r4;
216 let d_dist_dr2 = k1 + 2.0 * k2 * r2;
217
218 let dxn_dz = x_n * inv_neg_z;
219 let dyn_dz = y_n * inv_neg_z;
220
221 let dx_d_dxn = distortion + x_n * d_dist_dr2 * 2.0 * x_n;
222 let dx_d_dyn = x_n * d_dist_dr2 * 2.0 * y_n;
223 let dy_d_dxn = y_n * d_dist_dr2 * 2.0 * x_n;
224 let dy_d_dyn = distortion + y_n * d_dist_dr2 * 2.0 * y_n;
225
226 let du_dx = self.f * (dx_d_dxn * inv_neg_z);
227 let du_dy = self.f * (dx_d_dyn * inv_neg_z);
228 let du_dz = self.f * (dx_d_dxn * dxn_dz + dx_d_dyn * dyn_dz);
229
230 let dv_dx = self.f * (dy_d_dxn * inv_neg_z);
231 let dv_dy = self.f * (dy_d_dyn * inv_neg_z);
232 let dv_dz = self.f * (dy_d_dxn * dxn_dz + dy_d_dyn * dyn_dz);
233
234 SMatrix::<f64, 2, 3>::new(du_dx, du_dy, du_dz, dv_dx, dv_dy, dv_dz)
235 }
236
237 fn jacobian_pose(
244 &self,
245 p_world: &Vector3<f64>,
246 pose: &SE3,
247 ) -> (Self::PointJacobian, SMatrix<f64, 3, 6>) {
248 let p_cam = pose.act(p_world, None, None);
249
250 let d_uv_d_pcam = self.jacobian_point(&p_cam);
251
252 let rotation = pose.rotation_so3().rotation_matrix();
256 let p_world_skew = skew_symmetric(p_world);
257
258 let d_pcam_d_pose = SMatrix::<f64, 3, 6>::from_fn(|r, c| {
259 if c < 3 {
260 rotation[(r, c)]
261 } else {
262 let col = c - 3;
263 -(0..3)
264 .map(|k| rotation[(r, k)] * p_world_skew[(k, col)])
265 .sum::<f64>()
266 }
267 });
268
269 (d_uv_d_pcam, d_pcam_d_pose)
270 }
271
272 fn jacobian_intrinsics(&self, p_cam: &Vector3<f64>) -> Self::IntrinsicJacobian {
277 let inv_neg_z = -1.0 / p_cam.z;
278 let x_n = p_cam.x * inv_neg_z;
279 let y_n = p_cam.y * inv_neg_z;
280
281 let (k1, k2) = self.distortion_params();
282 let r2 = x_n * x_n + y_n * y_n;
283 let r4 = r2 * r2;
284 let distortion = 1.0 + k1 * r2 + k2 * r4;
285
286 let x_d = x_n * distortion;
287 let y_d = y_n * distortion;
288
289 SMatrix::<f64, 2, 3>::new(
290 x_d,
291 self.f * x_n * r2,
292 self.f * x_n * r4,
293 y_d,
294 self.f * y_n * r2,
295 self.f * y_n * r4,
296 )
297 }
298
299 fn unproject(&self, point_2d: &Vector2<f64>) -> Result<Vector3<f64>, CameraModelError> {
303 let x_d = point_2d.x / self.f;
304 let y_d = point_2d.y / self.f;
305
306 let mut x_n = x_d;
307 let mut y_n = y_d;
308
309 let (k1, k2) = self.distortion_params();
310
311 for _ in 0..5 {
312 let r2 = x_n * x_n + y_n * y_n;
313 let distortion = 1.0 + k1 * r2 + k2 * r2 * r2;
314 x_n = x_d / distortion;
315 y_n = y_d / distortion;
316 }
317
318 let norm = (1.0 + x_n * x_n + y_n * y_n).sqrt();
319 Ok(Vector3::new(x_n / norm, y_n / norm, -1.0 / norm))
320 }
321
322 fn validate_params(&self) -> Result<(), CameraModelError> {
329 self.get_pinhole_params().validate()?;
330 self.get_distortion().validate()
331 }
332
333 fn get_pinhole_params(&self) -> PinholeParams {
335 PinholeParams {
336 fx: self.f,
337 fy: self.f,
338 cx: 0.0,
339 cy: 0.0,
340 }
341 }
342
343 fn get_distortion(&self) -> DistortionModel {
345 self.distortion
346 }
347
348 fn get_model_name(&self) -> &'static str {
350 "bal_pinhole"
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 type TestResult = Result<(), Box<dyn std::error::Error>>;
359
360 #[test]
361 fn test_bal_strict_camera_creation() -> TestResult {
362 let pinhole = PinholeParams::new(500.0, 500.0, 0.0, 0.0)?;
363 let distortion = DistortionModel::Radial { k1: 0.4, k2: -0.3 };
364 let camera = BALPinholeCameraStrict::new(pinhole, distortion)?;
365 let (k1, k2) = camera.distortion_params();
366
367 assert_eq!(camera.f, 500.0);
368 assert_eq!(k1, 0.4);
369 assert_eq!(k2, -0.3);
370 Ok(())
371 }
372
373 #[test]
374 fn test_bal_strict_rejects_different_focal_lengths() {
375 let pinhole = PinholeParams {
376 fx: 500.0,
377 fy: 505.0, cx: 0.0,
379 cy: 0.0,
380 };
381 let distortion = DistortionModel::Radial { k1: 0.0, k2: 0.0 };
382 let result = BALPinholeCameraStrict::new(pinhole, distortion);
383 assert!(result.is_err());
384 }
385
386 #[test]
387 fn test_bal_strict_rejects_non_zero_principal_point() {
388 let pinhole = PinholeParams {
389 fx: 500.0,
390 fy: 500.0,
391 cx: 320.0, cy: 0.0,
393 };
394 let distortion = DistortionModel::Radial { k1: 0.0, k2: 0.0 };
395 let result = BALPinholeCameraStrict::new(pinhole, distortion);
396 assert!(result.is_err());
397 }
398
399 #[test]
400 fn test_bal_strict_projection_at_optical_axis() -> TestResult {
401 let camera = BALPinholeCameraStrict::new_no_distortion(500.0)?;
402 let p_cam = Vector3::new(0.0, 0.0, -1.0);
403
404 let uv = camera.project(&p_cam)?;
405
406 assert!(uv.x.abs() < 1e-10);
408 assert!(uv.y.abs() < 1e-10);
409
410 Ok(())
411 }
412
413 #[test]
414 fn test_bal_strict_projection_off_axis() -> TestResult {
415 let camera = BALPinholeCameraStrict::new_no_distortion(500.0)?;
416 let p_cam = Vector3::new(0.1, 0.2, -1.0);
417
418 let uv = camera.project(&p_cam)?;
419
420 assert!((uv.x - 50.0).abs() < 1e-10);
423 assert!((uv.y - 100.0).abs() < 1e-10);
424
425 Ok(())
426 }
427
428 #[test]
429 fn test_bal_strict_from_into_traits() -> TestResult {
430 let camera = BALPinholeCameraStrict::new_no_distortion(400.0)?;
431
432 let params: DVector<f64> = (&camera).into();
434 assert_eq!(params.len(), 3);
435 assert_eq!(params[0], 400.0);
436 assert_eq!(params[1], 0.0);
437 assert_eq!(params[2], 0.0);
438
439 let arr: [f64; 3] = (&camera).into();
441 assert_eq!(arr, [400.0, 0.0, 0.0]);
442
443 let params_slice = [450.0, 0.1, 0.01];
445 let camera2 = BALPinholeCameraStrict::try_from(¶ms_slice[..])?;
446 let (cam2_k1, cam2_k2) = camera2.distortion_params();
447 assert_eq!(camera2.f, 450.0);
448 assert_eq!(cam2_k1, 0.1);
449 assert_eq!(cam2_k2, 0.01);
450
451 let camera3 = BALPinholeCameraStrict::from([500.0, 0.2, 0.02]);
453 let (cam3_k1, cam3_k2) = camera3.distortion_params();
454 assert_eq!(camera3.f, 500.0);
455 assert_eq!(cam3_k1, 0.2);
456 assert_eq!(cam3_k2, 0.02);
457
458 Ok(())
459 }
460
461 #[test]
462 fn test_project_unproject_round_trip() -> TestResult {
463 let camera = BALPinholeCameraStrict::new_no_distortion(500.0)?;
464
465 let test_points = [
467 Vector3::new(0.1, 0.2, -1.0),
468 Vector3::new(-0.3, 0.1, -2.0),
469 Vector3::new(0.05, -0.1, -0.5),
470 ];
471
472 for p_cam in &test_points {
473 let uv = camera.project(p_cam)?;
474 let ray = camera.unproject(&uv)?;
475 let dot = ray.dot(&p_cam.normalize());
476 assert!(
477 (dot - 1.0).abs() < 1e-6,
478 "Round-trip failed: dot={dot}, expected ~1.0"
479 );
480 }
481
482 Ok(())
483 }
484
485 #[test]
486 fn test_jacobian_pose_numerical() -> TestResult {
487 use apex_manifolds::LieGroup;
488 use apex_manifolds::se3::{SE3, SE3Tangent};
489
490 let camera = BALPinholeCameraStrict::new_no_distortion(500.0)?;
491
492 let pose = SE3::from_translation_euler(0.1, -0.05, 0.2, 0.0, 0.0, 0.0);
495 let p_world = Vector3::new(0.1, 0.05, -3.0);
496
497 let (d_uv_d_pcam, d_pcam_d_pose) = camera.jacobian_pose(&p_world, &pose);
498 let d_uv_d_pose = d_uv_d_pcam * d_pcam_d_pose;
499
500 let eps = crate::NUMERICAL_DERIVATIVE_EPS;
501
502 for i in 0..6 {
503 let mut d = [0.0f64; 6];
504 d[i] = eps;
505 let delta_plus = SE3Tangent::from_components(d[0], d[1], d[2], d[3], d[4], d[5]);
506 d[i] = -eps;
507 let delta_minus = SE3Tangent::from_components(d[0], d[1], d[2], d[3], d[4], d[5]);
508
509 let p_cam_plus = pose.plus(&delta_plus, None, None).act(&p_world, None, None);
511 let p_cam_minus = pose
512 .plus(&delta_minus, None, None)
513 .act(&p_world, None, None);
514
515 let uv_plus = camera.project(&p_cam_plus)?;
516 let uv_minus = camera.project(&p_cam_minus)?;
517
518 let num_deriv = (uv_plus - uv_minus) / (2.0 * eps);
519
520 for r in 0..2 {
521 let analytical = d_uv_d_pose[(r, i)];
522 let numerical = num_deriv[r];
523 assert!(
524 analytical.is_finite(),
525 "jacobian_pose[{r},{i}] is not finite"
526 );
527 let rel_err = (analytical - numerical).abs() / (1.0 + numerical.abs());
528 assert!(
529 rel_err < crate::JACOBIAN_TEST_TOLERANCE,
530 "jacobian_pose mismatch at ({r},{i}): analytical={analytical}, numerical={numerical}"
531 );
532 }
533 }
534
535 Ok(())
536 }
537
538 #[test]
539 fn test_project_returns_error_behind_camera() -> TestResult {
540 let camera = BALPinholeCameraStrict::new_no_distortion(500.0)?;
541 assert!(camera.project(&Vector3::new(0.0, 0.0, 1.0)).is_err());
543 Ok(())
544 }
545
546 #[test]
547 fn test_project_at_min_depth_boundary() -> TestResult {
548 let camera = BALPinholeCameraStrict::new_no_distortion(500.0)?;
549 let p_min = Vector3::new(0.0, 0.0, -crate::MIN_DEPTH);
551 if let Ok(uv) = camera.project(&p_min) {
552 assert!(uv.x.is_finite() && uv.y.is_finite());
553 }
554 Ok(())
555 }
556}