Skip to main content

fast_mesh_ops/
icp.rs

1//! Rigid registration: point-to-plane ICP against a mesh and its BVH.
2//!
3//! [`icp()`] moves a cloud of points onto a fixed mesh. Each iteration finds
4//! every point's closest point on the mesh with the BVH, keeps the closest
5//! `overlap` fraction of them (trimmed ICP, so a partial overlap and outliers
6//! do not pull the result), and solves one linearised least-squares step for
7//! the rotation and translation that reduce the distances to those
8//! triangles' planes.
9//!
10//! The iteration is local: it starts at the `initial` transform and finds the
11//! nearest minimum, so the start has to be roughly right. "Converged" means
12//! the iteration stopped moving with every degree of freedom constrained, not
13//! that the alignment is the right one: read [`IcpReport::rmse`] for that.
14//!
15//! Directions with no constraint at all (a plane sliding in its own plane, a
16//! tube along its axis) are left out of each step instead of being solved from
17//! rounding noise.
18//!
19//! A settled iteration does not show that the data chose its pose: the facets
20//! of a tessellated sphere, or noise on a plane, give every step something to
21//! fit, so the normal equations look well conditioned and the iteration settles
22//! on an arbitrary spin or slide. So [`icp()`] checks. Along each of the six
23//! eigenmotions it displaces the pose and iterates again, on at most 10,000 of
24//! the points. A pose is determined along a motion when the restart comes back,
25//! or ends somewhere that fits clearly worse. When it stays displaced with a fit
26//! as good, the data do not choose, and the motion is reported in
27//! [`IcpReport::undetermined`] with the status [`IcpStatus::Underconstrained`].
28//! This is a local test: a symmetry farther away than the displacements (a cube
29//! turned by 90 degrees) is not seen.
30
31use 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
39/// Fewest correspondences that can determine a rigid transform.
40const MIN_CORRESPONDENCES: usize = 6;
41
42/// A motion is left out of the steps when its eigenvalue of the normalised
43/// normal equations is below this fraction of the largest: it has no constraint
44/// worth solving. The eigenvalue is a squared singular value, so this is a ratio
45/// of `1e-3` in how strongly the distances respond to that motion. It sits above
46/// the noise of `f32` positions, whose rounding jitters the face normals of a
47/// finely tessellated, tilted plane. Passing it does not make a motion
48/// determined: see [`undetermined_motions`].
49pub(crate) const MIN_CONDITIONING: f64 = 1e-6;
50
51/// The largest rotation of one step, in radians. Larger steps are shortened:
52/// the linearisation means nothing that far out.
53const MAX_STEP_ANGLE: f64 = 0.5;
54
55/// The most points each restart of the determination check uses. The check's
56/// cost is bounded by this, whatever the size of the input.
57const PROBE_SAMPLES: usize = 10_000;
58
59/// How far the determination check displaces the pose, as root mean square
60/// point movement over the points' extent. Two distances, so that a repeating
61/// surface whose period makes one of them land back on the same fit is seen by
62/// the other.
63const PROBE_DISTANCES: [f64; 2] = [0.1, 0.25];
64
65/// The most steps of a restart of the determination check.
66const PROBE_STEPS: usize = 12;
67
68/// A restart stayed away if this fraction of the displacement along the motion
69/// remains at its end. Measured restarts end near 0 or near 1 (`icp_survey` in
70/// the tests, and `docs/benchmarks.md`).
71const PROBE_MOVED: f64 = 0.25;
72
73/// A different pose fits as well if its rmse is at most this multiple of the
74/// pose under test's. Without a noise model, fits this close cannot be told
75/// apart. Measured on `icp_survey`: at most 1.8 for flat motions, and at least
76/// 3.5 for restarts that stayed away from a pose the data do determine.
77const PROBE_AS_GOOD: f64 = 2.5;
78
79/// ... plus this fraction of the extent, the level below which rmse is rounding
80/// of the stored `f32` positions.
81const PROBE_RMSE_FLOOR: f64 = 1e-6;
82
83/// Distances per parallel chunk, and the fewest that go parallel.
84const DISTANCE_CHUNK: usize = 1024;
85const PAR_MIN_DISTANCES: usize = 4096;
86
87/// Options for [`icp()`].
88#[derive(Debug, Clone, Copy, PartialEq)]
89pub struct IcpOptions {
90    /// The most steps to take. Zero evaluates the initial transform only.
91    /// Default 50.
92    pub max_iterations: usize,
93    /// Stops when one step moves every point by at most this fraction of the
94    /// points' extent (their largest distance from their centroid). Zero or
95    /// greater. Default `1e-6`.
96    pub tolerance: f64,
97    /// The fraction of the points expected to lie over the fixed mesh, in
98    /// `(0, 1]`: each iteration uses that many of them, closest first. Set it
99    /// a little below the true overlap for partial scans. Default 1 (every
100    /// point counts, so nothing is rejected).
101    pub overlap: f64,
102    /// Points farther than this from the fixed mesh are not used. Positive.
103    /// Default `f32::INFINITY`.
104    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/// Why [`icp()`] stopped.
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum IcpStatus {
121    /// A step fell below the tolerance, and the determination check (see the
122    /// [module docs](self)) found every motion determined by the data.
123    Converged,
124    /// `max_iterations` steps ran out first, and no motion was found
125    /// undetermined (the check does not run with zero iterations).
126    MaxIterations,
127    /// The data do not determine the pose: [`IcpReport::undetermined`] says
128    /// along which motions, when the check could tell. The pose along those is
129    /// arbitrary, whatever the rmse; distances are still minimised along the
130    /// rest. Iteration may also have run out of steps, wandering along such a
131    /// motion.
132    Underconstrained,
133    /// Fewer than six usable correspondences (see [`IcpOptions::max_distance`]).
134    TooFewCorrespondences,
135    /// A step took a point out of the range the query can represent.
136    Diverged,
137}
138
139/// What [`icp()`] found. Every number is finite, except that `rmse` is
140/// infinite when no point has a correspondence. `rmse` is measured in `f64` from
141/// the points to the chosen triangles.
142#[derive(Debug, Clone, PartialEq)]
143pub struct IcpReport {
144    /// The transform from the points to the fixed mesh's local coordinates.
145    /// When the status is [`IcpStatus::Converged`] or
146    /// [`IcpStatus::Underconstrained`], the pose iteration stopped at;
147    /// otherwise the best pose seen (most points used, then lowest `rmse`).
148    pub transform: Rigid,
149    /// The root mean square distance from the used points to the fixed mesh
150    /// at `transform`.
151    pub rmse: f64,
152    /// Steps taken.
153    pub iterations: usize,
154    /// Why iteration stopped.
155    pub status: IcpStatus,
156    /// Points given.
157    pub samples: usize,
158    /// Points used at `transform`.
159    pub inliers: usize,
160    /// The smallest eigenvalue over the largest of the normalised normal
161    /// equations at the last step, in `[0, 1]`. Geometry only, with no reference
162    /// to noise: a low value warns of a weak motion, but a high one does not
163    /// show that the motion is determined, for which see `undetermined`. Zero
164    /// when no step ran.
165    pub conditioning: f64,
166    /// The motions the data do not determine: moving the pose along one and
167    /// iterating again ended at a different pose that fits as well. Empty when
168    /// the status is [`IcpStatus::Converged`]. Weakest first.
169    pub undetermined: Vec<IcpMotion>,
170    /// The points' extent: their largest distance from their centroid.
171    pub extent: f64,
172}
173
174/// A rigid motion the data do not determine, in the fixed mesh's local
175/// coordinates: a point `p` moves along `translation + rotation x (p - about)`,
176/// scaled so that the points move by 1 (root mean square).
177#[derive(Debug, Clone, Copy, PartialEq)]
178pub struct IcpMotion {
179    /// The translation part.
180    pub translation: [f64; 3],
181    /// The rotation part, as an axis scaled by its rate.
182    pub rotation: [f64; 3],
183    /// The point the rotation turns about: the moved points' centroid.
184    pub about: [f64; 3],
185    /// The rmse of the other pose found along this motion (over at most
186    /// 10,000 of the points).
187    pub alternative_rmse: f64,
188}
189
190impl IcpReport {
191    /// Whether the status is [`IcpStatus::Converged`].
192    pub fn converged(&self) -> bool {
193        self.status == IcpStatus::Converged
194    }
195}
196
197/// Up to `max_samples` evenly spaced points on the surface of `mesh`, in its
198/// stored local coordinates, without the origin: its triangles' centroids,
199/// then the vertices its triangles use, the first of each stride of the two
200/// lists together.
201///
202/// Vertices alone will not do on a coarse mesh: a vertex is equally close to
203/// all the triangles around it, so its correspondences do not see all of a
204/// corner's planes (a box's eight corners can all report the same two faces).
205///
206/// # Errors
207/// [`OpsError::Store`] when out of memory.
208pub 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        // Taken when it starts a new stride of `total / take`. `u128`: the
227        // product of two counts.
228        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/// A correspondence kept this iteration.
281#[derive(Clone, Copy)]
282struct Kept {
283    /// The point's index.
284    point: u32,
285    /// The closest distance.
286    distance: f64,
287}
288
289/// The inputs and working buffers of one call.
290struct 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    /// Finds the correspondences at `transform` and trims them. Returns the
330    /// rmse over the kept ones (infinite with none), or `None` when a moved
331    /// point leaves `f32` range.
332    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        // The query's `f32` distance picks the triangle. The distance itself is
353        // measured again in `f64`, since on long thin triangles the `f32` one
354        // is mostly rounding.
355        {
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            // A total order, so the kept set is unique: it does not depend on
385            // the selection's internals.
386            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    /// The normal equations of the kept correspondences, about `center` with
402    /// rotations scaled by `extent`: `(hessian, gradient, rows)`, in the
403    /// unknowns `(translation, rotation * extent)`. Corners are read in `f64`,
404    /// so the residuals carry no `f32` query rounding. A correspondence on a
405    /// zero-area triangle has no plane and is skipped.
406    #[allow(clippy::needless_range_loop)] // Matrix indices read better here.
407    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    /// A corner of the hit's triangle and its unit normal, or `None` for a
443    /// triangle with no area.
444    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
450/// Aligns `points` to the mesh `fixed` by rigid, point-to-plane ICP, starting
451/// from `initial`. See the [module docs](self).
452///
453/// The report's transform maps the points to `fixed`'s local coordinates (the
454/// stored `f32` positions, without the origin). The correspondence search
455/// rounds moved points to `f32`; the distances the solver minimises and reports
456/// are computed in `f64`. Results do not depend on the `parallel` feature or the
457/// thread count. The determination check adds a bounded cost: up to 12 restarts
458/// of up to 12 steps on at most 10,000 points.
459///
460/// # Errors
461/// [`OpsError::InvalidInput`] for no points, non-finite points, a fixed mesh
462/// without triangles, or an invalid option; [`OpsError::Query`] for an invalid
463/// `initial`, one that takes a point out of `f32` range, or a `bvh` that does
464/// not match `fixed`; [`OpsError::Store`] when out of memory.
465pub 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    // Only a pose iteration reached has anything to check; zero iterations
490    // were asked to measure, not to determine.
491    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
515/// Where one run of the iteration ended.
516struct Run {
517    transform: Rigid,
518    rmse: f64,
519    inliers: usize,
520    iterations: usize,
521    status: IcpStatus,
522    conditioning: f64,
523}
524
525/// Iterates from `start`. When the first evaluation already leaves `f32` range
526/// the status is [`IcpStatus::Diverged`] with no iterations.
527fn 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    // Set once a step fell below the tolerance: the next pass only evaluates.
542    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        // An upper bound on how far any point moved, over the extent.
581        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
600/// The determination check: a pose is determined if the data bring the
601/// iteration back to it. Along each of the six eigenmotions of the normal
602/// equations at `run`'s pose, restarts displaced by [`PROBE_DISTANCES`] and
603/// reports the motions along which a restart stayed away with a fit as good, so
604/// that the data do not choose between the two.
605fn 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    // Weakest first, and independent of the order Jacobi left them in.
636    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        // Scaled so that the motion moves the points by one unit, rms.
642        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            // How much of the displacement along this motion is still there.
678            // Only along it: a restart may also wander along another motion,
679            // which is that motion's own result.
680            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
713/// The centroid of `points` and their largest distance from it (1 when they
714/// all coincide, so that dividing by it is safe).
715fn 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
729/// Solves `h x = g` over the constrained eigendirections only, and returns
730/// `x` with the conditioning (smallest eigenvalue over the largest).
731fn 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/// The eigenvalues of a symmetric matrix and its eigenvectors (columns), by
756/// cyclic Jacobi rotations.
757#[allow(clippy::needless_range_loop)] // Matrix indices read better here.
758fn 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    // Jacobi converges quadratically: 6 x 6 settles within a dozen sweeps.
764    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
803/// `transform` followed by the rotation vector `rotation` about `center` and
804/// the step `translation`, re-orthonormalised.
805fn 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
824/// Rodrigues' formula for a rotation vector.
825fn rotation_of(w: [f64; 3]) -> [[f64; 3]; 3] {
826    let angle = norm(w);
827    // sin(a)/a and (1 - cos(a))/a², with their limits near zero.
828    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
853/// Gram-Schmidt on the rows; the third row is the cross product, which keeps
854/// the determinant at +1. The input is within rounding of a rotation, so the
855/// fallbacks are never reached in practice.
856fn 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
898/// `a` over its length, or `None` for the zero vector.
899fn 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;