1use crate::nearest_neighbor::KdTree;
4use rayon::prelude::*;
5use threecrate_core::{NearestNeighborSearch, Point3f, PointCloud, Result};
6
7pub fn voxel_grid_filter(
39 cloud: &PointCloud<Point3f>,
40 voxel_size: f32,
41) -> Result<PointCloud<Point3f>> {
42 if cloud.is_empty() {
43 return Ok(PointCloud::new());
44 }
45
46 if !voxel_size.is_finite() || voxel_size <= 0.0 {
47 return Err(threecrate_core::Error::InvalidData(
48 "voxel_size must be positive".to_string(),
49 ));
50 }
51
52 let finite = || {
55 cloud
56 .points
57 .iter()
58 .filter(|p| p.coords.iter().all(|v| v.is_finite()))
59 };
60 let (mut min, mut max) = ([f32::INFINITY; 3], [f32::NEG_INFINITY; 3]);
61 for p in finite() {
62 for axis in 0..3 {
63 min[axis] = min[axis].min(p[axis]);
64 max[axis] = max[axis].max(p[axis]);
65 }
66 }
67 if min[0] > max[0] {
68 return Ok(PointCloud::new()); }
70 if (0..3).any(|axis| (max[axis] - min[axis]) / voxel_size >= i32::MAX as f32) {
73 return Err(threecrate_core::Error::InvalidData(format!(
74 "voxel_size {voxel_size} is too small for a cloud this large"
75 )));
76 }
77 let [min_x, min_y, min_z] = min;
78
79 let get_voxel_coords = |point: &Point3f| -> (i32, i32, i32) {
81 let x = ((point.x - min_x) / voxel_size).floor() as i32;
82 let y = ((point.y - min_y) / voxel_size).floor() as i32;
83 let z = ((point.z - min_z) / voxel_size).floor() as i32;
84 (x, y, z)
85 };
86
87 let mut voxel_map: std::collections::HashMap<(i32, i32, i32), ([f64; 3], usize)> =
93 std::collections::HashMap::new();
94
95 for point in finite() {
96 let voxel_coords = get_voxel_coords(point);
97 let entry = voxel_map.entry(voxel_coords).or_insert(([0.0; 3], 0));
98 entry.0[0] += point.x as f64;
99 entry.0[1] += point.y as f64;
100 entry.0[2] += point.z as f64;
101 entry.1 += 1;
102 }
103
104 let filtered_points: Vec<Point3f> = voxel_map
105 .values()
106 .map(|(sum, count)| {
107 let inv = 1.0 / *count as f64;
108 Point3f::new(
109 (sum[0] * inv) as f32,
110 (sum[1] * inv) as f32,
111 (sum[2] * inv) as f32,
112 )
113 })
114 .collect();
115
116 Ok(PointCloud::from_points(filtered_points))
117}
118
119pub fn radius_outlier_removal(
152 cloud: &PointCloud<Point3f>,
153 radius: f32,
154 min_neighbors: usize,
155) -> Result<PointCloud<Point3f>> {
156 if cloud.is_empty() {
157 return Ok(PointCloud::new());
158 }
159
160 if radius <= 0.0 {
161 return Err(threecrate_core::Error::InvalidData(
162 "radius must be positive".to_string(),
163 ));
164 }
165
166 if min_neighbors == 0 {
167 return Err(threecrate_core::Error::InvalidData(
168 "min_neighbors must be greater than 0".to_string(),
169 ));
170 }
171
172 let nn_search = KdTree::new(&cloud.points)?;
175
176 let neighbor_counts: Vec<usize> = cloud
178 .points
179 .par_iter()
180 .map(|point| {
181 let neighbors = nn_search.find_radius_neighbors(point, radius);
182 neighbors.len().saturating_sub(1)
184 })
185 .collect();
186
187 let filtered_points: Vec<Point3f> = cloud
189 .points
190 .iter()
191 .zip(neighbor_counts.iter())
192 .filter(|(_, &count)| count >= min_neighbors)
193 .map(|(point, _)| *point)
194 .collect();
195
196 Ok(PointCloud::from_points(filtered_points))
197}
198
199pub fn statistical_outlier_removal(
234 cloud: &PointCloud<Point3f>,
235 k_neighbors: usize,
236 std_dev_multiplier: f32,
237) -> Result<PointCloud<Point3f>> {
238 if cloud.is_empty() {
239 return Ok(PointCloud::new());
240 }
241
242 if k_neighbors == 0 {
243 return Err(threecrate_core::Error::InvalidData(
244 "k_neighbors must be greater than 0".to_string(),
245 ));
246 }
247
248 if std_dev_multiplier <= 0.0 {
249 return Err(threecrate_core::Error::InvalidData(
250 "std_dev_multiplier must be positive".to_string(),
251 ));
252 }
253
254 let nn_search = KdTree::new(&cloud.points)?;
257
258 let mean_distances: Vec<f32> = cloud
260 .points
261 .par_iter()
262 .map(|point| {
263 let neighbors = nn_search.find_k_nearest(point, k_neighbors + 1); if neighbors.is_empty() {
265 return 0.0;
266 }
267
268 let distances: Vec<f32> = neighbors
270 .iter()
271 .filter(|(idx, _)| cloud.points[*idx] != *point) .map(|(_, distance)| *distance)
273 .collect();
274
275 if distances.is_empty() {
276 return 0.0;
277 }
278
279 distances.iter().sum::<f32>() / distances.len() as f32
280 })
281 .collect();
282
283 let global_mean = mean_distances.iter().sum::<f32>() / mean_distances.len() as f32;
285
286 let variance = mean_distances
287 .iter()
288 .map(|&d| (d - global_mean).powi(2))
289 .sum::<f32>()
290 / mean_distances.len() as f32;
291
292 let global_std_dev = variance.sqrt();
293 let threshold = global_mean + std_dev_multiplier * global_std_dev;
294
295 let filtered_points: Vec<Point3f> = cloud
297 .points
298 .iter()
299 .zip(mean_distances.iter())
300 .filter(|(_, &mean_dist)| mean_dist <= threshold)
301 .map(|(point, _)| *point)
302 .collect();
303
304 Ok(PointCloud::from_points(filtered_points))
305}
306
307pub fn statistical_outlier_removal_with_threshold(
320 cloud: &PointCloud<Point3f>,
321 k_neighbors: usize,
322 threshold: f32,
323) -> Result<PointCloud<Point3f>> {
324 if cloud.is_empty() {
325 return Ok(PointCloud::new());
326 }
327
328 if k_neighbors == 0 {
329 return Err(threecrate_core::Error::InvalidData(
330 "k_neighbors must be greater than 0".to_string(),
331 ));
332 }
333
334 if threshold <= 0.0 {
335 return Err(threecrate_core::Error::InvalidData(
336 "threshold must be positive".to_string(),
337 ));
338 }
339
340 let nn_search = KdTree::new(&cloud.points)?;
343
344 let mean_distances: Vec<f32> = cloud
346 .points
347 .par_iter()
348 .map(|point| {
349 let neighbors = nn_search.find_k_nearest(point, k_neighbors + 1); if neighbors.is_empty() {
351 return 0.0;
352 }
353
354 let distances: Vec<f32> = neighbors
356 .iter()
357 .filter(|(idx, _)| cloud.points[*idx] != *point) .map(|(_, distance)| *distance)
359 .collect();
360
361 if distances.is_empty() {
362 return 0.0;
363 }
364
365 distances.iter().sum::<f32>() / distances.len() as f32
366 })
367 .collect();
368
369 let filtered_points: Vec<Point3f> = cloud
371 .points
372 .iter()
373 .zip(mean_distances.iter())
374 .filter(|(_, &mean_dist)| mean_dist <= threshold)
375 .map(|(point, _)| *point)
376 .collect();
377
378 Ok(PointCloud::from_points(filtered_points))
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384 use threecrate_core::Point3f;
385
386 fn grid_10x10() -> Vec<Point3f> {
387 (0..100)
388 .map(|i| Point3f::new((i % 10) as f32, (i / 10) as f32, 0.0))
389 .collect()
390 }
391
392 #[test]
393 fn test_voxel_grid_skips_nan_and_inf_points() {
394 let clean = voxel_grid_filter(&PointCloud::from_points(grid_10x10()), 2.0).unwrap();
395 for bad in [f32::NAN, f32::NEG_INFINITY] {
396 let mut points = grid_10x10();
397 points.push(Point3f::new(bad, 0.0, 0.0));
398 let filtered = voxel_grid_filter(&PointCloud::from_points(points), 2.0).unwrap();
399 assert_eq!(filtered.len(), clean.len(), "with {bad}");
400 }
401 let only_nan = PointCloud::from_points(vec![Point3f::new(f32::NAN, 0.0, 0.0)]);
402 assert!(voxel_grid_filter(&only_nan, 1.0).unwrap().is_empty());
403 }
404
405 #[test]
406 fn test_voxel_grid_rejects_unusable_voxel_sizes() {
407 let cloud = PointCloud::from_points(grid_10x10());
408 for size in [0.0, -1.0, f32::NAN, f32::INFINITY, 1e-30] {
409 assert!(
410 voxel_grid_filter(&cloud, size).is_err(),
411 "voxel_size {size}"
412 );
413 }
414 }
415
416 #[test]
417 fn test_statistical_outlier_removal_empty_cloud() {
418 let cloud = PointCloud::<Point3f>::new();
419 let result = statistical_outlier_removal(&cloud, 5, 1.0);
420 assert!(result.is_ok());
421 assert_eq!(result.unwrap().len(), 0);
422 }
423
424 #[test]
425 fn test_statistical_outlier_removal_single_point() {
426 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
427 let result = statistical_outlier_removal(&cloud, 1, 1.0);
428 assert!(result.is_ok());
429 assert_eq!(result.unwrap().len(), 1);
430 }
431
432 #[test]
433 fn test_statistical_outlier_removal_with_outliers() {
434 let mut points = Vec::new();
436
437 for i in 0..10 {
439 for j in 0..10 {
440 for k in 0..10 {
441 points.push(Point3f::new(i as f32 * 0.1, j as f32 * 0.1, k as f32 * 0.1));
442 }
443 }
444 }
445
446 points.push(Point3f::new(10.0, 10.0, 10.0));
448 points.push(Point3f::new(-10.0, -10.0, -10.0));
449 points.push(Point3f::new(5.0, 5.0, 5.0));
450
451 let cloud = PointCloud::from_points(points);
452 let original_count = cloud.len();
453
454 let result = statistical_outlier_removal(&cloud, 5, 1.0);
455 assert!(result.is_ok());
456
457 let filtered = result.unwrap();
458 assert!(filtered.len() < original_count);
459 assert!(filtered.len() > 0);
460
461 let has_outlier_1 = filtered.points.iter().any(|p| {
463 (p.x - 10.0).abs() < 0.1 && (p.y - 10.0).abs() < 0.1 && (p.z - 10.0).abs() < 0.1
464 });
465 let has_outlier_2 = filtered.points.iter().any(|p| {
466 (p.x + 10.0).abs() < 0.1 && (p.y + 10.0).abs() < 0.1 && (p.z + 10.0).abs() < 0.1
467 });
468
469 assert!(!has_outlier_1);
470 assert!(!has_outlier_2);
471 }
472
473 #[test]
474 fn test_statistical_outlier_removal_no_outliers() {
475 let mut points = Vec::new();
477 for i in 0..5 {
478 for j in 0..5 {
479 for k in 0..5 {
480 points.push(Point3f::new(i as f32 * 0.1, j as f32 * 0.1, k as f32 * 0.1));
481 }
482 }
483 }
484
485 let cloud = PointCloud::from_points(points);
486 let original_count = cloud.len();
487
488 let result = statistical_outlier_removal(&cloud, 5, 1.0);
489 assert!(result.is_ok());
490
491 let filtered = result.unwrap();
492 assert!(filtered.len() > original_count * 8 / 10);
494 }
495
496 #[test]
497 fn test_statistical_outlier_removal_invalid_k() {
498 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
499 let result = statistical_outlier_removal(&cloud, 0, 1.0);
500 assert!(result.is_err());
501 }
502
503 #[test]
504 fn test_statistical_outlier_removal_invalid_std_dev() {
505 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
506 let result = statistical_outlier_removal(&cloud, 5, 0.0);
507 assert!(result.is_err());
508
509 let result = statistical_outlier_removal(&cloud, 5, -1.0);
510 assert!(result.is_err());
511 }
512
513 #[test]
514 fn test_statistical_outlier_removal_with_threshold() {
515 let points = vec![
517 Point3f::new(0.0, 0.0, 0.0),
518 Point3f::new(0.1, 0.0, 0.0),
519 Point3f::new(0.0, 0.1, 0.0),
520 Point3f::new(0.0, 0.0, 0.1),
521 Point3f::new(10.0, 10.0, 10.0), ];
523
524 let cloud = PointCloud::from_points(points);
525
526 let result = statistical_outlier_removal_with_threshold(&cloud, 3, 0.5);
528 assert!(result.is_ok());
529
530 let filtered = result.unwrap();
531 assert_eq!(filtered.len(), 4); let has_outlier = filtered.points.iter().any(|p| {
535 (p.x - 10.0).abs() < 0.1 && (p.y - 10.0).abs() < 0.1 && (p.z - 10.0).abs() < 0.1
536 });
537 assert!(!has_outlier);
538 }
539
540 #[test]
541 fn test_statistical_outlier_removal_with_threshold_invalid() {
542 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
543 let result = statistical_outlier_removal_with_threshold(&cloud, 0, 1.0);
544 assert!(result.is_err());
545
546 let result = statistical_outlier_removal_with_threshold(&cloud, 5, 0.0);
547 assert!(result.is_err());
548 }
549
550 #[test]
551 fn test_voxel_grid_filter_empty_cloud() {
552 let cloud = PointCloud::<Point3f>::new();
553 let result = voxel_grid_filter(&cloud, 0.1);
554 assert!(result.is_ok());
555 assert_eq!(result.unwrap().len(), 0);
556 }
557
558 #[test]
559 fn test_voxel_grid_filter_single_point() {
560 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
561 let result = voxel_grid_filter(&cloud, 0.1);
562 assert!(result.is_ok());
563 assert_eq!(result.unwrap().len(), 1);
564 }
565
566 #[test]
567 fn test_voxel_grid_filter_with_duplicates() {
568 let cloud = PointCloud::from_points(vec![
569 Point3f::new(0.0, 0.0, 0.0),
570 Point3f::new(0.0, 0.0, 0.0), Point3f::new(0.1, 0.0, 0.0),
572 Point3f::new(0.1, 0.0, 0.0), Point3f::new(0.0, 0.1, 0.0),
574 ]);
575
576 let result = voxel_grid_filter(&cloud, 0.05);
577 assert!(result.is_ok());
578 let filtered = result.unwrap();
579 assert_eq!(filtered.len(), 3); }
581
582 #[test]
583 fn test_voxel_grid_filter_invalid_voxel_size() {
584 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
585 let result = voxel_grid_filter(&cloud, 0.0);
586 assert!(result.is_err());
587
588 let result = voxel_grid_filter(&cloud, -1.0);
589 assert!(result.is_err());
590 }
591
592 #[test]
593 fn test_radius_outlier_removal_empty_cloud() {
594 let cloud = PointCloud::<Point3f>::new();
595 let result = radius_outlier_removal(&cloud, 0.5, 3);
596 assert!(result.is_ok());
597 assert_eq!(result.unwrap().len(), 0);
598 }
599
600 #[test]
601 fn test_radius_outlier_removal_single_point() {
602 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
603 let result = radius_outlier_removal(&cloud, 0.5, 1);
604 assert!(result.is_ok());
605 assert_eq!(result.unwrap().len(), 0);
607 }
608
609 #[test]
610 fn test_radius_outlier_removal_with_outliers() {
611 let mut points = Vec::new();
613
614 for i in 0..5 {
616 for j in 0..5 {
617 points.push(Point3f::new(i as f32 * 0.1, j as f32 * 0.1, 0.0));
618 }
619 }
620
621 points.push(Point3f::new(10.0, 10.0, 10.0));
623 points.push(Point3f::new(-10.0, -10.0, -10.0));
624
625 let cloud = PointCloud::from_points(points);
626 let original_count = cloud.len();
627
628 let result = radius_outlier_removal(&cloud, 0.5, 2);
629 assert!(result.is_ok());
630
631 let filtered = result.unwrap();
632 assert!(filtered.len() < original_count);
633 assert!(filtered.len() > 0);
634
635 let has_outlier_1 = filtered.points.iter().any(|p| {
637 (p.x - 10.0).abs() < 0.1 && (p.y - 10.0).abs() < 0.1 && (p.z - 10.0).abs() < 0.1
638 });
639 let has_outlier_2 = filtered.points.iter().any(|p| {
640 (p.x + 10.0).abs() < 0.1 && (p.y + 10.0).abs() < 0.1 && (p.z + 10.0).abs() < 0.1
641 });
642
643 assert!(!has_outlier_1);
644 assert!(!has_outlier_2);
645 }
646
647 #[test]
648 fn test_radius_outlier_removal_invalid_parameters() {
649 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
650
651 let result = radius_outlier_removal(&cloud, 0.0, 3);
652 assert!(result.is_err());
653
654 let result = radius_outlier_removal(&cloud, -1.0, 3);
655 assert!(result.is_err());
656
657 let result = radius_outlier_removal(&cloud, 0.5, 0);
658 assert!(result.is_err());
659 }
660
661 #[test]
662 fn test_radius_outlier_removal_different_parameters() {
663 let mut points = Vec::new();
664
665 for i in 0..3 {
667 for j in 0..3 {
668 points.push(Point3f::new(i as f32 * 0.1, j as f32 * 0.1, 0.0));
669 }
670 }
671
672 points.push(Point3f::new(1.0, 1.0, 1.0));
674
675 let cloud = PointCloud::from_points(points);
676
677 let result1 = radius_outlier_removal(&cloud, 0.2, 2).unwrap();
679 let result2 = radius_outlier_removal(&cloud, 0.5, 2).unwrap();
680
681 assert!(result2.len() >= result1.len());
683 }
684}