use nalgebra::{DMatrix, DVector, Matrix2xX, Matrix3xX};
use std::marker::PhantomData;
use tracing::warn;
use crate::factors::{Factor, OptimizeParams};
use apex_camera_models::CameraModel;
use apex_manifolds::LieGroup;
use apex_manifolds::se3::SE3;
pub trait OptimizationConfig: Send + Sync + 'static + Clone + Copy + Default {
const POSE: bool;
const LANDMARK: bool;
const INTRINSIC: bool;
}
impl<const P: bool, const L: bool, const I: bool> OptimizationConfig for OptimizeParams<P, L, I> {
const POSE: bool = P;
const LANDMARK: bool = L;
const INTRINSIC: bool = I;
}
#[derive(Clone)]
pub struct ProjectionFactor<CAM, OP>
where
CAM: CameraModel,
OP: OptimizationConfig,
{
pub observations: Matrix2xX<f64>,
pub camera: CAM,
pub fixed_pose: Option<SE3>,
pub fixed_landmarks: Option<Matrix3xX<f64>>,
pub verbose_cheirality: bool,
_phantom: PhantomData<OP>,
}
impl<CAM, OP> ProjectionFactor<CAM, OP>
where
CAM: CameraModel,
OP: OptimizationConfig,
{
pub fn new(observations: Matrix2xX<f64>, camera: CAM) -> Self {
Self {
observations,
camera,
fixed_pose: None,
fixed_landmarks: None,
verbose_cheirality: false,
_phantom: PhantomData,
}
}
pub fn with_fixed_pose(mut self, pose: SE3) -> Self {
self.fixed_pose = Some(pose);
self
}
pub fn with_fixed_landmarks(mut self, landmarks: Matrix3xX<f64>) -> Self {
self.fixed_landmarks = Some(landmarks);
self
}
pub fn with_verbose_cheirality(mut self) -> Self {
self.verbose_cheirality = true;
self
}
pub fn num_observations(&self) -> usize {
self.observations.ncols()
}
fn evaluate_internal(
&self,
pose: &SE3,
landmarks: &Matrix3xX<f64>,
camera: &CAM,
compute_jacobian: bool,
) -> (DVector<f64>, Option<DMatrix<f64>>) {
let n = self.observations.ncols();
let residual_dim = n * 2;
let mut residuals = DVector::zeros(residual_dim);
let mut jacobian_cols = 0;
if OP::POSE {
jacobian_cols += 6; }
if OP::LANDMARK {
jacobian_cols += n * 3; }
if OP::INTRINSIC {
jacobian_cols += CAM::INTRINSIC_DIM;
}
let mut jacobian_matrix = if compute_jacobian {
Some(DMatrix::zeros(residual_dim, jacobian_cols))
} else {
None
};
for i in 0..n {
let observation = self.observations.column(i);
let p_world = landmarks.column(i).into_owned();
let p_cam = pose.act(&p_world, None, None);
let uv = match camera.project(&p_cam) {
Ok(proj) => proj,
Err(_) => {
if self.verbose_cheirality {
warn!(
"Point {} behind camera or invalid: p_cam = ({}, {}, {})",
i, p_cam.x, p_cam.y, p_cam.z
);
}
residuals[i * 2] = 0.0;
residuals[i * 2 + 1] = 0.0;
continue;
}
};
residuals[i * 2] = uv.x - observation.x;
residuals[i * 2 + 1] = uv.y - observation.y;
if let Some(ref mut jac) = jacobian_matrix {
let mut col_offset = 0;
if OP::POSE {
let (d_uv_d_pcam, d_pcam_d_pose) = camera.jacobian_pose(&p_world, pose);
let d_uv_d_pose = d_uv_d_pcam * d_pcam_d_pose;
for r in 0..2 {
for c in 0..6 {
jac[(i * 2 + r, col_offset + c)] = d_uv_d_pose[(r, c)];
}
}
col_offset += 6;
}
if OP::LANDMARK {
let d_uv_d_pcam = camera.jacobian_point(&p_cam);
let rotation = pose.rotation_so3().rotation_matrix();
let d_uv_d_landmark = d_uv_d_pcam * rotation;
for r in 0..2 {
for c in 0..3 {
jac[(i * 2 + r, col_offset + i * 3 + c)] = d_uv_d_landmark[(r, c)];
}
}
}
if OP::LANDMARK {
col_offset += n * 3;
}
if OP::INTRINSIC {
let d_uv_d_intrinsics = camera.jacobian_intrinsics(&p_cam);
for r in 0..2 {
for c in 0..CAM::INTRINSIC_DIM {
jac[(i * 2 + r, col_offset + c)] = d_uv_d_intrinsics[(r, c)];
}
}
}
}
}
(residuals, jacobian_matrix)
}
}
impl<CAM, OP> Factor for ProjectionFactor<CAM, OP>
where
CAM: CameraModel,
for<'a> CAM: From<&'a [f64]>,
OP: OptimizationConfig,
{
fn linearize(
&self,
params: &[DVector<f64>],
compute_jacobian: bool,
) -> (DVector<f64>, Option<DMatrix<f64>>) {
let mut param_idx = 0;
let pose: SE3 = if OP::POSE {
params
.get(param_idx)
.map(|p| {
param_idx += 1;
SE3::from(p.clone())
})
.unwrap_or_else(SE3::identity)
} else {
self.fixed_pose.clone().unwrap_or_else(SE3::identity)
};
let landmarks: Matrix3xX<f64> = if OP::LANDMARK {
params
.get(param_idx)
.map(|flat| {
let n = flat.len() / 3;
param_idx += 1;
Matrix3xX::from_fn(n, |r, c| flat[c * 3 + r])
})
.unwrap_or_else(|| Matrix3xX::zeros(0))
} else {
self.fixed_landmarks
.clone()
.unwrap_or_else(|| Matrix3xX::zeros(0))
};
let camera: CAM = if OP::INTRINSIC {
params
.get(param_idx)
.map(|p| <CAM as From<&[f64]>>::from(p.as_slice()))
.unwrap_or_else(|| self.camera.clone())
} else {
self.camera.clone()
};
let n = self.observations.ncols();
assert_eq!(
landmarks.ncols(),
n,
"Number of landmarks ({}) must match observations ({})",
landmarks.ncols(),
n
);
self.evaluate_internal(&pose, &landmarks, &camera, compute_jacobian)
}
fn get_dimension(&self) -> usize {
self.observations.ncols() * 2
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::factors::{BundleAdjustment, OnlyIntrinsics, SelfCalibration};
use apex_camera_models::PinholeCamera;
use nalgebra::{Vector2, Vector3};
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[test]
fn test_projection_factor_creation() -> TestResult {
let camera = PinholeCamera::from([500.0, 500.0, 320.0, 240.0]);
let observations = Matrix2xX::from_columns(&[Vector2::new(100.0, 150.0)]);
let factor: ProjectionFactor<PinholeCamera, BundleAdjustment> =
ProjectionFactor::new(observations, camera);
assert_eq!(factor.num_observations(), 1);
assert_eq!(factor.get_dimension(), 2);
Ok(())
}
#[test]
fn test_bundle_adjustment_factor() -> TestResult {
let camera = PinholeCamera::from([500.0, 500.0, 320.0, 240.0]);
let p_world = Vector3::new(0.1, 0.2, 1.0);
let pose = SE3::identity();
let p_cam = pose.act(&p_world, None, None);
let uv = camera.project(&p_cam)?;
let observations = Matrix2xX::from_columns(&[uv]);
let factor: ProjectionFactor<PinholeCamera, BundleAdjustment> =
ProjectionFactor::new(observations, camera);
let pose_vec: DVector<f64> = pose.clone().into();
let landmarks_vec = DVector::from_vec(vec![p_world.x, p_world.y, p_world.z]);
let params = vec![pose_vec, landmarks_vec];
let (residual, jacobian) = factor.linearize(¶ms, true);
assert!(residual.norm() < 1e-10, "Residual: {:?}", residual);
let jac = jacobian.ok_or("Jacobian should be Some")?;
assert_eq!(jac.nrows(), 2);
assert_eq!(jac.ncols(), 9);
Ok(())
}
#[test]
fn test_self_calibration_factor() -> TestResult {
let camera = PinholeCamera::from([500.0, 500.0, 320.0, 240.0]);
let p_world = Vector3::new(0.1, 0.2, 1.0);
let pose = SE3::identity();
let p_cam = pose.act(&p_world, None, None);
let uv = camera.project(&p_cam)?;
let observations = Matrix2xX::from_columns(&[uv]);
let factor: ProjectionFactor<PinholeCamera, SelfCalibration> =
ProjectionFactor::new(observations, camera);
let pose_vec: DVector<f64> = pose.into();
let landmarks_vec = DVector::from_vec(vec![p_world.x, p_world.y, p_world.z]);
let intrinsics_vec = DVector::from_vec(vec![500.0, 500.0, 320.0, 240.0]);
let params = vec![pose_vec, landmarks_vec, intrinsics_vec];
let (residual, jacobian) = factor.linearize(¶ms, true);
assert!(residual.norm() < 1e-10);
let jac = jacobian.ok_or("Jacobian should be Some")?;
assert_eq!(jac.nrows(), 2);
assert_eq!(jac.ncols(), 13);
Ok(())
}
#[test]
fn test_calibration_factor() -> TestResult {
let camera = PinholeCamera::from([500.0, 500.0, 320.0, 240.0]);
let pose = SE3::identity();
let p_world = Vector3::new(0.1, 0.2, 1.0);
let p_cam = pose.act(&p_world, None, None);
let uv = camera.project(&p_cam)?;
let observations = Matrix2xX::from_columns(&[uv]);
let landmarks = Matrix3xX::from_columns(&[p_world]);
let factor: ProjectionFactor<PinholeCamera, OnlyIntrinsics> =
ProjectionFactor::new(observations, camera)
.with_fixed_pose(pose)
.with_fixed_landmarks(landmarks);
let intrinsics_vec = DVector::from_vec(vec![500.0, 500.0, 320.0, 240.0]);
let params = vec![intrinsics_vec];
let (residual, jacobian) = factor.linearize(¶ms, true);
assert!(residual.norm() < 1e-10);
let jac = jacobian.ok_or("Jacobian should be Some")?;
assert_eq!(jac.nrows(), 2);
assert_eq!(jac.ncols(), 4);
Ok(())
}
#[test]
fn test_invalid_projection_handling() -> TestResult {
let camera = PinholeCamera::from([500.0, 500.0, 320.0, 240.0]);
let observations = Matrix2xX::from_columns(&[Vector2::new(100.0, 150.0)]);
let factor: ProjectionFactor<PinholeCamera, BundleAdjustment> =
ProjectionFactor::new(observations, camera).with_verbose_cheirality();
let pose = SE3::identity();
let _landmarks = Matrix3xX::from_columns(&[Vector3::new(0.0, 0.0, -1.0)]);
let pose_vec: DVector<f64> = pose.into();
let landmarks_vec = DVector::from_vec(vec![0.0, 0.0, -1.0]);
let params = vec![pose_vec, landmarks_vec];
let (residual, _) = factor.linearize(¶ms, false);
assert!(residual[0].abs() < 1e-10);
assert!(residual[1].abs() < 1e-10);
Ok(())
}
}