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