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.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    // NaN or infinite points have no voxel; skip them (one NaN used to panic
53    // here, and one inf collapsed the whole grid).
54    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()); // no finite points
69    }
70    // Voxel indices are i32; a tiny voxel_size would overflow them and merge
71    // far-apart points into one voxel.
72    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    // Function to get voxel coordinates for a point
80    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    // Accumulate the centroid of every voxel in a single pass. We keep the
88    // running sum in f64 so that large-coordinate clouds (e.g. LiDAR scans far
89    // from the origin) do not lose precision. Returning the centroid — rather
90    // than an arbitrary first point — matches the semantics of Open3D's
91    // `voxel_down_sample` and PCL's `VoxelGrid`.
92    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
119/// Radius outlier removal filter
120///
121/// This algorithm removes points that have fewer than `min_neighbors` neighbors
122/// within a specified radius. This is useful for removing isolated noise points
123/// that are far from the main point cloud.
124///
125/// # Arguments
126/// * `cloud` - Input point cloud
127/// * `radius` - Search radius for neighbor counting
128/// * `min_neighbors` - Minimum number of neighbors required to keep a point
129///
130/// # Returns
131/// * `Result<PointCloud<Point3f>>` - Filtered point cloud with outliers removed
132///
133/// # Example
134/// ```rust
135/// use threecrate_core::{PointCloud, Point3f};
136/// use threecrate_algorithms::radius_outlier_removal;
137///
138/// fn main() -> threecrate_core::Result<()> {
139///     let cloud = PointCloud::from_points(vec![
140///         Point3f::new(0.0, 0.0, 0.0),
141///         Point3f::new(0.1, 0.0, 0.0),
142///         Point3f::new(0.0, 0.1, 0.0),
143///         Point3f::new(10.0, 10.0, 10.0), // outlier
144///     ]);
145///
146///     let filtered = radius_outlier_removal(&cloud, 0.5, 2)?;
147///     println!("Filtered cloud has {} points", filtered.len());
148///     Ok(())
149/// }
150/// ```
151pub 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    // Create nearest neighbor search structure (KD-tree: O(n log n) build,
173    // ~O(log n) per query instead of the O(n) brute-force scan).
174    let nn_search = KdTree::new(&cloud.points)?;
175
176    // Count neighbors within radius for each point
177    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            // Subtract 1 to exclude the point itself
183            neighbors.len().saturating_sub(1)
184        })
185        .collect();
186
187    // Filter out points with insufficient neighbors
188    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
199/// Statistical outlier removal filter
200///
201/// This algorithm removes points that are statistical outliers based on the distance
202/// to their k-nearest neighbors. For each point, it computes the mean distance to
203/// its k nearest neighbors. Points with mean distances that deviate more than
204/// `std_dev_multiplier` standard deviations from the global mean are considered
205/// outliers and removed.
206///
207/// # Arguments
208/// * `cloud` - Input point cloud
209/// * `k_neighbors` - Number of nearest neighbors to consider for each point
210/// * `std_dev_multiplier` - Standard deviation multiplier for outlier detection
211///
212/// # Returns
213/// * `Result<PointCloud<Point3f>>` - Filtered point cloud with outliers removed
214///
215/// # Example
216/// ```rust
217/// use threecrate_core::{PointCloud, Point3f};
218/// use threecrate_algorithms::statistical_outlier_removal;
219///
220/// fn main() -> threecrate_core::Result<()> {
221///     let cloud = PointCloud::from_points(vec![
222///         Point3f::new(0.0, 0.0, 0.0),
223///         Point3f::new(1.0, 0.0, 0.0),
224///         Point3f::new(0.0, 1.0, 0.0),
225///         Point3f::new(10.0, 10.0, 10.0), // outlier
226///     ]);
227///
228///     let filtered = statistical_outlier_removal(&cloud, 3, 1.0)?;
229///     println!("Filtered cloud has {} points", filtered.len());
230///     Ok(())
231/// }
232/// ```
233pub 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    // Create nearest neighbor search structure (KD-tree: O(n log n) build,
255    // ~O(log n) per query instead of the O(n) brute-force scan).
256    let nn_search = KdTree::new(&cloud.points)?;
257
258    // Compute mean distances for all points
259    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); // +1 to exclude self
264            if neighbors.is_empty() {
265                return 0.0;
266            }
267
268            // Calculate mean distance to neighbors (skip first neighbor if it's the point itself)
269            let distances: Vec<f32> = neighbors
270                .iter()
271                .filter(|(idx, _)| cloud.points[*idx] != *point) // Skip self
272                .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    // Compute global statistics
284    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    // Filter out outliers
296    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
307/// Statistical outlier removal with custom threshold
308///
309/// This variant allows you to specify a custom threshold instead of using
310/// the automatic standard deviation calculation.
311///
312/// # Arguments
313/// * `cloud` - Input point cloud
314/// * `k_neighbors` - Number of nearest neighbors to consider for each point
315/// * `threshold` - Custom threshold for outlier detection
316///
317/// # Returns
318/// * `Result<PointCloud<Point3f>>` - Filtered point cloud with outliers removed
319pub 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    // Create nearest neighbor search structure (KD-tree: O(n log n) build,
341    // ~O(log n) per query instead of the O(n) brute-force scan).
342    let nn_search = KdTree::new(&cloud.points)?;
343
344    // Compute mean distances for all points
345    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); // +1 to exclude self
350            if neighbors.is_empty() {
351                return 0.0;
352            }
353
354            // Calculate mean distance to neighbors (skip first neighbor if it's the point itself)
355            let distances: Vec<f32> = neighbors
356                .iter()
357                .filter(|(idx, _)| cloud.points[*idx] != *point) // Skip self
358                .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    // Filter out outliers using custom threshold
370    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        // Create a point cloud with some outliers
435        let mut points = Vec::new();
436
437        // Main cluster
438        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        // Add some outliers
447        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        // Check that outliers were removed
462        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        // Create a uniform point cloud without outliers
476        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        // Should keep most points since there are no real outliers
493        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        // Create a point cloud with known outliers
516        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), // outlier
522        ];
523
524        let cloud = PointCloud::from_points(points);
525
526        // Use a very low threshold to remove outliers
527        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); // Should remove the outlier
532
533        // Check that the outlier was removed
534        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), // duplicate
571            Point3f::new(0.1, 0.0, 0.0),
572            Point3f::new(0.1, 0.0, 0.0), // duplicate
573            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); // Should remove duplicates
580    }
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        // Should remove the single point since it has no neighbors
606        assert_eq!(result.unwrap().len(), 0);
607    }
608
609    #[test]
610    fn test_radius_outlier_removal_with_outliers() {
611        // Create a point cloud with some outliers
612        let mut points = Vec::new();
613
614        // Main cluster
615        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        // Add some outliers
622        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        // Check that outliers were removed
636        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        // Create a cluster
666        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        // Add some outliers
673        points.push(Point3f::new(1.0, 1.0, 1.0));
674
675        let cloud = PointCloud::from_points(points);
676
677        // Test different radius values
678        let result1 = radius_outlier_removal(&cloud, 0.2, 2).unwrap();
679        let result2 = radius_outlier_removal(&cloud, 0.5, 2).unwrap();
680
681        // Larger radius should keep more points
682        assert!(result2.len() >= result1.len());
683    }
684}