1use nalgebra::{Matrix3, Vector3};
20
21use crate::lie::{Se3, So3};
22
23pub fn absolute_orientation(from: &[Vector3<f64>], to: &[Vector3<f64>]) -> Option<Se3> {
40 const DEGENERATE: f64 = 1e-8;
43
44 if from.len() < 3 || from.len() != to.len() {
45 return None;
46 }
47
48 let count = from.len() as f64;
49 let centre = |points: &[Vector3<f64>]| points.iter().sum::<Vector3<f64>>() / count;
50 let (from_centre, to_centre) = (centre(from), centre(to));
51
52 let mut correlation = Matrix3::zeros();
53 for (a, b) in from.iter().zip(to) {
54 correlation += (b - to_centre) * (a - from_centre).transpose();
55 }
56
57 let svd = correlation.svd(true, true);
58 let (u, v_t) = (svd.u?, svd.v_t?);
59 let values = svd.singular_values;
60 if values[0] <= 0.0 || values[1] <= values[0] * DEGENERATE {
61 return None;
62 }
63
64 let mut rotation = u * v_t;
65 if rotation.determinant() < 0.0 {
66 let mut flip = Matrix3::identity();
70 flip[(2, 2)] = -1.0;
71 rotation = u * flip * v_t;
72 }
73
74 let rotation = So3::from_matrix_unchecked(rotation);
75 let translation = to_centre - rotation.matrix() * from_centre;
76 Some(Se3::from_parts(rotation, translation))
77}
78
79#[cfg(test)]
80mod tests {
81 use approx::assert_relative_eq;
82 use nalgebra::Vector6;
83
84 use super::*;
85
86 fn cloud() -> Vec<Vector3<f64>> {
89 vec![
90 Vector3::new(0.0, 0.0, 0.0),
91 Vector3::new(1.0, 0.0, 0.0),
92 Vector3::new(0.0, 1.0, 0.0),
93 Vector3::new(0.0, 0.0, 1.0),
94 Vector3::new(0.7, -0.3, 0.2),
95 ]
96 }
97
98 #[test]
100 fn a_known_motion_comes_back() {
101 let truth = Se3::exp(&Vector6::new(0.4, -0.2, 0.1, 0.3, -0.5, 0.9));
102 let from = cloud();
103 let to: Vec<_> = from.iter().map(|p| truth.transform_point(p)).collect();
104
105 let found = absolute_orientation(&from, &to).expect("three points are enough");
106 assert_relative_eq!(found.matrix(), truth.matrix(), epsilon = 1e-12);
107 }
108
109 #[test]
111 fn three_pairs_are_enough() {
112 let truth = Se3::exp(&Vector6::new(-1.0, 2.0, 0.5, 0.0, 0.0, 1.2));
113 let from = cloud()[..3].to_vec();
114 let to: Vec<_> = from.iter().map(|p| truth.transform_point(p)).collect();
115
116 let found = absolute_orientation(&from, &to).expect("three points are enough");
117 assert_relative_eq!(found.matrix(), truth.matrix(), epsilon = 1e-12);
118 }
119
120 #[test]
122 fn two_pairs_are_not_enough() {
123 let from = cloud()[..2].to_vec();
124 let to = from.clone();
125 assert!(absolute_orientation(&from, &to).is_none());
126 }
127
128 #[test]
131 fn collinear_points_determine_nothing() {
132 let from = vec![
133 Vector3::new(0.0, 0.0, 0.0),
134 Vector3::new(1.0, 0.0, 0.0),
135 Vector3::new(2.0, 0.0, 0.0),
136 Vector3::new(3.0, 0.0, 0.0),
137 ];
138 let to = from.clone();
139 assert!(absolute_orientation(&from, &to).is_none());
140 }
141
142 #[test]
148 fn a_coplanar_set_does_not_come_back_mirrored() {
149 let truth = Se3::exp(&Vector6::new(0.1, 0.2, -0.3, 0.0, 0.0, 2.0));
150 let from = vec![
151 Vector3::new(0.0, 0.0, 0.0),
152 Vector3::new(1.0, 0.0, 0.0),
153 Vector3::new(0.0, 1.0, 0.0),
154 Vector3::new(1.0, 1.0, 0.0),
155 ];
156 let to: Vec<_> = from.iter().map(|p| truth.transform_point(p)).collect();
157
158 let found = absolute_orientation(&from, &to).expect("four coplanar points are enough");
159 assert_relative_eq!(
160 found.rotation().matrix().determinant(),
161 1.0,
162 epsilon = 1e-12
163 );
164 assert_relative_eq!(found.matrix(), truth.matrix(), epsilon = 1e-12);
165 }
166
167 #[test]
169 fn noise_perturbs_the_answer_in_proportion() {
170 let truth = Se3::exp(&Vector6::new(0.5, 0.0, 0.0, 0.0, 0.4, 0.0));
171 let from = cloud();
172 let mut to: Vec<_> = from.iter().map(|p| truth.transform_point(p)).collect();
173 to[2] += Vector3::new(0.001, -0.001, 0.001);
175
176 let found = absolute_orientation(&from, &to).expect("three points are enough");
177 let error = (found * truth.inverse()).log().norm();
178 assert!(error < 0.01, "a millimetre moved the answer by {error}");
179 }
180}