1use fast_mesh_bvh::Bvh;
32use fast_mesh_query::{
33 ClosestHit, ClosestOptions, QueryError, Rigid, closest_point_many, distance_to_triangle,
34};
35use fast_mesh_store::{Mesh, VertexId, par};
36
37use crate::{OpsError, Result, filled, reserve};
38
39const MIN_CORRESPONDENCES: usize = 6;
41
42pub(crate) const MIN_CONDITIONING: f64 = 1e-6;
50
51const MAX_STEP_ANGLE: f64 = 0.5;
54
55const PROBE_SAMPLES: usize = 10_000;
58
59const PROBE_DISTANCES: [f64; 2] = [0.1, 0.25];
64
65const PROBE_STEPS: usize = 12;
67
68const PROBE_MOVED: f64 = 0.25;
72
73const PROBE_AS_GOOD: f64 = 2.5;
78
79const PROBE_RMSE_FLOOR: f64 = 1e-6;
82
83const DISTANCE_CHUNK: usize = 1024;
85const PAR_MIN_DISTANCES: usize = 4096;
86
87#[derive(Debug, Clone, Copy, PartialEq)]
89pub struct IcpOptions {
90 pub max_iterations: usize,
93 pub tolerance: f64,
97 pub overlap: f64,
102 pub max_distance: f32,
105}
106
107impl Default for IcpOptions {
108 fn default() -> Self {
109 IcpOptions {
110 max_iterations: 50,
111 tolerance: 1e-6,
112 overlap: 1.0,
113 max_distance: f32::INFINITY,
114 }
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum IcpStatus {
121 Converged,
124 MaxIterations,
127 Underconstrained,
133 TooFewCorrespondences,
135 Diverged,
137}
138
139#[derive(Debug, Clone, PartialEq)]
143pub struct IcpReport {
144 pub transform: Rigid,
149 pub rmse: f64,
152 pub iterations: usize,
154 pub status: IcpStatus,
156 pub samples: usize,
158 pub inliers: usize,
160 pub conditioning: f64,
166 pub undetermined: Vec<IcpMotion>,
170 pub extent: f64,
172}
173
174#[derive(Debug, Clone, Copy, PartialEq)]
178pub struct IcpMotion {
179 pub translation: [f64; 3],
181 pub rotation: [f64; 3],
183 pub about: [f64; 3],
185 pub alternative_rmse: f64,
188}
189
190impl IcpReport {
191 pub fn converged(&self) -> bool {
193 self.status == IcpStatus::Converged
194 }
195}
196
197pub fn sample_surface(mesh: &Mesh, max_samples: usize) -> Result<Vec<[f64; 3]>> {
209 let mut used = filled(mesh.vertex_slots(), false)?;
210 for t in 0..mesh.triangle_slots() {
211 if let Some(c) = mesh.triangle(t as u32) {
212 for v in c {
213 used[v as usize] = true;
214 }
215 }
216 }
217 let total = mesh.triangle_count() + used.iter().filter(|&&u| u).count();
218 let take = total.min(max_samples);
219 let mut out = Vec::new();
220 reserve(&mut out, take)?;
221 if take == 0 {
222 return Ok(out);
223 }
224 let mut seen = 0;
225 let mut offer = |out: &mut Vec<[f64; 3]>, p: [f64; 3]| {
226 let next = (out.len() as u128 * total as u128).div_ceil(take as u128);
229 if out.len() < take && seen as u128 == next {
230 out.push(p);
231 }
232 seen += 1;
233 };
234 for t in 0..mesh.triangle_slots() {
235 let Some([a, b, c]) = mesh.triangle(t as u32) else {
236 continue;
237 };
238 if let (Some(a), Some(b), Some(c)) = (mesh.position(a), mesh.position(b), mesh.position(c))
239 {
240 let centroid = std::array::from_fn(|i| {
241 (f64::from(a[i]) + f64::from(b[i]) + f64::from(c[i])) / 3.0
242 });
243 offer(&mut out, centroid);
244 }
245 }
246 for (v, _) in used.iter().enumerate().filter(|(_, u)| **u) {
247 if let Some(p) = mesh.position(v as VertexId) {
248 offer(&mut out, p.map(f64::from));
249 }
250 }
251 Ok(out)
252}
253
254fn validate(points: &[[f64; 3]], fixed: &Mesh, options: &IcpOptions) -> Result<()> {
255 let invalid = |m| Err(OpsError::InvalidInput(m));
256 if points.is_empty() {
257 return invalid("icp needs at least one point");
258 }
259 if u32::try_from(points.len()).is_err() {
260 return invalid("icp takes fewer than 2^32 points");
261 }
262 if fixed.triangle_count() == 0 {
263 return invalid("icp needs a fixed mesh with triangles");
264 }
265 if !points.iter().flatten().all(|c| c.is_finite()) {
266 return invalid("icp points must be finite");
267 }
268 if !(options.tolerance >= 0.0 && options.tolerance.is_finite()) {
269 return invalid("tolerance must be finite and zero or greater");
270 }
271 if !(options.overlap > 0.0 && options.overlap <= 1.0) {
272 return invalid("overlap must be in (0, 1]");
273 }
274 if options.max_distance.is_nan() || options.max_distance <= 0.0 {
275 return invalid("max_distance must be positive");
276 }
277 Ok(())
278}
279
280#[derive(Clone, Copy)]
282struct Kept {
283 point: u32,
285 distance: f64,
287}
288
289struct Evaluator<'a> {
291 points: &'a [[f64; 3]],
292 fixed: &'a Mesh,
293 bvh: &'a Bvh,
294 query: ClosestOptions,
295 keep: usize,
296 moved: Vec<[f64; 3]>,
297 rounded: Vec<[f32; 3]>,
298 hits: Vec<Option<ClosestHit>>,
299 distances: Vec<f64>,
300 kept: Vec<Kept>,
301}
302
303impl<'a> Evaluator<'a> {
304 fn new(
305 points: &'a [[f64; 3]],
306 fixed: &'a Mesh,
307 bvh: &'a Bvh,
308 options: &IcpOptions,
309 ) -> Result<Self> {
310 let n = points.len();
311 let mut kept = Vec::new();
312 reserve(&mut kept, n)?;
313 Ok(Evaluator {
314 points,
315 fixed,
316 bvh,
317 query: ClosestOptions {
318 max_distance: options.max_distance,
319 },
320 keep: (options.overlap * n as f64).ceil().clamp(1.0, n as f64) as usize,
321 moved: filled(n, [0.0; 3])?,
322 rounded: filled(n, [0.0; 3])?,
323 hits: filled(n, None)?,
324 distances: filled(n, 0.0)?,
325 kept,
326 })
327 }
328
329 fn evaluate(&mut self, transform: &Rigid) -> Result<Option<f64>> {
333 for ((p, m), r) in self
334 .points
335 .iter()
336 .zip(&mut self.moved)
337 .zip(&mut self.rounded)
338 {
339 *m = apply(transform, *p);
340 *r = m.map(|c| c as f32);
341 if !r.iter().all(|c| c.is_finite()) {
342 return Ok(None);
343 }
344 }
345 closest_point_many(
346 self.fixed,
347 self.bvh,
348 &self.rounded,
349 &self.query,
350 &mut self.hits,
351 )?;
352 {
356 let (fixed, moved, hits) = (self.fixed, &self.moved, &self.hits);
357 par::for_each_chunk_mut(
358 &mut self.distances,
359 DISTANCE_CHUNK,
360 PAR_MIN_DISTANCES,
361 |i, slots| {
362 let start = i * DISTANCE_CHUNK;
363 for (k, slot) in slots.iter_mut().enumerate() {
364 let Some(hit) = hits[start + k] else {
365 continue;
366 };
367 *slot = corners_of(fixed, hit).map_or(f64::from(hit.distance), |c| {
368 distance_to_triangle(moved[start + k], c)
369 });
370 }
371 },
372 );
373 }
374 self.kept.clear();
375 for (i, hit) in self.hits.iter().enumerate() {
376 if hit.is_some() {
377 self.kept.push(Kept {
378 point: i as u32,
379 distance: self.distances[i],
380 });
381 }
382 }
383 if self.kept.len() > self.keep {
384 let order = |a: &Kept, b: &Kept| {
387 a.distance
388 .total_cmp(&b.distance)
389 .then(a.point.cmp(&b.point))
390 };
391 self.kept.select_nth_unstable_by(self.keep - 1, order);
392 self.kept.truncate(self.keep);
393 }
394 if self.kept.is_empty() {
395 return Ok(Some(f64::INFINITY));
396 }
397 let sum: f64 = self.kept.iter().map(|k| k.distance * k.distance).sum();
398 Ok(Some((sum / self.kept.len() as f64).sqrt()))
399 }
400
401 #[allow(clippy::needless_range_loop)] fn assemble(&self, center: [f64; 3], extent: f64) -> ([[f64; 6]; 6], [f64; 6], usize) {
408 let mut h = [[0.0; 6]; 6];
409 let mut g = [0.0; 6];
410 let mut rows = 0;
411 for k in &self.kept {
412 let Some((a, n)) = self.hits[k.point as usize].and_then(|hit| self.plane(hit)) else {
413 continue;
414 };
415 let p = self.moved[k.point as usize];
416 let residual = dot(n, sub(p, a));
417 let lever = cross(sub(p, center), n);
418 let row = [
419 n[0],
420 n[1],
421 n[2],
422 lever[0] / extent,
423 lever[1] / extent,
424 lever[2] / extent,
425 ];
426 for (i, ri) in row.iter().enumerate() {
427 g[i] -= ri * residual;
428 for (j, rj) in row.iter().enumerate().skip(i) {
429 h[i][j] += ri * rj;
430 }
431 }
432 rows += 1;
433 }
434 for i in 0..6 {
435 for j in 0..i {
436 h[i][j] = h[j][i];
437 }
438 }
439 (h, g, rows)
440 }
441
442 fn plane(&self, hit: ClosestHit) -> Option<([f64; 3], [f64; 3])> {
445 let [a, b, c] = corners_of(self.fixed, hit)?;
446 Some((a, unit(cross(sub(b, a), sub(c, a)))?))
447 }
448}
449
450pub fn icp(
466 points: &[[f64; 3]],
467 fixed: &Mesh,
468 bvh: &Bvh,
469 initial: &Rigid,
470 options: &IcpOptions,
471) -> Result<IcpReport> {
472 validate(points, fixed, options)?;
473 initial.validate()?;
474 let (center0, extent) = spread(points);
475 let mut eval = Evaluator::new(points, fixed, bvh, options)?;
476 let run = iterate(
477 &mut eval,
478 initial,
479 center0,
480 extent,
481 options.max_iterations,
482 options.tolerance,
483 )?;
484 if run.iterations == 0 && run.status == IcpStatus::Diverged {
485 return Err(QueryError::InvalidInput("query point must be finite").into());
486 }
487 let mut status = run.status;
488 let mut undetermined = Vec::new();
489 if run.iterations > 0
492 && matches!(
493 status,
494 IcpStatus::Converged | IcpStatus::MaxIterations | IcpStatus::Underconstrained
495 )
496 {
497 undetermined = undetermined_motions(points, fixed, bvh, &run, center0, extent, options)?;
498 if !undetermined.is_empty() {
499 status = IcpStatus::Underconstrained;
500 }
501 }
502 Ok(IcpReport {
503 transform: run.transform,
504 rmse: run.rmse,
505 iterations: run.iterations,
506 status,
507 samples: points.len(),
508 inliers: run.inliers,
509 conditioning: run.conditioning,
510 undetermined,
511 extent,
512 })
513}
514
515struct Run {
517 transform: Rigid,
518 rmse: f64,
519 inliers: usize,
520 iterations: usize,
521 status: IcpStatus,
522 conditioning: f64,
523}
524
525fn iterate(
528 eval: &mut Evaluator,
529 start: &Rigid,
530 center0: [f64; 3],
531 extent: f64,
532 max_iterations: usize,
533 tolerance: f64,
534) -> Result<Run> {
535 let mut transform = *start;
536 let mut best = (*start, f64::INFINITY, 0);
537 let mut last = best;
538 let mut evaluated = false;
539 let mut iterations = 0;
540 let mut conditioning = 0.0;
541 let mut settled = None;
543 let status = loop {
544 let Some(rmse) = eval.evaluate(&transform)? else {
545 break IcpStatus::Diverged;
546 };
547 let inliers = eval.kept.len();
548 last = (transform, rmse, inliers);
549 if !evaluated || inliers > best.2 || (inliers == best.2 && rmse < best.1) {
550 best = last;
551 }
552 evaluated = true;
553 if let Some(status) = settled {
554 break status;
555 }
556 if inliers < MIN_CORRESPONDENCES {
557 break IcpStatus::TooFewCorrespondences;
558 }
559 if iterations == max_iterations {
560 break IcpStatus::MaxIterations;
561 }
562 let center = apply(&transform, center0);
563 let (h, g, rows) = eval.assemble(center, extent);
564 if rows < MIN_CORRESPONDENCES {
565 break IcpStatus::TooFewCorrespondences;
566 }
567 let (step, c) = solve(&h, &g);
568 conditioning = c;
569 iterations += 1;
570 let rotation = [step[3] / extent, step[4] / extent, step[5] / extent];
571 let angle = norm(rotation);
572 let shrink = if angle > MAX_STEP_ANGLE {
573 MAX_STEP_ANGLE / angle
574 } else {
575 1.0
576 };
577 let translation = [step[0], step[1], step[2]].map(|v| v * shrink);
578 let rotation = rotation.map(|v| v * shrink);
579 transform = compose_step(&transform, center, translation, rotation);
580 if (norm(translation) + norm(rotation) * extent) / extent <= tolerance {
582 settled = Some(if conditioning >= MIN_CONDITIONING {
583 IcpStatus::Converged
584 } else {
585 IcpStatus::Underconstrained
586 });
587 }
588 };
589 let (transform, rmse, inliers) = if settled == Some(status) { last } else { best };
590 Ok(Run {
591 transform,
592 rmse,
593 inliers,
594 iterations,
595 status,
596 conditioning,
597 })
598}
599
600fn undetermined_motions(
606 points: &[[f64; 3]],
607 fixed: &Mesh,
608 bvh: &Bvh,
609 run: &Run,
610 center0: [f64; 3],
611 extent: f64,
612 options: &IcpOptions,
613) -> Result<Vec<IcpMotion>> {
614 let take = points.len().min(PROBE_SAMPLES);
615 let mut subset = Vec::new();
616 reserve(&mut subset, take)?;
617 subset.extend((0..take).map(|i| points[i * points.len() / take]));
618 let mut eval = Evaluator::new(&subset, fixed, bvh, options)?;
619 let mut out = Vec::new();
620 let Some(reference) = eval.evaluate(&run.transform)? else {
621 return Ok(out);
622 };
623 if eval.kept.len() < MIN_CORRESPONDENCES {
624 return Ok(out);
625 }
626 let center = apply(&run.transform, center0);
627 let (h, _, rows) = eval.assemble(center, extent);
628 if rows < MIN_CORRESPONDENCES {
629 return Ok(out);
630 }
631 let mut here = Vec::new();
632 reserve(&mut here, eval.moved.len())?;
633 here.extend_from_slice(&eval.moved);
634 let (values, vectors) = jacobi_eigen(h);
635 let mut order = [0, 1, 2, 3, 4, 5];
637 order.sort_by(|&a, &b| values[a].total_cmp(&values[b]).then(a.cmp(&b)));
638 for k in order {
639 let translation = [vectors[0][k], vectors[1][k], vectors[2][k]];
640 let rotation = [vectors[3][k], vectors[4][k], vectors[5][k]].map(|v| v / extent);
641 let sum: f64 = here
643 .iter()
644 .map(|p| {
645 let d = add(translation, cross(rotation, sub(*p, center)));
646 dot(d, d)
647 })
648 .sum();
649 let rms = (sum / here.len() as f64).sqrt();
650 if rms <= 0.0 {
651 continue;
652 }
653 let translation = translation.map(|v| v / rms);
654 let rotation = rotation.map(|v| v / rms);
655 for fraction in PROBE_DISTANCES {
656 let distance = fraction * extent;
657 let start = compose_step(
658 &run.transform,
659 center,
660 translation.map(|v| v * distance),
661 rotation.map(|v| v * distance),
662 );
663 let restart = iterate(
664 &mut eval,
665 &start,
666 center0,
667 extent,
668 PROBE_STEPS,
669 options.tolerance,
670 )?;
671 if matches!(
672 restart.status,
673 IcpStatus::TooFewCorrespondences | IcpStatus::Diverged
674 ) {
675 continue;
676 }
677 let sum: f64 = subset
681 .iter()
682 .zip(&here)
683 .map(|(p, h)| {
684 let along = add(translation, cross(rotation, sub(*h, center)));
685 dot(sub(apply(&restart.transform, *p), *h), along)
686 })
687 .sum();
688 let moved = (sum / here.len() as f64).abs();
689 let as_good = restart.rmse <= PROBE_AS_GOOD * reference + PROBE_RMSE_FLOOR * extent;
690 if moved >= PROBE_MOVED * distance && as_good {
691 out.push(IcpMotion {
692 translation,
693 rotation,
694 about: center,
695 alternative_rmse: restart.rmse,
696 });
697 break;
698 }
699 }
700 }
701 Ok(out)
702}
703
704fn corners_of(mesh: &Mesh, hit: ClosestHit) -> Option<[[f64; 3]; 3]> {
705 let [a, b, c] = mesh.triangle(hit.triangle)?;
706 Some([
707 mesh.position(a)?.map(f64::from),
708 mesh.position(b)?.map(f64::from),
709 mesh.position(c)?.map(f64::from),
710 ])
711}
712
713fn spread(points: &[[f64; 3]]) -> ([f64; 3], f64) {
716 let mut sum = [0.0; 3];
717 for p in points {
718 sum = add(sum, *p);
719 }
720 let center = sum.map(|c| c / points.len() as f64);
721 let far = points
722 .iter()
723 .map(|p| dot(sub(*p, center), sub(*p, center)))
724 .fold(0.0, f64::max)
725 .sqrt();
726 (center, if far > 0.0 { far } else { 1.0 })
727}
728
729fn solve(h: &[[f64; 6]; 6], g: &[f64; 6]) -> ([f64; 6], f64) {
732 let (values, vectors) = jacobi_eigen(*h);
733 let largest = values.iter().copied().fold(0.0, f64::max);
734 let smallest = values
735 .iter()
736 .copied()
737 .fold(f64::INFINITY, f64::min)
738 .max(0.0);
739 let mut x = [0.0; 6];
740 if largest <= 0.0 {
741 return (x, 0.0);
742 }
743 for (k, &value) in values.iter().enumerate() {
744 if value < MIN_CONDITIONING * largest {
745 continue;
746 }
747 let along = (0..6).map(|i| vectors[i][k] * g[i]).sum::<f64>() / value;
748 for (xi, row) in x.iter_mut().zip(&vectors) {
749 *xi += row[k] * along;
750 }
751 }
752 (x, smallest / largest)
753}
754
755#[allow(clippy::needless_range_loop)] fn jacobi_eigen(mut a: [[f64; 6]; 6]) -> ([f64; 6], [[f64; 6]; 6]) {
759 let mut v = [[0.0; 6]; 6];
760 for (i, row) in v.iter_mut().enumerate() {
761 row[i] = 1.0;
762 }
763 for _ in 0..32 {
765 let off: f64 = (0..6)
766 .flat_map(|i| (i + 1..6).map(move |j| (i, j)))
767 .map(|(i, j)| a[i][j] * a[i][j])
768 .sum();
769 let diag: f64 = (0..6).map(|i| a[i][i] * a[i][i]).sum();
770 if off <= 1e-36 * diag {
771 break;
772 }
773 for p in 0..6 {
774 for q in p + 1..6 {
775 if a[p][q] == 0.0 {
776 continue;
777 }
778 let theta = (a[q][q] - a[p][p]) / (2.0 * a[p][q]);
779 let t = theta.signum() / (theta.abs() + (theta * theta + 1.0).sqrt());
780 let c = 1.0 / (t * t + 1.0).sqrt();
781 let s = t * c;
782 for row in &mut a {
783 let (ap, aq) = (row[p], row[q]);
784 row[p] = c * ap - s * aq;
785 row[q] = s * ap + c * aq;
786 }
787 for k in 0..6 {
788 let (ap, aq) = (a[p][k], a[q][k]);
789 a[p][k] = c * ap - s * aq;
790 a[q][k] = s * ap + c * aq;
791 }
792 for row in &mut v {
793 let (vp, vq) = (row[p], row[q]);
794 row[p] = c * vp - s * vq;
795 row[q] = s * vp + c * vq;
796 }
797 }
798 }
799 }
800 (std::array::from_fn(|i| a[i][i]), v)
801}
802
803fn compose_step(
806 transform: &Rigid,
807 center: [f64; 3],
808 translation: [f64; 3],
809 rotation: [f64; 3],
810) -> Rigid {
811 let d = rotation_of(rotation);
812 let r = &transform.rotation;
813 let product: [[f64; 3]; 3] = std::array::from_fn(|i| {
814 std::array::from_fn(|j| d[i][0] * r[0][j] + d[i][1] * r[1][j] + d[i][2] * r[2][j])
815 });
816 let t = sub(transform.translation, center);
817 let turned = [dot(d[0], t), dot(d[1], t), dot(d[2], t)];
818 Rigid {
819 rotation: orthonormal(product),
820 translation: add(add(center, translation), turned),
821 }
822}
823
824fn rotation_of(w: [f64; 3]) -> [[f64; 3]; 3] {
826 let angle = norm(w);
827 let (s, c) = if angle < 1e-8 {
829 (1.0, 0.5)
830 } else {
831 (angle.sin() / angle, (1.0 - angle.cos()) / (angle * angle))
832 };
833 let [x, y, z] = w;
834 [
835 [
836 1.0 - c * (y * y + z * z),
837 c * x * y - s * z,
838 c * x * z + s * y,
839 ],
840 [
841 c * x * y + s * z,
842 1.0 - c * (x * x + z * z),
843 c * y * z - s * x,
844 ],
845 [
846 c * x * z - s * y,
847 c * y * z + s * x,
848 1.0 - c * (x * x + y * y),
849 ],
850 ]
851}
852
853fn orthonormal(m: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
857 let r0 = unit(m[0]).unwrap_or([1.0, 0.0, 0.0]);
858 let d = dot(m[1], r0);
859 let r1 = unit([
860 m[1][0] - d * r0[0],
861 m[1][1] - d * r0[1],
862 m[1][2] - d * r0[2],
863 ])
864 .unwrap_or([0.0, 1.0, 0.0]);
865 [r0, r1, cross(r0, r1)]
866}
867
868fn apply(x: &Rigid, p: [f64; 3]) -> [f64; 3] {
869 let r = &x.rotation;
870 let t = x.translation;
871 std::array::from_fn(|i| r[i][0] * p[0] + r[i][1] * p[1] + r[i][2] * p[2] + t[i])
872}
873
874fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
875 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
876}
877
878fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
879 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
880}
881
882fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
883 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
884}
885
886fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
887 [
888 a[1] * b[2] - a[2] * b[1],
889 a[2] * b[0] - a[0] * b[2],
890 a[0] * b[1] - a[1] * b[0],
891 ]
892}
893
894fn norm(a: [f64; 3]) -> f64 {
895 dot(a, a).sqrt()
896}
897
898fn unit(a: [f64; 3]) -> Option<[f64; 3]> {
900 let n = norm(a);
901 (n > 0.0).then(|| a.map(|c| c / n))
902}
903
904#[cfg(test)]
905#[path = "icp_tests.rs"]
906mod tests;