1use faer::prelude::ReborrowMut;
4use nalgebra::{Matrix2xX, Matrix3xX};
5use std::convert::TryFrom;
6use std::marker::PhantomData;
7use tracing::warn;
8
9use crate::factors::{Factor, OptimizeParams};
10use apex_camera_models::CameraModel;
11use apex_manifolds::LieGroup;
12use apex_manifolds::se3::SE3;
13
14pub trait OptimizationConfig: Send + Sync + 'static {
19 const POSE: bool;
20 const LANDMARK: bool;
21 const INTRINSIC: bool;
22}
23
24impl<const P: bool, const L: bool, const I: bool> OptimizationConfig for OptimizeParams<P, L, I> {
25 const POSE: bool = P;
26 const LANDMARK: bool = L;
27 const INTRINSIC: bool = I;
28}
29
30#[derive(Clone)]
63pub struct ProjectionFactor<CAM, OP>
64where
65 CAM: CameraModel,
66 OP: OptimizationConfig,
67{
68 pub observations: Matrix2xX<f64>,
70
71 pub camera: CAM,
73
74 pub fixed_pose: Option<SE3>,
76
77 pub fixed_landmarks: Option<Matrix3xX<f64>>,
79
80 pub verbose_cheirality: bool,
82
83 _phantom: PhantomData<OP>,
85}
86
87impl<CAM, OP> ProjectionFactor<CAM, OP>
88where
89 CAM: CameraModel,
90 OP: OptimizationConfig,
91{
92 pub fn new(observations: Matrix2xX<f64>, camera: CAM) -> Self {
115 Self {
116 observations,
117 camera,
118 fixed_pose: None,
119 fixed_landmarks: None,
120 verbose_cheirality: false,
121 _phantom: PhantomData,
122 }
123 }
124
125 pub fn with_fixed_pose(mut self, pose: SE3) -> Self {
144 self.fixed_pose = Some(pose);
145 self
146 }
147
148 pub fn with_fixed_landmarks(mut self, landmarks: Matrix3xX<f64>) -> Self {
167 self.fixed_landmarks = Some(landmarks);
168 self
169 }
170
171 pub fn with_verbose_cheirality(mut self) -> Self {
175 self.verbose_cheirality = true;
176 self
177 }
178
179 pub fn num_observations(&self) -> usize {
181 self.observations.ncols()
182 }
183
184 fn evaluate_internal(
187 &self,
188 pose: &SE3,
189 landmarks: &Matrix3xX<f64>,
190 camera: &CAM,
191 residual: &mut [f64],
192 mut jacobian: Option<faer::mat::MatMut<'_, f64>>,
193 ) {
194 let n = self.observations.ncols();
195
196 for i in 0..n {
198 let observation = self.observations.column(i);
199 let p_world = landmarks.column(i).into_owned();
200
201 let p_cam = pose.act(&p_world, None, None);
206
207 let uv = match camera.project(&p_cam) {
209 Ok(proj) => proj,
210 Err(cam_err) => {
211 if self.verbose_cheirality {
212 warn!("Invalid projection for point {}: {}", i, cam_err);
213 }
214 residual[i * 2] = 0.0;
216 residual[i * 2 + 1] = 0.0;
217 continue;
219 }
220 };
221
222 residual[i * 2] = uv.x - observation.x;
224 residual[i * 2 + 1] = uv.y - observation.y;
225
226 if let Some(ref mut jac) = jacobian {
228 let mut col_offset = 0;
229
230 if OP::POSE {
232 let (d_uv_d_pcam, d_pcam_d_pose) = camera.jacobian_pose(&p_world, pose);
233 let d_uv_d_pose = d_uv_d_pcam * d_pcam_d_pose;
234 for r in 0..2 {
235 for c in 0..6 {
236 *jac.rb_mut().get_mut(i * 2 + r, col_offset + c) = d_uv_d_pose[(r, c)];
237 }
238 }
239 col_offset += 6;
240 }
241
242 if OP::LANDMARK {
244 let d_uv_d_pcam = camera.jacobian_point(&p_cam);
246 let rotation = pose.rotation_so3().rotation_matrix();
250 let d_uv_d_landmark = d_uv_d_pcam * rotation;
251
252 for r in 0..2 {
253 for c in 0..3 {
254 *jac.rb_mut().get_mut(i * 2 + r, col_offset + i * 3 + c) =
255 d_uv_d_landmark[(r, c)];
256 }
257 }
258 }
259
260 if OP::LANDMARK {
262 col_offset += n * 3;
263 }
264
265 if OP::INTRINSIC {
267 let d_uv_d_intrinsics = camera.jacobian_intrinsics(&p_cam);
268 for r in 0..2 {
269 for c in 0..CAM::INTRINSIC_DIM {
270 *jac.rb_mut().get_mut(i * 2 + r, col_offset + c) =
271 d_uv_d_intrinsics[(r, c)];
272 }
273 }
274 }
275 }
276 }
277 }
278}
279
280impl<CAM, OP> Factor for ProjectionFactor<CAM, OP>
282where
283 CAM: CameraModel,
284 for<'a> CAM: TryFrom<&'a [f64]>,
285 OP: OptimizationConfig,
286{
287 fn linearize(
288 &self,
289 params: &[&[f64]],
290 residual: &mut [f64],
291 jacobian: Option<faer::mat::MatMut<'_, f64>>,
292 ) {
293 let mut param_idx = 0;
294
295 let pose: SE3 = if OP::POSE {
296 let p = SE3::from_param_slice(params[param_idx]);
297 param_idx += 1;
298 p
299 } else {
300 self.fixed_pose.clone().unwrap_or_else(SE3::identity)
301 };
302
303 let landmarks: Matrix3xX<f64> = if OP::LANDMARK {
304 let flat = params[param_idx];
305 let n = flat.len() / 3;
306 param_idx += 1;
307 Matrix3xX::from_fn(n, |r, c| flat[c * 3 + r])
308 } else {
309 self.fixed_landmarks
310 .clone()
311 .unwrap_or_else(|| Matrix3xX::zeros(0))
312 };
313
314 let camera: CAM = if OP::INTRINSIC {
315 CAM::try_from(params[param_idx])
316 .ok()
317 .unwrap_or_else(|| self.camera.clone())
318 } else {
319 self.camera.clone()
320 };
321
322 let n = self.observations.ncols();
323 assert_eq!(
324 landmarks.ncols(),
325 n,
326 "Number of landmarks ({}) must match observations ({})",
327 landmarks.ncols(),
328 n
329 );
330
331 self.evaluate_internal(&pose, &landmarks, &camera, residual, jacobian);
333 }
334
335 fn residual_dim(&self) -> usize {
336 self.observations.ncols() * 2
337 }
338
339 fn jacobian_shape(&self) -> (usize, usize) {
340 let n = self.observations.ncols();
341 let mut cols = 0;
342 if OP::POSE {
343 cols += 6;
344 }
345 if OP::LANDMARK {
346 cols += n * 3;
347 }
348 if OP::INTRINSIC {
349 cols += CAM::INTRINSIC_DIM;
350 }
351 (n * 2, cols)
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use crate::factors::{BundleAdjustment, OnlyIntrinsics, SelfCalibration};
359 use apex_camera_models::PinholeCamera;
360 use nalgebra::{DMatrix, DVector, Vector2, Vector3};
361
362 type TestResult = Result<(), Box<dyn std::error::Error>>;
363
364 fn call_linearize(
365 factor: &impl Factor,
366 params: &[DVector<f64>],
367 with_jacobian: bool,
368 ) -> (Vec<f64>, Option<DMatrix<f64>>) {
369 let param_slices: Vec<&[f64]> = params.iter().map(|p| p.as_slice()).collect();
370 let mut residual = vec![0.0f64; factor.residual_dim()];
371 if with_jacobian {
372 let (rows, cols) = factor.jacobian_shape();
373 let mut jac_buf = vec![0.0f64; rows * cols];
374 let jac_mut = faer::mat::MatMut::from_column_major_slice_mut(&mut jac_buf, rows, cols);
375 factor.linearize(¶m_slices, &mut residual, Some(jac_mut));
376 let jac = DMatrix::from_column_slice(rows, cols, &jac_buf);
377 (residual, Some(jac))
378 } else {
379 factor.linearize(¶m_slices, &mut residual, None);
380 (residual, None)
381 }
382 }
383
384 #[test]
385 fn test_projection_factor_creation() -> TestResult {
386 let camera = PinholeCamera::from([500.0, 500.0, 320.0, 240.0]);
387 let observations = Matrix2xX::from_columns(&[Vector2::new(100.0, 150.0)]);
388
389 let factor: ProjectionFactor<PinholeCamera, BundleAdjustment> =
390 ProjectionFactor::new(observations, camera);
391
392 assert_eq!(factor.num_observations(), 1);
393 assert_eq!(factor.residual_dim(), 2);
394
395 Ok(())
396 }
397
398 #[test]
399 fn test_bundle_adjustment_factor() -> TestResult {
400 let camera = PinholeCamera::from([500.0, 500.0, 320.0, 240.0]);
401
402 let p_world = Vector3::new(0.1, 0.2, 1.0);
403 let pose = SE3::identity();
404
405 let p_cam = pose.act(&p_world, None, None);
406 let uv = camera.project(&p_cam)?;
407
408 let observations = Matrix2xX::from_columns(&[uv]);
409
410 let factor: ProjectionFactor<PinholeCamera, BundleAdjustment> =
411 ProjectionFactor::new(observations, camera);
412
413 let pose_vec = DVector::from_column_slice(pose.as_param_slice());
414 let landmarks_vec = DVector::from_vec(vec![p_world.x, p_world.y, p_world.z]);
415 let params = vec![pose_vec, landmarks_vec];
416
417 let (residual, jacobian) = call_linearize(&factor, ¶ms, true);
418
419 let res_norm: f64 = residual.iter().map(|x| x * x).sum::<f64>().sqrt();
420 assert!(res_norm < 1e-10, "Residual: {:?}", residual);
421
422 let jac = jacobian.ok_or("Jacobian should be Some")?;
423 assert_eq!(jac.nrows(), 2);
424 assert_eq!(jac.ncols(), 9);
425
426 Ok(())
427 }
428
429 #[test]
430 fn test_self_calibration_factor() -> TestResult {
431 let camera = PinholeCamera::from([500.0, 500.0, 320.0, 240.0]);
432 let p_world = Vector3::new(0.1, 0.2, 1.0);
433 let pose = SE3::identity();
434
435 let p_cam = pose.act(&p_world, None, None);
436 let uv = camera.project(&p_cam)?;
437
438 let observations = Matrix2xX::from_columns(&[uv]);
439 let factor: ProjectionFactor<PinholeCamera, SelfCalibration> =
440 ProjectionFactor::new(observations, camera);
441
442 let pose_vec = DVector::from_column_slice(pose.as_param_slice());
443 let landmarks_vec = DVector::from_vec(vec![p_world.x, p_world.y, p_world.z]);
444 let intrinsics_vec = DVector::from_vec(vec![500.0, 500.0, 320.0, 240.0]);
445 let params = vec![pose_vec, landmarks_vec, intrinsics_vec];
446
447 let (residual, jacobian) = call_linearize(&factor, ¶ms, true);
448
449 let res_norm: f64 = residual.iter().map(|x| x * x).sum::<f64>().sqrt();
450 assert!(res_norm < 1e-10);
451
452 let jac = jacobian.ok_or("Jacobian should be Some")?;
453 assert_eq!(jac.nrows(), 2);
454 assert_eq!(jac.ncols(), 13);
455
456 Ok(())
457 }
458
459 #[test]
460 fn test_calibration_factor() -> TestResult {
461 let camera = PinholeCamera::from([500.0, 500.0, 320.0, 240.0]);
462 let pose = SE3::identity();
463 let p_world = Vector3::new(0.1, 0.2, 1.0);
464
465 let p_cam = pose.act(&p_world, None, None);
466 let uv = camera.project(&p_cam)?;
467
468 let observations = Matrix2xX::from_columns(&[uv]);
469 let landmarks = Matrix3xX::from_columns(&[p_world]);
470
471 let factor: ProjectionFactor<PinholeCamera, OnlyIntrinsics> =
472 ProjectionFactor::new(observations, camera)
473 .with_fixed_pose(pose)
474 .with_fixed_landmarks(landmarks);
475
476 let intrinsics_vec = DVector::from_vec(vec![500.0, 500.0, 320.0, 240.0]);
477 let params = vec![intrinsics_vec];
478
479 let (residual, jacobian) = call_linearize(&factor, ¶ms, true);
480
481 let res_norm: f64 = residual.iter().map(|x| x * x).sum::<f64>().sqrt();
482 assert!(res_norm < 1e-10);
483
484 let jac = jacobian.ok_or("Jacobian should be Some")?;
485 assert_eq!(jac.nrows(), 2);
486 assert_eq!(jac.ncols(), 4);
487
488 Ok(())
489 }
490
491 #[test]
492 fn test_invalid_projection_handling() -> TestResult {
493 let camera = PinholeCamera::from([500.0, 500.0, 320.0, 240.0]);
494 let observations = Matrix2xX::from_columns(&[Vector2::new(100.0, 150.0)]);
495
496 let factor: ProjectionFactor<PinholeCamera, BundleAdjustment> =
497 ProjectionFactor::new(observations, camera).with_verbose_cheirality();
498
499 let pose = SE3::identity();
500 let pose_vec = DVector::from_column_slice(pose.as_param_slice());
501 let landmarks_vec = DVector::from_vec(vec![0.0, 0.0, -1.0]);
502 let params = vec![pose_vec, landmarks_vec];
503
504 let (residual, _) = call_linearize(&factor, ¶ms, false);
505
506 assert!(residual[0].abs() < 1e-10);
507 assert!(residual[1].abs() < 1e-10);
508
509 Ok(())
510 }
511}