Skip to main content

oxiphysics_geometry/point_cloud/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::types::{
6    IcpRegistration, KdTree3D, NormalEstimation, PointCloud, PointCloudFilter, RansacPlaneResult,
7};
8
9pub(super) fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
10    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
11}
12pub(super) fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
13    [
14        a[1] * b[2] - a[2] * b[1],
15        a[2] * b[0] - a[0] * b[2],
16        a[0] * b[1] - a[1] * b[0],
17    ]
18}
19pub(super) fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
20    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
21}
22pub(super) fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
23    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
24}
25pub(super) fn scale(a: [f64; 3], s: f64) -> [f64; 3] {
26    [a[0] * s, a[1] * s, a[2] * s]
27}
28pub(super) fn length(a: [f64; 3]) -> f64 {
29    dot(a, a).sqrt()
30}
31pub(super) fn normalize(a: [f64; 3]) -> [f64; 3] {
32    let len = length(a);
33    if len < 1e-15 {
34        [0.0, 0.0, 1.0]
35    } else {
36        scale(a, 1.0 / len)
37    }
38}
39pub(super) fn dist2(a: [f64; 3], b: [f64; 3]) -> f64 {
40    let d = sub(a, b);
41    dot(d, d)
42}
43pub(super) fn mat_vec_3(m: &[[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
44    [
45        m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
46        m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
47        m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
48    ]
49}
50pub(super) fn identity_3x3() -> [[f64; 3]; 3] {
51    [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
52}
53pub(super) fn mat_transpose_3(m: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
54    let mut t = [[0.0f64; 3]; 3];
55    for i in 0..3 {
56        for j in 0..3 {
57            t[i][j] = m[j][i];
58        }
59    }
60    t
61}
62pub(super) fn mat_mul_3(a: &[[f64; 3]; 3], b: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
63    let mut c = [[0.0f64; 3]; 3];
64    for i in 0..3 {
65        for j in 0..3 {
66            for k in 0..3 {
67                c[i][j] += a[i][k] * b[k][j];
68            }
69        }
70    }
71    c
72}
73pub(super) fn mat_det_3(m: &[[f64; 3]; 3]) -> f64 {
74    m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
75        - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
76        + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
77}
78pub(super) fn mean_points(pts: &[[f64; 3]]) -> [f64; 3] {
79    if pts.is_empty() {
80        return [0.0; 3];
81    }
82    let n = pts.len() as f64;
83    let mut s = [0.0f64; 3];
84    for &p in pts {
85        for i in 0..3 {
86            s[i] += p[i];
87        }
88    }
89    [s[0] / n, s[1] / n, s[2] / n]
90}
91/// One-sided Jacobi SVD for 3×3 matrices.
92/// Returns (U, singular_values, V) such that A = U * diag(s) * V^T.
93pub(super) fn svd_3x3_sym_jacobi(a: [[f64; 3]; 3]) -> ([[f64; 3]; 3], [f64; 3], [[f64; 3]; 3]) {
94    let ata = mat_mul_3(&mat_transpose_3(&a), &a);
95    let (v, eigenvalues) = jacobi_eigen_3x3(ata);
96    let s = [
97        eigenvalues[0].max(0.0).sqrt(),
98        eigenvalues[1].max(0.0).sqrt(),
99        eigenvalues[2].max(0.0).sqrt(),
100    ];
101    let mut u = [[0.0f64; 3]; 3];
102    for j in 0..3 {
103        let vj = [v[0][j], v[1][j], v[2][j]];
104        let avj = mat_vec_3(&a, vj);
105        let col = if s[j] > 1e-12 {
106            scale(avj, 1.0 / s[j])
107        } else {
108            gram_schmidt_fallback(&u, j)
109        };
110        u[0][j] = col[0];
111        u[1][j] = col[1];
112        u[2][j] = col[2];
113    }
114    (u, s, v)
115}
116pub(super) fn gram_schmidt_fallback(u: &[[f64; 3]; 3], col: usize) -> [f64; 3] {
117    let candidates: [[f64; 3]; 3] = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
118    for &cand in &candidates {
119        let mut v = cand;
120        for (j, _) in u[0].iter().enumerate().take(col) {
121            let uj = [u[0][j], u[1][j], u[2][j]];
122            let proj = dot(v, uj);
123            v = sub(v, scale(uj, proj));
124        }
125        let len = length(v);
126        if len > 1e-12 {
127            return scale(v, 1.0 / len);
128        }
129    }
130    [0.0, 0.0, 1.0]
131}
132/// Jacobi eigendecomposition of a symmetric 3×3 matrix.
133/// Returns (eigenvectors_col_matrix, eigenvalues).
134pub(super) fn jacobi_eigen_3x3(mut a: [[f64; 3]; 3]) -> ([[f64; 3]; 3], [f64; 3]) {
135    let mut v = identity_3x3();
136    for _ in 0..50 {
137        let mut max_val = 0.0f64;
138        let mut p = 0;
139        let mut q = 1;
140        for (i, a_row) in a.iter().enumerate() {
141            for (j, a_ij) in a_row.iter().enumerate().skip(i + 1) {
142                if a_ij.abs() > max_val {
143                    max_val = a_ij.abs();
144                    p = i;
145                    q = j;
146                }
147            }
148        }
149        if max_val < 1e-12 {
150            break;
151        }
152        let theta = 0.5 * (a[q][q] - a[p][p]) / a[p][q];
153        let t = if theta >= 0.0 {
154            1.0 / (theta + (1.0 + theta * theta).sqrt())
155        } else {
156            -1.0 / (-theta + (1.0 + theta * theta).sqrt())
157        };
158        let c = 1.0 / (1.0 + t * t).sqrt();
159        let s = t * c;
160        let app = a[p][p];
161        let aqq = a[q][q];
162        let apq = a[p][q];
163        a[p][p] = c * c * app - 2.0 * s * c * apq + s * s * aqq;
164        a[q][q] = s * s * app + 2.0 * s * c * apq + c * c * aqq;
165        a[p][q] = 0.0;
166        a[q][p] = 0.0;
167        for (r, a_row) in a.iter_mut().enumerate() {
168            if r != p && r != q {
169                let arp = a_row[p];
170                let arq = a_row[q];
171                a_row[p] = c * arp - s * arq;
172                a_row[q] = s * arp + c * arq;
173            }
174        }
175        // Fix symmetry - collect indices to update to avoid borrow conflicts
176        let sym_updates: Vec<(usize, f64, f64)> = (0..3)
177            .filter(|&r| r != p && r != q)
178            .map(|r| (r, a[r][p], a[r][q]))
179            .collect();
180        for (r, arp, arq) in sym_updates {
181            a[p][r] = arp;
182            a[q][r] = arq;
183        }
184        for (r, v_row) in v.iter_mut().enumerate() {
185            let _ = r;
186            let vrp = v_row[p];
187            let vrq = v_row[q];
188            v_row[p] = c * vrp - s * vrq;
189            v_row[q] = s * vrp + c * vrq;
190        }
191    }
192    let eigenvalues = [a[0][0], a[1][1], a[2][2]];
193    (v, eigenvalues)
194}
195/// Thin wrapper that returns only the eigenvalues from `jacobi_eigen_3x3`.
196pub(super) fn jacobi_eigenvalues_3x3(a: [[f64; 3]; 3]) -> [f64; 3] {
197    let (_vecs, vals) = jacobi_eigen_3x3(a);
198    vals
199}
200/// Estimate per-point normals for `cloud` using k-nearest-neighbor PCA.
201///
202/// Delegates to [`NormalEstimation`] with the given neighbor count.
203pub fn estimate_normals(cloud: &PointCloud, k_neighbors: usize) -> Vec<[f64; 3]> {
204    NormalEstimation::new(k_neighbors).estimate(cloud)
205}
206/// Downsample `cloud` by averaging points in each voxel of side `voxel_size`.
207///
208/// Delegates to [`PointCloudFilter::voxel_downsample`].
209pub fn voxel_downsample(cloud: &PointCloud, voxel_size: f64) -> PointCloud {
210    PointCloudFilter::voxel_downsample(cloud, voxel_size)
211}
212/// Remove statistical outliers from `cloud`.
213///
214/// Points whose mean distance to their `k` nearest neighbors exceeds
215/// `mean + std_ratio * std` are removed.
216/// Delegates to [`PointCloudFilter::statistical_outlier_removal`].
217pub fn statistical_outlier_removal(cloud: &PointCloud, k: usize, std_ratio: f64) -> PointCloud {
218    PointCloudFilter::statistical_outlier_removal(cloud, k, std_ratio)
219}
220/// Compute the axis-aligned bounding box of `cloud`.
221///
222/// Returns `(min_corner, max_corner)` as `[f64;3]` arrays.
223/// Returns `([0;3], [0;3])` for an empty cloud.
224pub fn compute_bounding_box(cloud: &PointCloud) -> ([f64; 3], [f64; 3]) {
225    cloud.bounding_box()
226}
227/// Align `source` onto `target` using point-to-point ICP.
228///
229/// Returns `(transform_4x4_row_major, final_rms_error)`.
230/// The transform encodes rotation R and translation t as a 4×4 homogeneous
231/// matrix stored row-major: `[R | t; 0 0 0 1]`.
232pub fn icp_align(source: &PointCloud, target: &PointCloud, max_iter: usize) -> ([f64; 16], f64) {
233    let icp = IcpRegistration::new(max_iter, 1e-8);
234    let result = icp.run(source, target);
235    let r = result.rotation;
236    let t = result.translation;
237    let m = [
238        r[0][0], r[0][1], r[0][2], t[0], r[1][0], r[1][1], r[1][2], t[1], r[2][0], r[2][1],
239        r[2][2], t[2], 0.0, 0.0, 0.0, 1.0,
240    ];
241    (m, result.final_error)
242}
243/// Point-to-point ICP alignment returning a 4×3 matrix (rotation 3 rows + translation as last row).
244///
245/// Layout: rows 0-2 are the 3×3 rotation matrix, row 3 is the translation vector.
246pub fn icp_point_to_point(
247    source: &[[f64; 3]],
248    target: &[[f64; 3]],
249    max_iter: usize,
250) -> [[f64; 3]; 4] {
251    let src_cloud = PointCloud::from_points(source.to_vec());
252    let tgt_cloud = PointCloud::from_points(target.to_vec());
253    let icp = IcpRegistration::new(max_iter, 1e-8);
254    let result = icp.run(&src_cloud, &tgt_cloud);
255    let r = result.rotation;
256    [r[0], r[1], r[2], result.translation]
257}
258/// Compute per-point normals for a point slice using PCA of k nearest neighbors.
259pub fn compute_point_cloud_normals(points: &[[f64; 3]], k: usize) -> Vec<[f64; 3]> {
260    let cloud = PointCloud::from_points(points.to_vec());
261    NormalEstimation::new(k).estimate(&cloud)
262}
263/// Simplified FPFH-like descriptor for point at `idx` within radius `r`.
264///
265/// Computes a 33-bin histogram of angular features between the point's normal
266/// and its neighbors, returning a `Vec`f64` of length 33.
267pub fn fpfh_feature(points: &[[f64; 3]], normals: &[[f64; 3]], idx: usize, r: f64) -> Vec<f64> {
268    let tree = KdTree3D::build(points);
269    let neighbors = tree.range_search(points[idx], r);
270    let n_bins = 33usize;
271    let mut hist = vec![0.0f64; n_bins];
272    let ni = normals[idx];
273    let pi = points[idx];
274    for &j in &neighbors {
275        if j == idx {
276            continue;
277        }
278        let nj = normals[j];
279        let pj = points[j];
280        let d = sub(pj, pi);
281        let d_len = length(d);
282        if d_len < 1e-12 {
283            continue;
284        }
285        let cos_alpha = dot(ni, nj).clamp(-1.0, 1.0);
286        let d_norm = scale(d, 1.0 / d_len);
287        let cos_phi = dot(ni, d_norm).clamp(-1.0, 1.0);
288        let bin_a = ((cos_alpha + 1.0) / 2.0 * (n_bins / 3) as f64) as usize;
289        let bin_b = ((cos_phi + 1.0) / 2.0 * (n_bins / 3) as f64) as usize;
290        let bin_c = (dot(nj, d_norm).clamp(-1.0, 1.0) + 1.0) / 2.0 * (n_bins / 3) as f64;
291        let bin_c = bin_c as usize;
292        hist[(bin_a).min(n_bins / 3 - 1)] += 1.0;
293        hist[(n_bins / 3 + bin_b).min(2 * n_bins / 3 - 1)] += 1.0;
294        hist[(2 * n_bins / 3 + bin_c).min(n_bins - 1)] += 1.0;
295    }
296    let total: f64 = hist.iter().sum();
297    if total > 0.0 {
298        for v in &mut hist {
299            *v /= total;
300        }
301    }
302    hist
303}
304/// Selects `k` points from a cloud using Farthest Point Sampling (FPS).
305///
306/// Starts from the point at index `seed_idx`, then iteratively picks the
307/// point farthest from the already-selected set (measured as minimum squared
308/// distance to any selected point).
309///
310/// Returns indices into `points`.
311pub fn farthest_point_sampling(points: &[[f64; 3]], k: usize, seed_idx: usize) -> Vec<usize> {
312    let n = points.len();
313    if n == 0 || k == 0 {
314        return Vec::new();
315    }
316    let k = k.min(n);
317    let mut selected = Vec::with_capacity(k);
318    let mut min_dist = vec![f64::MAX; n];
319    let seed = seed_idx % n;
320    selected.push(seed);
321    for i in 0..n {
322        min_dist[i] = dist2_pts(points[i], points[seed]);
323    }
324    for _ in 1..k {
325        let next = min_dist
326            .iter()
327            .enumerate()
328            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
329            .map(|(i, _)| i)
330            .unwrap_or(0);
331        selected.push(next);
332        for i in 0..n {
333            let d = dist2_pts(points[i], points[next]);
334            if d < min_dist[i] {
335                min_dist[i] = d;
336            }
337        }
338    }
339    selected
340}
341pub(super) fn dist2_pts(a: [f64; 3], b: [f64; 3]) -> f64 {
342    let d = sub(a, b);
343    dot(d, d)
344}
345/// Fit a plane to a point cloud using RANSAC.
346///
347/// Randomly samples 3 points per iteration, computes the plane normal, and
348/// counts inliers within `threshold` distance to the plane.
349///
350/// # Parameters
351/// - `points`: input point cloud.
352/// - `n_iter`: number of RANSAC iterations (use ≥ 100 for noisy data).
353/// - `threshold`: maximum point-to-plane distance to count as inlier.
354///
355/// Returns `None` if the cloud has fewer than 3 points.
356pub fn ransac_fit_plane(
357    points: &[[f64; 3]],
358    n_iter: usize,
359    threshold: f64,
360) -> Option<RansacPlaneResult> {
361    let n = points.len();
362    if n < 3 {
363        return None;
364    }
365    let mut best_inliers: Vec<usize> = Vec::new();
366    let mut best_normal = [0.0f64; 3];
367    let mut best_d = 0.0_f64;
368    let step1 = 1usize;
369    let step2 = n / 3 + 1;
370    let step3 = 2 * n / 3 + 1;
371    for iter in 0..n_iter {
372        let i0 = (iter * step1) % n;
373        let i1 = (iter * step2 + 1) % n;
374        let i2 = (iter * step3 + 2) % n;
375        if i0 == i1 || i1 == i2 || i0 == i2 {
376            continue;
377        }
378        let p0 = points[i0];
379        let p1 = points[i1];
380        let p2 = points[i2];
381        let e1 = sub(p1, p0);
382        let e2 = sub(p2, p0);
383        let normal_raw = cross(e1, e2);
384        let normal = normalize(normal_raw);
385        if length(normal) < 0.5 {
386            continue;
387        }
388        let d = dot(normal, p0);
389        let inliers: Vec<usize> = points
390            .iter()
391            .enumerate()
392            .filter(|(_, p)| (dot(normal, **p) - d).abs() <= threshold)
393            .map(|(i, _)| i)
394            .collect();
395        if inliers.len() > best_inliers.len() {
396            best_inliers = inliers;
397            best_normal = normal;
398            best_d = d;
399        }
400    }
401    if best_inliers.is_empty() {
402        return None;
403    }
404    let n_in = best_inliers.len() as f64;
405    let mut centroid = [0.0f64; 3];
406    for &i in &best_inliers {
407        for (c, p) in centroid.iter_mut().zip(points[i].iter()) {
408            *c += p;
409        }
410    }
411    for c in centroid.iter_mut() {
412        *c /= n_in;
413    }
414    let n_inliers = best_inliers.len();
415    let point_on_plane = sub(
416        centroid,
417        scale(best_normal, dot(best_normal, centroid) - best_d),
418    );
419    Some(RansacPlaneResult {
420        normal: best_normal,
421        point_on_plane,
422        inliers: best_inliers,
423        n_inliers,
424    })
425}
426/// Axis-aligned bounding box extent: `(dimensions, center)`.
427///
428/// `dimensions\[i\]` is the length along axis i; `center` is the AABB center.
429pub fn aabb_extent(points: &[[f64; 3]]) -> ([f64; 3], [f64; 3]) {
430    if points.is_empty() {
431        return ([0.0; 3], [0.0; 3]);
432    }
433    let mut mn = points[0];
434    let mut mx = points[0];
435    for &p in points.iter().skip(1) {
436        for i in 0..3 {
437            if p[i] < mn[i] {
438                mn[i] = p[i];
439            }
440            if p[i] > mx[i] {
441                mx[i] = p[i];
442            }
443        }
444    }
445    let dims = [mx[0] - mn[0], mx[1] - mn[1], mx[2] - mn[2]];
446    let center = [
447        (mn[0] + mx[0]) * 0.5,
448        (mn[1] + mx[1]) * 0.5,
449        (mn[2] + mx[2]) * 0.5,
450    ];
451    (dims, center)
452}
453/// PCA-based Oriented Bounding Box (OBB) approximation.
454///
455/// Returns `(axes, half_extents, center)` where `axes` is a 3×3 column
456/// matrix of the principal directions (sorted by decreasing variance).
457pub fn pca_obb(points: &[[f64; 3]]) -> ([[f64; 3]; 3], [f64; 3], [f64; 3]) {
458    if points.is_empty() {
459        return (
460            [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
461            [0.0; 3],
462            [0.0; 3],
463        );
464    }
465    let n = points.len() as f64;
466    let mut centroid = [0.0f64; 3];
467    for &p in points {
468        for (c, p_i) in centroid.iter_mut().zip(p.iter()) {
469            *c += p_i;
470        }
471    }
472    for c in centroid.iter_mut() {
473        *c /= n;
474    }
475    let mut cov = [[0.0f64; 3]; 3];
476    for &p in points {
477        let d = sub(p, centroid);
478        for i in 0..3 {
479            for j in 0..3 {
480                cov[i][j] += d[i] * d[j];
481            }
482        }
483    }
484    let axis0 = power_iteration_largest(&cov);
485    let deflated = deflate_matrix(&cov, &axis0);
486    let axis1 = power_iteration_largest(&deflated);
487    let axis2 = normalize(cross(axis0, axis1));
488    let axes = [axis0, axis1, axis2];
489    let mut min_proj = [f64::MAX; 3];
490    let mut max_proj = [f64::MIN; 3];
491    for &p in points {
492        let d = sub(p, centroid);
493        for k in 0..3 {
494            let proj = dot(d, axes[k]);
495            if proj < min_proj[k] {
496                min_proj[k] = proj;
497            }
498            if proj > max_proj[k] {
499                max_proj[k] = proj;
500            }
501        }
502    }
503    let half_extents = [
504        (max_proj[0] - min_proj[0]) * 0.5,
505        (max_proj[1] - min_proj[1]) * 0.5,
506        (max_proj[2] - min_proj[2]) * 0.5,
507    ];
508    (axes, half_extents, centroid)
509}
510pub(super) fn power_iteration_largest(m: &[[f64; 3]; 3]) -> [f64; 3] {
511    let mut v = [1.0f64, 0.0, 0.0];
512    for _ in 0..30 {
513        let mv = mat_vec_3_pca(m, v);
514        let len = length(mv);
515        if len < 1e-15 {
516            break;
517        }
518        v = scale(mv, 1.0 / len);
519    }
520    normalize(v)
521}
522pub(super) fn deflate_matrix(m: &[[f64; 3]; 3], axis: &[f64; 3]) -> [[f64; 3]; 3] {
523    let mv = mat_vec_3_pca(m, *axis);
524    let lambda = dot(mv, *axis);
525    let mut result = *m;
526    for i in 0..3 {
527        for j in 0..3 {
528            result[i][j] -= lambda * axis[i] * axis[j];
529        }
530    }
531    result
532}
533pub(super) fn mat_vec_3_pca(m: &[[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
534    [
535        m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
536        m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
537        m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
538    ]
539}