1use nalgebra::{DMatrix, DVector, Matrix2xX, Matrix3xX};
4use std::marker::PhantomData;
5use tracing::warn;
6
7use crate::factors::{Factor, OptimizeParams};
8use apex_camera_models::CameraModel;
9use apex_manifolds::LieGroup;
10use apex_manifolds::se3::SE3;
11
12pub trait OptimizationConfig: Send + Sync + 'static + Clone + Copy + Default {
17 const POSE: bool;
18 const LANDMARK: bool;
19 const INTRINSIC: bool;
20}
21
22impl<const P: bool, const L: bool, const I: bool> OptimizationConfig for OptimizeParams<P, L, I> {
23 const POSE: bool = P;
24 const LANDMARK: bool = L;
25 const INTRINSIC: bool = I;
26}
27
28#[derive(Clone)]
61pub struct ProjectionFactor<CAM, OP>
62where
63 CAM: CameraModel,
64 OP: OptimizationConfig,
65{
66 pub observations: Matrix2xX<f64>,
68
69 pub camera: CAM,
71
72 pub fixed_pose: Option<SE3>,
74
75 pub fixed_landmarks: Option<Matrix3xX<f64>>,
77
78 pub verbose_cheirality: bool,
80
81 _phantom: PhantomData<OP>,
83}
84
85impl<CAM, OP> ProjectionFactor<CAM, OP>
86where
87 CAM: CameraModel,
88 OP: OptimizationConfig,
89{
90 pub fn new(observations: Matrix2xX<f64>, camera: CAM) -> Self {
113 Self {
114 observations,
115 camera,
116 fixed_pose: None,
117 fixed_landmarks: None,
118 verbose_cheirality: false,
119 _phantom: PhantomData,
120 }
121 }
122
123 pub fn with_fixed_pose(mut self, pose: SE3) -> Self {
142 self.fixed_pose = Some(pose);
143 self
144 }
145
146 pub fn with_fixed_landmarks(mut self, landmarks: Matrix3xX<f64>) -> Self {
165 self.fixed_landmarks = Some(landmarks);
166 self
167 }
168
169 pub fn with_verbose_cheirality(mut self) -> Self {
173 self.verbose_cheirality = true;
174 self
175 }
176
177 pub fn num_observations(&self) -> usize {
179 self.observations.ncols()
180 }
181
182 fn evaluate_internal(
184 &self,
185 pose: &SE3,
186 landmarks: &Matrix3xX<f64>,
187 camera: &CAM,
188 compute_jacobian: bool,
189 ) -> (DVector<f64>, Option<DMatrix<f64>>) {
190 let n = self.observations.ncols();
191 let residual_dim = n * 2;
192
193 let mut residuals = DVector::zeros(residual_dim);
195
196 let mut jacobian_cols = 0;
198 if OP::POSE {
199 jacobian_cols += 6; }
201 if OP::LANDMARK {
202 jacobian_cols += n * 3; }
204 if OP::INTRINSIC {
205 jacobian_cols += CAM::INTRINSIC_DIM;
206 }
207
208 let mut jacobian_matrix = if compute_jacobian {
209 Some(DMatrix::zeros(residual_dim, jacobian_cols))
210 } else {
211 None
212 };
213
214 for i in 0..n {
216 let observation = self.observations.column(i);
217 let p_world = landmarks.column(i).into_owned();
218
219 let p_cam = pose.act(&p_world, None, None);
224
225 let uv = match camera.project(&p_cam) {
227 Ok(proj) => proj,
228 Err(_) => {
229 if self.verbose_cheirality {
230 warn!(
231 "Point {} behind camera or invalid: p_cam = ({}, {}, {})",
232 i, p_cam.x, p_cam.y, p_cam.z
233 );
234 }
235 residuals[i * 2] = 0.0;
237 residuals[i * 2 + 1] = 0.0;
238 continue;
240 }
241 };
242
243 residuals[i * 2] = uv.x - observation.x;
245 residuals[i * 2 + 1] = uv.y - observation.y;
246
247 if let Some(ref mut jac) = jacobian_matrix {
249 let mut col_offset = 0;
250
251 if OP::POSE {
253 let (d_uv_d_pcam, d_pcam_d_pose) = camera.jacobian_pose(&p_world, pose);
254 let d_uv_d_pose = d_uv_d_pcam * d_pcam_d_pose;
255 for r in 0..2 {
256 for c in 0..6 {
257 jac[(i * 2 + r, col_offset + c)] = d_uv_d_pose[(r, c)];
258 }
259 }
260 col_offset += 6;
261 }
262
263 if OP::LANDMARK {
265 let d_uv_d_pcam = camera.jacobian_point(&p_cam);
267 let rotation = pose.rotation_so3().rotation_matrix();
271 let d_uv_d_landmark = d_uv_d_pcam * rotation;
272
273 for r in 0..2 {
274 for c in 0..3 {
275 jac[(i * 2 + r, col_offset + i * 3 + c)] = d_uv_d_landmark[(r, c)];
276 }
277 }
278 }
279
280 if OP::LANDMARK {
282 col_offset += n * 3;
283 }
284
285 if OP::INTRINSIC {
287 let d_uv_d_intrinsics = camera.jacobian_intrinsics(&p_cam);
288 for r in 0..2 {
289 for c in 0..CAM::INTRINSIC_DIM {
290 jac[(i * 2 + r, col_offset + c)] = d_uv_d_intrinsics[(r, c)];
291 }
292 }
293 }
294 }
295 }
296
297 (residuals, jacobian_matrix)
298 }
299}
300
301impl<CAM, OP> Factor for ProjectionFactor<CAM, OP>
303where
304 CAM: CameraModel,
305 for<'a> CAM: From<&'a [f64]>,
306 OP: OptimizationConfig,
307{
308 fn linearize(
309 &self,
310 params: &[DVector<f64>],
311 compute_jacobian: bool,
312 ) -> (DVector<f64>, Option<DMatrix<f64>>) {
313 let mut param_idx = 0;
314
315 let pose: SE3 = if OP::POSE {
317 params
318 .get(param_idx)
319 .map(|p| {
320 param_idx += 1;
321 SE3::from(p.clone())
322 })
323 .unwrap_or_else(SE3::identity)
324 } else {
325 self.fixed_pose.clone().unwrap_or_else(SE3::identity)
326 };
327
328 let landmarks: Matrix3xX<f64> = if OP::LANDMARK {
330 params
331 .get(param_idx)
332 .map(|flat| {
333 let n = flat.len() / 3;
334 param_idx += 1;
335 Matrix3xX::from_fn(n, |r, c| flat[c * 3 + r])
336 })
337 .unwrap_or_else(|| Matrix3xX::zeros(0))
338 } else {
339 self.fixed_landmarks
340 .clone()
341 .unwrap_or_else(|| Matrix3xX::zeros(0))
342 };
343
344 let camera: CAM = if OP::INTRINSIC {
346 params
347 .get(param_idx)
348 .map(|p| <CAM as From<&[f64]>>::from(p.as_slice()))
349 .unwrap_or_else(|| self.camera.clone())
350 } else {
351 self.camera.clone()
352 };
353
354 let n = self.observations.ncols();
356 assert_eq!(
357 landmarks.ncols(),
358 n,
359 "Number of landmarks ({}) must match observations ({})",
360 landmarks.ncols(),
361 n
362 );
363
364 self.evaluate_internal(&pose, &landmarks, &camera, compute_jacobian)
366 }
367
368 fn get_dimension(&self) -> usize {
369 self.observations.ncols() * 2
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use crate::factors::{BundleAdjustment, OnlyIntrinsics, SelfCalibration};
377 use apex_camera_models::PinholeCamera;
378 use nalgebra::{Vector2, Vector3};
379
380 type TestResult = Result<(), Box<dyn std::error::Error>>;
381
382 #[test]
383 fn test_projection_factor_creation() -> TestResult {
384 let camera = PinholeCamera::from([500.0, 500.0, 320.0, 240.0]);
385 let observations = Matrix2xX::from_columns(&[Vector2::new(100.0, 150.0)]);
386
387 let factor: ProjectionFactor<PinholeCamera, BundleAdjustment> =
389 ProjectionFactor::new(observations, camera);
390
391 assert_eq!(factor.num_observations(), 1);
392 assert_eq!(factor.get_dimension(), 2);
393
394 Ok(())
395 }
396
397 #[test]
398 fn test_bundle_adjustment_factor() -> TestResult {
399 let camera = PinholeCamera::from([500.0, 500.0, 320.0, 240.0]);
400
401 let p_world = Vector3::new(0.1, 0.2, 1.0);
405 let pose = SE3::identity();
406
407 let p_cam = pose.act(&p_world, None, None);
409 let uv = camera.project(&p_cam)?;
410
411 let observations = Matrix2xX::from_columns(&[uv]);
412
413 let factor: ProjectionFactor<PinholeCamera, BundleAdjustment> =
415 ProjectionFactor::new(observations, camera);
416
417 let pose_vec: DVector<f64> = pose.clone().into();
419 let landmarks_vec = DVector::from_vec(vec![p_world.x, p_world.y, p_world.z]);
420 let params = vec![pose_vec, landmarks_vec];
421
422 let (residual, jacobian) = factor.linearize(¶ms, true);
423
424 assert!(residual.norm() < 1e-10, "Residual: {:?}", residual);
426
427 let jac = jacobian.ok_or("Jacobian should be Some")?;
429 assert_eq!(jac.nrows(), 2);
430 assert_eq!(jac.ncols(), 9);
431
432 Ok(())
433 }
434
435 #[test]
436 fn test_self_calibration_factor() -> TestResult {
437 let camera = PinholeCamera::from([500.0, 500.0, 320.0, 240.0]);
438 let p_world = Vector3::new(0.1, 0.2, 1.0);
439 let pose = SE3::identity();
440
441 let p_cam = pose.act(&p_world, None, None);
443 let uv = camera.project(&p_cam)?;
444
445 let observations = Matrix2xX::from_columns(&[uv]);
446 let factor: ProjectionFactor<PinholeCamera, SelfCalibration> =
447 ProjectionFactor::new(observations, camera);
448
449 let pose_vec: DVector<f64> = pose.into();
451 let landmarks_vec = DVector::from_vec(vec![p_world.x, p_world.y, p_world.z]);
452 let intrinsics_vec = DVector::from_vec(vec![500.0, 500.0, 320.0, 240.0]);
453 let params = vec![pose_vec, landmarks_vec, intrinsics_vec];
454
455 let (residual, jacobian) = factor.linearize(¶ms, true);
456
457 assert!(residual.norm() < 1e-10);
458
459 let jac = jacobian.ok_or("Jacobian should be Some")?;
461 assert_eq!(jac.nrows(), 2);
462 assert_eq!(jac.ncols(), 13);
463
464 Ok(())
465 }
466
467 #[test]
468 fn test_calibration_factor() -> TestResult {
469 let camera = PinholeCamera::from([500.0, 500.0, 320.0, 240.0]);
470 let pose = SE3::identity();
471 let p_world = Vector3::new(0.1, 0.2, 1.0);
472
473 let p_cam = pose.act(&p_world, None, None);
475 let uv = camera.project(&p_cam)?;
476
477 let observations = Matrix2xX::from_columns(&[uv]);
478 let landmarks = Matrix3xX::from_columns(&[p_world]);
479
480 let factor: ProjectionFactor<PinholeCamera, OnlyIntrinsics> =
481 ProjectionFactor::new(observations, camera)
482 .with_fixed_pose(pose)
483 .with_fixed_landmarks(landmarks);
484
485 let intrinsics_vec = DVector::from_vec(vec![500.0, 500.0, 320.0, 240.0]);
487 let params = vec![intrinsics_vec];
488
489 let (residual, jacobian) = factor.linearize(¶ms, true);
490
491 assert!(residual.norm() < 1e-10);
492
493 let jac = jacobian.ok_or("Jacobian should be Some")?;
495 assert_eq!(jac.nrows(), 2);
496 assert_eq!(jac.ncols(), 4);
497
498 Ok(())
499 }
500
501 #[test]
502 fn test_invalid_projection_handling() -> TestResult {
503 let camera = PinholeCamera::from([500.0, 500.0, 320.0, 240.0]);
504 let observations = Matrix2xX::from_columns(&[Vector2::new(100.0, 150.0)]);
505
506 let factor: ProjectionFactor<PinholeCamera, BundleAdjustment> =
507 ProjectionFactor::new(observations, camera).with_verbose_cheirality();
508
509 let pose = SE3::identity();
510 let _landmarks = Matrix3xX::from_columns(&[Vector3::new(0.0, 0.0, -1.0)]);
512
513 let pose_vec: DVector<f64> = pose.into();
514 let landmarks_vec = DVector::from_vec(vec![0.0, 0.0, -1.0]);
515 let params = vec![pose_vec, landmarks_vec];
516
517 let (residual, _) = factor.linearize(¶ms, false);
518
519 assert!(residual[0].abs() < 1e-10);
521 assert!(residual[1].abs() < 1e-10);
522
523 Ok(())
524 }
525}