Skip to main content

threecrate_algorithms/
filtering.rs

1//! Filtering algorithms
2
3use crate::nearest_neighbor::KdTree;
4use rayon::prelude::*;
5use threecrate_core::{NearestNeighborSearch, Point3f, PointCloud, Result};
6
7/// Voxel grid filtering
8///
9/// This algorithm reduces the density of a point cloud by grouping points into voxels
10/// and keeping only one representative point per voxel. This is useful for downsampling
11/// large point clouds while preserving their overall structure.
12///
13/// # Arguments
14/// * `cloud` - Input point cloud
15/// * `voxel_size` - Size of each voxel cube
16///
17/// # Returns
18/// * `Result<PointCloud<Point3f>>` - Downsampled point cloud
19///
20/// # Example
21/// ```rust
22/// use threecrate_core::{PointCloud, Point3f};
23/// use threecrate_algorithms::voxel_grid_filter;
24///
25/// fn main() -> threecrate_core::Result<()> {
26///     let cloud = PointCloud::from_points(vec![
27///         Point3f::new(0.0, 0.0, 0.0),
28///         Point3f::new(0.1, 0.0, 0.0),
29///         Point3f::new(0.0, 0.1, 0.0),
30///         Point3f::new(0.0, 0.0, 0.1),
31///     ]);
32///
33///     let filtered = voxel_grid_filter(&cloud, 0.2)?;
34///     println!("Filtered cloud has {} points", filtered.len());
35///     Ok(())
36/// }
37/// ```
38pub 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    // Compute bounding box
53    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    // Compute grid dimensions (for reference, not used in hash-based approach)
91    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    // Function to get voxel coordinates for a point
96    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    // Accumulate the centroid of every voxel in a single pass. We keep the
104    // running sum in f64 so that large-coordinate clouds (e.g. LiDAR scans far
105    // from the origin) do not lose precision. Returning the centroid — rather
106    // than an arbitrary first point — matches the semantics of Open3D's
107    // `voxel_down_sample` and PCL's `VoxelGrid`.
108    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
135/// Radius outlier removal filter
136///
137/// This algorithm removes points that have fewer than `min_neighbors` neighbors
138/// within a specified radius. This is useful for removing isolated noise points
139/// that are far from the main point cloud.
140///
141/// # Arguments
142/// * `cloud` - Input point cloud
143/// * `radius` - Search radius for neighbor counting
144/// * `min_neighbors` - Minimum number of neighbors required to keep a point
145///
146/// # Returns
147/// * `Result<PointCloud<Point3f>>` - Filtered point cloud with outliers removed
148///
149/// # Example
150/// ```rust
151/// use threecrate_core::{PointCloud, Point3f};
152/// use threecrate_algorithms::radius_outlier_removal;
153///
154/// fn main() -> threecrate_core::Result<()> {
155///     let cloud = PointCloud::from_points(vec![
156///         Point3f::new(0.0, 0.0, 0.0),
157///         Point3f::new(0.1, 0.0, 0.0),
158///         Point3f::new(0.0, 0.1, 0.0),
159///         Point3f::new(10.0, 10.0, 10.0), // outlier
160///     ]);
161///
162///     let filtered = radius_outlier_removal(&cloud, 0.5, 2)?;
163///     println!("Filtered cloud has {} points", filtered.len());
164///     Ok(())
165/// }
166/// ```
167pub 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    // Create nearest neighbor search structure (KD-tree: O(n log n) build,
189    // ~O(log n) per query instead of the O(n) brute-force scan).
190    let nn_search = KdTree::new(&cloud.points)?;
191
192    // Count neighbors within radius for each point
193    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            // Subtract 1 to exclude the point itself
199            neighbors.len().saturating_sub(1)
200        })
201        .collect();
202
203    // Filter out points with insufficient neighbors
204    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
215/// Statistical outlier removal filter
216///
217/// This algorithm removes points that are statistical outliers based on the distance
218/// to their k-nearest neighbors. For each point, it computes the mean distance to
219/// its k nearest neighbors. Points with mean distances that deviate more than
220/// `std_dev_multiplier` standard deviations from the global mean are considered
221/// outliers and removed.
222///
223/// # Arguments
224/// * `cloud` - Input point cloud
225/// * `k_neighbors` - Number of nearest neighbors to consider for each point
226/// * `std_dev_multiplier` - Standard deviation multiplier for outlier detection
227///
228/// # Returns
229/// * `Result<PointCloud<Point3f>>` - Filtered point cloud with outliers removed
230///
231/// # Example
232/// ```rust
233/// use threecrate_core::{PointCloud, Point3f};
234/// use threecrate_algorithms::statistical_outlier_removal;
235///
236/// fn main() -> threecrate_core::Result<()> {
237///     let cloud = PointCloud::from_points(vec![
238///         Point3f::new(0.0, 0.0, 0.0),
239///         Point3f::new(1.0, 0.0, 0.0),
240///         Point3f::new(0.0, 1.0, 0.0),
241///         Point3f::new(10.0, 10.0, 10.0), // outlier
242///     ]);
243///
244///     let filtered = statistical_outlier_removal(&cloud, 3, 1.0)?;
245///     println!("Filtered cloud has {} points", filtered.len());
246///     Ok(())
247/// }
248/// ```
249pub 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    // Create nearest neighbor search structure (KD-tree: O(n log n) build,
271    // ~O(log n) per query instead of the O(n) brute-force scan).
272    let nn_search = KdTree::new(&cloud.points)?;
273
274    // Compute mean distances for all points
275    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); // +1 to exclude self
280            if neighbors.is_empty() {
281                return 0.0;
282            }
283
284            // Calculate mean distance to neighbors (skip first neighbor if it's the point itself)
285            let distances: Vec<f32> = neighbors
286                .iter()
287                .filter(|(idx, _)| cloud.points[*idx] != *point) // Skip self
288                .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    // Compute global statistics
300    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    // Filter out outliers
312    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
323/// Statistical outlier removal with custom threshold
324///
325/// This variant allows you to specify a custom threshold instead of using
326/// the automatic standard deviation calculation.
327///
328/// # Arguments
329/// * `cloud` - Input point cloud
330/// * `k_neighbors` - Number of nearest neighbors to consider for each point
331/// * `threshold` - Custom threshold for outlier detection
332///
333/// # Returns
334/// * `Result<PointCloud<Point3f>>` - Filtered point cloud with outliers removed
335pub 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    // Create nearest neighbor search structure (KD-tree: O(n log n) build,
357    // ~O(log n) per query instead of the O(n) brute-force scan).
358    let nn_search = KdTree::new(&cloud.points)?;
359
360    // Compute mean distances for all points
361    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); // +1 to exclude self
366            if neighbors.is_empty() {
367                return 0.0;
368            }
369
370            // Calculate mean distance to neighbors (skip first neighbor if it's the point itself)
371            let distances: Vec<f32> = neighbors
372                .iter()
373                .filter(|(idx, _)| cloud.points[*idx] != *point) // Skip self
374                .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    // Filter out outliers using custom threshold
386    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        // Create a point cloud with some outliers
421        let mut points = Vec::new();
422
423        // Main cluster
424        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        // Add some outliers
433        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        // Check that outliers were removed
448        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        // Create a uniform point cloud without outliers
462        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        // Should keep most points since there are no real outliers
479        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        // Create a point cloud with known outliers
502        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), // outlier
508        ];
509
510        let cloud = PointCloud::from_points(points);
511
512        // Use a very low threshold to remove outliers
513        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); // Should remove the outlier
518
519        // Check that the outlier was removed
520        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), // duplicate
557            Point3f::new(0.1, 0.0, 0.0),
558            Point3f::new(0.1, 0.0, 0.0), // duplicate
559            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); // Should remove duplicates
566    }
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        // Should remove the single point since it has no neighbors
592        assert_eq!(result.unwrap().len(), 0);
593    }
594
595    #[test]
596    fn test_radius_outlier_removal_with_outliers() {
597        // Create a point cloud with some outliers
598        let mut points = Vec::new();
599
600        // Main cluster
601        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        // Add some outliers
608        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        // Check that outliers were removed
622        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        // Create a cluster
652        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        // Add some outliers
659        points.push(Point3f::new(1.0, 1.0, 1.0));
660
661        let cloud = PointCloud::from_points(points);
662
663        // Test different radius values
664        let result1 = radius_outlier_removal(&cloud, 0.2, 2).unwrap();
665        let result2 = radius_outlier_removal(&cloud, 0.5, 2).unwrap();
666
667        // Larger radius should keep more points
668        assert!(result2.len() >= result1.len());
669    }
670}