spatialrust_segmentation/
plane_gpu.rs1use spatialrust_core::{HasPositions3, PointCloud, SpatialError, SpatialResult};
2use spatialrust_gpu::{score_ransac_plane_hypotheses_gpu, WgpuRuntime};
3use spatialrust_math::Vec3;
4
5use crate::plane::{
6 finalize_plane_segmentation, PlaneModel, RansacPlaneConfig, RansacPlaneSegmentation,
7};
8use crate::plane_ransac::{collect_inliers, generate_hypotheses};
9use crate::segmenter::PointCloudSegmenter;
10
11#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct GpuRansacPlaneSegmenter {
17 config: RansacPlaneConfig,
18}
19
20impl GpuRansacPlaneSegmenter {
21 #[must_use]
23 pub const fn new(config: RansacPlaneConfig) -> Self {
24 Self { config }
25 }
26
27 #[must_use]
29 pub const fn config(&self) -> RansacPlaneConfig {
30 self.config
31 }
32
33 pub fn segment(&self, input: &PointCloud) -> SpatialResult<RansacPlaneSegmentation> {
35 if input.is_empty() {
36 return Err(SpatialError::InvalidArgument(
37 "cannot segment plane from empty point cloud".to_owned(),
38 ));
39 }
40
41 let (x, y, z) = input.positions3()?;
42 let len = input.len();
43 if len < 3 {
44 return Err(SpatialError::InvalidArgument(
45 "plane segmentation requires at least three points".to_owned(),
46 ));
47 }
48
49 let runtime = WgpuRuntime::shared()?;
50 let hypotheses_usize =
51 generate_hypotheses(len, self.config.max_iterations, self.config.seed);
52 let hypotheses_u32: Vec<[u32; 3]> = hypotheses_usize
53 .iter()
54 .map(|indices| [indices[0] as u32, indices[1] as u32, indices[2] as u32])
55 .collect();
56
57 let scores = score_ransac_plane_hypotheses_gpu(
58 &runtime,
59 x,
60 y,
61 z,
62 &hypotheses_u32,
63 self.config.distance_threshold,
64 )?;
65
66 let (best_index, best_count) = scores
67 .iter()
68 .enumerate()
69 .map(|(index, score)| (index, score.inlier_count as usize))
70 .max_by_key(|(_, count)| *count)
71 .unwrap_or((0, 0));
72
73 let best_score = &scores[best_index];
74 let best_model = PlaneModel {
75 normal: Vec3::new(best_score.normal[0], best_score.normal[1], best_score.normal[2]),
76 d: best_score.d,
77 };
78 let best_inliers = if best_count > 0 {
79 collect_inliers(x, y, z, &best_model, self.config.distance_threshold)
80 } else {
81 Vec::new()
82 };
83
84 finalize_plane_segmentation(input, x, y, z, &self.config, best_inliers, Some(best_model))
85 }
86}
87
88impl PointCloudSegmenter for GpuRansacPlaneSegmenter {
89 fn name(&self) -> &'static str {
90 "GpuRansacPlaneSegmenter"
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::GpuRansacPlaneSegmenter;
97 use crate::plane::{RansacPlaneConfig, RansacPlaneSegmenter};
98 use spatialrust_core::{PointCloudBuilder, StandardSchemas};
99 use spatialrust_gpu::WgpuRuntime;
100
101 fn plane_with_outliers() -> spatialrust_core::PointCloud {
102 let mut builder = PointCloudBuilder::new(StandardSchemas::point_xyz());
103 for x in 0..10 {
104 for y in 0..10 {
105 builder.push_point([x as f32, y as f32, 0.0]).unwrap();
106 }
107 }
108 builder.push_point([0.0, 0.0, 5.0]).unwrap();
109 builder.push_point([1.0, 1.0, 5.0]).unwrap();
110 builder.build().unwrap()
111 }
112
113 #[test]
114 fn gpu_matches_cpu_on_planar_patch() {
115 if WgpuRuntime::shared().is_err() {
116 return;
117 }
118
119 let input = plane_with_outliers();
120 let config = RansacPlaneConfig {
121 distance_threshold: 0.05,
122 max_iterations: 500,
123 min_inliers: 50,
124 seed: 7,
125 ..Default::default()
126 };
127 let cpu = RansacPlaneSegmenter::new(config).segment(&input).unwrap();
128 let gpu = GpuRansacPlaneSegmenter::new(config).segment(&input).unwrap();
129 assert_eq!(cpu.inlier_count, gpu.inlier_count);
130 assert_eq!(cpu.outliers.len(), gpu.outliers.len());
131 assert!((cpu.model.normal.z - gpu.model.normal.z).abs() < 1e-3);
132 }
133}