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 jacobian_point(&self, p_cam: &Vector3<f64>) -> Self::PointJacobian {
196 let inv_neg_z = -1.0 / p_cam.z;
197 let x_n = p_cam.x * inv_neg_z;
198 let y_n = p_cam.y * inv_neg_z;
199
200 let (k1, k2) = self.distortion_params();
201 let r2 = x_n * x_n + y_n * y_n;
202 let r4 = r2 * r2;
203 let distortion = 1.0 + k1 * r2 + k2 * r4;
204 let d_dist_dr2 = k1 + 2.0 * k2 * r2;
205
206 let dxn_dz = x_n * inv_neg_z;
207 let dyn_dz = y_n * inv_neg_z;
208
209 let dx_d_dxn = distortion + x_n * d_dist_dr2 * 2.0 * x_n;
210 let dx_d_dyn = x_n * d_dist_dr2 * 2.0 * y_n;
211 let dy_d_dxn = y_n * d_dist_dr2 * 2.0 * x_n;
212 let dy_d_dyn = distortion + y_n * d_dist_dr2 * 2.0 * y_n;
213
214 let du_dx = self.f * (dx_d_dxn * inv_neg_z);
215 let du_dy = self.f * (dx_d_dyn * inv_neg_z);
216 let du_dz = self.f * (dx_d_dxn * dxn_dz + dx_d_dyn * dyn_dz);
217
218 let dv_dx = self.f * (dy_d_dxn * inv_neg_z);
219 let dv_dy = self.f * (dy_d_dyn * inv_neg_z);
220 let dv_dz = self.f * (dy_d_dxn * dxn_dz + dy_d_dyn * dyn_dz);
221
222 SMatrix::<f64, 2, 3>::new(du_dx, du_dy, du_dz, dv_dx, dv_dy, dv_dz)
223 }
224
225 fn jacobian_pose(
232 &self,
233 p_world: &Vector3<f64>,
234 pose: &SE3,
235 ) -> (Self::PointJacobian, SMatrix<f64, 3, 6>) {
236 let p_cam = pose.act(p_world, None, None);
237
238 let d_uv_d_pcam = self.jacobian_point(&p_cam);
239
240 let rotation = pose.rotation_so3().rotation_matrix();
244 let p_world_skew = skew_symmetric(p_world);
245
246 let d_pcam_d_pose = SMatrix::<f64, 3, 6>::from_fn(|r, c| {
247 if c < 3 {
248 rotation[(r, c)]
249 } else {
250 let col = c - 3;
251 -(0..3)
252 .map(|k| rotation[(r, k)] * p_world_skew[(k, col)])
253 .sum::<f64>()
254 }
255 });
256
257 (d_uv_d_pcam, d_pcam_d_pose)
258 }
259
260 fn jacobian_intrinsics(&self, p_cam: &Vector3<f64>) -> Self::IntrinsicJacobian {
265 let inv_neg_z = -1.0 / p_cam.z;
266 let x_n = p_cam.x * inv_neg_z;
267 let y_n = p_cam.y * inv_neg_z;
268
269 let (k1, k2) = self.distortion_params();
270 let r2 = x_n * x_n + y_n * y_n;
271 let r4 = r2 * r2;
272 let distortion = 1.0 + k1 * r2 + k2 * r4;
273
274 let x_d = x_n * distortion;
275 let y_d = y_n * distortion;
276
277 SMatrix::<f64, 2, 3>::new(
278 x_d,
279 self.f * x_n * r2,
280 self.f * x_n * r4,
281 y_d,
282 self.f * y_n * r2,
283 self.f * y_n * r4,
284 )
285 }
286
287 fn unproject(&self, point_2d: &Vector2<f64>) -> Result<Vector3<f64>, CameraModelError> {
291 let x_d = point_2d.x / self.f;
292 let y_d = point_2d.y / self.f;
293
294 let mut x_n = x_d;
295 let mut y_n = y_d;
296
297 let (k1, k2) = self.distortion_params();
298
299 for _ in 0..5 {
300 let r2 = x_n * x_n + y_n * y_n;
301 let distortion = 1.0 + k1 * r2 + k2 * r2 * r2;
302 x_n = x_d / distortion;
303 y_n = y_d / distortion;
304 }
305
306 let norm = (1.0 + x_n * x_n + y_n * y_n).sqrt();
307 Ok(Vector3::new(x_n / norm, y_n / norm, -1.0 / norm))
308 }
309
310 fn validate_params(&self) -> Result<(), CameraModelError> {
317 self.get_pinhole_params().validate()?;
318 self.get_distortion().validate()
319 }
320
321 fn get_pinhole_params(&self) -> PinholeParams {
323 PinholeParams {
324 fx: self.f,
325 fy: self.f,
326 cx: 0.0,
327 cy: 0.0,
328 }
329 }
330
331 fn get_distortion(&self) -> DistortionModel {
333 self.distortion
334 }
335
336 fn get_model_name(&self) -> &'static str {
338 "bal_pinhole"
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 type TestResult = Result<(), Box<dyn std::error::Error>>;
347
348 #[test]
349 fn test_bal_strict_camera_creation() -> TestResult {
350 let pinhole = PinholeParams::new(500.0, 500.0, 0.0, 0.0)?;
351 let distortion = DistortionModel::Radial { k1: 0.4, k2: -0.3 };
352 let camera = BALPinholeCameraStrict::new(pinhole, distortion)?;
353 let (k1, k2) = camera.distortion_params();
354
355 assert_eq!(camera.f, 500.0);
356 assert_eq!(k1, 0.4);
357 assert_eq!(k2, -0.3);
358 Ok(())
359 }
360
361 #[test]
362 fn test_bal_strict_rejects_different_focal_lengths() {
363 let pinhole = PinholeParams {
364 fx: 500.0,
365 fy: 505.0, cx: 0.0,
367 cy: 0.0,
368 };
369 let distortion = DistortionModel::Radial { k1: 0.0, k2: 0.0 };
370 let result = BALPinholeCameraStrict::new(pinhole, distortion);
371 assert!(result.is_err());
372 }
373
374 #[test]
375 fn test_bal_strict_rejects_non_zero_principal_point() {
376 let pinhole = PinholeParams {
377 fx: 500.0,
378 fy: 500.0,
379 cx: 320.0, cy: 0.0,
381 };
382 let distortion = DistortionModel::Radial { k1: 0.0, k2: 0.0 };
383 let result = BALPinholeCameraStrict::new(pinhole, distortion);
384 assert!(result.is_err());
385 }
386
387 #[test]
388 fn test_bal_strict_projection_at_optical_axis() -> TestResult {
389 let camera = BALPinholeCameraStrict::new_no_distortion(500.0)?;
390 let p_cam = Vector3::new(0.0, 0.0, -1.0);
391
392 let uv = camera.project(&p_cam)?;
393
394 assert!(uv.x.abs() < 1e-10);
396 assert!(uv.y.abs() < 1e-10);
397
398 Ok(())
399 }
400
401 #[test]
402 fn test_bal_strict_projection_off_axis() -> TestResult {
403 let camera = BALPinholeCameraStrict::new_no_distortion(500.0)?;
404 let p_cam = Vector3::new(0.1, 0.2, -1.0);
405
406 let uv = camera.project(&p_cam)?;
407
408 assert!((uv.x - 50.0).abs() < 1e-10);
411 assert!((uv.y - 100.0).abs() < 1e-10);
412
413 Ok(())
414 }
415
416 #[test]
417 fn test_bal_strict_from_into_traits() -> TestResult {
418 let camera = BALPinholeCameraStrict::new_no_distortion(400.0)?;
419
420 let params: DVector<f64> = (&camera).into();
422 assert_eq!(params.len(), 3);
423 assert_eq!(params[0], 400.0);
424 assert_eq!(params[1], 0.0);
425 assert_eq!(params[2], 0.0);
426
427 let arr: [f64; 3] = (&camera).into();
429 assert_eq!(arr, [400.0, 0.0, 0.0]);
430
431 let params_slice = [450.0, 0.1, 0.01];
433 let camera2 = BALPinholeCameraStrict::try_from(¶ms_slice[..])?;
434 let (cam2_k1, cam2_k2) = camera2.distortion_params();
435 assert_eq!(camera2.f, 450.0);
436 assert_eq!(cam2_k1, 0.1);
437 assert_eq!(cam2_k2, 0.01);
438
439 let camera3 = BALPinholeCameraStrict::from([500.0, 0.2, 0.02]);
441 let (cam3_k1, cam3_k2) = camera3.distortion_params();
442 assert_eq!(camera3.f, 500.0);
443 assert_eq!(cam3_k1, 0.2);
444 assert_eq!(cam3_k2, 0.02);
445
446 Ok(())
447 }
448
449 #[test]
450 fn test_project_unproject_round_trip() -> TestResult {
451 let camera = BALPinholeCameraStrict::new_no_distortion(500.0)?;
452
453 let test_points = [
455 Vector3::new(0.1, 0.2, -1.0),
456 Vector3::new(-0.3, 0.1, -2.0),
457 Vector3::new(0.05, -0.1, -0.5),
458 ];
459
460 for p_cam in &test_points {
461 let uv = camera.project(p_cam)?;
462 let ray = camera.unproject(&uv)?;
463 let dot = ray.dot(&p_cam.normalize());
464 assert!(
465 (dot - 1.0).abs() < 1e-6,
466 "Round-trip failed: dot={dot}, expected ~1.0"
467 );
468 }
469
470 Ok(())
471 }
472
473 #[test]
474 fn test_jacobian_pose_numerical() -> TestResult {
475 use apex_manifolds::LieGroup;
476 use apex_manifolds::se3::{SE3, SE3Tangent};
477
478 let camera = BALPinholeCameraStrict::new_no_distortion(500.0)?;
479
480 let pose = SE3::from_translation_euler(0.1, -0.05, 0.2, 0.0, 0.0, 0.0);
483 let p_world = Vector3::new(0.1, 0.05, -3.0);
484
485 let (d_uv_d_pcam, d_pcam_d_pose) = camera.jacobian_pose(&p_world, &pose);
486 let d_uv_d_pose = d_uv_d_pcam * d_pcam_d_pose;
487
488 let eps = crate::NUMERICAL_DERIVATIVE_EPS;
489
490 for i in 0..6 {
491 let mut d = [0.0f64; 6];
492 d[i] = eps;
493 let delta_plus = SE3Tangent::from_components(d[0], d[1], d[2], d[3], d[4], d[5]);
494 d[i] = -eps;
495 let delta_minus = SE3Tangent::from_components(d[0], d[1], d[2], d[3], d[4], d[5]);
496
497 let p_cam_plus = pose.plus(&delta_plus, None, None).act(&p_world, None, None);
499 let p_cam_minus = pose
500 .plus(&delta_minus, None, None)
501 .act(&p_world, None, None);
502
503 let uv_plus = camera.project(&p_cam_plus)?;
504 let uv_minus = camera.project(&p_cam_minus)?;
505
506 let num_deriv = (uv_plus - uv_minus) / (2.0 * eps);
507
508 for r in 0..2 {
509 let analytical = d_uv_d_pose[(r, i)];
510 let numerical = num_deriv[r];
511 assert!(
512 analytical.is_finite(),
513 "jacobian_pose[{r},{i}] is not finite"
514 );
515 let rel_err = (analytical - numerical).abs() / (1.0 + numerical.abs());
516 assert!(
517 rel_err < crate::JACOBIAN_TEST_TOLERANCE,
518 "jacobian_pose mismatch at ({r},{i}): analytical={analytical}, numerical={numerical}"
519 );
520 }
521 }
522
523 Ok(())
524 }
525
526 #[test]
527 fn test_project_returns_error_behind_camera() -> TestResult {
528 let camera = BALPinholeCameraStrict::new_no_distortion(500.0)?;
529 assert!(camera.project(&Vector3::new(0.0, 0.0, 1.0)).is_err());
531 Ok(())
532 }
533
534 #[test]
535 fn test_project_at_min_depth_boundary() -> TestResult {
536 let camera = BALPinholeCameraStrict::new_no_distortion(500.0)?;
537 let p_min = Vector3::new(0.0, 0.0, -crate::MIN_DEPTH);
539 if let Ok(uv) = camera.project(&p_min) {
540 assert!(uv.x.is_finite() && uv.y.is_finite());
541 }
542 Ok(())
543 }
544}