1use cumulus3d_core::ransac::{lo_ransac, Estimator, RansacParams, RansacReport};
35use cumulus3d_core::{Mat3, Mat3x4, Sim3, Vec3};
36use nalgebra::Matrix3xX;
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
40pub enum RankCheck {
41 #[default]
44 Uncentered,
45 Centered,
47 None,
49}
50
51fn numerical_rank(m: &Matrix3xX<f64>) -> usize {
52 let n = m.ncols();
53 if n == 0 {
54 return 0;
55 }
56 let sv = m.clone().svd(false, false).singular_values;
57 let max = sv.iter().cloned().fold(0.0f64, f64::max);
58 if max <= 0.0 {
59 return 0;
60 }
61 let tol = max * f64::EPSILON * (n.max(3) as f64);
63 sv.iter().filter(|s| **s > tol).count()
64}
65
66fn passes_rank(pts: &[Vec3], check: RankCheck) -> bool {
67 match check {
68 RankCheck::None => true,
69 RankCheck::Uncentered => numerical_rank(&Matrix3xX::from_columns(pts)) >= 3,
70 RankCheck::Centered => {
71 let mu = pts.iter().sum::<Vec3>() / pts.len() as f64;
72 let c: Vec<Vec3> = pts.iter().map(|p| p - mu).collect();
73 numerical_rank(&Matrix3xX::from_columns(&c)) >= 2
74 }
75 }
76}
77
78pub fn umeyama(src: &[Vec3], dst: &[Vec3], estimate_scale: bool) -> Option<Sim3> {
81 let n = src.len();
82 if n < 3 || n != dst.len() {
83 return None;
84 }
85 let inv_n = 1.0 / n as f64;
86 let mx = src.iter().sum::<Vec3>() * inv_n;
87 let my = dst.iter().sum::<Vec3>() * inv_n;
88 let mut var_x = 0.0;
89 let mut cov = Mat3::zeros();
90 for (x, y) in src.iter().zip(dst) {
91 let dx = x - mx;
92 var_x += dx.norm_squared();
93 cov += (y - my) * dx.transpose();
94 }
95 var_x *= inv_n;
96 cov *= inv_n;
97 let (u, d, v) = cumulus3d_core::linalg::svd3(&cov)?;
99 let vt = v.transpose();
100 let mut s_diag = Vec3::new(1.0, 1.0, 1.0);
101 if u.determinant() * vt.determinant() < 0.0 {
102 s_diag.z = -1.0;
103 }
104 let r = u * Mat3::from_diagonal(&s_diag) * vt;
105 let scale = if estimate_scale {
106 if var_x <= 0.0 {
107 return None;
108 }
109 d.dot(&s_diag) / var_x
110 } else {
111 1.0
112 };
113 let t = my - scale * r * mx;
114 let mut m = Mat3x4::zeros();
115 m.fixed_view_mut::<3, 3>(0, 0).copy_from(&(scale * r));
116 m.set_column(3, &t);
117 if m.iter().any(|v| !v.is_finite()) || scale <= 0.0 {
118 return None;
119 }
120 Some(Sim3::from_matrix(&m))
122}
123
124#[derive(Clone, Copy, Debug, Default)]
126pub struct Sim3Estimator {
127 pub rank_check: RankCheck,
129}
130
131impl Estimator for Sim3Estimator {
132 type X = Vec3;
133 type Y = Vec3;
134 type Model = Sim3;
135 fn min_num_samples(&self) -> usize {
136 3
137 }
138 fn estimate(&self, x: &[Vec3], y: &[Vec3], models: &mut Vec<Sim3>) {
139 if !passes_rank(x, self.rank_check) || !passes_rank(y, self.rank_check) {
140 return;
141 }
142 if let Some(m) = umeyama(x, y, true) {
143 models.push(m);
144 }
145 }
146 fn residuals(&self, x: &[Vec3], y: &[Vec3], model: &Sim3, residuals: &mut Vec<f64>) {
147 let sr = model.rotation.to_rotation_matrix() * model.scale;
148 residuals.clear();
149 residuals.extend(x.iter().zip(y).map(|(a, b)| (b - (sr * a + model.translation)).norm_squared()));
150 }
151}
152
153pub fn estimate_sim3_ransac(src: &[Vec3], dst: &[Vec3], opts: &RansacParams, rank_check: RankCheck) -> RansacReport<Sim3> {
155 let est = Sim3Estimator { rank_check };
156 lo_ransac(&est, &est, opts, src, dst)
157}
158
159#[derive(Clone, Debug, PartialEq)]
161pub struct RobustUmeyamaOptions {
162 pub iterations: usize,
164 pub factor: f64,
166 pub min_threshold: f64,
168 pub estimate_scale: bool,
170}
171
172impl Default for RobustUmeyamaOptions {
173 fn default() -> Self {
174 Self { iterations: 5, factor: 3.0, min_threshold: 0.3, estimate_scale: true }
175 }
176}
177
178#[derive(Clone, Debug)]
180pub struct RobustUmeyamaResult {
181 pub sim3: Sim3,
183 pub median_residual: f64,
185 pub num_inliers: usize,
187 pub inlier_mask: Vec<bool>,
189 pub residuals: Vec<f64>,
191}
192
193pub(crate) fn median(v: &mut [f64]) -> f64 {
195 let n = v.len();
196 if n == 0 {
197 return f64::NAN;
198 }
199 v.sort_unstable_by(|a, b| a.total_cmp(b));
200 if n % 2 == 1 {
201 v[n / 2]
202 } else {
203 0.5 * (v[n / 2 - 1] + v[n / 2])
204 }
205}
206
207pub fn robust_umeyama(src: &[Vec3], dst: &[Vec3], opts: &RobustUmeyamaOptions) -> Option<RobustUmeyamaResult> {
210 let n = src.len();
211 if n < 3 || n != dst.len() {
212 return None;
213 }
214 let mut keep = vec![true; n];
215 let mut best: Option<(Sim3, Vec<f64>)> = None;
216 let mut xs = Vec::with_capacity(n);
217 let mut ys = Vec::with_capacity(n);
218 for _ in 0..opts.iterations.max(1) {
219 xs.clear();
220 ys.clear();
221 for i in 0..n {
222 if keep[i] {
223 xs.push(src[i]);
224 ys.push(dst[i]);
225 }
226 }
227 let Some(sim) = umeyama(&xs, &ys, opts.estimate_scale) else { break };
229 let r: Vec<f64> = src.iter().zip(dst).map(|(a, b)| (b - sim.transform_point(a)).norm()).collect();
230 let mut rk: Vec<f64> = r.iter().zip(&keep).filter(|(_, k)| **k).map(|(v, _)| *v).collect();
231 let thr = (opts.factor * median(&mut rk)).max(opts.min_threshold);
232 let new_keep: Vec<bool> = r.iter().map(|v| *v < thr).collect();
233 best = Some((sim, r));
234 if new_keep.iter().filter(|k| **k).count() < 3 {
235 break;
236 }
237 keep = new_keep;
238 }
239 let (sim3, residuals) = best?;
240 let mut rk: Vec<f64> = residuals.iter().zip(&keep).filter(|(_, k)| **k).map(|(v, _)| *v).collect();
241 Some(RobustUmeyamaResult {
242 sim3,
243 median_residual: median(&mut rk),
244 num_inliers: keep.iter().filter(|k| **k).count(),
245 inlier_mask: keep,
246 residuals,
247 })
248}
249
250#[cfg(test)]
251pub(crate) mod tests {
252 use super::*;
253 use cumulus3d_core::Quat;
254 use rand::RngExt;
255 use rand_pcg::Pcg64;
256
257 pub fn random_sim3(rng: &mut Pcg64) -> Sim3 {
258 let axis = Vec3::new(rng.random_range(-1.0..1.0), rng.random_range(-1.0..1.0), rng.random_range(-1.0..1.0));
259 let s = 10f64.powf(rng.random_range(-1.0..1.0));
260 Sim3::new(
261 s,
262 Quat::from_axis_angle(&axis.normalize(), rng.random_range(-3.0..3.0)),
263 Vec3::new(rng.random_range(-100.0..100.0), rng.random_range(-100.0..100.0), rng.random_range(-100.0..100.0)),
264 )
265 }
266 pub fn rand_vec(rng: &mut Pcg64, r: f64) -> Vec3 {
267 Vec3::new(rng.random_range(-r..r), rng.random_range(-r..r), rng.random_range(-r..r))
268 }
269 pub fn gauss(rng: &mut Pcg64) -> f64 {
270 let u1: f64 = rng.random_range(1e-12..1.0);
271 let u2: f64 = rng.random::<f64>();
272 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
273 }
274 pub fn sim_close(a: &Sim3, b: &Sim3, tol: f64) {
275 assert!((a.scale - b.scale).abs() / b.scale < tol, "s {} vs {}", a.scale, b.scale);
276 let ra = a.rotation.to_rotation_matrix();
277 let rb = b.rotation.to_rotation_matrix();
278 assert!((ra - rb).norm() < tol, "R");
279 assert!((a.translation - b.translation).norm() / b.translation.norm().max(1.0) < tol, "t");
280 }
281
282 #[test]
283 fn umeyama_exact() {
284 let mut rng = cumulus3d_core::ransac::make_rng(Some(1));
285 for _ in 0..50 {
286 let t = random_sim3(&mut rng);
287 let src: Vec<Vec3> = (0..10).map(|_| rand_vec(&mut rng, 10.0)).collect();
288 let dst: Vec<Vec3> = src.iter().map(|p| t.transform_point(p)).collect();
289 let e = umeyama(&src, &dst, true).unwrap();
290 sim_close(&e, &t, 1e-9);
291 assert!((e.rotation.to_rotation_matrix().determinant() - 1.0).abs() < 1e-9);
292 }
293 }
294
295 #[test]
296 fn umeyama_isotropic_points() {
297 let mut rng = cumulus3d_core::ransac::make_rng(Some(7));
299 let src: Vec<Vec3> = [Vec3::x(), -Vec3::x(), Vec3::y(), -Vec3::y(), Vec3::z(), -Vec3::z()].into();
300 for _ in 0..50 {
301 let t = random_sim3(&mut rng);
302 let dst: Vec<Vec3> = src.iter().map(|p| t.transform_point(p)).collect();
303 sim_close(&umeyama(&src, &dst, true).unwrap(), &t, 1e-9);
304 }
305 }
306
307 #[test]
308 fn umeyama_reflection_gives_rotation() {
309 let mut rng = cumulus3d_core::ransac::make_rng(Some(2));
311 let src: Vec<Vec3> = (0..10).map(|_| rand_vec(&mut rng, 10.0)).collect();
312 let dst: Vec<Vec3> = src.iter().map(|p| Vec3::new(p.x, p.y, -p.z)).collect();
313 let e = umeyama(&src, &dst, true).unwrap();
314 assert!((e.rotation.to_rotation_matrix().determinant() - 1.0).abs() < 1e-9);
315 assert!(e.scale > 0.0);
316 }
317
318 #[test]
319 fn rank_checks() {
320 let p = vec![Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), Vec3::new(1.0, 1.0, 0.0)];
322 assert!(!passes_rank(&p, RankCheck::Uncentered));
323 assert!(passes_rank(&p, RankCheck::Centered));
324 let q: Vec<Vec3> = p.iter().map(|v| v + Vec3::new(0.0, 0.0, 5.0)).collect();
325 assert!(passes_rank(&q, RankCheck::Uncentered));
326 let line = vec![Vec3::new(1.0, 1.0, 1.0), Vec3::new(2.0, 2.0, 2.0), Vec3::new(3.0, 3.0, 3.0)];
327 assert!(!passes_rank(&line, RankCheck::Centered));
328 }
329
330 #[test]
332 fn sim3_recovery_with_outliers() {
333 let mut rng = cumulus3d_core::ransac::make_rng(Some(3));
334 let truth = random_sim3(&mut rng);
335 let n = 200;
336 let src: Vec<Vec3> = (0..n).map(|_| rand_vec(&mut rng, 50.0)).collect();
337 let mut dst: Vec<Vec3> = src.iter().map(|p| truth.transform_point(p)).collect();
338 let mut is_out = vec![false; n];
339 for i in 0..n {
340 if i % 10 < 3 {
341 is_out[i] = true;
342 dst[i] += rand_vec(&mut rng, 1.0).normalize() * (truth.scale * rng.random_range(30.0..200.0));
343 }
344 }
345 let opts = RansacParams { max_error: 1e-3 * truth.scale, random_seed: Some(9), ..Default::default() };
346 let rep = estimate_sim3_ransac(&src, &dst, &opts, RankCheck::Uncentered);
347 assert!(rep.success);
348 sim_close(rep.model.as_ref().unwrap(), &truth, 1e-8);
349 let want: Vec<bool> = is_out.iter().map(|o| !o).collect();
350 assert_eq!(rep.inlier_mask, want);
351 let r = robust_umeyama(&src, &dst, &RobustUmeyamaOptions { min_threshold: 1e-6, ..Default::default() }).unwrap();
352 sim_close(&r.sim3, &truth, 1e-8);
353 assert_eq!(r.num_inliers, n - is_out.iter().filter(|b| **b).count());
354 assert!(r.median_residual < 1e-6);
355 }
356
357 #[test]
358 fn median_even_odd() {
359 assert_eq!(median(&mut [3.0, 1.0, 2.0]), 2.0);
360 assert_eq!(median(&mut [4.0, 1.0, 2.0, 3.0]), 2.5);
361 }
362}