1use crate::spatial::{GpuKdTree, KD_TREE_WGSL};
4use crate::GpuContext;
5use threecrate_core::{Point3f, PointCloud, Result};
6
7const STATISTICAL_OUTLIER_SHADER: &str = r#"
8struct GpuPoint {
9 position: vec3<f32>,
10 _padding: f32, // Ensure 16-byte alignment
11}
12
13@group(0) @binding(0) var<storage, read> input_points: array<GpuPoint>;
14@group(0) @binding(1) var<storage, read> neighbors: array<array<u32, MAX_NEIGHBORS>>;
15@group(0) @binding(2) var<storage, read_write> is_outlier: array<u32>;
16@group(0) @binding(3) var<uniform> params: FilterParams;
17
18struct FilterParams {
19 num_points: u32,
20 k_neighbors: u32,
21 std_dev_threshold: f32,
22 mean_distance: f32,
23}
24
25@compute @workgroup_size(64)
26fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
27 let index = global_id.x;
28 if (index >= params.num_points) {
29 return;
30 }
31
32 let center_point = input_points[index].position;
33
34 // Compute mean distance to k-nearest neighbors
35 var total_distance = 0.0;
36 var count = 0u;
37
38 for (var i = 0u; i < params.k_neighbors; i++) {
39 let neighbor_idx = neighbors[index][i];
40 if (neighbor_idx < params.num_points) {
41 let neighbor_point = input_points[neighbor_idx].position;
42 let distance = length(center_point - neighbor_point);
43 total_distance += distance;
44 count++;
45 }
46 }
47
48 if (count == 0u) {
49 is_outlier[index] = 0u;
50 return;
51 }
52
53 let mean_distance = total_distance / f32(count);
54
55 // Mark as outlier if distance is beyond threshold
56 let deviation = abs(mean_distance - params.mean_distance);
57 is_outlier[index] = select(0u, 1u, deviation > params.std_dev_threshold);
58}
59"#;
60
61fn radius_outlier_shader() -> String {
65 format!(
66 "{}{}",
67 KD_TREE_WGSL,
68 r#"
69struct RadiusOutlierParams {
70 num_nodes: u32,
71 radius_sq: f32,
72 min_neighbors: u32,
73 _padding: u32,
74}
75
76@group(0) @binding(1) var<storage, read_write> is_outlier: array<u32>;
77@group(0) @binding(2) var<uniform> params: RadiusOutlierParams;
78
79@compute @workgroup_size(64)
80fn radius_outlier(@builtin(global_invocation_id) gid: vec3<u32>) {
81 let node_id = gid.x;
82 if (node_id >= params.num_nodes) {
83 return;
84 }
85 let count = count_within(
86 nodes[node_id].pos,
87 params.radius_sq,
88 node_id,
89 params.min_neighbors,
90 );
91 is_outlier[nodes[node_id].index] = select(0u, 1u, count < params.min_neighbors);
92}
93"#
94 )
95}
96
97const VOXEL_GRID_SHADER: &str = r#"
98struct GpuPoint {
99 position: vec3<f32>,
100 _padding: f32, // Ensure 16-byte alignment
101}
102
103@group(0) @binding(0) var<storage, read> input_points: array<GpuPoint>;
104@group(0) @binding(1) var<storage, read_write> voxel_indices: array<u32>;
105@group(0) @binding(2) var<uniform> params: VoxelGridParams;
106
107struct VoxelGridParams {
108 num_points: u32,
109 voxel_size: f32,
110 min_x: f32,
111 min_y: f32,
112 min_z: f32,
113 max_x: f32,
114 max_y: f32,
115 max_z: f32,
116}
117
118fn get_voxel_index(point: vec3<f32>, params: VoxelGridParams) -> u32 {
119 // Simple coordinate-based voxel index (similar to CPU version)
120 let voxel_x = i32(floor((point.x - params.min_x) / params.voxel_size));
121 let voxel_y = i32(floor((point.y - params.min_y) / params.voxel_size));
122 let voxel_z = i32(floor((point.z - params.min_z) / params.voxel_size));
123
124 // Use a simple hash based on coordinates
125 let hash_x = u32(voxel_x * voxel_x + voxel_x);
126 let hash_y = u32(voxel_y * voxel_y + voxel_y * 7);
127 let hash_z = u32(voxel_z * voxel_z + voxel_z * 13);
128
129 return (hash_x + hash_y * 31u + hash_z * 97u) % 100000u;
130}
131
132@compute @workgroup_size(64)
133fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
134 let index = global_id.x;
135 if (index >= params.num_points) {
136 return;
137 }
138
139 let point = input_points[index].position;
140 let voxel_idx = get_voxel_index(point, params);
141
142 // Store voxel index for this point
143 voxel_indices[index] = voxel_idx;
144}
145"#;
146
147impl GpuContext {
148 pub async fn remove_statistical_outliers(
150 &self,
151 points: &[Point3f],
152 k_neighbors: usize,
153 std_dev_multiplier: f32,
154 ) -> Result<Vec<Point3f>> {
155 if points.is_empty() {
156 return Ok(Vec::new());
157 }
158
159 #[repr(C)]
161 #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
162 struct GpuPoint {
163 position: [f32; 3],
164 _padding: f32,
165 }
166
167 let point_data: Vec<GpuPoint> = points
168 .iter()
169 .map(|p| GpuPoint {
170 position: [p.x, p.y, p.z],
171 _padding: 0.0,
172 })
173 .collect();
174
175 let point_data_flat: Vec<[f32; 3]> = point_data.iter().map(|p| p.position).collect();
177
178 let neighbors = self.compute_neighbors_simple(&point_data_flat, k_neighbors);
180
181 let global_mean =
183 self.compute_global_mean_distance(&point_data_flat, &neighbors, k_neighbors);
184
185 let input_buffer =
187 self.create_buffer_init("Input Points", &point_data, wgpu::BufferUsages::STORAGE);
188
189 let neighbors_buffer =
190 self.create_buffer_init("Neighbors", &neighbors, wgpu::BufferUsages::STORAGE);
191
192 let outlier_buffer = self.create_buffer(
193 "Outlier Flags",
194 (point_data.len() * std::mem::size_of::<u32>()) as u64,
195 wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
196 );
197
198 #[repr(C)]
199 #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
200 struct FilterParams {
201 num_points: u32,
202 k_neighbors: u32,
203 std_dev_threshold: f32,
204 mean_distance: f32,
205 }
206
207 let params = FilterParams {
208 num_points: points.len() as u32,
209 k_neighbors: k_neighbors as u32,
210 std_dev_threshold: global_mean * std_dev_multiplier,
211 mean_distance: global_mean,
212 };
213
214 let params_buffer =
215 self.create_buffer_init("Filter Params", &[params], wgpu::BufferUsages::UNIFORM);
216
217 let shader_source =
219 STATISTICAL_OUTLIER_SHADER.replace("MAX_NEIGHBORS", &k_neighbors.to_string());
220 let shader = self.create_shader_module("Statistical Outlier Filter", &shader_source);
221
222 let bind_group_layout = self.create_bind_group_layout(
224 "Outlier Filter",
225 &[
226 wgpu::BindGroupLayoutEntry {
227 binding: 0,
228 visibility: wgpu::ShaderStages::COMPUTE,
229 ty: wgpu::BindingType::Buffer {
230 ty: wgpu::BufferBindingType::Storage { read_only: true },
231 has_dynamic_offset: false,
232 min_binding_size: None,
233 },
234 count: None,
235 },
236 wgpu::BindGroupLayoutEntry {
237 binding: 1,
238 visibility: wgpu::ShaderStages::COMPUTE,
239 ty: wgpu::BindingType::Buffer {
240 ty: wgpu::BufferBindingType::Storage { read_only: true },
241 has_dynamic_offset: false,
242 min_binding_size: None,
243 },
244 count: None,
245 },
246 wgpu::BindGroupLayoutEntry {
247 binding: 2,
248 visibility: wgpu::ShaderStages::COMPUTE,
249 ty: wgpu::BindingType::Buffer {
250 ty: wgpu::BufferBindingType::Storage { read_only: false },
251 has_dynamic_offset: false,
252 min_binding_size: None,
253 },
254 count: None,
255 },
256 wgpu::BindGroupLayoutEntry {
257 binding: 3,
258 visibility: wgpu::ShaderStages::COMPUTE,
259 ty: wgpu::BindingType::Buffer {
260 ty: wgpu::BufferBindingType::Uniform,
261 has_dynamic_offset: false,
262 min_binding_size: None,
263 },
264 count: None,
265 },
266 ],
267 );
268
269 let pipeline = self
271 .device
272 .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
273 label: Some("Outlier Filter Pipeline"),
274 layout: Some(&self.device.create_pipeline_layout(
275 &wgpu::PipelineLayoutDescriptor {
276 label: Some("Outlier Filter Layout"),
277 bind_group_layouts: &[Some(&bind_group_layout)],
278 immediate_size: 0,
279 },
280 )),
281 module: &shader,
282 entry_point: Some("main"),
283 compilation_options: wgpu::PipelineCompilationOptions::default(),
284 cache: None,
285 });
286
287 let bind_group = self.create_bind_group(
289 "Outlier Filter",
290 &bind_group_layout,
291 &[
292 wgpu::BindGroupEntry {
293 binding: 0,
294 resource: input_buffer.as_entire_binding(),
295 },
296 wgpu::BindGroupEntry {
297 binding: 1,
298 resource: neighbors_buffer.as_entire_binding(),
299 },
300 wgpu::BindGroupEntry {
301 binding: 2,
302 resource: outlier_buffer.as_entire_binding(),
303 },
304 wgpu::BindGroupEntry {
305 binding: 3,
306 resource: params_buffer.as_entire_binding(),
307 },
308 ],
309 );
310
311 let mut encoder = self
313 .device
314 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
315 label: Some("Outlier Filter"),
316 });
317
318 {
319 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
320 label: Some("Outlier Filter Pass"),
321 timestamp_writes: None,
322 });
323 compute_pass.set_pipeline(&pipeline);
324 compute_pass.set_bind_group(0, &bind_group, &[]);
325 let workgroup_count = (points.len() + 63) / 64;
326 compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1);
327 }
328
329 let staging_buffer = self.create_buffer(
331 "Outlier Staging",
332 (point_data.len() * std::mem::size_of::<u32>()) as u64,
333 wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
334 );
335
336 encoder.copy_buffer_to_buffer(
337 &outlier_buffer,
338 0,
339 &staging_buffer,
340 0,
341 (point_data.len() * std::mem::size_of::<u32>()) as u64,
342 );
343
344 self.queue.submit(std::iter::once(encoder.finish()));
345
346 let buffer_slice = staging_buffer.slice(..);
348 let (sender, receiver) = futures_intrusive::channel::shared::oneshot_channel();
349 buffer_slice.map_async(wgpu::MapMode::Read, move |v| sender.send(v).unwrap());
350
351 self.device.poll(wgpu::PollType::Wait {
352 submission_index: None,
353 timeout: None,
354 });
355
356 if let Some(Ok(())) = receiver.receive().await {
357 let data = buffer_slice.get_mapped_range();
358 let outlier_flags: Vec<u32> = bytemuck::cast_slice(&data).to_vec();
359
360 let filtered_points: Vec<Point3f> = points
361 .iter()
362 .zip(outlier_flags.iter())
363 .filter(|&(_, &is_outlier)| is_outlier == 0)
364 .map(|(point, _)| *point)
365 .collect();
366
367 drop(data);
368 staging_buffer.unmap();
369
370 Ok(filtered_points)
371 } else {
372 Err(threecrate_core::Error::Gpu(
373 "Failed to read GPU filtering results".to_string(),
374 ))
375 }
376 }
377
378 fn compute_global_mean_distance(
380 &self,
381 points: &[[f32; 3]],
382 neighbors: &[[u32; 64]],
383 k: usize,
384 ) -> f32 {
385 let k = k.min(64).min(points.len());
386 let mut total_distance = 0.0;
387 let mut count = 0;
388
389 for (i, point) in points.iter().enumerate() {
390 for j in 0..k {
391 let neighbor_idx = neighbors[i][j] as usize;
392 if neighbor_idx < points.len() {
393 let neighbor_point = &points[neighbor_idx];
394 let dx = point[0] - neighbor_point[0];
395 let dy = point[1] - neighbor_point[1];
396 let dz = point[2] - neighbor_point[2];
397 let distance = (dx * dx + dy * dy + dz * dz).sqrt();
398 total_distance += distance;
399 count += 1;
400 }
401 }
402 }
403
404 if count > 0 {
405 total_distance / count as f32
406 } else {
407 0.0
408 }
409 }
410
411 pub async fn remove_radius_outliers(
417 &self,
418 points: &[Point3f],
419 radius: f32,
420 min_neighbors: usize,
421 ) -> Result<Vec<Point3f>> {
422 if points.is_empty() {
423 return Ok(Vec::new());
424 }
425
426 let tree = GpuKdTree::new(self, points)?;
427 let initial_flag = (min_neighbors > 0) as u32;
430 let outlier_buffer = self.create_buffer_init(
431 "Outlier Flags",
432 &vec![initial_flag; points.len()],
433 wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
434 );
435
436 if !tree.is_empty() {
437 #[repr(C)]
438 #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
439 struct RadiusOutlierParams {
440 num_nodes: u32,
441 radius_sq: f32,
442 min_neighbors: u32,
443 _padding: u32,
444 }
445 let params = RadiusOutlierParams {
446 num_nodes: tree.node_to_index.len() as u32,
447 radius_sq: if radius < 0.0 { -1.0 } else { radius * radius },
450 min_neighbors: min_neighbors.min(u32::MAX as usize) as u32,
451 _padding: 0,
452 };
453 let params_buffer = self.create_buffer_init(
454 "Radius Outlier Params",
455 &[params],
456 wgpu::BufferUsages::UNIFORM,
457 );
458
459 let pipeline = self.cached_pipeline(
460 &self.pipelines.radius_outlier,
461 "Radius Outlier Filter",
462 radius_outlier_shader,
463 "radius_outlier",
464 );
465 let bind_group = self.create_bind_group(
466 "Radius Outlier Filter",
467 &pipeline.get_bind_group_layout(0),
468 &[
469 wgpu::BindGroupEntry {
470 binding: 0,
471 resource: tree.nodes.as_entire_binding(),
472 },
473 wgpu::BindGroupEntry {
474 binding: 1,
475 resource: outlier_buffer.as_entire_binding(),
476 },
477 wgpu::BindGroupEntry {
478 binding: 2,
479 resource: params_buffer.as_entire_binding(),
480 },
481 ],
482 );
483
484 let mut encoder = self
485 .device
486 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
487 label: Some("Radius Outlier Filter"),
488 });
489 {
490 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
491 label: Some("Radius Outlier Filter"),
492 timestamp_writes: None,
493 });
494 pass.set_pipeline(pipeline);
495 pass.set_bind_group(0, &bind_group, &[]);
496 pass.dispatch_workgroups(tree.node_to_index.len().div_ceil(64) as u32, 1, 1);
497 }
498 self.queue.submit(std::iter::once(encoder.finish()));
499 }
500
501 let outlier_flags: Vec<u32> = self.read_buffer(&outlier_buffer)?;
502 Ok(points
503 .iter()
504 .zip(&outlier_flags)
505 .filter(|&(_, &is_outlier)| is_outlier == 0)
506 .map(|(point, _)| *point)
507 .collect())
508 }
509
510 pub async fn voxel_grid_filter(
512 &self,
513 points: &[Point3f],
514 voxel_size: f32,
515 ) -> Result<Vec<Point3f>> {
516 if points.is_empty() {
517 return Ok(Vec::new());
518 }
519
520 if voxel_size <= 0.0 {
521 return Err(threecrate_core::Error::InvalidData(
522 "Voxel size must be positive".to_string(),
523 ));
524 }
525
526 #[repr(C)]
528 #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
529 struct GpuPoint {
530 position: [f32; 3],
531 _padding: f32,
532 }
533
534 let point_data: Vec<GpuPoint> = points
535 .iter()
536 .map(|p| GpuPoint {
537 position: [p.x, p.y, p.z],
538 _padding: 0.0,
539 })
540 .collect();
541
542 let min_x = points
544 .iter()
545 .map(|p| p.x)
546 .min_by(|a, b| a.partial_cmp(b).unwrap())
547 .unwrap();
548 let min_y = points
549 .iter()
550 .map(|p| p.y)
551 .min_by(|a, b| a.partial_cmp(b).unwrap())
552 .unwrap();
553 let min_z = points
554 .iter()
555 .map(|p| p.z)
556 .min_by(|a, b| a.partial_cmp(b).unwrap())
557 .unwrap();
558 let max_x = points
559 .iter()
560 .map(|p| p.x)
561 .max_by(|a, b| a.partial_cmp(b).unwrap())
562 .unwrap();
563 let max_y = points
564 .iter()
565 .map(|p| p.y)
566 .max_by(|a, b| a.partial_cmp(b).unwrap())
567 .unwrap();
568 let max_z = points
569 .iter()
570 .map(|p| p.z)
571 .max_by(|a, b| a.partial_cmp(b).unwrap())
572 .unwrap();
573
574 let total_voxels = points.len() * 2; let input_buffer =
579 self.create_buffer_init("Input Points", &point_data, wgpu::BufferUsages::STORAGE);
580
581 let voxel_indices_buffer = self.create_buffer(
582 "Voxel Indices",
583 (point_data.len() * std::mem::size_of::<u32>()) as u64,
584 wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
585 );
586
587 #[repr(C)]
588 #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
589 struct VoxelGridParams {
590 num_points: u32,
591 voxel_size: f32,
592 min_x: f32,
593 min_y: f32,
594 min_z: f32,
595 max_x: f32,
596 max_y: f32,
597 max_z: f32,
598 }
599
600 let params = VoxelGridParams {
601 num_points: points.len() as u32,
602 voxel_size,
603 min_x,
604 min_y,
605 min_z,
606 max_x,
607 max_y,
608 max_z,
609 };
610
611 let params_buffer =
612 self.create_buffer_init("Voxel Grid Params", &[params], wgpu::BufferUsages::UNIFORM);
613
614 let shader = self.create_shader_module("Voxel Grid Filter", VOXEL_GRID_SHADER);
616
617 let bind_group_layout = self.create_bind_group_layout(
619 "Voxel Grid Filter",
620 &[
621 wgpu::BindGroupLayoutEntry {
622 binding: 0,
623 visibility: wgpu::ShaderStages::COMPUTE,
624 ty: wgpu::BindingType::Buffer {
625 ty: wgpu::BufferBindingType::Storage { read_only: true },
626 has_dynamic_offset: false,
627 min_binding_size: None,
628 },
629 count: None,
630 },
631 wgpu::BindGroupLayoutEntry {
632 binding: 1,
633 visibility: wgpu::ShaderStages::COMPUTE,
634 ty: wgpu::BindingType::Buffer {
635 ty: wgpu::BufferBindingType::Storage { read_only: false },
636 has_dynamic_offset: false,
637 min_binding_size: None,
638 },
639 count: None,
640 },
641 wgpu::BindGroupLayoutEntry {
642 binding: 2,
643 visibility: wgpu::ShaderStages::COMPUTE,
644 ty: wgpu::BindingType::Buffer {
645 ty: wgpu::BufferBindingType::Uniform,
646 has_dynamic_offset: false,
647 min_binding_size: None,
648 },
649 count: None,
650 },
651 ],
652 );
653
654 let pipeline = self
656 .device
657 .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
658 label: Some("Voxel Grid Filter Pipeline"),
659 layout: Some(&self.device.create_pipeline_layout(
660 &wgpu::PipelineLayoutDescriptor {
661 label: Some("Voxel Grid Filter Layout"),
662 bind_group_layouts: &[Some(&bind_group_layout)],
663 immediate_size: 0,
664 },
665 )),
666 module: &shader,
667 entry_point: Some("main"),
668 compilation_options: wgpu::PipelineCompilationOptions::default(),
669 cache: None,
670 });
671
672 let bind_group = self.create_bind_group(
674 "Voxel Grid Filter",
675 &bind_group_layout,
676 &[
677 wgpu::BindGroupEntry {
678 binding: 0,
679 resource: input_buffer.as_entire_binding(),
680 },
681 wgpu::BindGroupEntry {
682 binding: 1,
683 resource: voxel_indices_buffer.as_entire_binding(),
684 },
685 wgpu::BindGroupEntry {
686 binding: 2,
687 resource: params_buffer.as_entire_binding(),
688 },
689 ],
690 );
691
692 let mut encoder = self
694 .device
695 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
696 label: Some("Voxel Grid Filter"),
697 });
698
699 {
700 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
701 label: Some("Voxel Grid Filter Pass"),
702 timestamp_writes: None,
703 });
704 compute_pass.set_pipeline(&pipeline);
705 compute_pass.set_bind_group(0, &bind_group, &[]);
706 let workgroup_count = (points.len() + 63) / 64;
707 compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1);
708 }
709
710 let indices_staging = self.create_buffer(
712 "Voxel Indices Staging",
713 (point_data.len() * std::mem::size_of::<u32>()) as u64,
714 wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
715 );
716
717 encoder.copy_buffer_to_buffer(
718 &voxel_indices_buffer,
719 0,
720 &indices_staging,
721 0,
722 (point_data.len() * std::mem::size_of::<u32>()) as u64,
723 );
724
725 self.queue.submit(std::iter::once(encoder.finish()));
726
727 let indices_slice = indices_staging.slice(..);
729
730 let (indices_sender, indices_receiver) =
731 futures_intrusive::channel::shared::oneshot_channel();
732
733 indices_slice.map_async(wgpu::MapMode::Read, move |v| {
734 indices_sender.send(v).unwrap()
735 });
736
737 self.device.poll(wgpu::PollType::Wait {
738 submission_index: None,
739 timeout: None,
740 });
741
742 if let Some(Ok(())) = indices_receiver.receive().await {
743 let indices_data = indices_slice.get_mapped_range();
744
745 let voxel_indices: Vec<u32> = bytemuck::cast_slice(&indices_data).to_vec();
746
747 let mut voxel_used = vec![false; total_voxels];
749 let mut filtered_points = Vec::new();
750
751 for (point_idx, &voxel_idx) in voxel_indices.iter().enumerate() {
752 if voxel_idx < total_voxels as u32 && !voxel_used[voxel_idx as usize] {
753 filtered_points.push(points[point_idx]);
754 voxel_used[voxel_idx as usize] = true;
755 }
756 }
757
758 drop(indices_data);
759 indices_staging.unmap();
760
761 Ok(filtered_points)
762 } else {
763 Err(threecrate_core::Error::Gpu(
764 "Failed to read GPU voxel grid filtering results".to_string(),
765 ))
766 }
767 }
768}
769
770pub async fn gpu_remove_statistical_outliers(
772 gpu_context: &GpuContext,
773 cloud: &PointCloud<Point3f>,
774 k_neighbors: usize,
775 std_dev_multiplier: f32,
776) -> Result<PointCloud<Point3f>> {
777 let filtered_points = gpu_context
778 .remove_statistical_outliers(&cloud.points, k_neighbors, std_dev_multiplier)
779 .await?;
780 Ok(PointCloud::from_points(filtered_points))
781}
782
783pub async fn gpu_radius_outlier_removal(
785 gpu_context: &GpuContext,
786 cloud: &PointCloud<Point3f>,
787 radius: f32,
788 min_neighbors: usize,
789) -> Result<PointCloud<Point3f>> {
790 let filtered_points = gpu_context
791 .remove_radius_outliers(&cloud.points, radius, min_neighbors)
792 .await?;
793 Ok(PointCloud::from_points(filtered_points))
794}
795
796pub async fn gpu_voxel_grid_filter(
798 gpu_context: &GpuContext,
799 cloud: &PointCloud<Point3f>,
800 voxel_size: f32,
801) -> Result<PointCloud<Point3f>> {
802 let filtered_points = gpu_context
803 .voxel_grid_filter(&cloud.points, voxel_size)
804 .await?;
805 Ok(PointCloud::from_points(filtered_points))
806}
807
808#[cfg(test)]
809mod tests {
810 use super::*;
811 use std::time::Instant;
812 use threecrate_core::{Point3f, PointCloud};
813
814 async fn try_create_gpu_context() -> Option<GpuContext> {
815 GpuContext::new().await.ok()
816 }
817
818 fn create_test_point_cloud_with_outliers() -> PointCloud<Point3f> {
819 let mut points = Vec::new();
820
821 for i in 0..50 {
823 for j in 0..50 {
824 let x = (i as f32 - 25.0) * 0.1;
825 let y = (j as f32 - 25.0) * 0.1;
826 let z = 0.0;
827 points.push(Point3f::new(x, y, z));
828 }
829 }
830
831 for i in 0..20 {
833 let x = 50.0 + (i as f32 * 2.0);
834 let y = 50.0 + (i as f32 * 2.0);
835 let z = 50.0 + (i as f32 * 2.0);
836 points.push(Point3f::new(x, y, z));
837 }
838
839 PointCloud::from_points(points)
840 }
841
842 fn create_test_point_cloud_no_outliers() -> PointCloud<Point3f> {
843 let mut points = Vec::new();
844
845 for i in 0..25 {
847 for j in 0..25 {
848 for k in 0..5 {
849 let x = (i as f32 - 12.5) * 0.1;
850 let y = (j as f32 - 12.5) * 0.1;
851 let z = (k as f32 - 2.5) * 0.1;
852 points.push(Point3f::new(x, y, z));
853 }
854 }
855 }
856
857 PointCloud::from_points(points)
858 }
859
860 #[tokio::test]
861 async fn test_gpu_statistical_outlier_removal_basic() {
862 let gpu_context = match try_create_gpu_context().await {
863 Some(ctx) => ctx,
864 None => {
865 println!("Skipping GPU test - no GPU available");
866 return;
867 }
868 };
869
870 let cloud = create_test_point_cloud_with_outliers();
871 let original_count = cloud.len();
872
873 let filtered = gpu_remove_statistical_outliers(&gpu_context, &cloud, 10, 1.0)
874 .await
875 .unwrap();
876 let filtered_count = filtered.len();
877
878 assert!(filtered_count < original_count);
880 assert!(filtered_count > 0);
881
882 println!(
883 "GPU outlier removal: {} -> {} points",
884 original_count, filtered_count
885 );
886 }
887
888 #[tokio::test]
889 async fn test_gpu_statistical_outlier_removal_no_outliers() {
890 let gpu_context = match try_create_gpu_context().await {
891 Some(ctx) => ctx,
892 None => {
893 println!("Skipping GPU test - no GPU available");
894 return;
895 }
896 };
897
898 let cloud = create_test_point_cloud_no_outliers();
899 let original_count = cloud.len();
900
901 let filtered = gpu_remove_statistical_outliers(&gpu_context, &cloud, 10, 1.0)
902 .await
903 .unwrap();
904 let filtered_count = filtered.len();
905
906 println!(
907 "GPU outlier removal (no outliers): {} -> {} points",
908 original_count, filtered_count
909 );
910
911 assert!(filtered_count > 0);
914 assert!(filtered_count < original_count);
915 }
916
917 #[tokio::test]
918 async fn test_gpu_statistical_outlier_removal_empty_cloud() {
919 let gpu_context = match try_create_gpu_context().await {
920 Some(ctx) => ctx,
921 None => {
922 println!("Skipping GPU test - no GPU available");
923 return;
924 }
925 };
926
927 let cloud = PointCloud::<Point3f>::new();
928 let filtered = gpu_remove_statistical_outliers(&gpu_context, &cloud, 10, 1.0)
929 .await
930 .unwrap();
931
932 assert_eq!(filtered.len(), 0);
933 }
934
935 #[tokio::test]
936 async fn test_gpu_statistical_outlier_removal_single_point() {
937 let gpu_context = match try_create_gpu_context().await {
938 Some(ctx) => ctx,
939 None => {
940 println!("Skipping GPU test - no GPU available");
941 return;
942 }
943 };
944
945 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
946 let filtered = gpu_remove_statistical_outliers(&gpu_context, &cloud, 1, 1.0)
947 .await
948 .unwrap();
949
950 assert_eq!(filtered.len(), 1);
952 }
953
954 #[tokio::test]
955 async fn test_gpu_vs_cpu_statistical_outlier_removal() {
956 let gpu_context = match try_create_gpu_context().await {
957 Some(ctx) => ctx,
958 None => {
959 println!("Skipping GPU vs CPU test - no GPU available");
960 return;
961 }
962 };
963
964 let cloud = create_test_point_cloud_with_outliers();
965
966 let cpu_start = Instant::now();
968 let cpu_filtered_count = {
969 let points = &cloud.points;
971 let k = 10;
972 let std_dev_multiplier = 1.0;
973
974 if points.is_empty() {
975 0
976 } else {
977 let mut mean_distances = Vec::new();
979 for point in points {
980 let mut distances = Vec::new();
981 for other_point in points {
982 if other_point != point {
983 let dx = point.x - other_point.x;
984 let dy = point.y - other_point.y;
985 let dz = point.z - other_point.z;
986 let distance = (dx * dx + dy * dy + dz * dz).sqrt();
987 distances.push(distance);
988 }
989 }
990 distances.sort_by(|a, b| a.partial_cmp(b).unwrap());
991 let k_neighbors = k.min(distances.len());
992 if k_neighbors > 0 {
993 let mean =
994 distances[..k_neighbors].iter().sum::<f32>() / k_neighbors as f32;
995 mean_distances.push(mean);
996 } else {
997 mean_distances.push(0.0);
998 }
999 }
1000
1001 let global_mean = mean_distances.iter().sum::<f32>() / mean_distances.len() as f32;
1002 let variance = mean_distances
1003 .iter()
1004 .map(|&d| (d - global_mean).powi(2))
1005 .sum::<f32>()
1006 / mean_distances.len() as f32;
1007 let global_std_dev = variance.sqrt();
1008 let threshold = global_mean + std_dev_multiplier * global_std_dev;
1009
1010 mean_distances.iter().filter(|&&d| d <= threshold).count()
1011 }
1012 };
1013 let cpu_time = cpu_start.elapsed();
1014
1015 let gpu_start = Instant::now();
1017 let gpu_filtered = gpu_remove_statistical_outliers(&gpu_context, &cloud, 10, 1.0)
1018 .await
1019 .unwrap();
1020 let gpu_time = gpu_start.elapsed();
1021
1022 println!(
1023 "CPU outlier removal: {} -> {} points in {:?}",
1024 cloud.len(),
1025 cpu_filtered_count,
1026 cpu_time
1027 );
1028 println!(
1029 "GPU outlier removal: {} -> {} points in {:?}",
1030 cloud.len(),
1031 gpu_filtered.len(),
1032 gpu_time
1033 );
1034
1035 assert!(cpu_filtered_count < cloud.len());
1037 assert!(gpu_filtered.len() < cloud.len());
1038
1039 if cloud.len() > 1000 {
1041 println!(
1042 "GPU speedup: {:.2}x",
1043 cpu_time.as_secs_f32() / gpu_time.as_secs_f32()
1044 );
1045 }
1046 }
1047
1048 #[tokio::test]
1049 async fn test_gpu_statistical_outlier_removal_different_parameters() {
1050 let gpu_context = match try_create_gpu_context().await {
1051 Some(ctx) => ctx,
1052 None => {
1053 println!("Skipping GPU test - no GPU available");
1054 return;
1055 }
1056 };
1057
1058 let cloud = create_test_point_cloud_with_outliers();
1059 let original_count = cloud.len();
1060
1061 for k in [5, 10, 20] {
1063 let filtered = gpu_remove_statistical_outliers(&gpu_context, &cloud, k, 1.0)
1064 .await
1065 .unwrap();
1066 println!("k={}: {} -> {} points", k, original_count, filtered.len());
1067 assert!(filtered.len() > 0);
1068 }
1069
1070 for std_dev in [0.5, 1.0, 2.0] {
1072 let filtered = gpu_remove_statistical_outliers(&gpu_context, &cloud, 10, std_dev)
1073 .await
1074 .unwrap();
1075 println!(
1076 "std_dev={}: {} -> {} points",
1077 std_dev,
1078 original_count,
1079 filtered.len()
1080 );
1081 assert!(filtered.len() > 0);
1082 }
1083 }
1084
1085 #[tokio::test]
1086 async fn test_gpu_statistical_outlier_removal_large_dataset() {
1087 let gpu_context = match try_create_gpu_context().await {
1088 Some(ctx) => ctx,
1089 None => {
1090 println!("Skipping GPU test - no GPU available");
1091 return;
1092 }
1093 };
1094
1095 let mut points = Vec::new();
1097 for i in 0..50 {
1098 for j in 0..50 {
1099 let x = (i as f32 - 25.0) * 0.1;
1100 let y = (j as f32 - 25.0) * 0.1;
1101 let z = 0.0;
1102 points.push(Point3f::new(x, y, z));
1103 }
1104 }
1105
1106 for i in 0..10 {
1108 let x = 25.0 + (i as f32 * 2.0);
1109 let y = 25.0 + (i as f32 * 2.0);
1110 let z = 25.0 + (i as f32 * 2.0);
1111 points.push(Point3f::new(x, y, z));
1112 }
1113
1114 let cloud = PointCloud::from_points(points);
1115 let original_count = cloud.len();
1116
1117 let start = Instant::now();
1118 let filtered = gpu_remove_statistical_outliers(&gpu_context, &cloud, 5, 1.0)
1119 .await
1120 .unwrap();
1121 let elapsed = start.elapsed();
1122
1123 println!(
1124 "Large dataset GPU outlier removal: {} -> {} points in {:?}",
1125 original_count,
1126 filtered.len(),
1127 elapsed
1128 );
1129
1130 assert!(filtered.len() < original_count);
1131 assert!(filtered.len() > 0);
1132
1133 assert!(elapsed.as_secs() < 10);
1135 }
1136
1137 fn create_test_point_cloud_for_radius_outlier() -> PointCloud<Point3f> {
1138 let mut points = Vec::new();
1139
1140 for i in 0..20 {
1142 for j in 0..20 {
1143 let x = (i as f32 - 10.0) * 0.1;
1144 let y = (j as f32 - 10.0) * 0.1;
1145 let z = 0.0;
1146 points.push(Point3f::new(x, y, z));
1147 }
1148 }
1149
1150 for i in 0..5 {
1152 let x = 10.0 + (i as f32 * 2.0);
1153 let y = 10.0 + (i as f32 * 2.0);
1154 let z = 10.0 + (i as f32 * 2.0);
1155 points.push(Point3f::new(x, y, z));
1156 }
1157
1158 PointCloud::from_points(points)
1159 }
1160
1161 #[tokio::test]
1162 async fn test_gpu_radius_outlier_removal_matches_cpu() {
1163 let Some(gpu_context) = try_create_gpu_context().await else {
1164 return;
1165 };
1166
1167 let mut state = 7u32;
1169 let mut next = || {
1170 state = state.wrapping_mul(1664525).wrapping_add(1013904223);
1171 (state >> 8) as f32 / (1u32 << 24) as f32
1172 };
1173 let mut cloud = PointCloud::new();
1174 for _ in 0..15_000 {
1175 cloud.push(Point3f::new(next(), next(), 0.1 * next()));
1176 }
1177 for _ in 0..3_000 {
1178 cloud.push(Point3f::new(2.0 + 3.0 * next(), 3.0 * next(), next()));
1179 }
1180 cloud.push(Point3f::new(f32::NAN, 0.0, 0.0));
1181
1182 let gpu = gpu_radius_outlier_removal(&gpu_context, &cloud, 0.05, 4)
1183 .await
1184 .unwrap();
1185 let finite = PointCloud::from_points(
1186 cloud
1187 .points
1188 .iter()
1189 .copied()
1190 .filter(|p| p.coords.iter().all(|c| c.is_finite()))
1191 .collect(),
1192 );
1193 let cpu = threecrate_algorithms::radius_outlier_removal(&finite, 0.05, 4).unwrap();
1194
1195 assert_eq!(gpu.len(), cpu.len());
1196 assert!(gpu.points.iter().zip(&cpu.points).all(|(g, c)| g == c));
1197 }
1198
1199 #[tokio::test]
1200 async fn test_gpu_radius_outlier_removal_negative_radius() {
1201 let Some(gpu_context) = try_create_gpu_context().await else {
1202 return;
1203 };
1204 let cloud = PointCloud::from_points(
1205 (0..50)
1206 .map(|i| Point3f::new(i as f32 * 0.01, 0.0, 0.0))
1207 .collect(),
1208 );
1209 let filtered = gpu_radius_outlier_removal(&gpu_context, &cloud, -0.2, 3)
1211 .await
1212 .unwrap();
1213 assert!(filtered.is_empty());
1214 }
1215
1216 #[tokio::test]
1217 async fn test_gpu_radius_outlier_removal_basic() {
1218 let gpu_context = match try_create_gpu_context().await {
1219 Some(ctx) => ctx,
1220 None => {
1221 println!("Skipping GPU test - no GPU available");
1222 return;
1223 }
1224 };
1225
1226 let cloud = create_test_point_cloud_for_radius_outlier();
1227 let original_count = cloud.len();
1228
1229 let filtered = gpu_radius_outlier_removal(&gpu_context, &cloud, 0.5, 3)
1230 .await
1231 .unwrap();
1232 let filtered_count = filtered.len();
1233
1234 assert!(filtered_count < original_count);
1236 assert!(filtered_count > 0);
1237
1238 println!(
1239 "GPU radius outlier removal: {} -> {} points",
1240 original_count, filtered_count
1241 );
1242 }
1243
1244 #[tokio::test]
1245 async fn test_gpu_radius_outlier_removal_empty_cloud() {
1246 let gpu_context = match try_create_gpu_context().await {
1247 Some(ctx) => ctx,
1248 None => {
1249 println!("Skipping GPU test - no GPU available");
1250 return;
1251 }
1252 };
1253
1254 let cloud = PointCloud::<Point3f>::new();
1255 let filtered = gpu_radius_outlier_removal(&gpu_context, &cloud, 0.5, 3)
1256 .await
1257 .unwrap();
1258
1259 assert_eq!(filtered.len(), 0);
1260 }
1261
1262 #[tokio::test]
1263 async fn test_gpu_radius_outlier_removal_single_point() {
1264 let gpu_context = match try_create_gpu_context().await {
1265 Some(ctx) => ctx,
1266 None => {
1267 println!("Skipping GPU test - no GPU available");
1268 return;
1269 }
1270 };
1271
1272 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
1273 let filtered = gpu_radius_outlier_removal(&gpu_context, &cloud, 0.5, 1)
1274 .await
1275 .unwrap();
1276
1277 assert_eq!(filtered.len(), 0);
1279 }
1280
1281 #[tokio::test]
1282 async fn test_gpu_radius_outlier_removal_different_parameters() {
1283 let gpu_context = match try_create_gpu_context().await {
1284 Some(ctx) => ctx,
1285 None => {
1286 println!("Skipping GPU test - no GPU available");
1287 return;
1288 }
1289 };
1290
1291 let cloud = create_test_point_cloud_for_radius_outlier();
1292 let original_count = cloud.len();
1293
1294 for radius in [0.2, 0.5, 1.0] {
1296 let filtered = gpu_radius_outlier_removal(&gpu_context, &cloud, radius, 3)
1297 .await
1298 .unwrap();
1299 println!(
1300 "radius={}: {} -> {} points",
1301 radius,
1302 original_count,
1303 filtered.len()
1304 );
1305 assert!(filtered.len() > 0);
1306 }
1307
1308 for min_neighbors in [1, 3, 5] {
1310 let filtered = gpu_radius_outlier_removal(&gpu_context, &cloud, 0.5, min_neighbors)
1311 .await
1312 .unwrap();
1313 println!(
1314 "min_neighbors={}: {} -> {} points",
1315 min_neighbors,
1316 original_count,
1317 filtered.len()
1318 );
1319 assert!(filtered.len() > 0);
1320 }
1321 }
1322
1323 fn create_test_point_cloud_for_voxel_grid() -> PointCloud<Point3f> {
1324 let mut points = Vec::new();
1325
1326 for i in 0..10 {
1328 for j in 0..10 {
1329 for k in 0..5 {
1330 let x = (i as f32 - 5.0) * 0.1;
1331 let y = (j as f32 - 5.0) * 0.1;
1332 let z = (k as f32 - 2.5) * 0.1;
1333 points.push(Point3f::new(x, y, z));
1334 }
1335 }
1336 }
1337
1338 for i in 0..5 {
1340 for j in 0..5 {
1341 let x = (i as f32 - 2.5) * 0.1;
1342 let y = (j as f32 - 2.5) * 0.1;
1343 let z = 0.0;
1344 points.push(Point3f::new(x, y, z));
1345 points.push(Point3f::new(x, y, z)); }
1347 }
1348
1349 PointCloud::from_points(points)
1350 }
1351
1352 #[tokio::test]
1353 async fn test_gpu_voxel_grid_filter_basic() {
1354 let gpu_context = match try_create_gpu_context().await {
1355 Some(ctx) => ctx,
1356 None => {
1357 println!("Skipping GPU test - no GPU available");
1358 return;
1359 }
1360 };
1361
1362 let cloud = create_test_point_cloud_for_voxel_grid();
1363 let original_count = cloud.len();
1364
1365 let filtered = gpu_voxel_grid_filter(&gpu_context, &cloud, 0.1)
1366 .await
1367 .unwrap();
1368 let filtered_count = filtered.len();
1369
1370 assert!(filtered_count < original_count);
1372 assert!(filtered_count > 0);
1373
1374 println!(
1375 "GPU voxel grid filter: {} -> {} points",
1376 original_count, filtered_count
1377 );
1378 }
1379
1380 #[tokio::test]
1381 async fn test_gpu_voxel_grid_filter_empty_cloud() {
1382 let gpu_context = match try_create_gpu_context().await {
1383 Some(ctx) => ctx,
1384 None => {
1385 println!("Skipping GPU test - no GPU available");
1386 return;
1387 }
1388 };
1389
1390 let cloud = PointCloud::<Point3f>::new();
1391 let filtered = gpu_voxel_grid_filter(&gpu_context, &cloud, 0.1)
1392 .await
1393 .unwrap();
1394
1395 assert_eq!(filtered.len(), 0);
1396 }
1397
1398 #[tokio::test]
1399 async fn test_gpu_voxel_grid_filter_single_point() {
1400 let gpu_context = match try_create_gpu_context().await {
1401 Some(ctx) => ctx,
1402 None => {
1403 println!("Skipping GPU test - no GPU available");
1404 return;
1405 }
1406 };
1407
1408 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
1409 let filtered = gpu_voxel_grid_filter(&gpu_context, &cloud, 0.1)
1410 .await
1411 .unwrap();
1412
1413 assert_eq!(filtered.len(), 1);
1415 }
1416
1417 #[tokio::test]
1418 async fn test_gpu_voxel_grid_filter_different_voxel_sizes() {
1419 let gpu_context = match try_create_gpu_context().await {
1420 Some(ctx) => ctx,
1421 None => {
1422 println!("Skipping GPU test - no GPU available");
1423 return;
1424 }
1425 };
1426
1427 let cloud = create_test_point_cloud_for_voxel_grid();
1428 let original_count = cloud.len();
1429
1430 for voxel_size in [0.05, 0.1, 0.2] {
1432 let filtered = gpu_voxel_grid_filter(&gpu_context, &cloud, voxel_size)
1433 .await
1434 .unwrap();
1435 println!(
1436 "voxel_size={}: {} -> {} points",
1437 voxel_size,
1438 original_count,
1439 filtered.len()
1440 );
1441 assert!(filtered.len() > 0);
1442 assert!(filtered.len() <= original_count);
1443 }
1444 }
1445
1446 #[tokio::test]
1447 async fn test_gpu_voxel_grid_filter_invalid_voxel_size() {
1448 let gpu_context = match try_create_gpu_context().await {
1449 Some(ctx) => ctx,
1450 None => {
1451 println!("Skipping GPU test - no GPU available");
1452 return;
1453 }
1454 };
1455
1456 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
1457 let result = gpu_voxel_grid_filter(&gpu_context, &cloud, 0.0).await;
1458 assert!(result.is_err());
1459
1460 let result = gpu_voxel_grid_filter(&gpu_context, &cloud, -1.0).await;
1461 assert!(result.is_err());
1462 }
1463
1464 #[tokio::test]
1465 async fn test_gpu_vs_cpu_performance_comparison() {
1466 let gpu_context = match try_create_gpu_context().await {
1467 Some(ctx) => ctx,
1468 None => {
1469 println!("Skipping GPU vs CPU test - no GPU available");
1470 return;
1471 }
1472 };
1473
1474 let cloud = create_test_point_cloud_for_radius_outlier();
1475
1476 let gpu_start = Instant::now();
1478 let gpu_filtered = gpu_radius_outlier_removal(&gpu_context, &cloud, 0.5, 3)
1479 .await
1480 .unwrap();
1481 let gpu_time = gpu_start.elapsed();
1482
1483 let gpu_voxel_start = Instant::now();
1485 let gpu_voxel_filtered = gpu_voxel_grid_filter(&gpu_context, &cloud, 0.1)
1486 .await
1487 .unwrap();
1488 let gpu_voxel_time = gpu_voxel_start.elapsed();
1489
1490 println!(
1491 "GPU radius outlier removal: {} -> {} points in {:?}",
1492 cloud.len(),
1493 gpu_filtered.len(),
1494 gpu_time
1495 );
1496 println!(
1497 "GPU voxel grid filter: {} -> {} points in {:?}",
1498 cloud.len(),
1499 gpu_voxel_filtered.len(),
1500 gpu_voxel_time
1501 );
1502
1503 assert!(gpu_filtered.len() > 0);
1505 assert!(gpu_voxel_filtered.len() > 0);
1506
1507 assert!(gpu_time.as_secs() < 10);
1509 assert!(gpu_voxel_time.as_secs() < 10);
1510 }
1511}