use nalgebra::{Matrix3, Vector3};
use crate::lie::{Se3, So3};
pub fn absolute_orientation(from: &[Vector3<f64>], to: &[Vector3<f64>]) -> Option<Se3> {
const DEGENERATE: f64 = 1e-8;
if from.len() < 3 || from.len() != to.len() {
return None;
}
let count = from.len() as f64;
let centre = |points: &[Vector3<f64>]| points.iter().sum::<Vector3<f64>>() / count;
let (from_centre, to_centre) = (centre(from), centre(to));
let mut correlation = Matrix3::zeros();
for (a, b) in from.iter().zip(to) {
correlation += (b - to_centre) * (a - from_centre).transpose();
}
let svd = correlation.svd(true, true);
let (u, v_t) = (svd.u?, svd.v_t?);
let values = svd.singular_values;
if values[0] <= 0.0 || values[1] <= values[0] * DEGENERATE {
return None;
}
let mut rotation = u * v_t;
if rotation.determinant() < 0.0 {
let mut flip = Matrix3::identity();
flip[(2, 2)] = -1.0;
rotation = u * flip * v_t;
}
let rotation = So3::from_matrix_unchecked(rotation);
let translation = to_centre - rotation.matrix() * from_centre;
Some(Se3::from_parts(rotation, translation))
}
#[cfg(test)]
mod tests {
use approx::assert_relative_eq;
use nalgebra::Vector6;
use super::*;
fn cloud() -> Vec<Vector3<f64>> {
vec![
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(0.7, -0.3, 0.2),
]
}
#[test]
fn a_known_motion_comes_back() {
let truth = Se3::exp(&Vector6::new(0.4, -0.2, 0.1, 0.3, -0.5, 0.9));
let from = cloud();
let to: Vec<_> = from.iter().map(|p| truth.transform_point(p)).collect();
let found = absolute_orientation(&from, &to).expect("three points are enough");
assert_relative_eq!(found.matrix(), truth.matrix(), epsilon = 1e-12);
}
#[test]
fn three_pairs_are_enough() {
let truth = Se3::exp(&Vector6::new(-1.0, 2.0, 0.5, 0.0, 0.0, 1.2));
let from = cloud()[..3].to_vec();
let to: Vec<_> = from.iter().map(|p| truth.transform_point(p)).collect();
let found = absolute_orientation(&from, &to).expect("three points are enough");
assert_relative_eq!(found.matrix(), truth.matrix(), epsilon = 1e-12);
}
#[test]
fn two_pairs_are_not_enough() {
let from = cloud()[..2].to_vec();
let to = from.clone();
assert!(absolute_orientation(&from, &to).is_none());
}
#[test]
fn collinear_points_determine_nothing() {
let from = vec![
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(2.0, 0.0, 0.0),
Vector3::new(3.0, 0.0, 0.0),
];
let to = from.clone();
assert!(absolute_orientation(&from, &to).is_none());
}
#[test]
fn a_coplanar_set_does_not_come_back_mirrored() {
let truth = Se3::exp(&Vector6::new(0.1, 0.2, -0.3, 0.0, 0.0, 2.0));
let from = vec![
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(1.0, 1.0, 0.0),
];
let to: Vec<_> = from.iter().map(|p| truth.transform_point(p)).collect();
let found = absolute_orientation(&from, &to).expect("four coplanar points are enough");
assert_relative_eq!(
found.rotation().matrix().determinant(),
1.0,
epsilon = 1e-12
);
assert_relative_eq!(found.matrix(), truth.matrix(), epsilon = 1e-12);
}
#[test]
fn noise_perturbs_the_answer_in_proportion() {
let truth = Se3::exp(&Vector6::new(0.5, 0.0, 0.0, 0.0, 0.4, 0.0));
let from = cloud();
let mut to: Vec<_> = from.iter().map(|p| truth.transform_point(p)).collect();
to[2] += Vector3::new(0.001, -0.001, 0.001);
let found = absolute_orientation(&from, &to).expect("three points are enough");
let error = (found * truth.inverse()).log().norm();
assert!(error < 0.01, "a millimetre moved the answer by {error}");
}
}