1use threecrate_core::{PointCloud, Result, Point3f, Vector3f, Error};
4use nalgebra::{Vector4};
5use rayon::prelude::*;
6use rand::prelude::*;
7use std::collections::HashSet;
8
9#[derive(Debug, Clone, PartialEq)]
11pub struct PlaneModel {
12 pub coefficients: Vector4<f32>,
14}
15
16impl PlaneModel {
17 pub fn new(a: f32, b: f32, c: f32, d: f32) -> Self {
19 Self {
20 coefficients: Vector4::new(a, b, c, d),
21 }
22 }
23
24 pub fn from_points(p1: &Point3f, p2: &Point3f, p3: &Point3f) -> Option<Self> {
26 let v1 = p2 - p1;
28 let v2 = p3 - p1;
29
30 let normal = v1.cross(&v2);
32
33 if normal.magnitude() < 1e-8 {
35 return None;
36 }
37
38 let normal = normal.normalize();
39
40 let d = -normal.dot(&p1.coords);
42
43 Some(PlaneModel::new(normal.x, normal.y, normal.z, d))
44 }
45
46 pub fn normal(&self) -> Vector3f {
48 Vector3f::new(
49 self.coefficients.x,
50 self.coefficients.y,
51 self.coefficients.z,
52 )
53 }
54
55 pub fn distance_to_point(&self, point: &Point3f) -> f32 {
57 let normal = self.normal();
58 let normal_magnitude = normal.magnitude();
59
60 if normal_magnitude < 1e-8 {
61 return f32::INFINITY;
62 }
63
64 (self.coefficients.x * point.x +
65 self.coefficients.y * point.y +
66 self.coefficients.z * point.z +
67 self.coefficients.w).abs() / normal_magnitude
68 }
69
70 pub fn count_inliers(&self, points: &[Point3f], threshold: f32) -> usize {
72 points.iter()
73 .filter(|point| self.distance_to_point(point) <= threshold)
74 .count()
75 }
76
77 pub fn get_inliers(&self, points: &[Point3f], threshold: f32) -> Vec<usize> {
79 points.iter()
80 .enumerate()
81 .filter(|(_, point)| self.distance_to_point(point) <= threshold)
82 .map(|(i, _)| i)
83 .collect()
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct PlaneSegmentationResult {
90 pub model: PlaneModel,
92 pub inliers: Vec<usize>,
94 pub iterations: usize,
96}
97
98pub fn segment_plane(
111 cloud: &PointCloud<Point3f>,
112 threshold: f32,
113 max_iters: usize
114) -> Result<PlaneSegmentationResult> {
115 if cloud.len() < 3 {
116 return Err(Error::InvalidData("Need at least 3 points for plane segmentation".to_string()));
117 }
118
119 if threshold <= 0.0 {
120 return Err(Error::InvalidData("Threshold must be positive".to_string()));
121 }
122
123 if max_iters == 0 {
124 return Err(Error::InvalidData("Max iterations must be positive".to_string()));
125 }
126
127 let points = &cloud.points;
128 let mut rng = thread_rng();
129 let mut best_model: Option<PlaneModel> = None;
130 let mut best_inliers = Vec::new();
131 let mut best_score = 0;
132
133 for _iteration in 0..max_iters {
134 let mut indices = HashSet::new();
136 while indices.len() < 3 {
137 indices.insert(rng.gen_range(0..points.len()));
138 }
139 let indices: Vec<usize> = indices.into_iter().collect();
140
141 let p1 = &points[indices[0]];
142 let p2 = &points[indices[1]];
143 let p3 = &points[indices[2]];
144
145 if let Some(model) = PlaneModel::from_points(p1, p2, p3) {
147 let inlier_count = model.count_inliers(points, threshold);
149
150 if inlier_count > best_score {
152 best_score = inlier_count;
153 best_inliers = model.get_inliers(points, threshold);
154 best_model = Some(model);
155 }
156 }
157 }
158
159 match best_model {
160 Some(model) => Ok(PlaneSegmentationResult {
161 model,
162 inliers: best_inliers,
163 iterations: max_iters,
164 }),
165 None => Err(Error::Algorithm("Failed to find valid plane model".to_string())),
166 }
167}
168
169pub fn segment_plane_parallel(
182 cloud: &PointCloud<Point3f>,
183 threshold: f32,
184 max_iters: usize
185) -> Result<PlaneSegmentationResult> {
186 if cloud.len() < 3 {
187 return Err(Error::InvalidData("Need at least 3 points for plane segmentation".to_string()));
188 }
189
190 if threshold <= 0.0 {
191 return Err(Error::InvalidData("Threshold must be positive".to_string()));
192 }
193
194 if max_iters == 0 {
195 return Err(Error::InvalidData("Max iterations must be positive".to_string()));
196 }
197
198 let points = &cloud.points;
199
200 let results: Vec<_> = (0..max_iters)
202 .into_par_iter()
203 .filter_map(|_| {
204 let mut rng = thread_rng();
205
206 let mut indices = HashSet::new();
208 while indices.len() < 3 {
209 indices.insert(rng.gen_range(0..points.len()));
210 }
211 let indices: Vec<usize> = indices.into_iter().collect();
212
213 let p1 = &points[indices[0]];
214 let p2 = &points[indices[1]];
215 let p3 = &points[indices[2]];
216
217 PlaneModel::from_points(p1, p2, p3).map(|model| {
219 let inliers = model.get_inliers(points, threshold);
220 let score = inliers.len();
221 (model, inliers, score)
222 })
223 })
224 .collect();
225
226 let best = results.into_iter()
228 .max_by_key(|(_, _, score)| *score);
229
230 match best {
231 Some((model, inliers, _)) => Ok(PlaneSegmentationResult {
232 model,
233 inliers,
234 iterations: max_iters,
235 }),
236 None => Err(Error::Algorithm("Failed to find valid plane model".to_string())),
237 }
238}
239
240#[deprecated(note = "Use segment_plane instead which returns a complete result")]
242pub fn segment_plane_legacy(cloud: &PointCloud<Point3f>, threshold: f32) -> Result<Vec<usize>> {
243 let result = segment_plane(cloud, threshold, 1000)?;
244 Ok(result.inliers)
245}
246
247pub fn segment_plane_ransac(
280 cloud: &PointCloud<Point3f>,
281 max_iters: usize,
282 threshold: f32,
283) -> Result<(Vector4<f32>, Vec<usize>)> {
284 let result = segment_plane(cloud, threshold, max_iters)?;
285 Ok((result.model.coefficients, result.inliers))
286}
287
288pub fn plane_segmentation_ransac(
301 cloud: &PointCloud<Point3f>,
302 max_iters: usize,
303 threshold: f32,
304) -> Result<(Vector4<f32>, Vec<usize>)> {
305 segment_plane_ransac(cloud, max_iters, threshold)
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use approx::assert_relative_eq;
312
313 #[test]
314 fn test_plane_model_from_points() {
315 let p1 = Point3f::new(0.0, 0.0, 0.0);
317 let p2 = Point3f::new(1.0, 0.0, 0.0);
318 let p3 = Point3f::new(0.0, 1.0, 0.0);
319
320 let model = PlaneModel::from_points(&p1, &p2, &p3).unwrap();
321
322 let normal = model.normal();
324 assert!(normal.z.abs() > 0.9, "Normal should be primarily in Z direction: {:?}", normal);
325
326 assert!(model.distance_to_point(&p1) < 1e-6);
328 assert!(model.distance_to_point(&p2) < 1e-6);
329 assert!(model.distance_to_point(&p3) < 1e-6);
330 }
331
332 #[test]
333 fn test_plane_model_collinear_points() {
334 let p1 = Point3f::new(0.0, 0.0, 0.0);
336 let p2 = Point3f::new(1.0, 0.0, 0.0);
337 let p3 = Point3f::new(2.0, 0.0, 0.0);
338
339 let model = PlaneModel::from_points(&p1, &p2, &p3);
340 assert!(model.is_none(), "Should return None for collinear points");
341 }
342
343 #[test]
344 fn test_plane_distance_calculation() {
345 let model = PlaneModel::new(0.0, 0.0, 1.0, -1.0);
347
348 let point_on_plane = Point3f::new(0.0, 0.0, 1.0);
349 let point_above_plane = Point3f::new(0.0, 0.0, 2.0);
350 let point_below_plane = Point3f::new(0.0, 0.0, 0.0);
351
352 assert_relative_eq!(model.distance_to_point(&point_on_plane), 0.0, epsilon = 1e-6);
353 assert_relative_eq!(model.distance_to_point(&point_above_plane), 1.0, epsilon = 1e-6);
354 assert_relative_eq!(model.distance_to_point(&point_below_plane), 1.0, epsilon = 1e-6);
355 }
356
357 #[test]
358 fn test_segment_plane_simple() {
359 let mut cloud = PointCloud::new();
361
362 for i in 0..10 {
364 for j in 0..10 {
365 cloud.push(Point3f::new(i as f32, j as f32, 0.0));
366 }
367 }
368
369 cloud.push(Point3f::new(5.0, 5.0, 10.0));
371 cloud.push(Point3f::new(5.0, 5.0, -10.0));
372
373 let result = segment_plane(&cloud, 0.1, 100).unwrap();
374
375 assert!(result.inliers.len() >= 95, "Should find most points as inliers");
377
378 let normal = result.model.normal();
380 assert!(normal.z.abs() > 0.9, "Normal should be primarily in Z direction");
381 }
382
383 #[test]
384 fn test_segment_plane_insufficient_points() {
385 let mut cloud = PointCloud::new();
386 cloud.push(Point3f::new(0.0, 0.0, 0.0));
387 cloud.push(Point3f::new(1.0, 0.0, 0.0));
388
389 let result = segment_plane(&cloud, 0.1, 100);
390 assert!(result.is_err(), "Should fail with insufficient points");
391 }
392
393 #[test]
394 fn test_segment_plane_invalid_threshold() {
395 let mut cloud = PointCloud::new();
396 cloud.push(Point3f::new(0.0, 0.0, 0.0));
397 cloud.push(Point3f::new(1.0, 0.0, 0.0));
398 cloud.push(Point3f::new(0.0, 1.0, 0.0));
399
400 let result = segment_plane(&cloud, -0.1, 100);
401 assert!(result.is_err(), "Should fail with negative threshold");
402 }
403
404 #[test]
405 fn test_segment_plane_parallel() {
406 let mut cloud = PointCloud::new();
408
409 for i in 0..10 {
411 for j in 0..10 {
412 cloud.push(Point3f::new(i as f32, j as f32, 0.0));
413 }
414 }
415
416 let result = segment_plane_parallel(&cloud, 0.1, 100).unwrap();
417
418 assert!(result.inliers.len() >= 95, "Should find most points as inliers");
420 }
421
422 #[test]
423 fn test_segment_plane_ransac_simple() {
424 let mut cloud = PointCloud::new();
426
427 for i in 0..10 {
429 for j in 0..10 {
430 cloud.push(Point3f::new(i as f32, j as f32, 0.0));
431 }
432 }
433
434 cloud.push(Point3f::new(5.0, 5.0, 10.0));
436 cloud.push(Point3f::new(5.0, 5.0, -10.0));
437
438 let (coefficients, inliers) = segment_plane_ransac(&cloud, 100, 0.1).unwrap();
439
440 assert!(inliers.len() >= 95, "Should find most points as inliers");
442
443 let normal = Vector3f::new(coefficients.x, coefficients.y, coefficients.z);
445 assert!(normal.z.abs() > 0.9, "Normal should be primarily in Z direction: {:?}", normal);
446 }
447
448 #[test]
449 fn test_segment_plane_ransac_noisy() {
450 let mut cloud = PointCloud::new();
452 let mut rng = thread_rng();
453
454 for i in 0..20 {
456 for j in 0..20 {
457 let x = i as f32;
458 let y = j as f32;
459 let z = rng.gen_range(-0.05..0.05); cloud.push(Point3f::new(x, y, z));
461 }
462 }
463
464 for _ in 0..20 {
466 let x = rng.gen_range(0.0..20.0);
467 let y = rng.gen_range(0.0..20.0);
468 let z = rng.gen_range(1.0..5.0); cloud.push(Point3f::new(x, y, z));
470 }
471
472 let (coefficients, inliers) = segment_plane_ransac(&cloud, 1000, 0.1).unwrap();
473
474 assert!(inliers.len() >= 350, "Should find most planar points as inliers");
476
477 let normal = Vector3f::new(coefficients.x, coefficients.y, coefficients.z);
479 assert!(normal.z.abs() > 0.8, "Normal should be primarily in Z direction: {:?}", normal);
480
481 let outlier_indices: Vec<usize> = (400..420).collect();
483 let outlier_inliers: Vec<usize> = inliers.iter()
484 .filter(|&&idx| outlier_indices.contains(&idx))
485 .cloned()
486 .collect();
487 assert!(outlier_inliers.len() <= 2, "Should not include many outliers in inliers");
488 }
489
490 #[test]
491 fn test_segment_plane_ransac_tilted_plane() {
492 let mut cloud = PointCloud::new();
494 let mut rng = thread_rng();
495
496 for i in 0..15 {
498 for j in 0..15 {
499 let x = i as f32;
500 let y = j as f32;
501 let z = -(x + y); let noise_x = rng.gen_range(-0.02..0.02);
505 let noise_y = rng.gen_range(-0.02..0.02);
506 let noise_z = rng.gen_range(-0.02..0.02);
507
508 cloud.push(Point3f::new(x + noise_x, y + noise_y, z + noise_z));
509 }
510 }
511
512 for _ in 0..30 {
514 let x = rng.gen_range(0.0..15.0);
515 let y = rng.gen_range(0.0..15.0);
516 let z = rng.gen_range(5.0..10.0); cloud.push(Point3f::new(x, y, z));
518 }
519
520 let (coefficients, inliers) = segment_plane_ransac(&cloud, 1000, 0.1).unwrap();
521
522 assert!(inliers.len() >= 200, "Should find most planar points as inliers");
524
525 let normal = Vector3f::new(coefficients.x, coefficients.y, coefficients.z);
527 let expected_normal = Vector3f::new(1.0, 1.0, 1.0).normalize();
528 let dot_product = normal.dot(&expected_normal).abs();
529 assert!(dot_product > 0.8, "Normal should be close to expected direction: {:?}", normal);
530 }
531
532 #[test]
533 fn test_plane_segmentation_ransac_alias() {
534 let mut cloud = PointCloud::new();
536
537 for i in 0..5 {
539 for j in 0..5 {
540 cloud.push(Point3f::new(i as f32, j as f32, 0.0));
541 }
542 }
543
544 let result1 = segment_plane_ransac(&cloud, 100, 0.1).unwrap();
545 let result2 = plane_segmentation_ransac(&cloud, 100, 0.1).unwrap();
546
547 assert!(result1.1.len() >= 20, "Should find most points as inliers");
549 assert!(result2.1.len() >= 20, "Should find most points as inliers");
550
551 let diff = (result1.1.len() as i32 - result2.1.len() as i32).abs();
553 assert!(diff <= 5, "Inlier counts should be similar: {} vs {}", result1.1.len(), result2.1.len());
554 }
555
556 #[test]
557 fn test_segment_plane_ransac_insufficient_points() {
558 let mut cloud = PointCloud::new();
559 cloud.push(Point3f::new(0.0, 0.0, 0.0));
560 cloud.push(Point3f::new(1.0, 0.0, 0.0));
561
562 let result = segment_plane_ransac(&cloud, 100, 0.1);
563 assert!(result.is_err(), "Should fail with insufficient points");
564 }
565
566 #[test]
567 fn test_segment_plane_ransac_invalid_threshold() {
568 let mut cloud = PointCloud::new();
569 cloud.push(Point3f::new(0.0, 0.0, 0.0));
570 cloud.push(Point3f::new(1.0, 0.0, 0.0));
571 cloud.push(Point3f::new(0.0, 1.0, 0.0));
572
573 let result = segment_plane_ransac(&cloud, 100, -0.1);
574 assert!(result.is_err(), "Should fail with negative threshold");
575 }
576
577 #[test]
578 fn test_segment_plane_ransac_zero_iterations() {
579 let mut cloud = PointCloud::new();
580 cloud.push(Point3f::new(0.0, 0.0, 0.0));
581 cloud.push(Point3f::new(1.0, 0.0, 0.0));
582 cloud.push(Point3f::new(0.0, 1.0, 0.0));
583
584 let result = segment_plane_ransac(&cloud, 0, 0.1);
585 assert!(result.is_err(), "Should fail with zero iterations");
586 }
587}