use apex_manifolds::LieGroup;
use apex_manifolds::Tangent;
use apex_manifolds::se3::SE3;
use apex_manifolds::so3::{SO3, SO3Tangent};
use faer::prelude::ReborrowMut;
use nalgebra::{Matrix3, Vector3};
use crate::core::variable::ManifoldVariable;
use crate::factors::Factor;
#[derive(Clone)]
pub struct EssentialMatrixFactor {
pub points_1: Vec<Vector3<f64>>,
pub points_2: Vec<Vector3<f64>>,
}
impl EssentialMatrixFactor {
pub fn new(points_1: Vec<Vector3<f64>>, points_2: Vec<Vector3<f64>>) -> Result<Self, String> {
if points_1.is_empty() || points_1.len() != points_2.len() {
return Err(format!(
"EssentialMatrixFactor needs non-empty, equal-length point lists \
(got {} and {})",
points_1.len(),
points_2.len()
));
}
Ok(Self { points_1, points_2 })
}
pub fn num_points(&self) -> usize {
self.points_1.len()
}
}
impl Factor for EssentialMatrixFactor {
fn linearize(
&self,
params: &[&[f64]],
residual: &mut [f64],
mut jacobian: Option<faer::mat::MatMut<'_, f64>>,
) {
debug_assert_eq!(
params.len(),
1,
"EssentialMatrixFactor expects 1 pose block"
);
debug_assert_eq!(params[0].len(), 7, "params[0] must be SE3 (7D)");
let pose = SE3::from_param_slice(params[0]);
let rotation = pose.rotation_so3().rotation_matrix();
let translation = pose.translation();
let t_norm = translation.norm();
for (i, (p1, p2)) in self.points_1.iter().zip(&self.points_2).enumerate() {
if t_norm < f64::EPSILON {
residual[i] = 1.0;
continue;
}
let u = translation / t_norm;
let u_x = Matrix3::new(0.0, -u.z, u.y, u.z, 0.0, -u.x, -u.y, u.x, 0.0);
let m = rotation * p1;
let e_row = p2.transpose() * u_x * rotation; residual[i] = (e_row * p1)[(0, 0)];
let Some(jac) = jacobian.as_mut() else {
continue;
};
let cross_mp2 = m.cross(p2);
let proj = cross_mp2 - u * cross_mp2.dot(&u);
let d_rho = (proj.transpose() * rotation) / t_norm;
let p1_x = Matrix3::new(0.0, -p1.z, p1.y, p1.z, 0.0, -p1.x, -p1.y, p1.x, 0.0);
let d_theta = -(e_row * p1_x);
for c in 0..3 {
*jac.rb_mut().get_mut(i, c) = d_rho[(0, c)];
*jac.rb_mut().get_mut(i, 3 + c) = d_theta[(0, c)];
}
}
}
fn residual_dim(&self) -> usize {
self.points_1.len()
}
fn jacobian_shape(&self) -> (usize, usize) {
(self.points_1.len(), 6)
}
fn validate_variables(&self, variables: &[&dyn ManifoldVariable]) -> Result<(), String> {
if variables.len() != 1 || variables[0].as_param_slice().len() != SE3::REP_SIZE {
return Err("EssentialMatrixFactor requires exactly one SE3 pose variable".into());
}
Ok(())
}
}
#[derive(Clone)]
pub struct EssentialMatrixConstraint {
pub rotation_e: SO3,
pub direction_e: Vector3<f64>,
}
impl EssentialMatrixConstraint {
pub fn new(rotation_e: SO3, direction_e: Vector3<f64>) -> Result<Self, String> {
let norm = direction_e.norm();
if !(norm.is_finite() && norm > 1e-12) {
return Err(
"EssentialMatrixConstraint requires a non-zero translation direction".into(),
);
}
Ok(Self {
rotation_e,
direction_e: direction_e / norm,
})
}
}
impl Factor for EssentialMatrixConstraint {
fn linearize(
&self,
params: &[&[f64]],
residual: &mut [f64],
jacobian: Option<faer::mat::MatMut<'_, f64>>,
) {
debug_assert_eq!(
params.len(),
1,
"EssentialMatrixConstraint expects 1 pose block"
);
debug_assert_eq!(params[0].len(), 7, "params[0] must be SE3 (7D)");
let pose = SE3::from_param_slice(params[0]);
let rotation = pose.rotation_so3();
let translation = pose.translation();
let t_norm = translation.norm();
let rel = self.rotation_e.inverse(None).compose(&rotation, None, None);
let theta = rel.log(None);
for (i, v) in theta.coeffs().iter().enumerate() {
residual[i] = *v;
}
if t_norm > f64::EPSILON {
let u = translation / t_norm;
let diff = u - self.direction_e;
for (i, v) in diff.iter().enumerate() {
residual[3 + i] = *v;
}
} else {
for i in 0..3 {
residual[3 + i] = 1.0;
}
}
let Some(mut jac) = jacobian else { return };
let jr_inv = SO3Tangent::new(theta.coeffs()).right_jacobian_inv();
for r in 0..3 {
for c in 0..3 {
*jac.rb_mut().get_mut(r, 3 + c) = jr_inv[(r, c)];
}
}
if t_norm > f64::EPSILON {
let u = translation / t_norm;
let rotation_m = pose.rotation_so3().rotation_matrix();
let proj_u = Matrix3::identity() - u * u.transpose();
let d_dir = (proj_u * rotation_m) / t_norm;
for r in 0..3 {
for c in 0..3 {
*jac.rb_mut().get_mut(3 + r, c) = d_dir[(r, c)];
}
}
}
}
fn residual_dim(&self) -> usize {
6
}
fn jacobian_shape(&self) -> (usize, usize) {
(6, 6)
}
fn validate_variables(&self, variables: &[&dyn ManifoldVariable]) -> Result<(), String> {
if variables.len() != 1 || variables[0].as_param_slice().len() != SE3::REP_SIZE {
return Err("EssentialMatrixConstraint requires exactly one SE3 pose variable".into());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use apex_manifolds::Tangent;
use apex_manifolds::se3::SE3Tangent;
type TestResult<T> = Result<T, Box<dyn std::error::Error>>;
fn truth_pose() -> SE3 {
SE3::from_isometry(nalgebra::Isometry3::from_parts(
nalgebra::Translation3::new(1.0, 0.2, -0.1),
nalgebra::UnitQuaternion::from_euler_angles(0.02, 0.05, -0.03),
))
}
fn sample_points() -> (Vec<Vector3<f64>>, Vec<Vector3<f64>>, SE3) {
let pose = truth_pose();
let p1s: Vec<Vector3<f64>> = (0..4)
.map(|i| {
Vector3::new(
0.1 + 0.2 * i as f64,
-0.1 + 0.1 * i as f64,
2.0 + 0.5 * i as f64,
)
})
.collect();
let p2s = p1s.iter().map(|p| pose.act(p, None, None)).collect();
(p1s, p2s, pose)
}
#[test]
fn zero_residual_at_truth_pose() -> TestResult<()> {
let (p1s, p2s, pose) = sample_points();
let factor = EssentialMatrixFactor::new(p1s, p2s)?;
let mut residual = vec![0.0; factor.residual_dim()];
factor.linearize(&[pose.as_param_slice()], &mut residual, None);
for (i, r) in residual.iter().enumerate() {
assert!(r.abs() < 1e-10, "residual[{i}] = {r}");
}
Ok(())
}
#[test]
fn scale_invariance() -> TestResult<()> {
let (p1s, p2s, pose) = sample_points();
let factor = EssentialMatrixFactor::new(p1s, p2s)?;
let t = pose.translation();
let scaled = SE3::new(t * 2.0, pose.rotation_quaternion());
let mut r1 = vec![0.0; factor.residual_dim()];
let mut r2 = vec![0.0; factor.residual_dim()];
factor.linearize(&[pose.as_param_slice()], &mut r1, None);
factor.linearize(&[scaled.as_param_slice()], &mut r2, None);
for i in 0..r1.len() {
assert!(
(r1[i] - r2[i]).abs() < 1e-9,
"row {i}: {} vs {}",
r1[i],
r2[i]
);
}
Ok(())
}
#[test]
fn finite_difference_jacobians() -> TestResult<()> {
let (p1s, p2s, pose) = sample_points();
let factor = EssentialMatrixFactor::new(p1s, p2s)?;
let pose_vec: Vec<f64> = pose.as_param_slice().to_vec();
let (rows, cols) = factor.jacobian_shape();
let mut r0 = vec![0.0; rows];
let mut jac_buf = vec![0.0; rows * cols];
let jac_mut = faer::mat::MatMut::from_column_major_slice_mut(&mut jac_buf, rows, cols);
factor.linearize(&[&pose_vec], &mut r0, Some(jac_mut));
const EPS: f64 = 1e-6;
const TOL: f64 = 1e-4;
for col in 0..6 {
let mut tan = [0.0f64; 6];
tan[col] = EPS;
let perturbed: Vec<f64> = pose
.right_plus(&SE3Tangent::from_slice(&tan), None, None)
.as_param_slice()
.to_vec();
let mut r_pert = vec![0.0; rows];
factor.linearize(&[&perturbed], &mut r_pert, None);
for row in 0..rows {
let fd = (r_pert[row] - r0[row]) / EPS;
let ana = jac_buf[col * rows + row];
assert!(
(fd - ana).abs() < TOL,
"J[{row},{col}]: analytical={ana:.6} fd={fd:.6}"
);
}
}
Ok(())
}
#[test]
fn constraint_zero_residual_at_truth() -> TestResult<()> {
let pose = truth_pose();
let r_e = pose.rotation_so3();
let u_e = pose.translation().normalize();
let constraint = EssentialMatrixConstraint::new(r_e, u_e)?;
let mut residual = vec![0.0; 6];
constraint.linearize(&[pose.as_param_slice()], &mut residual, None);
for (i, r) in residual.iter().enumerate() {
assert!(r.abs() < 1e-9, "residual[{i}] = {r}");
}
Ok(())
}
#[test]
fn constraint_fd_jacobians() -> TestResult<()> {
let pose = truth_pose();
let r_e = pose.rotation_so3();
let u_e = pose.translation().normalize();
let constraint = EssentialMatrixConstraint::new(r_e, u_e)?;
let pose_vec: Vec<f64> = pose.as_param_slice().to_vec();
let mut r0 = vec![0.0; 6];
let mut jac_buf = vec![0.0; 36];
let jac_mut = faer::mat::MatMut::from_column_major_slice_mut(&mut jac_buf, 6, 6);
constraint.linearize(&[&pose_vec], &mut r0, Some(jac_mut));
const EPS: f64 = 1e-6;
const TOL: f64 = 1e-4;
for col in 0..6 {
let mut tan = [0.0f64; 6];
tan[col] = EPS;
let perturbed: Vec<f64> = pose
.right_plus(&SE3Tangent::from_slice(&tan), None, None)
.as_param_slice()
.to_vec();
let mut r_pert = vec![0.0; 6];
constraint.linearize(&[&perturbed], &mut r_pert, None);
for row in 0..6 {
let fd = (r_pert[row] - r0[row]) / EPS;
let ana = jac_buf[col * 6 + row];
assert!(
(fd - ana).abs() < TOL,
"J[{row},{col}]: analytical={ana:.6} fd={fd:.6}"
);
}
}
Ok(())
}
#[test]
fn rejects_bad_construction() -> TestResult<()> {
assert!(EssentialMatrixFactor::new(vec![], vec![]).is_err());
assert!(EssentialMatrixFactor::new(vec![Vector3::zeros()], vec![]).is_err());
assert!(EssentialMatrixConstraint::new(SO3::identity(), Vector3::zeros()).is_err());
Ok(())
}
}