1use crate::camera::CameraIntrinsics;
17use std::collections::HashMap;
18
19#[derive(Debug, Clone)]
28pub struct PointCloud3D {
29 pub points: Vec<[f32; 3]>,
31 pub normals: Option<Vec<[f32; 3]>>,
33 pub colors: Option<Vec<[u8; 3]>>,
35 pub intensities: Option<Vec<f32>>,
37}
38
39impl PointCloud3D {
40 pub fn new(points: Vec<[f32; 3]>) -> Self {
44 Self {
45 points,
46 normals: None,
47 colors: None,
48 intensities: None,
49 }
50 }
51
52 pub fn from_depth_map(depth: &[Vec<f32>], intrinsics: &CameraIntrinsics) -> Self {
61 let rows = depth.len();
62 let mut pts = Vec::new();
63 for (r, row) in depth.iter().enumerate() {
64 let cols = row.len();
65 for (c, &d) in row.iter().enumerate() {
66 if d <= 0.0 {
67 continue;
68 }
69 let xn = (c as f64 - intrinsics.cx) / intrinsics.fx;
70 let yn = (r as f64 - intrinsics.cy) / intrinsics.fy;
71 let z = d as f64;
72 pts.push([(xn * z) as f32, (yn * z) as f32, z as f32]);
73 }
74 let _ = rows; }
76 Self::new(pts)
77 }
78
79 #[inline]
83 pub fn len(&self) -> usize {
84 self.points.len()
85 }
86
87 #[inline]
89 pub fn is_empty(&self) -> bool {
90 self.points.is_empty()
91 }
92
93 pub fn centroid(&self) -> [f32; 3] {
95 let n = self.points.len();
96 if n == 0 {
97 return [0.0; 3];
98 }
99 let mut sum = [0.0f64; 3];
100 for p in &self.points {
101 sum[0] += p[0] as f64;
102 sum[1] += p[1] as f64;
103 sum[2] += p[2] as f64;
104 }
105 let nf = n as f64;
106 [
107 (sum[0] / nf) as f32,
108 (sum[1] / nf) as f32,
109 (sum[2] / nf) as f32,
110 ]
111 }
112
113 pub fn bounding_box(&self) -> ([f32; 3], [f32; 3]) {
115 if self.points.is_empty() {
116 return ([0.0; 3], [0.0; 3]);
117 }
118 let mut mn = [f32::INFINITY; 3];
119 let mut mx = [f32::NEG_INFINITY; 3];
120 for p in &self.points {
121 for k in 0..3 {
122 if p[k] < mn[k] {
123 mn[k] = p[k];
124 }
125 if p[k] > mx[k] {
126 mx[k] = p[k];
127 }
128 }
129 }
130 (mn, mx)
131 }
132
133 pub fn remove_statistical_outliers(&mut self, k: usize, std_ratio: f64) {
144 let n = self.points.len();
145 if n == 0 || k == 0 {
146 return;
147 }
148
149 let mut mean_dists = Vec::with_capacity(n);
151 for i in 0..n {
152 let pi = self.points[i];
153 let mut dists: Vec<f32> = (0..n)
155 .filter(|&j| j != i)
156 .map(|j| dist3(pi, self.points[j]))
157 .collect();
158 dists.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
159 let k_actual = k.min(dists.len());
160 let mean: f64 =
161 dists[..k_actual].iter().map(|&v| v as f64).sum::<f64>() / k_actual as f64;
162 mean_dists.push(mean);
163 }
164
165 let global_mean = mean_dists.iter().sum::<f64>() / n as f64;
167 let variance = mean_dists
168 .iter()
169 .map(|&v| (v - global_mean).powi(2))
170 .sum::<f64>()
171 / n as f64;
172 let global_std = variance.sqrt();
173 let threshold = global_mean + std_ratio * global_std;
174
175 let keep: Vec<bool> = mean_dists.iter().map(|&d| d <= threshold).collect();
177 self.filter_by_mask(&keep);
178 }
179
180 pub fn voxel_downsample(&self, voxel_size: f32) -> Self {
188 if self.points.is_empty() || voxel_size <= 0.0 {
189 return self.clone();
190 }
191
192 let mut voxels: HashMap<(i64, i64, i64), ([f64; 3], usize)> = HashMap::new();
194
195 for p in &self.points {
196 let key = (
197 (p[0] / voxel_size).floor() as i64,
198 (p[1] / voxel_size).floor() as i64,
199 (p[2] / voxel_size).floor() as i64,
200 );
201 let entry = voxels.entry(key).or_insert(([0.0; 3], 0));
202 entry.0[0] += p[0] as f64;
203 entry.0[1] += p[1] as f64;
204 entry.0[2] += p[2] as f64;
205 entry.1 += 1;
206 }
207
208 let pts: Vec<[f32; 3]> = voxels
209 .values()
210 .map(|(sum, cnt)| {
211 let n = *cnt as f64;
212 [
213 (sum[0] / n) as f32,
214 (sum[1] / n) as f32,
215 (sum[2] / n) as f32,
216 ]
217 })
218 .collect();
219
220 Self::new(pts)
221 }
222
223 pub fn estimate_normals(&mut self, k: usize) {
234 let n = self.points.len();
235 if n == 0 {
236 return;
237 }
238
239 let mut normals = Vec::with_capacity(n);
240
241 for i in 0..n {
242 let pi = self.points[i];
243 let neighbours = self.k_nearest_neighbors(pi, k.min(n - 1).max(1));
244
245 if neighbours.is_empty() {
246 normals.push([0.0f32, 0.0, 1.0]);
247 continue;
248 }
249
250 let mut cx = 0.0f64;
252 let mut cy = 0.0f64;
253 let mut cz = 0.0f64;
254 for &idx in &neighbours {
255 let q = self.points[idx];
256 cx += q[0] as f64;
257 cy += q[1] as f64;
258 cz += q[2] as f64;
259 }
260 let m = neighbours.len() as f64;
261 cx /= m;
262 cy /= m;
263 cz /= m;
264
265 let mut cov = [[0.0f64; 3]; 3];
267 for &idx in &neighbours {
268 let q = self.points[idx];
269 let dx = q[0] as f64 - cx;
270 let dy = q[1] as f64 - cy;
271 let dz = q[2] as f64 - cz;
272 let diff = [dx, dy, dz];
273 for a in 0..3 {
274 for b in 0..3 {
275 cov[a][b] += diff[a] * diff[b];
276 }
277 }
278 }
279
280 let normal = smallest_eigenvector_3x3(&cov);
281 let nf = if normal[2] < 0.0 {
283 [-normal[0] as f32, -normal[1] as f32, -normal[2] as f32]
284 } else {
285 [normal[0] as f32, normal[1] as f32, normal[2] as f32]
286 };
287 normals.push(nf);
288 }
289
290 self.normals = Some(normals);
291 }
292
293 pub fn icp_align(
306 source: &PointCloud3D,
307 target: &PointCloud3D,
308 max_iterations: usize,
309 tolerance: f64,
310 ) -> (PointCloud3D, [[f64; 4]; 4]) {
311 if source.is_empty() || target.is_empty() {
312 return (source.clone(), identity4());
313 }
314
315 let mut current_pts = source.points.clone();
316 let mut cumulative = identity4();
317
318 let mut prev_mean_dist = f64::INFINITY;
319
320 for _iter in 0..max_iterations {
321 let mut src_corr = Vec::with_capacity(current_pts.len());
323 let mut tgt_corr = Vec::with_capacity(current_pts.len());
324 let mut total_dist = 0.0f64;
325
326 for &sp in ¤t_pts {
327 let (nn_idx, dist) = nearest_in_slice(&target.points, sp);
328 src_corr.push(sp);
329 tgt_corr.push(target.points[nn_idx]);
330 total_dist += dist as f64;
331 }
332
333 let mean_dist = total_dist / current_pts.len() as f64;
334
335 let (r, t) = optimal_rigid_transform(&src_corr, &tgt_corr);
337
338 for p in current_pts.iter_mut() {
340 *p = apply_rigid(r, t, *p);
341 }
342
343 cumulative = compose_mat4(mat4_from_rt(r, t), cumulative);
345
346 if (prev_mean_dist - mean_dist).abs() < tolerance {
347 break;
348 }
349 prev_mean_dist = mean_dist;
350 }
351
352 let aligned = PointCloud3D::new(current_pts);
353 (aligned, cumulative)
354 }
355
356 pub fn k_nearest_neighbors(&self, query: [f32; 3], k: usize) -> Vec<usize> {
360 k_nearest_in_slice(&self.points, query, k)
361 }
362
363 pub fn to_ply_string(&self) -> String {
367 let n = self.points.len();
368 let has_normals = self.normals.as_ref().map(|v| v.len() == n).unwrap_or(false);
369 let has_colors = self.colors.as_ref().map(|v| v.len() == n).unwrap_or(false);
370
371 let mut s = String::with_capacity(512 + n * 64);
372 s.push_str("ply\nformat ascii 1.0\n");
373 s.push_str(&format!("element vertex {}\n", n));
374 s.push_str("property float x\nproperty float y\nproperty float z\n");
375 if has_normals {
376 s.push_str("property float nx\nproperty float ny\nproperty float nz\n");
377 }
378 if has_colors {
379 s.push_str("property uchar red\nproperty uchar green\nproperty uchar blue\n");
380 }
381 s.push_str("end_header\n");
382
383 for i in 0..n {
384 let p = self.points[i];
385 s.push_str(&format!("{} {} {}", p[0], p[1], p[2]));
386 if has_normals {
387 let nm = self
388 .normals
389 .as_ref()
390 .expect("normals present - guarded by has_normals check")[i];
391 s.push_str(&format!(" {} {} {}", nm[0], nm[1], nm[2]));
392 }
393 if has_colors {
394 let c = self
395 .colors
396 .as_ref()
397 .expect("colors present - guarded by has_colors check")[i];
398 s.push_str(&format!(" {} {} {}", c[0], c[1], c[2]));
399 }
400 s.push('\n');
401 }
402 s
403 }
404
405 pub fn from_ply_str(s: &str) -> Result<Self, String> {
410 let mut lines = s.lines();
411
412 let first = lines.next().ok_or("Empty PLY")?;
414 if first.trim() != "ply" {
415 return Err(format!("Not a PLY file (got '{}')", first));
416 }
417
418 let mut n_vertices: Option<usize> = None;
419 let mut has_nx = false;
420 let mut has_red = false;
421
422 loop {
423 let line = lines.next().ok_or("Truncated PLY header")?;
424 let line = line.trim();
425 if line == "end_header" {
426 break;
427 }
428 if let Some(rest) = line.strip_prefix("element vertex ") {
429 n_vertices = Some(
430 rest.trim()
431 .parse::<usize>()
432 .map_err(|e| format!("Bad vertex count: {}", e))?,
433 );
434 } else if line == "property float nx" {
435 has_nx = true;
436 } else if line == "property uchar red" {
437 has_red = true;
438 }
439 }
440
441 let n_vertices = n_vertices.ok_or("Missing 'element vertex' in PLY header")?;
442 let mut points = Vec::with_capacity(n_vertices);
443 let mut normals_v = if has_nx {
444 Some(Vec::with_capacity(n_vertices))
445 } else {
446 None
447 };
448 let mut colors_v = if has_red {
449 Some(Vec::with_capacity(n_vertices))
450 } else {
451 None
452 };
453
454 for line in lines.take(n_vertices) {
455 let mut toks = line.split_whitespace();
456 let x: f32 = toks
457 .next()
458 .and_then(|v| v.parse().ok())
459 .ok_or_else(|| format!("Bad x in '{}'", line))?;
460 let y: f32 = toks
461 .next()
462 .and_then(|v| v.parse().ok())
463 .ok_or_else(|| format!("Bad y in '{}'", line))?;
464 let z: f32 = toks
465 .next()
466 .and_then(|v| v.parse().ok())
467 .ok_or_else(|| format!("Bad z in '{}'", line))?;
468 points.push([x, y, z]);
469
470 if let Some(ref mut nv) = normals_v {
471 let nx: f32 = toks
472 .next()
473 .and_then(|v| v.parse().ok())
474 .ok_or_else(|| format!("Bad nx in '{}'", line))?;
475 let ny: f32 = toks
476 .next()
477 .and_then(|v| v.parse().ok())
478 .ok_or_else(|| format!("Bad ny in '{}'", line))?;
479 let nz: f32 = toks
480 .next()
481 .and_then(|v| v.parse().ok())
482 .ok_or_else(|| format!("Bad nz in '{}'", line))?;
483 nv.push([nx, ny, nz]);
484 }
485
486 if let Some(ref mut cv) = colors_v {
487 let r: u8 = toks
488 .next()
489 .and_then(|v| v.parse().ok())
490 .ok_or_else(|| format!("Bad red in '{}'", line))?;
491 let g: u8 = toks
492 .next()
493 .and_then(|v| v.parse().ok())
494 .ok_or_else(|| format!("Bad green in '{}'", line))?;
495 let b: u8 = toks
496 .next()
497 .and_then(|v| v.parse().ok())
498 .ok_or_else(|| format!("Bad blue in '{}'", line))?;
499 cv.push([r, g, b]);
500 }
501 }
502
503 Ok(Self {
504 normals: normals_v,
505 colors: colors_v,
506 intensities: None,
507 points,
508 })
509 }
510
511 fn filter_by_mask(&mut self, keep: &[bool]) {
514 let pts: Vec<[f32; 3]> = self
515 .points
516 .iter()
517 .zip(keep.iter())
518 .filter(|(_, &k)| k)
519 .map(|(&p, _)| p)
520 .collect();
521
522 if let Some(ref mut norms) = self.normals {
523 *norms = norms
524 .iter()
525 .zip(keep.iter())
526 .filter(|(_, &k)| k)
527 .map(|(&n, _)| n)
528 .collect();
529 }
530 if let Some(ref mut cols) = self.colors {
531 *cols = cols
532 .iter()
533 .zip(keep.iter())
534 .filter(|(_, &k)| k)
535 .map(|(&c, _)| c)
536 .collect();
537 }
538 if let Some(ref mut ints) = self.intensities {
539 *ints = ints
540 .iter()
541 .zip(keep.iter())
542 .filter(|(_, &k)| k)
543 .map(|(&iv, _)| iv)
544 .collect();
545 }
546 self.points = pts;
547 }
548}
549
550pub fn ransac_plane_fit(
581 cloud: &PointCloud3D,
582 distance_threshold: f32,
583 max_iterations: usize,
584) -> Option<([f32; 4], Vec<usize>)> {
585 let n = cloud.points.len();
586 if n < 3 {
587 return None;
588 }
589
590 let mut rng_state = 12345u64;
592 let mut rng_next = |max: usize| -> usize {
593 rng_state = rng_state
594 .wrapping_mul(6364136223846793005)
595 .wrapping_add(1442695040888963407);
596 ((rng_state >> 33) as usize) % max
597 };
598
599 let mut best_inliers: Vec<usize> = Vec::new();
600 let mut best_plane = [0.0f32; 4];
601
602 for _ in 0..max_iterations {
603 let i0 = rng_next(n);
605 let mut i1 = rng_next(n);
606 while i1 == i0 {
607 i1 = rng_next(n);
608 }
609 let mut i2 = rng_next(n);
610 while i2 == i0 || i2 == i1 {
611 i2 = rng_next(n);
612 }
613
614 let p0 = cloud.points[i0];
615 let p1 = cloud.points[i1];
616 let p2 = cloud.points[i2];
617
618 let v1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
620 let v2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
621 let nx = v1[1] * v2[2] - v1[2] * v2[1];
622 let ny = v1[2] * v2[0] - v1[0] * v2[2];
623 let nz = v1[0] * v2[1] - v1[1] * v2[0];
624 let len = (nx * nx + ny * ny + nz * nz).sqrt();
625 if len < 1e-6 {
626 continue; }
628 let (nx, ny, nz) = (nx / len, ny / len, nz / len);
629 let d = -(nx * p0[0] + ny * p0[1] + nz * p0[2]);
630
631 let inliers: Vec<usize> = (0..n)
633 .filter(|&i| {
634 let p = cloud.points[i];
635 (nx * p[0] + ny * p[1] + nz * p[2] + d).abs() <= distance_threshold
636 })
637 .collect();
638
639 if inliers.len() > best_inliers.len() {
640 best_inliers = inliers;
641 best_plane = [nx, ny, nz, d];
642 }
643 }
644
645 if best_inliers.is_empty() {
646 None
647 } else {
648 Some((best_plane, best_inliers))
649 }
650}
651
652#[inline]
657fn dist3(a: [f32; 3], b: [f32; 3]) -> f32 {
658 let dx = a[0] - b[0];
659 let dy = a[1] - b[1];
660 let dz = a[2] - b[2];
661 (dx * dx + dy * dy + dz * dz).sqrt()
662}
663
664fn nearest_in_slice(pts: &[[f32; 3]], query: [f32; 3]) -> (usize, f32) {
666 let mut best_idx = 0;
667 let mut best_dist = f32::INFINITY;
668 for (i, &p) in pts.iter().enumerate() {
669 let d = dist3(p, query);
670 if d < best_dist {
671 best_dist = d;
672 best_idx = i;
673 }
674 }
675 (best_idx, best_dist)
676}
677
678fn k_nearest_in_slice(pts: &[[f32; 3]], query: [f32; 3], k: usize) -> Vec<usize> {
680 if pts.is_empty() || k == 0 {
681 return Vec::new();
682 }
683 let mut indexed: Vec<(usize, f32)> = pts
684 .iter()
685 .enumerate()
686 .map(|(i, &p)| (i, dist3(p, query)))
687 .collect();
688 indexed.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
689 indexed
691 .iter()
692 .filter(|&&(_, d)| d > 0.0)
693 .take(k)
694 .map(|&(i, _)| i)
695 .collect()
696}
697
698fn identity4() -> [[f64; 4]; 4] {
700 [
701 [1.0, 0.0, 0.0, 0.0],
702 [0.0, 1.0, 0.0, 0.0],
703 [0.0, 0.0, 1.0, 0.0],
704 [0.0, 0.0, 0.0, 1.0],
705 ]
706}
707
708fn mat4_from_rt(r: [[f64; 3]; 3], t: [f64; 3]) -> [[f64; 4]; 4] {
709 [
710 [r[0][0], r[0][1], r[0][2], t[0]],
711 [r[1][0], r[1][1], r[1][2], t[1]],
712 [r[2][0], r[2][1], r[2][2], t[2]],
713 [0.0, 0.0, 0.0, 1.0],
714 ]
715}
716
717fn compose_mat4(a: [[f64; 4]; 4], b: [[f64; 4]; 4]) -> [[f64; 4]; 4] {
718 let mut c = [[0.0f64; 4]; 4];
719 for i in 0..4 {
720 for j in 0..4 {
721 for k in 0..4 {
722 c[i][j] += a[i][k] * b[k][j];
723 }
724 }
725 }
726 c
727}
728
729fn apply_rigid(r: [[f64; 3]; 3], t: [f64; 3], p: [f32; 3]) -> [f32; 3] {
730 let x = p[0] as f64;
731 let y = p[1] as f64;
732 let z = p[2] as f64;
733 [
734 (r[0][0] * x + r[0][1] * y + r[0][2] * z + t[0]) as f32,
735 (r[1][0] * x + r[1][1] * y + r[1][2] * z + t[1]) as f32,
736 (r[2][0] * x + r[2][1] * y + r[2][2] * z + t[2]) as f32,
737 ]
738}
739
740fn optimal_rigid_transform(src: &[[f32; 3]], tgt: &[[f32; 3]]) -> ([[f64; 3]; 3], [f64; 3]) {
743 let n = src.len();
744 if n == 0 {
745 return (
746 [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
747 [0.0; 3],
748 );
749 }
750
751 let mut cs = [0.0f64; 3];
753 let mut ct = [0.0f64; 3];
754 for i in 0..n {
755 for k in 0..3 {
756 cs[k] += src[i][k] as f64;
757 }
758 for k in 0..3 {
759 ct[k] += tgt[i][k] as f64;
760 }
761 }
762 let nf = n as f64;
763 for k in 0..3 {
764 cs[k] /= nf;
765 ct[k] /= nf;
766 }
767
768 let mut h = [[0.0f64; 3]; 3];
770 for i in 0..n {
771 let ds = [
772 src[i][0] as f64 - cs[0],
773 src[i][1] as f64 - cs[1],
774 src[i][2] as f64 - cs[2],
775 ];
776 let dt = [
777 tgt[i][0] as f64 - ct[0],
778 tgt[i][1] as f64 - ct[1],
779 tgt[i][2] as f64 - ct[2],
780 ];
781 for a in 0..3 {
782 for b in 0..3 {
783 h[a][b] += ds[a] * dt[b];
784 }
785 }
786 }
787
788 let (u, _s, v) = svd3x3_jacobi(h);
790 let r = mat3_mul(v, mat3_transpose(u));
791
792 let r = ensure_rotation(r);
794
795 let rcs = mat3_vec(r, cs);
797 let t = [ct[0] - rcs[0], ct[1] - rcs[1], ct[2] - rcs[2]];
798
799 (r, t)
800}
801
802fn smallest_eigenvector_3x3(cov: &[[f64; 3]; 3]) -> [f64; 3] {
805 let (_, vecs) = jacobi_eigen3(cov);
807 let (evals, evecs) = jacobi_eigen3(cov);
810 let mut min_idx = 0;
811 let mut min_val = evals[0];
812 for (i, &ev) in evals.iter().enumerate().skip(1) {
813 if ev < min_val {
814 min_val = ev;
815 min_idx = i;
816 }
817 }
818 let _ = vecs;
819 [evecs[0][min_idx], evecs[1][min_idx], evecs[2][min_idx]]
820}
821
822fn jacobi_eigen3(a: &[[f64; 3]; 3]) -> ([f64; 3], [[f64; 3]; 3]) {
825 let mut m = *a;
826 let mut v = [[1.0f64, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]; for _ in 0..50 {
829 let mut max_val = 0.0f64;
831 let mut p = 0usize;
832 let mut q = 1usize;
833 #[allow(clippy::needless_range_loop)]
834 for i in 0..3 {
835 for j in (i + 1)..3 {
836 if m[i][j].abs() > max_val {
837 max_val = m[i][j].abs();
838 p = i;
839 q = j;
840 }
841 }
842 }
843 if max_val < 1e-12 {
844 break;
845 }
846
847 let theta = (m[q][q] - m[p][p]) / (2.0 * m[p][q]);
849 let t = if theta >= 0.0 {
850 1.0 / (theta + (1.0 + theta * theta).sqrt())
851 } else {
852 1.0 / (theta - (1.0 + theta * theta).sqrt())
853 };
854 let cos = 1.0 / (1.0 + t * t).sqrt();
855 let sin = t * cos;
856 let tau = sin / (1.0 + cos);
857
858 let mpq = m[p][q];
860 m[p][p] -= t * mpq;
861 m[q][q] += t * mpq;
862 m[p][q] = 0.0;
863 m[q][p] = 0.0;
864
865 #[allow(clippy::needless_range_loop)]
866 for r in 0..3 {
867 if r != p && r != q {
868 let mrp = m[r][p];
869 let mrq = m[r][q];
870 m[r][p] = mrp - sin * (mrq + tau * mrp);
871 m[p][r] = m[r][p];
872 m[r][q] = mrq + sin * (mrp - tau * mrq);
873 m[q][r] = m[r][q];
874 }
875 }
876
877 #[allow(clippy::needless_range_loop)]
879 for r in 0..3 {
880 let vp = v[r][p];
881 let vq = v[r][q];
882 v[r][p] = vp - sin * (vq + tau * vp);
883 v[r][q] = vq + sin * (vp - tau * vq);
884 }
885 }
886
887 ([m[0][0], m[1][1], m[2][2]], v)
888}
889
890fn svd3x3_jacobi(a: [[f64; 3]; 3]) -> ([[f64; 3]; 3], [f64; 3], [[f64; 3]; 3]) {
893 let mut ata = [[0.0f64; 3]; 3];
895 #[allow(clippy::needless_range_loop)]
896 for i in 0..3 {
897 for j in 0..3 {
898 for k in 0..3 {
899 ata[i][j] += a[k][i] * a[k][j];
900 }
901 }
902 }
903 let (evals, v) = jacobi_eigen3(&ata);
904
905 let s = [
907 evals[0].max(0.0).sqrt(),
908 evals[1].max(0.0).sqrt(),
909 evals[2].max(0.0).sqrt(),
910 ];
911
912 let mut u = [[0.0f64; 3]; 3];
914 for j in 0..3 {
915 if s[j] > 1e-10 {
916 for i in 0..3 {
917 u[i][j] = (a[i][0] * v[0][j] + a[i][1] * v[1][j] + a[i][2] * v[2][j]) / s[j];
918 }
919 } else {
920 if j == 0 {
922 u[0][j] = 1.0;
923 } else if j == 1 {
924 u[1][j] = 1.0;
925 } else {
926 u[2][j] = 1.0;
927 }
928 }
929 }
930
931 (u, s, v)
932}
933
934fn mat3_mul(a: [[f64; 3]; 3], b: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
935 let mut c = [[0.0f64; 3]; 3];
936 for i in 0..3 {
937 for j in 0..3 {
938 for k in 0..3 {
939 c[i][j] += a[i][k] * b[k][j];
940 }
941 }
942 }
943 c
944}
945
946fn mat3_transpose(m: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
947 [
948 [m[0][0], m[1][0], m[2][0]],
949 [m[0][1], m[1][1], m[2][1]],
950 [m[0][2], m[1][2], m[2][2]],
951 ]
952}
953
954fn mat3_vec(m: [[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
955 [
956 m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
957 m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
958 m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
959 ]
960}
961
962fn ensure_rotation(r: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
963 let det = r[0][0] * (r[1][1] * r[2][2] - r[1][2] * r[2][1])
965 - r[0][1] * (r[1][0] * r[2][2] - r[1][2] * r[2][0])
966 + r[0][2] * (r[1][0] * r[2][1] - r[1][1] * r[2][0]);
967 if det < 0.0 {
968 let mut r2 = r;
969 r2[0][2] = -r2[0][2];
970 r2[1][2] = -r2[1][2];
971 r2[2][2] = -r2[2][2];
972 r2
973 } else {
974 r
975 }
976}
977
978#[cfg(test)]
983mod tests {
984 use super::*;
985
986 fn plane_cloud() -> PointCloud3D {
987 let pts: Vec<[f32; 3]> = (0..10)
989 .flat_map(|i| (0..10).map(move |j| [i as f32, j as f32, 0.0f32]))
990 .collect();
991 PointCloud3D::new(pts)
992 }
993
994 #[test]
995 fn test_centroid_simple() {
996 let cloud = PointCloud3D::new(vec![[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]]);
997 let c = cloud.centroid();
998 assert!((c[0] - 1.0).abs() < 1e-6);
999 assert!((c[1]).abs() < 1e-6);
1000 assert!((c[2]).abs() < 1e-6);
1001 }
1002
1003 #[test]
1004 fn test_bounding_box() {
1005 let cloud = PointCloud3D::new(vec![[1.0, 2.0, 3.0], [-1.0, -2.0, -3.0]]);
1006 let (mn, mx) = cloud.bounding_box();
1007 assert!((mn[0] - (-1.0)).abs() < 1e-6);
1008 assert!((mx[0] - 1.0).abs() < 1e-6);
1009 assert!((mn[2] - (-3.0)).abs() < 1e-6);
1010 assert!((mx[2] - 3.0).abs() < 1e-6);
1011 }
1012
1013 #[test]
1014 fn test_voxel_downsample_reduces_count() {
1015 let cloud = plane_cloud();
1016 let orig_n = cloud.len();
1017 let down = cloud.voxel_downsample(2.0);
1018 assert!(
1019 down.len() < orig_n,
1020 "downsampled: {}, orig: {}",
1021 down.len(),
1022 orig_n
1023 );
1024 }
1025
1026 #[test]
1027 fn test_voxel_downsample_empty() {
1028 let cloud = PointCloud3D::new(Vec::new());
1029 let down = cloud.voxel_downsample(1.0);
1030 assert!(down.is_empty());
1031 }
1032
1033 #[test]
1034 fn test_sor_removes_outliers() {
1035 let mut cloud = plane_cloud();
1036 cloud.points.push([100.0, 100.0, 100.0]);
1038 let n_before = cloud.len();
1039 cloud.remove_statistical_outliers(5, 1.0);
1040 let n_after = cloud.len();
1041 assert!(n_after < n_before, "before={}, after={}", n_before, n_after);
1042 }
1043
1044 #[test]
1045 fn test_estimate_normals() {
1046 let mut cloud = plane_cloud();
1047 cloud.estimate_normals(5);
1048 let normals = cloud
1049 .normals
1050 .as_ref()
1051 .expect("normals should be populated after estimate_normals");
1052 assert_eq!(normals.len(), cloud.len());
1053 for n in normals {
1055 assert!(n[2].abs() > 0.5, "normal z component too small: {:?}", n);
1056 }
1057 }
1058
1059 #[test]
1060 fn test_ransac_plane_fit_z0() {
1061 let cloud = plane_cloud();
1062 let result = ransac_plane_fit(&cloud, 0.01, 200);
1063 assert!(result.is_some(), "RANSAC found no plane");
1064 let (plane, inliers) = result.expect("RANSAC plane fit should find a plane");
1065 assert!(plane[2].abs() > 0.9, "plane={:?}", plane);
1067 assert!(inliers.len() > cloud.len() / 2);
1069 }
1070
1071 #[test]
1072 fn test_ply_roundtrip() {
1073 let pts = vec![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]];
1074 let cloud = PointCloud3D::new(pts);
1075 let ply = cloud.to_ply_string();
1076 let loaded =
1077 PointCloud3D::from_ply_str(&ply).expect("from_ply_str should succeed on valid PLY");
1078 assert_eq!(loaded.len(), 2);
1079 assert!((loaded.points[0][0] - 1.0).abs() < 1e-5);
1080 assert!((loaded.points[1][2] - 6.0).abs() < 1e-5);
1081 }
1082
1083 #[test]
1084 fn test_ply_with_normals_roundtrip() {
1085 let pts = vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0]];
1086 let norms = vec![[0.0f32, 0.0, 1.0], [0.0, 0.0, 1.0]];
1087 let cloud = PointCloud3D {
1088 points: pts,
1089 normals: Some(norms),
1090 colors: None,
1091 intensities: None,
1092 };
1093 let ply = cloud.to_ply_string();
1094 let loaded = PointCloud3D::from_ply_str(&ply)
1095 .expect("from_ply_str should succeed on PLY with normals");
1096 assert!(loaded.normals.is_some());
1097 let nv = loaded
1098 .normals
1099 .expect("normals should be present after round-trip with normals");
1100 assert!((nv[0][2] - 1.0).abs() < 1e-5);
1101 }
1102
1103 #[test]
1104 fn test_from_depth_map() {
1105 use crate::camera::CameraIntrinsics;
1106 let intrinsics = CameraIntrinsics::ideal(100.0, 100.0, 4.0, 4.0);
1107 let depth = vec![
1108 vec![0.0f32, 0.0, 1.0, 0.0, 0.0], vec![0.0f32; 5],
1110 ];
1111 let cloud = PointCloud3D::from_depth_map(&depth, &intrinsics);
1112 assert_eq!(cloud.len(), 1);
1113 assert!((cloud.points[0][2] - 1.0).abs() < 1e-5);
1114 }
1115
1116 #[test]
1117 fn test_icp_identity() {
1118 let cloud = plane_cloud();
1120 let (aligned, _tf) = PointCloud3D::icp_align(&cloud, &cloud, 5, 1e-6);
1121 for (a, b) in aligned.points.iter().zip(cloud.points.iter()) {
1123 let d = dist3(*a, *b);
1124 assert!(d < 0.5, "dist={}", d);
1125 }
1126 }
1127
1128 #[test]
1129 fn test_k_nearest_neighbors() {
1130 let cloud = PointCloud3D::new(vec![
1131 [0.0, 0.0, 0.0],
1132 [1.0, 0.0, 0.0],
1133 [2.0, 0.0, 0.0],
1134 [10.0, 0.0, 0.0],
1135 ]);
1136 let nn = cloud.k_nearest_neighbors([0.5, 0.0, 0.0], 2);
1137 assert_eq!(nn.len(), 2);
1138 assert!(nn.contains(&0) || nn.contains(&1));
1140 }
1141}