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 <= 0.0 {
47 return Err(threecrate_core::Error::InvalidData(
48 "voxel_size must be positive".to_string(),
49 ));
50 }
51
52 let min_x = cloud
54 .points
55 .iter()
56 .map(|p| p.x)
57 .min_by(|a, b| a.partial_cmp(b).unwrap())
58 .unwrap();
59 let min_y = cloud
60 .points
61 .iter()
62 .map(|p| p.y)
63 .min_by(|a, b| a.partial_cmp(b).unwrap())
64 .unwrap();
65 let min_z = cloud
66 .points
67 .iter()
68 .map(|p| p.z)
69 .min_by(|a, b| a.partial_cmp(b).unwrap())
70 .unwrap();
71 let max_x = cloud
72 .points
73 .iter()
74 .map(|p| p.x)
75 .max_by(|a, b| a.partial_cmp(b).unwrap())
76 .unwrap();
77 let max_y = cloud
78 .points
79 .iter()
80 .map(|p| p.y)
81 .max_by(|a, b| a.partial_cmp(b).unwrap())
82 .unwrap();
83 let max_z = cloud
84 .points
85 .iter()
86 .map(|p| p.z)
87 .max_by(|a, b| a.partial_cmp(b).unwrap())
88 .unwrap();
89
90 let _grid_dim_x = ((max_x - min_x) / voxel_size).ceil() as i32 + 1;
92 let _grid_dim_y = ((max_y - min_y) / voxel_size).ceil() as i32 + 1;
93 let _grid_dim_z = ((max_z - min_z) / voxel_size).ceil() as i32 + 1;
94
95 let get_voxel_coords = |point: &Point3f| -> (i32, i32, i32) {
97 let x = ((point.x - min_x) / voxel_size).floor() as i32;
98 let y = ((point.y - min_y) / voxel_size).floor() as i32;
99 let z = ((point.z - min_z) / voxel_size).floor() as i32;
100 (x, y, z)
101 };
102
103 let mut voxel_map: std::collections::HashMap<(i32, i32, i32), ([f64; 3], usize)> =
109 std::collections::HashMap::new();
110
111 for point in cloud.points.iter() {
112 let voxel_coords = get_voxel_coords(point);
113 let entry = voxel_map.entry(voxel_coords).or_insert(([0.0; 3], 0));
114 entry.0[0] += point.x as f64;
115 entry.0[1] += point.y as f64;
116 entry.0[2] += point.z as f64;
117 entry.1 += 1;
118 }
119
120 let filtered_points: Vec<Point3f> = voxel_map
121 .values()
122 .map(|(sum, count)| {
123 let inv = 1.0 / *count as f64;
124 Point3f::new(
125 (sum[0] * inv) as f32,
126 (sum[1] * inv) as f32,
127 (sum[2] * inv) as f32,
128 )
129 })
130 .collect();
131
132 Ok(PointCloud::from_points(filtered_points))
133}
134
135pub fn radius_outlier_removal(
168 cloud: &PointCloud<Point3f>,
169 radius: f32,
170 min_neighbors: usize,
171) -> Result<PointCloud<Point3f>> {
172 if cloud.is_empty() {
173 return Ok(PointCloud::new());
174 }
175
176 if radius <= 0.0 {
177 return Err(threecrate_core::Error::InvalidData(
178 "radius must be positive".to_string(),
179 ));
180 }
181
182 if min_neighbors == 0 {
183 return Err(threecrate_core::Error::InvalidData(
184 "min_neighbors must be greater than 0".to_string(),
185 ));
186 }
187
188 let nn_search = KdTree::new(&cloud.points)?;
191
192 let neighbor_counts: Vec<usize> = cloud
194 .points
195 .par_iter()
196 .map(|point| {
197 let neighbors = nn_search.find_radius_neighbors(point, radius);
198 neighbors.len().saturating_sub(1)
200 })
201 .collect();
202
203 let filtered_points: Vec<Point3f> = cloud
205 .points
206 .iter()
207 .zip(neighbor_counts.iter())
208 .filter(|(_, &count)| count >= min_neighbors)
209 .map(|(point, _)| *point)
210 .collect();
211
212 Ok(PointCloud::from_points(filtered_points))
213}
214
215pub fn statistical_outlier_removal(
250 cloud: &PointCloud<Point3f>,
251 k_neighbors: usize,
252 std_dev_multiplier: f32,
253) -> Result<PointCloud<Point3f>> {
254 if cloud.is_empty() {
255 return Ok(PointCloud::new());
256 }
257
258 if k_neighbors == 0 {
259 return Err(threecrate_core::Error::InvalidData(
260 "k_neighbors must be greater than 0".to_string(),
261 ));
262 }
263
264 if std_dev_multiplier <= 0.0 {
265 return Err(threecrate_core::Error::InvalidData(
266 "std_dev_multiplier must be positive".to_string(),
267 ));
268 }
269
270 let nn_search = KdTree::new(&cloud.points)?;
273
274 let mean_distances: Vec<f32> = cloud
276 .points
277 .par_iter()
278 .map(|point| {
279 let neighbors = nn_search.find_k_nearest(point, k_neighbors + 1); if neighbors.is_empty() {
281 return 0.0;
282 }
283
284 let distances: Vec<f32> = neighbors
286 .iter()
287 .filter(|(idx, _)| cloud.points[*idx] != *point) .map(|(_, distance)| *distance)
289 .collect();
290
291 if distances.is_empty() {
292 return 0.0;
293 }
294
295 distances.iter().sum::<f32>() / distances.len() as f32
296 })
297 .collect();
298
299 let global_mean = mean_distances.iter().sum::<f32>() / mean_distances.len() as f32;
301
302 let variance = mean_distances
303 .iter()
304 .map(|&d| (d - global_mean).powi(2))
305 .sum::<f32>()
306 / mean_distances.len() as f32;
307
308 let global_std_dev = variance.sqrt();
309 let threshold = global_mean + std_dev_multiplier * global_std_dev;
310
311 let filtered_points: Vec<Point3f> = cloud
313 .points
314 .iter()
315 .zip(mean_distances.iter())
316 .filter(|(_, &mean_dist)| mean_dist <= threshold)
317 .map(|(point, _)| *point)
318 .collect();
319
320 Ok(PointCloud::from_points(filtered_points))
321}
322
323pub fn statistical_outlier_removal_with_threshold(
336 cloud: &PointCloud<Point3f>,
337 k_neighbors: usize,
338 threshold: f32,
339) -> Result<PointCloud<Point3f>> {
340 if cloud.is_empty() {
341 return Ok(PointCloud::new());
342 }
343
344 if k_neighbors == 0 {
345 return Err(threecrate_core::Error::InvalidData(
346 "k_neighbors must be greater than 0".to_string(),
347 ));
348 }
349
350 if threshold <= 0.0 {
351 return Err(threecrate_core::Error::InvalidData(
352 "threshold must be positive".to_string(),
353 ));
354 }
355
356 let nn_search = KdTree::new(&cloud.points)?;
359
360 let mean_distances: Vec<f32> = cloud
362 .points
363 .par_iter()
364 .map(|point| {
365 let neighbors = nn_search.find_k_nearest(point, k_neighbors + 1); if neighbors.is_empty() {
367 return 0.0;
368 }
369
370 let distances: Vec<f32> = neighbors
372 .iter()
373 .filter(|(idx, _)| cloud.points[*idx] != *point) .map(|(_, distance)| *distance)
375 .collect();
376
377 if distances.is_empty() {
378 return 0.0;
379 }
380
381 distances.iter().sum::<f32>() / distances.len() as f32
382 })
383 .collect();
384
385 let filtered_points: Vec<Point3f> = cloud
387 .points
388 .iter()
389 .zip(mean_distances.iter())
390 .filter(|(_, &mean_dist)| mean_dist <= threshold)
391 .map(|(point, _)| *point)
392 .collect();
393
394 Ok(PointCloud::from_points(filtered_points))
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400 use threecrate_core::Point3f;
401
402 #[test]
403 fn test_statistical_outlier_removal_empty_cloud() {
404 let cloud = PointCloud::<Point3f>::new();
405 let result = statistical_outlier_removal(&cloud, 5, 1.0);
406 assert!(result.is_ok());
407 assert_eq!(result.unwrap().len(), 0);
408 }
409
410 #[test]
411 fn test_statistical_outlier_removal_single_point() {
412 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
413 let result = statistical_outlier_removal(&cloud, 1, 1.0);
414 assert!(result.is_ok());
415 assert_eq!(result.unwrap().len(), 1);
416 }
417
418 #[test]
419 fn test_statistical_outlier_removal_with_outliers() {
420 let mut points = Vec::new();
422
423 for i in 0..10 {
425 for j in 0..10 {
426 for k in 0..10 {
427 points.push(Point3f::new(i as f32 * 0.1, j as f32 * 0.1, k as f32 * 0.1));
428 }
429 }
430 }
431
432 points.push(Point3f::new(10.0, 10.0, 10.0));
434 points.push(Point3f::new(-10.0, -10.0, -10.0));
435 points.push(Point3f::new(5.0, 5.0, 5.0));
436
437 let cloud = PointCloud::from_points(points);
438 let original_count = cloud.len();
439
440 let result = statistical_outlier_removal(&cloud, 5, 1.0);
441 assert!(result.is_ok());
442
443 let filtered = result.unwrap();
444 assert!(filtered.len() < original_count);
445 assert!(filtered.len() > 0);
446
447 let has_outlier_1 = filtered.points.iter().any(|p| {
449 (p.x - 10.0).abs() < 0.1 && (p.y - 10.0).abs() < 0.1 && (p.z - 10.0).abs() < 0.1
450 });
451 let has_outlier_2 = filtered.points.iter().any(|p| {
452 (p.x + 10.0).abs() < 0.1 && (p.y + 10.0).abs() < 0.1 && (p.z + 10.0).abs() < 0.1
453 });
454
455 assert!(!has_outlier_1);
456 assert!(!has_outlier_2);
457 }
458
459 #[test]
460 fn test_statistical_outlier_removal_no_outliers() {
461 let mut points = Vec::new();
463 for i in 0..5 {
464 for j in 0..5 {
465 for k in 0..5 {
466 points.push(Point3f::new(i as f32 * 0.1, j as f32 * 0.1, k as f32 * 0.1));
467 }
468 }
469 }
470
471 let cloud = PointCloud::from_points(points);
472 let original_count = cloud.len();
473
474 let result = statistical_outlier_removal(&cloud, 5, 1.0);
475 assert!(result.is_ok());
476
477 let filtered = result.unwrap();
478 assert!(filtered.len() > original_count * 8 / 10);
480 }
481
482 #[test]
483 fn test_statistical_outlier_removal_invalid_k() {
484 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
485 let result = statistical_outlier_removal(&cloud, 0, 1.0);
486 assert!(result.is_err());
487 }
488
489 #[test]
490 fn test_statistical_outlier_removal_invalid_std_dev() {
491 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
492 let result = statistical_outlier_removal(&cloud, 5, 0.0);
493 assert!(result.is_err());
494
495 let result = statistical_outlier_removal(&cloud, 5, -1.0);
496 assert!(result.is_err());
497 }
498
499 #[test]
500 fn test_statistical_outlier_removal_with_threshold() {
501 let points = vec![
503 Point3f::new(0.0, 0.0, 0.0),
504 Point3f::new(0.1, 0.0, 0.0),
505 Point3f::new(0.0, 0.1, 0.0),
506 Point3f::new(0.0, 0.0, 0.1),
507 Point3f::new(10.0, 10.0, 10.0), ];
509
510 let cloud = PointCloud::from_points(points);
511
512 let result = statistical_outlier_removal_with_threshold(&cloud, 3, 0.5);
514 assert!(result.is_ok());
515
516 let filtered = result.unwrap();
517 assert_eq!(filtered.len(), 4); let has_outlier = filtered.points.iter().any(|p| {
521 (p.x - 10.0).abs() < 0.1 && (p.y - 10.0).abs() < 0.1 && (p.z - 10.0).abs() < 0.1
522 });
523 assert!(!has_outlier);
524 }
525
526 #[test]
527 fn test_statistical_outlier_removal_with_threshold_invalid() {
528 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
529 let result = statistical_outlier_removal_with_threshold(&cloud, 0, 1.0);
530 assert!(result.is_err());
531
532 let result = statistical_outlier_removal_with_threshold(&cloud, 5, 0.0);
533 assert!(result.is_err());
534 }
535
536 #[test]
537 fn test_voxel_grid_filter_empty_cloud() {
538 let cloud = PointCloud::<Point3f>::new();
539 let result = voxel_grid_filter(&cloud, 0.1);
540 assert!(result.is_ok());
541 assert_eq!(result.unwrap().len(), 0);
542 }
543
544 #[test]
545 fn test_voxel_grid_filter_single_point() {
546 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
547 let result = voxel_grid_filter(&cloud, 0.1);
548 assert!(result.is_ok());
549 assert_eq!(result.unwrap().len(), 1);
550 }
551
552 #[test]
553 fn test_voxel_grid_filter_with_duplicates() {
554 let cloud = PointCloud::from_points(vec![
555 Point3f::new(0.0, 0.0, 0.0),
556 Point3f::new(0.0, 0.0, 0.0), Point3f::new(0.1, 0.0, 0.0),
558 Point3f::new(0.1, 0.0, 0.0), Point3f::new(0.0, 0.1, 0.0),
560 ]);
561
562 let result = voxel_grid_filter(&cloud, 0.05);
563 assert!(result.is_ok());
564 let filtered = result.unwrap();
565 assert_eq!(filtered.len(), 3); }
567
568 #[test]
569 fn test_voxel_grid_filter_invalid_voxel_size() {
570 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
571 let result = voxel_grid_filter(&cloud, 0.0);
572 assert!(result.is_err());
573
574 let result = voxel_grid_filter(&cloud, -1.0);
575 assert!(result.is_err());
576 }
577
578 #[test]
579 fn test_radius_outlier_removal_empty_cloud() {
580 let cloud = PointCloud::<Point3f>::new();
581 let result = radius_outlier_removal(&cloud, 0.5, 3);
582 assert!(result.is_ok());
583 assert_eq!(result.unwrap().len(), 0);
584 }
585
586 #[test]
587 fn test_radius_outlier_removal_single_point() {
588 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
589 let result = radius_outlier_removal(&cloud, 0.5, 1);
590 assert!(result.is_ok());
591 assert_eq!(result.unwrap().len(), 0);
593 }
594
595 #[test]
596 fn test_radius_outlier_removal_with_outliers() {
597 let mut points = Vec::new();
599
600 for i in 0..5 {
602 for j in 0..5 {
603 points.push(Point3f::new(i as f32 * 0.1, j as f32 * 0.1, 0.0));
604 }
605 }
606
607 points.push(Point3f::new(10.0, 10.0, 10.0));
609 points.push(Point3f::new(-10.0, -10.0, -10.0));
610
611 let cloud = PointCloud::from_points(points);
612 let original_count = cloud.len();
613
614 let result = radius_outlier_removal(&cloud, 0.5, 2);
615 assert!(result.is_ok());
616
617 let filtered = result.unwrap();
618 assert!(filtered.len() < original_count);
619 assert!(filtered.len() > 0);
620
621 let has_outlier_1 = filtered.points.iter().any(|p| {
623 (p.x - 10.0).abs() < 0.1 && (p.y - 10.0).abs() < 0.1 && (p.z - 10.0).abs() < 0.1
624 });
625 let has_outlier_2 = filtered.points.iter().any(|p| {
626 (p.x + 10.0).abs() < 0.1 && (p.y + 10.0).abs() < 0.1 && (p.z + 10.0).abs() < 0.1
627 });
628
629 assert!(!has_outlier_1);
630 assert!(!has_outlier_2);
631 }
632
633 #[test]
634 fn test_radius_outlier_removal_invalid_parameters() {
635 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
636
637 let result = radius_outlier_removal(&cloud, 0.0, 3);
638 assert!(result.is_err());
639
640 let result = radius_outlier_removal(&cloud, -1.0, 3);
641 assert!(result.is_err());
642
643 let result = radius_outlier_removal(&cloud, 0.5, 0);
644 assert!(result.is_err());
645 }
646
647 #[test]
648 fn test_radius_outlier_removal_different_parameters() {
649 let mut points = Vec::new();
650
651 for i in 0..3 {
653 for j in 0..3 {
654 points.push(Point3f::new(i as f32 * 0.1, j as f32 * 0.1, 0.0));
655 }
656 }
657
658 points.push(Point3f::new(1.0, 1.0, 1.0));
660
661 let cloud = PointCloud::from_points(points);
662
663 let result1 = radius_outlier_removal(&cloud, 0.2, 2).unwrap();
665 let result2 = radius_outlier_removal(&cloud, 0.5, 2).unwrap();
666
667 assert!(result2.len() >= result1.len());
669 }
670}