use threecrate_core::{PointCloud, Result, Point3f, NearestNeighborSearch};
use crate::nearest_neighbor::BruteForceSearch;
use rayon::prelude::*;
pub fn voxel_grid_filter(cloud: &PointCloud<Point3f>, voxel_size: f32) -> Result<PointCloud<Point3f>> {
if cloud.is_empty() {
return Ok(PointCloud::new());
}
if voxel_size <= 0.0 {
return Err(threecrate_core::Error::InvalidData(
"voxel_size must be positive".to_string()
));
}
let min_x = cloud.points.iter().map(|p| p.x).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
let min_y = cloud.points.iter().map(|p| p.y).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
let min_z = cloud.points.iter().map(|p| p.z).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
let max_x = cloud.points.iter().map(|p| p.x).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
let max_y = cloud.points.iter().map(|p| p.y).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
let max_z = cloud.points.iter().map(|p| p.z).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
let _grid_dim_x = ((max_x - min_x) / voxel_size).ceil() as i32 + 1;
let _grid_dim_y = ((max_y - min_y) / voxel_size).ceil() as i32 + 1;
let _grid_dim_z = ((max_z - min_z) / voxel_size).ceil() as i32 + 1;
let get_voxel_coords = |point: &Point3f| -> (i32, i32, i32) {
let x = ((point.x - min_x) / voxel_size).floor() as i32;
let y = ((point.y - min_y) / voxel_size).floor() as i32;
let z = ((point.z - min_z) / voxel_size).floor() as i32;
(x, y, z)
};
let mut voxel_map = std::collections::HashMap::new();
for (idx, point) in cloud.points.iter().enumerate() {
let voxel_coords = get_voxel_coords(point);
voxel_map.entry(voxel_coords).or_insert_with(Vec::new).push(idx);
}
let filtered_points: Vec<Point3f> = voxel_map
.values()
.map(|indices| cloud.points[indices[0]])
.collect();
Ok(PointCloud::from_points(filtered_points))
}
pub fn radius_outlier_removal(
cloud: &PointCloud<Point3f>,
radius: f32,
min_neighbors: usize,
) -> Result<PointCloud<Point3f>> {
if cloud.is_empty() {
return Ok(PointCloud::new());
}
if radius <= 0.0 {
return Err(threecrate_core::Error::InvalidData(
"radius must be positive".to_string()
));
}
if min_neighbors == 0 {
return Err(threecrate_core::Error::InvalidData(
"min_neighbors must be greater than 0".to_string()
));
}
let nn_search = BruteForceSearch::new(&cloud.points);
let neighbor_counts: Vec<usize> = cloud.points
.par_iter()
.map(|point| {
let neighbors = nn_search.find_radius_neighbors(point, radius);
neighbors.len().saturating_sub(1)
})
.collect();
let filtered_points: Vec<Point3f> = cloud.points
.iter()
.zip(neighbor_counts.iter())
.filter(|(_, &count)| count >= min_neighbors)
.map(|(point, _)| *point)
.collect();
Ok(PointCloud::from_points(filtered_points))
}
pub fn statistical_outlier_removal(
cloud: &PointCloud<Point3f>,
k_neighbors: usize,
std_dev_multiplier: f32,
) -> Result<PointCloud<Point3f>> {
if cloud.is_empty() {
return Ok(PointCloud::new());
}
if k_neighbors == 0 {
return Err(threecrate_core::Error::InvalidData(
"k_neighbors must be greater than 0".to_string()
));
}
if std_dev_multiplier <= 0.0 {
return Err(threecrate_core::Error::InvalidData(
"std_dev_multiplier must be positive".to_string()
));
}
let nn_search = BruteForceSearch::new(&cloud.points);
let mean_distances: Vec<f32> = cloud.points
.par_iter()
.map(|point| {
let neighbors = nn_search.find_k_nearest(point, k_neighbors + 1); if neighbors.is_empty() {
return 0.0;
}
let distances: Vec<f32> = neighbors
.iter()
.filter(|(idx, _)| cloud.points[*idx] != *point) .map(|(_, distance)| *distance)
.collect();
if distances.is_empty() {
return 0.0;
}
distances.iter().sum::<f32>() / distances.len() as f32
})
.collect();
let global_mean = mean_distances.iter().sum::<f32>() / mean_distances.len() as f32;
let variance = mean_distances
.iter()
.map(|&d| (d - global_mean).powi(2))
.sum::<f32>() / mean_distances.len() as f32;
let global_std_dev = variance.sqrt();
let threshold = global_mean + std_dev_multiplier * global_std_dev;
let filtered_points: Vec<Point3f> = cloud.points
.iter()
.zip(mean_distances.iter())
.filter(|(_, &mean_dist)| mean_dist <= threshold)
.map(|(point, _)| *point)
.collect();
Ok(PointCloud::from_points(filtered_points))
}
pub fn statistical_outlier_removal_with_threshold(
cloud: &PointCloud<Point3f>,
k_neighbors: usize,
threshold: f32,
) -> Result<PointCloud<Point3f>> {
if cloud.is_empty() {
return Ok(PointCloud::new());
}
if k_neighbors == 0 {
return Err(threecrate_core::Error::InvalidData(
"k_neighbors must be greater than 0".to_string()
));
}
if threshold <= 0.0 {
return Err(threecrate_core::Error::InvalidData(
"threshold must be positive".to_string()
));
}
let nn_search = BruteForceSearch::new(&cloud.points);
let mean_distances: Vec<f32> = cloud.points
.par_iter()
.map(|point| {
let neighbors = nn_search.find_k_nearest(point, k_neighbors + 1); if neighbors.is_empty() {
return 0.0;
}
let distances: Vec<f32> = neighbors
.iter()
.filter(|(idx, _)| cloud.points[*idx] != *point) .map(|(_, distance)| *distance)
.collect();
if distances.is_empty() {
return 0.0;
}
distances.iter().sum::<f32>() / distances.len() as f32
})
.collect();
let filtered_points: Vec<Point3f> = cloud.points
.iter()
.zip(mean_distances.iter())
.filter(|(_, &mean_dist)| mean_dist <= threshold)
.map(|(point, _)| *point)
.collect();
Ok(PointCloud::from_points(filtered_points))
}
#[cfg(test)]
mod tests {
use super::*;
use threecrate_core::Point3f;
#[test]
fn test_statistical_outlier_removal_empty_cloud() {
let cloud = PointCloud::<Point3f>::new();
let result = statistical_outlier_removal(&cloud, 5, 1.0);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 0);
}
#[test]
fn test_statistical_outlier_removal_single_point() {
let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
let result = statistical_outlier_removal(&cloud, 1, 1.0);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 1);
}
#[test]
fn test_statistical_outlier_removal_with_outliers() {
let mut points = Vec::new();
for i in 0..10 {
for j in 0..10 {
for k in 0..10 {
points.push(Point3f::new(
i as f32 * 0.1,
j as f32 * 0.1,
k as f32 * 0.1,
));
}
}
}
points.push(Point3f::new(10.0, 10.0, 10.0));
points.push(Point3f::new(-10.0, -10.0, -10.0));
points.push(Point3f::new(5.0, 5.0, 5.0));
let cloud = PointCloud::from_points(points);
let original_count = cloud.len();
let result = statistical_outlier_removal(&cloud, 5, 1.0);
assert!(result.is_ok());
let filtered = result.unwrap();
assert!(filtered.len() < original_count);
assert!(filtered.len() > 0);
let has_outlier_1 = filtered.points.iter().any(|p|
(p.x - 10.0).abs() < 0.1 && (p.y - 10.0).abs() < 0.1 && (p.z - 10.0).abs() < 0.1
);
let has_outlier_2 = filtered.points.iter().any(|p|
(p.x + 10.0).abs() < 0.1 && (p.y + 10.0).abs() < 0.1 && (p.z + 10.0).abs() < 0.1
);
assert!(!has_outlier_1);
assert!(!has_outlier_2);
}
#[test]
fn test_statistical_outlier_removal_no_outliers() {
let mut points = Vec::new();
for i in 0..5 {
for j in 0..5 {
for k in 0..5 {
points.push(Point3f::new(
i as f32 * 0.1,
j as f32 * 0.1,
k as f32 * 0.1,
));
}
}
}
let cloud = PointCloud::from_points(points);
let original_count = cloud.len();
let result = statistical_outlier_removal(&cloud, 5, 1.0);
assert!(result.is_ok());
let filtered = result.unwrap();
assert!(filtered.len() > original_count * 8 / 10);
}
#[test]
fn test_statistical_outlier_removal_invalid_k() {
let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
let result = statistical_outlier_removal(&cloud, 0, 1.0);
assert!(result.is_err());
}
#[test]
fn test_statistical_outlier_removal_invalid_std_dev() {
let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
let result = statistical_outlier_removal(&cloud, 5, 0.0);
assert!(result.is_err());
let result = statistical_outlier_removal(&cloud, 5, -1.0);
assert!(result.is_err());
}
#[test]
fn test_statistical_outlier_removal_with_threshold() {
let points = vec![
Point3f::new(0.0, 0.0, 0.0),
Point3f::new(0.1, 0.0, 0.0),
Point3f::new(0.0, 0.1, 0.0),
Point3f::new(0.0, 0.0, 0.1),
Point3f::new(10.0, 10.0, 10.0), ];
let cloud = PointCloud::from_points(points);
let result = statistical_outlier_removal_with_threshold(&cloud, 3, 0.5);
assert!(result.is_ok());
let filtered = result.unwrap();
assert_eq!(filtered.len(), 4);
let has_outlier = filtered.points.iter().any(|p|
(p.x - 10.0).abs() < 0.1 && (p.y - 10.0).abs() < 0.1 && (p.z - 10.0).abs() < 0.1
);
assert!(!has_outlier);
}
#[test]
fn test_statistical_outlier_removal_with_threshold_invalid() {
let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
let result = statistical_outlier_removal_with_threshold(&cloud, 0, 1.0);
assert!(result.is_err());
let result = statistical_outlier_removal_with_threshold(&cloud, 5, 0.0);
assert!(result.is_err());
}
#[test]
fn test_voxel_grid_filter_empty_cloud() {
let cloud = PointCloud::<Point3f>::new();
let result = voxel_grid_filter(&cloud, 0.1);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 0);
}
#[test]
fn test_voxel_grid_filter_single_point() {
let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
let result = voxel_grid_filter(&cloud, 0.1);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 1);
}
#[test]
fn test_voxel_grid_filter_with_duplicates() {
let cloud = PointCloud::from_points(vec![
Point3f::new(0.0, 0.0, 0.0),
Point3f::new(0.0, 0.0, 0.0), Point3f::new(0.1, 0.0, 0.0),
Point3f::new(0.1, 0.0, 0.0), Point3f::new(0.0, 0.1, 0.0),
]);
let result = voxel_grid_filter(&cloud, 0.05);
assert!(result.is_ok());
let filtered = result.unwrap();
assert_eq!(filtered.len(), 3); }
#[test]
fn test_voxel_grid_filter_invalid_voxel_size() {
let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
let result = voxel_grid_filter(&cloud, 0.0);
assert!(result.is_err());
let result = voxel_grid_filter(&cloud, -1.0);
assert!(result.is_err());
}
#[test]
fn test_radius_outlier_removal_empty_cloud() {
let cloud = PointCloud::<Point3f>::new();
let result = radius_outlier_removal(&cloud, 0.5, 3);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 0);
}
#[test]
fn test_radius_outlier_removal_single_point() {
let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
let result = radius_outlier_removal(&cloud, 0.5, 1);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 0);
}
#[test]
fn test_radius_outlier_removal_with_outliers() {
let mut points = Vec::new();
for i in 0..5 {
for j in 0..5 {
points.push(Point3f::new(
i as f32 * 0.1,
j as f32 * 0.1,
0.0,
));
}
}
points.push(Point3f::new(10.0, 10.0, 10.0));
points.push(Point3f::new(-10.0, -10.0, -10.0));
let cloud = PointCloud::from_points(points);
let original_count = cloud.len();
let result = radius_outlier_removal(&cloud, 0.5, 2);
assert!(result.is_ok());
let filtered = result.unwrap();
assert!(filtered.len() < original_count);
assert!(filtered.len() > 0);
let has_outlier_1 = filtered.points.iter().any(|p|
(p.x - 10.0).abs() < 0.1 && (p.y - 10.0).abs() < 0.1 && (p.z - 10.0).abs() < 0.1
);
let has_outlier_2 = filtered.points.iter().any(|p|
(p.x + 10.0).abs() < 0.1 && (p.y + 10.0).abs() < 0.1 && (p.z + 10.0).abs() < 0.1
);
assert!(!has_outlier_1);
assert!(!has_outlier_2);
}
#[test]
fn test_radius_outlier_removal_invalid_parameters() {
let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
let result = radius_outlier_removal(&cloud, 0.0, 3);
assert!(result.is_err());
let result = radius_outlier_removal(&cloud, -1.0, 3);
assert!(result.is_err());
let result = radius_outlier_removal(&cloud, 0.5, 0);
assert!(result.is_err());
}
#[test]
fn test_radius_outlier_removal_different_parameters() {
let mut points = Vec::new();
for i in 0..3 {
for j in 0..3 {
points.push(Point3f::new(
i as f32 * 0.1,
j as f32 * 0.1,
0.0,
));
}
}
points.push(Point3f::new(1.0, 1.0, 1.0));
let cloud = PointCloud::from_points(points);
let result1 = radius_outlier_removal(&cloud, 0.2, 2).unwrap();
let result2 = radius_outlier_removal(&cloud, 0.5, 2).unwrap();
assert!(result2.len() >= result1.len());
}
}