Skip to main content

oxiphysics_geometry/point_cloud/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::*;
6use std::collections::HashMap;
7
8/// Static utility methods for filtering point clouds.
9pub struct PointCloudFilter;
10impl PointCloudFilter {
11    /// Downsamples by averaging points that fall into the same voxel.
12    pub fn voxel_downsample(cloud: &PointCloud, voxel_size: f64) -> PointCloud {
13        if cloud.points.is_empty() || voxel_size <= 0.0 {
14            return PointCloud::new();
15        }
16        let inv = 1.0 / voxel_size;
17        let mut voxels: HashMap<(i64, i64, i64), Vec<usize>> = HashMap::new();
18        for (i, &p) in cloud.points.iter().enumerate() {
19            let key = (
20                (p[0] * inv).floor() as i64,
21                (p[1] * inv).floor() as i64,
22                (p[2] * inv).floor() as i64,
23            );
24            voxels.entry(key).or_default().push(i);
25        }
26        let mut out = PointCloud::new();
27        for indices in voxels.values() {
28            let n = indices.len() as f64;
29            let mut sum = [0.0f64; 3];
30            for &i in indices {
31                let p = cloud.points[i];
32                for k in 0..3 {
33                    sum[k] += p[k];
34                }
35            }
36            out.add_point([sum[0] / n, sum[1] / n, sum[2] / n]);
37        }
38        out
39    }
40    /// Removes points whose mean distance to their k nearest neighbors
41    /// exceeds `mean + std_factor * std`.
42    pub fn statistical_outlier_removal(
43        cloud: &PointCloud,
44        k: usize,
45        std_factor: f64,
46    ) -> PointCloud {
47        if cloud.points.is_empty() {
48            return PointCloud::new();
49        }
50        let tree = KdTree3D::build(&cloud.points);
51        let mean_dists: Vec<f64> = cloud
52            .points
53            .iter()
54            .map(|&p| {
55                let neighbors = tree.k_nearest(p, k + 1);
56                let dists: Vec<f64> = neighbors
57                    .iter()
58                    .filter(|&&(idx, _)| cloud.points[idx] != p || idx != 0)
59                    .map(|&(_, d)| d.sqrt())
60                    .collect();
61                if dists.is_empty() {
62                    0.0
63                } else {
64                    dists.iter().sum::<f64>() / dists.len() as f64
65                }
66            })
67            .collect();
68        let mean = mean_dists.iter().sum::<f64>() / mean_dists.len() as f64;
69        let variance = mean_dists
70            .iter()
71            .map(|&d| (d - mean) * (d - mean))
72            .sum::<f64>()
73            / mean_dists.len() as f64;
74        let std = variance.sqrt();
75        let threshold = mean + std_factor * std;
76        let mut out = PointCloud::new();
77        for (i, &md) in mean_dists.iter().enumerate() {
78            if md <= threshold {
79                out.add_point(cloud.points[i]);
80                if !cloud.normals.is_empty() {
81                    out.normals.push(cloud.normals[i]);
82                }
83                if !cloud.colors.is_empty() {
84                    out.colors.push(cloud.colors[i]);
85                }
86            }
87        }
88        out
89    }
90    /// Removes points that have fewer than `min_neighbors` within `radius`.
91    pub fn radius_outlier_removal(
92        cloud: &PointCloud,
93        radius: f64,
94        min_neighbors: usize,
95    ) -> PointCloud {
96        if cloud.points.is_empty() {
97            return PointCloud::new();
98        }
99        let tree = KdTree3D::build(&cloud.points);
100        let mut out = PointCloud::new();
101        for (i, &p) in cloud.points.iter().enumerate() {
102            let count = tree.range_search(p, radius).len();
103            if count.saturating_sub(1) >= min_neighbors {
104                out.add_point(p);
105                if !cloud.normals.is_empty() {
106                    out.normals.push(cloud.normals[i]);
107                }
108                if !cloud.colors.is_empty() {
109                    out.colors.push(cloud.colors[i]);
110                }
111            }
112        }
113        out
114    }
115}
116/// A 3-D k-d tree for efficient nearest-neighbor queries.
117pub struct KdTree3D {
118    pub(super) points: Vec<[f64; 3]>,
119    pub(super) root: Option<Box<KdNode3D>>,
120}
121impl KdTree3D {
122    /// Builds a k-d tree from a slice of points.
123    pub fn build(points: &[[f64; 3]]) -> Self {
124        let mut indices: Vec<usize> = (0..points.len()).collect();
125        let root = Self::build_recursive(points, &mut indices, 0);
126        Self {
127            points: points.to_vec(),
128            root,
129        }
130    }
131    fn build_recursive(
132        points: &[[f64; 3]],
133        indices: &mut [usize],
134        depth: usize,
135    ) -> Option<Box<KdNode3D>> {
136        if indices.is_empty() {
137            return None;
138        }
139        let axis = depth % 3;
140        indices.sort_unstable_by(|&a, &b| {
141            points[a][axis]
142                .partial_cmp(&points[b][axis])
143                .unwrap_or(std::cmp::Ordering::Equal)
144        });
145        let mid = indices.len() / 2;
146        let idx = indices[mid];
147        let left = Self::build_recursive(points, &mut indices[..mid], depth + 1);
148        let right = Self::build_recursive(points, &mut indices[mid + 1..], depth + 1);
149        Some(Box::new(KdNode3D {
150            point_idx: idx,
151            split_axis: axis,
152            left,
153            right,
154        }))
155    }
156    /// Returns `(index, squared_distance)` for the closest point to `query`.
157    pub fn nearest_neighbor(&self, query: [f64; 3]) -> (usize, f64) {
158        let mut best = (0, f64::MAX);
159        if let Some(ref root) = self.root {
160            Self::nn_search(&self.points, root, query, &mut best);
161        }
162        best
163    }
164    fn nn_search(points: &[[f64; 3]], node: &KdNode3D, query: [f64; 3], best: &mut (usize, f64)) {
165        let p = points[node.point_idx];
166        let d2 = dist2(p, query);
167        if d2 < best.1 {
168            *best = (node.point_idx, d2);
169        }
170        let axis = node.split_axis;
171        let diff = query[axis] - p[axis];
172        let (near, far) = if diff <= 0.0 {
173            (node.left.as_deref(), node.right.as_deref())
174        } else {
175            (node.right.as_deref(), node.left.as_deref())
176        };
177        if let Some(near_node) = near {
178            Self::nn_search(points, near_node, query, best);
179        }
180        if diff * diff < best.1
181            && let Some(far_node) = far
182        {
183            Self::nn_search(points, far_node, query, best);
184        }
185    }
186    /// Returns up to `k` nearest neighbors, sorted by ascending squared distance.
187    pub fn k_nearest(&self, query: [f64; 3], k: usize) -> Vec<(usize, f64)> {
188        if k == 0 || self.points.is_empty() {
189            return Vec::new();
190        }
191        let mut heap: Vec<(usize, f64)> = Vec::with_capacity(k + 1);
192        if let Some(ref root) = self.root {
193            Self::knn_search(&self.points, root, query, k, &mut heap);
194        }
195        heap.sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
196        heap
197    }
198    fn knn_search(
199        points: &[[f64; 3]],
200        node: &KdNode3D,
201        query: [f64; 3],
202        k: usize,
203        heap: &mut Vec<(usize, f64)>,
204    ) {
205        let p = points[node.point_idx];
206        let d2 = dist2(p, query);
207        heap.push((node.point_idx, d2));
208        if heap.len() > k {
209            let worst_pos = heap
210                .iter()
211                .enumerate()
212                .max_by(|a, b| {
213                    a.1.1
214                        .partial_cmp(&b.1.1)
215                        .unwrap_or(std::cmp::Ordering::Equal)
216                })
217                .map(|(i, _)| i)
218                .unwrap_or(0);
219            heap.swap_remove(worst_pos);
220        }
221        let worst_dist2 = heap.iter().map(|e| e.1).fold(0.0_f64, f64::max);
222        let axis = node.split_axis;
223        let diff = query[axis] - p[axis];
224        let (near, far) = if diff <= 0.0 {
225            (node.left.as_deref(), node.right.as_deref())
226        } else {
227            (node.right.as_deref(), node.left.as_deref())
228        };
229        if let Some(near_node) = near {
230            Self::knn_search(points, near_node, query, k, heap);
231        }
232        let current_worst = heap.iter().map(|e| e.1).fold(0.0_f64, f64::max);
233        if (heap.len() < k || diff * diff < current_worst)
234            && let Some(far_node) = far
235        {
236            Self::knn_search(points, far_node, query, k, heap);
237        }
238        let _ = worst_dist2;
239    }
240    /// Returns indices of all points within `radius` of `center`.
241    pub fn range_search(&self, center: [f64; 3], radius: f64) -> Vec<usize> {
242        let mut result = Vec::new();
243        let r2 = radius * radius;
244        if let Some(ref root) = self.root {
245            Self::range_recursive(&self.points, root, center, r2, &mut result);
246        }
247        result
248    }
249    fn range_recursive(
250        points: &[[f64; 3]],
251        node: &KdNode3D,
252        center: [f64; 3],
253        r2: f64,
254        result: &mut Vec<usize>,
255    ) {
256        let p = points[node.point_idx];
257        if dist2(p, center) <= r2 {
258            result.push(node.point_idx);
259        }
260        let axis = node.split_axis;
261        let diff = center[axis] - p[axis];
262        if (diff * diff <= r2 || diff <= 0.0)
263            && let Some(ref left) = node.left
264        {
265            Self::range_recursive(points, left, center, r2, result);
266        }
267        if (diff * diff <= r2 || diff > 0.0)
268            && let Some(ref right) = node.right
269        {
270            Self::range_recursive(points, right, center, r2, result);
271        }
272    }
273}
274/// Internal node for the 3-D k-d tree.
275pub struct KdNode3D {
276    /// Index into the original points slice.
277    pub point_idx: usize,
278    /// Axis on which this node splits (0 = X, 1 = Y, 2 = Z).
279    pub split_axis: usize,
280    /// Left subtree.
281    pub left: Option<Box<KdNode3D>>,
282    /// Right subtree.
283    pub right: Option<Box<KdNode3D>>,
284}
285/// Estimates per-point normals via PCA of k-nearest-neighbor covariance.
286pub struct NormalEstimation {
287    /// Number of nearest neighbors to use for the local PCA.
288    pub k_neighbors: usize,
289}
290impl NormalEstimation {
291    /// Creates a new estimator with the given neighbor count.
292    pub fn new(k_neighbors: usize) -> Self {
293        Self { k_neighbors }
294    }
295    /// Estimates normals for every point in `cloud`.
296    pub fn estimate(&self, cloud: &PointCloud) -> Vec<[f64; 3]> {
297        let tree = KdTree3D::build(&cloud.points);
298        cloud
299            .points
300            .iter()
301            .map(|&p| {
302                let neighbors = tree.k_nearest(p, self.k_neighbors);
303                let pts: Vec<[f64; 3]> = neighbors
304                    .iter()
305                    .map(|&(idx, _)| cloud.points[idx])
306                    .collect();
307                if pts.len() < 3 {
308                    return [0.0, 0.0, 1.0];
309                }
310                let cov = Self::covariance_matrix(&pts);
311                Self::smallest_eigenvector_3x3_sym(&cov)
312            })
313            .collect()
314    }
315    /// Computes the 3×3 covariance matrix of the given points.
316    pub fn covariance_matrix(pts: &[[f64; 3]]) -> [[f64; 3]; 3] {
317        if pts.is_empty() {
318            return [[0.0; 3]; 3];
319        }
320        let n = pts.len() as f64;
321        let mut centroid = [0.0f64; 3];
322        for &p in pts {
323            for (c, p_i) in centroid.iter_mut().zip(p.iter()) {
324                *c += p_i;
325            }
326        }
327        for c in centroid.iter_mut() {
328            *c /= n;
329        }
330        let mut cov = [[0.0f64; 3]; 3];
331        for &p in pts {
332            let d = sub(p, centroid);
333            for (i, cov_row) in cov.iter_mut().enumerate() {
334                for (j, cov_ij) in cov_row.iter_mut().enumerate() {
335                    *cov_ij += d[i] * d[j];
336                }
337            }
338        }
339        cov
340    }
341    /// Returns the eigenvector corresponding to the smallest eigenvalue of a
342    /// symmetric 3×3 matrix using shifted power iteration.
343    pub fn smallest_eigenvector_3x3_sym(m: &[[f64; 3]; 3]) -> [f64; 3] {
344        let mut v = [1.0f64, 1.0, 1.0];
345        for _ in 0..20 {
346            v = mat_vec_3(m, v);
347            let len = length(v);
348            if len < 1e-15 {
349                break;
350            }
351            v = scale(v, 1.0 / len);
352        }
353        let lambda_max = dot(mat_vec_3(m, v), v);
354        let shifted = [
355            [lambda_max - m[0][0], -m[0][1], -m[0][2]],
356            [-m[1][0], lambda_max - m[1][1], -m[1][2]],
357            [-m[2][0], -m[2][1], lambda_max - m[2][2]],
358        ];
359        let mut u = [1.0f64, 0.0, 0.0];
360        for _ in 0..50 {
361            u = mat_vec_3(&shifted, u);
362            let len = length(u);
363            if len < 1e-15 {
364                break;
365            }
366            u = scale(u, 1.0 / len);
367        }
368        normalize(u)
369    }
370}
371/// Result of a RANSAC plane fitting.
372pub struct RansacPlaneResult {
373    /// Plane normal (unit vector).
374    pub normal: [f64; 3],
375    /// A point on the plane (centroid of inliers).
376    pub point_on_plane: [f64; 3],
377    /// Indices of inlier points.
378    pub inliers: Vec<usize>,
379    /// Number of inliers.
380    pub n_inliers: usize,
381}
382/// Result of an ICP alignment.
383pub struct IcpResult {
384    /// 3×3 rotation matrix (row-major).
385    pub rotation: [[f64; 3]; 3],
386    /// Translation vector.
387    pub translation: [f64; 3],
388    /// Mean squared distance after convergence.
389    pub final_error: f64,
390    /// Number of iterations performed.
391    pub iterations: usize,
392}
393impl IcpResult {
394    /// Applies the stored transform to `cloud` and returns the result.
395    pub fn apply_to(&self, cloud: &PointCloud) -> PointCloud {
396        let mut out = PointCloud::new();
397        for &p in &cloud.points {
398            let rp = mat_vec_3(&self.rotation, p);
399            out.add_point(add(rp, self.translation));
400        }
401        if !cloud.normals.is_empty() {
402            for &n in &cloud.normals {
403                out.normals.push(mat_vec_3(&self.rotation, n));
404            }
405        }
406        out.colors = cloud.colors.clone();
407        out
408    }
409}
410/// A collection of 3-D points with optional per-point normals and colors.
411pub struct PointCloud {
412    /// XYZ positions.
413    pub points: Vec<[f64; 3]>,
414    /// Per-point surface normals (may be empty).
415    pub normals: Vec<[f64; 3]>,
416    /// Per-point RGB colors in \[0, 1\] (may be empty).
417    pub colors: Vec<[f32; 3]>,
418}
419impl PointCloud {
420    /// Creates an empty point cloud.
421    pub fn new() -> Self {
422        Self {
423            points: Vec::new(),
424            normals: Vec::new(),
425            colors: Vec::new(),
426        }
427    }
428    /// Appends a point.
429    pub fn add_point(&mut self, p: [f64; 3]) {
430        self.points.push(p);
431    }
432    /// Returns the number of points.
433    pub fn len(&self) -> usize {
434        self.points.len()
435    }
436    /// Returns `true` if the cloud is empty.
437    pub fn is_empty(&self) -> bool {
438        self.points.is_empty()
439    }
440    /// Returns the axis-aligned bounding box as `(min, max)`.
441    pub fn bounding_box(&self) -> ([f64; 3], [f64; 3]) {
442        if self.points.is_empty() {
443            return ([0.0; 3], [0.0; 3]);
444        }
445        let mut mn = self.points[0];
446        let mut mx = self.points[0];
447        for &p in &self.points {
448            for i in 0..3 {
449                if p[i] < mn[i] {
450                    mn[i] = p[i];
451                }
452                if p[i] > mx[i] {
453                    mx[i] = p[i];
454                }
455            }
456        }
457        (mn, mx)
458    }
459    /// Returns the centroid (mean position) of all points.
460    pub fn centroid(&self) -> [f64; 3] {
461        if self.points.is_empty() {
462            return [0.0; 3];
463        }
464        let n = self.points.len() as f64;
465        let mut sum = [0.0f64; 3];
466        for &p in &self.points {
467            for i in 0..3 {
468                sum[i] += p[i];
469            }
470        }
471        [sum[0] / n, sum[1] / n, sum[2] / n]
472    }
473    /// Translates all points by `offset`.
474    pub fn translate(&mut self, offset: [f64; 3]) {
475        for p in &mut self.points {
476            *p = add(*p, offset);
477        }
478    }
479    /// Uniformly scales all points around the origin.
480    pub fn scale_uniform(&mut self, s: f64) {
481        for p in &mut self.points {
482            *p = scale(*p, s);
483        }
484    }
485    /// Builds a height-field point cloud from a regular `nx × ny` grid.
486    ///
487    /// Points are placed at `(i * dx, j * dx, height_fn(i * dx, j * dx))`.
488    pub fn from_grid(nx: usize, ny: usize, height_fn: impl Fn(f64, f64) -> f64, dx: f64) -> Self {
489        let mut cloud = Self::new();
490        for i in 0..nx {
491            for j in 0..ny {
492                let x = i as f64 * dx;
493                let y = j as f64 * dx;
494                let z = height_fn(x, y);
495                cloud.add_point([x, y, z]);
496            }
497        }
498        cloud
499    }
500}
501impl PointCloud {
502    /// Constructs a PointCloud from a vec of points (expansion API).
503    pub fn from_points(points: Vec<[f64; 3]>) -> Self {
504        Self {
505            points,
506            normals: Vec::new(),
507            colors: Vec::new(),
508        }
509    }
510    /// Estimate per-point normals using PCA of the `k` nearest neighbors.
511    /// Populates `self.normals`.
512    pub fn estimate_normals_pca(&mut self, k: usize) {
513        let tree = KdTree3D::build(&self.points);
514        self.normals = self
515            .points
516            .iter()
517            .map(|&p| {
518                let neighbors = tree.k_nearest(p, k);
519                let pts: Vec<[f64; 3]> =
520                    neighbors.iter().map(|&(idx, _)| self.points[idx]).collect();
521                if pts.len() < 3 {
522                    return [0.0, 0.0, 1.0];
523                }
524                let cov = NormalEstimation::covariance_matrix(&pts);
525                NormalEstimation::smallest_eigenvector_3x3_sym(&cov)
526            })
527            .collect();
528    }
529    /// Return a new downsampled cloud using a voxel grid of side `voxel_size`.
530    pub fn voxel_downsample(&self, voxel_size: f64) -> Self {
531        PointCloudFilter::voxel_downsample(self, voxel_size)
532    }
533    /// Return a new cloud with statistical outliers removed.
534    pub fn statistical_outlier_removal(&self, k: usize, std_ratio: f64) -> Self {
535        PointCloudFilter::statistical_outlier_removal(self, k, std_ratio)
536    }
537}
538impl PointCloud {
539    /// Farthest Point Sampling: select `k` well-spread points from this cloud.
540    ///
541    /// Returns a new PointCloud with the selected points (normals / colors
542    /// are copied if present).
543    pub fn farthest_point_sample(&self, k: usize) -> Self {
544        let indices = farthest_point_sampling(&self.points, k, 0);
545        let mut out = PointCloud::new();
546        for &i in &indices {
547            out.add_point(self.points[i]);
548            if !self.normals.is_empty() {
549                out.normals.push(self.normals[i]);
550            }
551            if !self.colors.is_empty() {
552                out.colors.push(self.colors[i]);
553            }
554        }
555        out
556    }
557    /// Fit a plane to the point cloud using RANSAC.
558    ///
559    /// Returns `None` if fewer than 3 points are present.
560    pub fn fit_plane_ransac(&self, n_iter: usize, threshold: f64) -> Option<RansacPlaneResult> {
561        ransac_fit_plane(&self.points, n_iter, threshold)
562    }
563    /// Compute the PCA-based OBB of this cloud.
564    ///
565    /// Returns `(principal_axes, half_extents, center)`.
566    pub fn pca_obb(&self) -> ([[f64; 3]; 3], [f64; 3], [f64; 3]) {
567        pca_obb(&self.points)
568    }
569    /// Compute PCA-based principal curvatures for each point.
570    ///
571    /// For each point a k-NN neighbourhood is gathered, the 3×3 covariance
572    /// matrix is formed, and the two smallest eigenvalues (λ₁ ≤ λ₂) are
573    /// returned as the approximate principal curvatures κ₁ and κ₂.
574    ///
575    /// Returns a `Vec<(f64, f64)>` of `(kappa1, kappa2)` for every point.
576    pub fn compute_principal_curvatures(&self, k: usize) -> Vec<(f64, f64)> {
577        if self.points.is_empty() {
578            return Vec::new();
579        }
580        let tree = KdTree3D::build(&self.points);
581        self.points
582            .iter()
583            .map(|&p| {
584                let neighbors = tree.k_nearest(p, k + 1);
585                let pts: Vec<[f64; 3]> =
586                    neighbors.iter().map(|&(idx, _)| self.points[idx]).collect();
587                if pts.len() < 3 {
588                    return (0.0, 0.0);
589                }
590                let n = pts.len() as f64;
591                let mut centroid = [0.0f64; 3];
592                for &q in &pts {
593                    for (c, q_i) in centroid.iter_mut().zip(q.iter()) {
594                        *c += q_i;
595                    }
596                }
597                for c in centroid.iter_mut() {
598                    *c /= n;
599                }
600                let mut cov = [[0.0f64; 3]; 3];
601                for &q in &pts {
602                    let d = sub(q, centroid);
603                    for (i, cov_row) in cov.iter_mut().enumerate() {
604                        for (j, cov_ij) in cov_row.iter_mut().enumerate() {
605                            *cov_ij += d[i] * d[j];
606                        }
607                    }
608                }
609                for cov_row in cov.iter_mut() {
610                    for cov_ij in cov_row.iter_mut() {
611                        *cov_ij /= n;
612                    }
613                }
614                let eigenvalues = jacobi_eigenvalues_3x3(cov);
615                let mut ev = eigenvalues;
616                ev.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
617                (ev[0], ev[1])
618            })
619            .collect()
620    }
621    /// Align this cloud onto `target` using ICP (point-to-point) and
622    /// return the registration result (rotation + translation + error).
623    ///
624    /// Calls the existing `IcpRegistration::align` helper.
625    pub fn icp_register(&self, target: &PointCloud) -> IcpResult {
626        IcpRegistration::align(self, target)
627    }
628}
629/// Iterative Closest Point registration.
630pub struct IcpRegistration {
631    /// Maximum number of ICP iterations.
632    pub max_iterations: usize,
633    /// Convergence threshold on mean squared distance change.
634    pub convergence_tol: f64,
635}
636impl IcpRegistration {
637    /// Creates a new ICP solver.
638    pub fn new(max_iterations: usize, convergence_tol: f64) -> Self {
639        Self {
640            max_iterations,
641            convergence_tol,
642        }
643    }
644    /// Aligns `source` onto `target` and returns the transform.
645    pub fn align(source: &PointCloud, target: &PointCloud) -> IcpResult {
646        let icp = Self::new(50, 1e-6);
647        icp.run(source, target)
648    }
649    /// Run ICP alignment: returns [`IcpResult`] after at most `self.max_iterations` steps.
650    pub fn run(&self, source: &PointCloud, target: &PointCloud) -> IcpResult {
651        let mut rot = identity_3x3();
652        let mut trans = [0.0f64; 3];
653        let mut current: Vec<[f64; 3]> = source.points.clone();
654        let target_tree = KdTree3D::build(&target.points);
655        let mut prev_error = f64::MAX;
656        let mut iters = 0;
657        for iter in 0..self.max_iterations {
658            iters = iter + 1;
659            let pairs: Vec<(usize, usize)> = current
660                .iter()
661                .enumerate()
662                .map(|(i, &p)| {
663                    let (ti, _) = target_tree.nearest_neighbor(p);
664                    (i, ti)
665                })
666                .collect();
667            let error: f64 = pairs
668                .iter()
669                .map(|&(si, ti)| dist2(current[si], target.points[ti]))
670                .sum::<f64>()
671                / pairs.len() as f64;
672            if (prev_error - error).abs() < self.convergence_tol {
673                break;
674            }
675            prev_error = error;
676            let src_centroid =
677                mean_points(&pairs.iter().map(|&(si, _)| current[si]).collect::<Vec<_>>());
678            let tgt_centroid = mean_points(
679                &pairs
680                    .iter()
681                    .map(|&(_, ti)| target.points[ti])
682                    .collect::<Vec<_>>(),
683            );
684            let mut h = [[0.0f64; 3]; 3];
685            for &(si, ti) in &pairs {
686                let s = sub(current[si], src_centroid);
687                let t = sub(target.points[ti], tgt_centroid);
688                for i in 0..3 {
689                    for j in 0..3 {
690                        h[i][j] += s[i] * t[j];
691                    }
692                }
693            }
694            let (u, _s_vals, v) = svd_3x3_sym_jacobi(h);
695            let det = mat_det_3(&mat_mul_3(&v, &mat_transpose_3(&u)));
696            let mut d = identity_3x3();
697            if det < 0.0 {
698                d[2][2] = -1.0;
699            }
700            let r_step = mat_mul_3(&v, &mat_mul_3(&d, &mat_transpose_3(&u)));
701            let t_step = sub(tgt_centroid, mat_vec_3(&r_step, src_centroid));
702            for p in &mut current {
703                *p = add(mat_vec_3(&r_step, *p), t_step);
704            }
705            rot = mat_mul_3(&r_step, &rot);
706            trans = add(mat_vec_3(&r_step, trans), t_step);
707        }
708        let target_tree2 = KdTree3D::build(&target.points);
709        let final_error = if current.is_empty() {
710            0.0
711        } else {
712            current
713                .iter()
714                .map(|&p| {
715                    let (ti, _) = target_tree2.nearest_neighbor(p);
716                    dist2(p, target.points[ti])
717                })
718                .sum::<f64>()
719                / current.len() as f64
720        };
721        IcpResult {
722            rotation: rot,
723            translation: trans,
724            final_error,
725            iterations: iters,
726        }
727    }
728    /// Returns all closest-point pairs `(source_idx, target_idx)`.
729    pub fn closest_point_pairs(source: &PointCloud, target: &PointCloud) -> Vec<(usize, usize)> {
730        let tree = KdTree3D::build(&target.points);
731        source
732            .points
733            .iter()
734            .enumerate()
735            .map(|(i, &p)| {
736                let (ti, _) = tree.nearest_neighbor(p);
737                (i, ti)
738            })
739            .collect()
740    }
741}