1use crate::KdTree;
4use nalgebra::Vector4;
5use rand::prelude::*;
6use rand::rng;
7use rayon::prelude::*;
8use std::collections::{HashSet, VecDeque};
9use threecrate_core::NearestNeighborSearch;
10use threecrate_core::{Error, Point3f, PointCloud, Result, Vector3f};
11
12#[derive(Debug, Clone, PartialEq)]
14pub struct PlaneModel {
15 pub coefficients: Vector4<f32>,
17}
18
19impl PlaneModel {
20 pub fn new(a: f32, b: f32, c: f32, d: f32) -> Self {
22 Self {
23 coefficients: Vector4::new(a, b, c, d),
24 }
25 }
26
27 pub fn from_points(p1: &Point3f, p2: &Point3f, p3: &Point3f) -> Option<Self> {
29 let v1 = p2 - p1;
31 let v2 = p3 - p1;
32
33 let normal = v1.cross(&v2);
35
36 if normal.magnitude() < 1e-8 {
38 return None;
39 }
40
41 let normal = normal.normalize();
42
43 let d = -normal.dot(&p1.coords);
45
46 Some(PlaneModel::new(normal.x, normal.y, normal.z, d))
47 }
48
49 pub fn normal(&self) -> Vector3f {
51 Vector3f::new(
52 self.coefficients.x,
53 self.coefficients.y,
54 self.coefficients.z,
55 )
56 }
57
58 pub fn distance_to_point(&self, point: &Point3f) -> f32 {
60 let normal = self.normal();
61 let normal_magnitude = normal.magnitude();
62
63 if normal_magnitude < 1e-8 {
64 return f32::INFINITY;
65 }
66
67 (self.coefficients.x * point.x
68 + self.coefficients.y * point.y
69 + self.coefficients.z * point.z
70 + self.coefficients.w)
71 .abs()
72 / normal_magnitude
73 }
74
75 pub fn count_inliers(&self, points: &[Point3f], threshold: f32) -> usize {
77 points
78 .iter()
79 .filter(|point| self.distance_to_point(point) <= threshold)
80 .count()
81 }
82
83 pub fn get_inliers(&self, points: &[Point3f], threshold: f32) -> Vec<usize> {
85 points
86 .iter()
87 .enumerate()
88 .filter(|(_, point)| self.distance_to_point(point) <= threshold)
89 .map(|(i, _)| i)
90 .collect()
91 }
92}
93
94#[derive(Debug, Clone)]
96pub struct PlaneSegmentationResult {
97 pub model: PlaneModel,
99 pub inliers: Vec<usize>,
101 pub iterations: usize,
103}
104
105pub fn segment_plane(
118 cloud: &PointCloud<Point3f>,
119 threshold: f32,
120 max_iters: usize,
121) -> Result<PlaneSegmentationResult> {
122 if cloud.len() < 3 {
123 return Err(Error::InvalidData(
124 "Need at least 3 points for plane segmentation".to_string(),
125 ));
126 }
127
128 if threshold <= 0.0 {
129 return Err(Error::InvalidData("Threshold must be positive".to_string()));
130 }
131
132 if max_iters == 0 {
133 return Err(Error::InvalidData(
134 "Max iterations must be positive".to_string(),
135 ));
136 }
137
138 let points = &cloud.points;
139 let mut rng = rng();
140 let mut best_model: Option<PlaneModel> = None;
141 let mut best_inliers = Vec::new();
142 let mut best_score = 0;
143
144 for _iteration in 0..max_iters {
145 let mut indices = HashSet::new();
147 while indices.len() < 3 {
148 indices.insert(rng.random_range(0..points.len()));
149 }
150 let indices: Vec<usize> = indices.into_iter().collect();
151
152 let p1 = &points[indices[0]];
153 let p2 = &points[indices[1]];
154 let p3 = &points[indices[2]];
155
156 if let Some(model) = PlaneModel::from_points(p1, p2, p3) {
158 let inlier_count = model.count_inliers(points, threshold);
160
161 if inlier_count > best_score {
163 best_score = inlier_count;
164 best_inliers = model.get_inliers(points, threshold);
165 best_model = Some(model);
166 }
167 }
168 }
169
170 match best_model {
171 Some(model) => Ok(PlaneSegmentationResult {
172 model,
173 inliers: best_inliers,
174 iterations: max_iters,
175 }),
176 None => Err(Error::Algorithm(
177 "Failed to find valid plane model".to_string(),
178 )),
179 }
180}
181
182pub fn segment_plane_parallel(
195 cloud: &PointCloud<Point3f>,
196 threshold: f32,
197 max_iters: usize,
198) -> Result<PlaneSegmentationResult> {
199 if cloud.len() < 3 {
200 return Err(Error::InvalidData(
201 "Need at least 3 points for plane segmentation".to_string(),
202 ));
203 }
204
205 if threshold <= 0.0 {
206 return Err(Error::InvalidData("Threshold must be positive".to_string()));
207 }
208
209 if max_iters == 0 {
210 return Err(Error::InvalidData(
211 "Max iterations must be positive".to_string(),
212 ));
213 }
214
215 let points = &cloud.points;
216
217 let results: Vec<_> = (0..max_iters)
219 .into_par_iter()
220 .filter_map(|_| {
221 let mut rng = rng();
222
223 let mut indices = HashSet::new();
225 while indices.len() < 3 {
226 indices.insert(rng.random_range(0..points.len()));
227 }
228 let indices: Vec<usize> = indices.into_iter().collect();
229
230 let p1 = &points[indices[0]];
231 let p2 = &points[indices[1]];
232 let p3 = &points[indices[2]];
233
234 PlaneModel::from_points(p1, p2, p3).map(|model| {
236 let inliers = model.get_inliers(points, threshold);
237 let score = inliers.len();
238 (model, inliers, score)
239 })
240 })
241 .collect();
242
243 let best = results.into_iter().max_by_key(|(_, _, score)| *score);
245
246 match best {
247 Some((model, inliers, _)) => Ok(PlaneSegmentationResult {
248 model,
249 inliers,
250 iterations: max_iters,
251 }),
252 None => Err(Error::Algorithm(
253 "Failed to find valid plane model".to_string(),
254 )),
255 }
256}
257
258#[deprecated(note = "Use segment_plane instead which returns a complete result")]
260pub fn segment_plane_legacy(cloud: &PointCloud<Point3f>, threshold: f32) -> Result<Vec<usize>> {
261 let result = segment_plane(cloud, threshold, 1000)?;
262 Ok(result.inliers)
263}
264
265pub fn segment_plane_ransac(
298 cloud: &PointCloud<Point3f>,
299 max_iters: usize,
300 threshold: f32,
301) -> Result<(Vector4<f32>, Vec<usize>)> {
302 let result = segment_plane(cloud, threshold, max_iters)?;
303 Ok((result.model.coefficients, result.inliers))
304}
305
306pub fn plane_segmentation_ransac(
319 cloud: &PointCloud<Point3f>,
320 max_iters: usize,
321 threshold: f32,
322) -> Result<(Vector4<f32>, Vec<usize>)> {
323 segment_plane_ransac(cloud, max_iters, threshold)
324}
325
326#[derive(Debug, Clone)]
328pub struct EuclideanClusterConfig {
329 pub tolerance: f32,
331 pub min_cluster_size: usize,
333 pub max_cluster_size: usize,
335}
336
337impl Default for EuclideanClusterConfig {
338 fn default() -> Self {
339 Self {
340 tolerance: 0.02,
341 min_cluster_size: 100,
342 max_cluster_size: 25000,
343 }
344 }
345}
346
347impl EuclideanClusterConfig {
348 pub fn new(tolerance: f32, min_cluster_size: usize, max_cluster_size: usize) -> Self {
350 Self {
351 tolerance,
352 min_cluster_size,
353 max_cluster_size,
354 }
355 }
356}
357
358#[derive(Debug, Clone)]
360pub struct ClusterExtractionResult {
361 pub clusters: Vec<Vec<usize>>,
364}
365
366impl ClusterExtractionResult {
367 pub fn num_clusters(&self) -> usize {
369 self.clusters.len()
370 }
371
372 pub fn get_cluster_cloud(
374 &self,
375 cloud: &PointCloud<Point3f>,
376 index: usize,
377 ) -> Option<PointCloud<Point3f>> {
378 self.clusters.get(index).map(|indices| {
379 PointCloud::from_points(indices.iter().map(|&i| cloud.points[i]).collect())
380 })
381 }
382}
383
384pub fn extract_euclidean_clusters(
397 cloud: &PointCloud<Point3f>,
398 config: &EuclideanClusterConfig,
399) -> Result<ClusterExtractionResult> {
400 if cloud.is_empty() {
401 return Err(Error::InvalidData("Point cloud is empty".to_string()));
402 }
403 if config.tolerance <= 0.0 {
404 return Err(Error::InvalidData("Tolerance must be positive".to_string()));
405 }
406 if config.min_cluster_size == 0 {
407 return Err(Error::InvalidData(
408 "min_cluster_size must be at least 1".to_string(),
409 ));
410 }
411 if config.min_cluster_size > config.max_cluster_size {
412 return Err(Error::InvalidData(
413 "min_cluster_size must not exceed max_cluster_size".to_string(),
414 ));
415 }
416
417 let points = &cloud.points;
418 let kdtree = KdTree::new(points)?;
419
420 let mut visited = vec![false; points.len()];
421 let mut clusters: Vec<Vec<usize>> = Vec::new();
422
423 for seed_idx in 0..points.len() {
424 if visited[seed_idx] {
425 continue;
426 }
427
428 let mut cluster = Vec::new();
430 let mut queue = VecDeque::new();
431
432 visited[seed_idx] = true;
433 queue.push_back(seed_idx);
434
435 while let Some(current) = queue.pop_front() {
436 cluster.push(current);
437
438 let neighbors = kdtree.find_radius_neighbors(&points[current], config.tolerance);
439 for (neighbor_idx, _dist) in neighbors {
440 if !visited[neighbor_idx] {
441 visited[neighbor_idx] = true;
442 queue.push_back(neighbor_idx);
443 }
444 }
445 }
446
447 if cluster.len() >= config.min_cluster_size && cluster.len() <= config.max_cluster_size {
448 clusters.push(cluster);
449 }
450 }
451
452 clusters.sort_by(|a, b| b.len().cmp(&a.len()));
454
455 Ok(ClusterExtractionResult { clusters })
456}
457
458pub fn extract_euclidean_clusters_parallel(
471 cloud: &PointCloud<Point3f>,
472 config: &EuclideanClusterConfig,
473) -> Result<ClusterExtractionResult> {
474 if cloud.is_empty() {
475 return Err(Error::InvalidData("Point cloud is empty".to_string()));
476 }
477 if config.tolerance <= 0.0 {
478 return Err(Error::InvalidData("Tolerance must be positive".to_string()));
479 }
480 if config.min_cluster_size == 0 {
481 return Err(Error::InvalidData(
482 "min_cluster_size must be at least 1".to_string(),
483 ));
484 }
485 if config.min_cluster_size > config.max_cluster_size {
486 return Err(Error::InvalidData(
487 "min_cluster_size must not exceed max_cluster_size".to_string(),
488 ));
489 }
490
491 let points = &cloud.points;
492 let kdtree = KdTree::new(points)?;
493 let tolerance = config.tolerance;
494
495 let neighbor_lists: Vec<Vec<usize>> = (0..points.len())
497 .into_par_iter()
498 .map(|i| {
499 kdtree
500 .find_radius_neighbors(&points[i], tolerance)
501 .into_iter()
502 .map(|(idx, _)| idx)
503 .collect()
504 })
505 .collect();
506
507 let mut visited = vec![false; points.len()];
508 let mut clusters: Vec<Vec<usize>> = Vec::new();
509
510 for seed_idx in 0..points.len() {
511 if visited[seed_idx] {
512 continue;
513 }
514
515 let mut cluster = Vec::new();
516 let mut queue = VecDeque::new();
517
518 visited[seed_idx] = true;
519 queue.push_back(seed_idx);
520
521 while let Some(current) = queue.pop_front() {
522 cluster.push(current);
523 for &neighbor_idx in &neighbor_lists[current] {
524 if !visited[neighbor_idx] {
525 visited[neighbor_idx] = true;
526 queue.push_back(neighbor_idx);
527 }
528 }
529 }
530
531 if cluster.len() >= config.min_cluster_size && cluster.len() <= config.max_cluster_size {
532 clusters.push(cluster);
533 }
534 }
535
536 clusters.sort_by(|a, b| b.len().cmp(&a.len()));
537
538 Ok(ClusterExtractionResult { clusters })
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544 use approx::assert_relative_eq;
545
546 #[test]
547 fn test_plane_model_from_points() {
548 let p1 = Point3f::new(0.0, 0.0, 0.0);
550 let p2 = Point3f::new(1.0, 0.0, 0.0);
551 let p3 = Point3f::new(0.0, 1.0, 0.0);
552
553 let model = PlaneModel::from_points(&p1, &p2, &p3).unwrap();
554
555 let normal = model.normal();
557 assert!(
558 normal.z.abs() > 0.9,
559 "Normal should be primarily in Z direction: {:?}",
560 normal
561 );
562
563 assert!(model.distance_to_point(&p1) < 1e-6);
565 assert!(model.distance_to_point(&p2) < 1e-6);
566 assert!(model.distance_to_point(&p3) < 1e-6);
567 }
568
569 #[test]
570 fn test_plane_model_collinear_points() {
571 let p1 = Point3f::new(0.0, 0.0, 0.0);
573 let p2 = Point3f::new(1.0, 0.0, 0.0);
574 let p3 = Point3f::new(2.0, 0.0, 0.0);
575
576 let model = PlaneModel::from_points(&p1, &p2, &p3);
577 assert!(model.is_none(), "Should return None for collinear points");
578 }
579
580 #[test]
581 fn test_plane_distance_calculation() {
582 let model = PlaneModel::new(0.0, 0.0, 1.0, -1.0);
584
585 let point_on_plane = Point3f::new(0.0, 0.0, 1.0);
586 let point_above_plane = Point3f::new(0.0, 0.0, 2.0);
587 let point_below_plane = Point3f::new(0.0, 0.0, 0.0);
588
589 assert_relative_eq!(
590 model.distance_to_point(&point_on_plane),
591 0.0,
592 epsilon = 1e-6
593 );
594 assert_relative_eq!(
595 model.distance_to_point(&point_above_plane),
596 1.0,
597 epsilon = 1e-6
598 );
599 assert_relative_eq!(
600 model.distance_to_point(&point_below_plane),
601 1.0,
602 epsilon = 1e-6
603 );
604 }
605
606 #[test]
607 fn test_segment_plane_simple() {
608 let mut cloud = PointCloud::new();
610
611 for i in 0..10 {
613 for j in 0..10 {
614 cloud.push(Point3f::new(i as f32, j as f32, 0.0));
615 }
616 }
617
618 cloud.push(Point3f::new(5.0, 5.0, 10.0));
620 cloud.push(Point3f::new(5.0, 5.0, -10.0));
621
622 let result = segment_plane(&cloud, 0.1, 100).unwrap();
623
624 assert!(
626 result.inliers.len() >= 95,
627 "Should find most points as inliers"
628 );
629
630 let normal = result.model.normal();
632 assert!(
633 normal.z.abs() > 0.9,
634 "Normal should be primarily in Z direction"
635 );
636 }
637
638 #[test]
639 fn test_segment_plane_insufficient_points() {
640 let mut cloud = PointCloud::new();
641 cloud.push(Point3f::new(0.0, 0.0, 0.0));
642 cloud.push(Point3f::new(1.0, 0.0, 0.0));
643
644 let result = segment_plane(&cloud, 0.1, 100);
645 assert!(result.is_err(), "Should fail with insufficient points");
646 }
647
648 #[test]
649 fn test_segment_plane_invalid_threshold() {
650 let mut cloud = PointCloud::new();
651 cloud.push(Point3f::new(0.0, 0.0, 0.0));
652 cloud.push(Point3f::new(1.0, 0.0, 0.0));
653 cloud.push(Point3f::new(0.0, 1.0, 0.0));
654
655 let result = segment_plane(&cloud, -0.1, 100);
656 assert!(result.is_err(), "Should fail with negative threshold");
657 }
658
659 #[test]
660 fn test_segment_plane_parallel() {
661 let mut cloud = PointCloud::new();
663
664 for i in 0..10 {
666 for j in 0..10 {
667 cloud.push(Point3f::new(i as f32, j as f32, 0.0));
668 }
669 }
670
671 let result = segment_plane_parallel(&cloud, 0.1, 100).unwrap();
672
673 assert!(
675 result.inliers.len() >= 95,
676 "Should find most points as inliers"
677 );
678 }
679
680 #[test]
681 fn test_segment_plane_ransac_simple() {
682 let mut cloud = PointCloud::new();
684
685 for i in 0..10 {
687 for j in 0..10 {
688 cloud.push(Point3f::new(i as f32, j as f32, 0.0));
689 }
690 }
691
692 cloud.push(Point3f::new(5.0, 5.0, 10.0));
694 cloud.push(Point3f::new(5.0, 5.0, -10.0));
695
696 let (coefficients, inliers) = segment_plane_ransac(&cloud, 100, 0.1).unwrap();
697
698 assert!(inliers.len() >= 95, "Should find most points as inliers");
700
701 let normal = Vector3f::new(coefficients.x, coefficients.y, coefficients.z);
703 assert!(
704 normal.z.abs() > 0.9,
705 "Normal should be primarily in Z direction: {:?}",
706 normal
707 );
708 }
709
710 #[test]
711 fn test_segment_plane_ransac_noisy() {
712 let mut cloud = PointCloud::new();
714 let mut rng = rng();
715
716 for i in 0..20 {
718 for j in 0..20 {
719 let x = i as f32;
720 let y = j as f32;
721 let z = rng.random_range(-0.05..0.05); cloud.push(Point3f::new(x, y, z));
723 }
724 }
725
726 for _ in 0..20 {
728 let x = rng.random_range(0.0..20.0);
729 let y = rng.random_range(0.0..20.0);
730 let z = rng.random_range(1.0..5.0); cloud.push(Point3f::new(x, y, z));
732 }
733
734 let (coefficients, inliers) = segment_plane_ransac(&cloud, 1000, 0.1).unwrap();
735
736 assert!(
738 inliers.len() >= 350,
739 "Should find most planar points as inliers"
740 );
741
742 let normal = Vector3f::new(coefficients.x, coefficients.y, coefficients.z);
744 assert!(
745 normal.z.abs() > 0.8,
746 "Normal should be primarily in Z direction: {:?}",
747 normal
748 );
749
750 let outlier_indices: Vec<usize> = (400..420).collect();
752 let outlier_inliers: Vec<usize> = inliers
753 .iter()
754 .filter(|&&idx| outlier_indices.contains(&idx))
755 .cloned()
756 .collect();
757 assert!(
758 outlier_inliers.len() <= 2,
759 "Should not include many outliers in inliers"
760 );
761 }
762
763 #[test]
764 fn test_segment_plane_ransac_tilted_plane() {
765 let mut cloud = PointCloud::new();
767 let mut rng = rng();
768
769 for i in 0..15 {
771 for j in 0..15 {
772 let x = i as f32;
773 let y = j as f32;
774 let z = -(x + y); let noise_x = rng.random_range(-0.02..0.02);
778 let noise_y = rng.random_range(-0.02..0.02);
779 let noise_z = rng.random_range(-0.02..0.02);
780
781 cloud.push(Point3f::new(x + noise_x, y + noise_y, z + noise_z));
782 }
783 }
784
785 for _ in 0..30 {
787 let x = rng.random_range(0.0..15.0);
788 let y = rng.random_range(0.0..15.0);
789 let z = rng.random_range(5.0..10.0); cloud.push(Point3f::new(x, y, z));
791 }
792
793 let (coefficients, inliers) = segment_plane_ransac(&cloud, 1000, 0.1).unwrap();
794
795 assert!(
797 inliers.len() >= 200,
798 "Should find most planar points as inliers"
799 );
800
801 let normal = Vector3f::new(coefficients.x, coefficients.y, coefficients.z);
803 let expected_normal = Vector3f::new(1.0, 1.0, 1.0).normalize();
804 let dot_product = normal.dot(&expected_normal).abs();
805 assert!(
806 dot_product > 0.8,
807 "Normal should be close to expected direction: {:?}",
808 normal
809 );
810 }
811
812 #[test]
813 fn test_plane_segmentation_ransac_alias() {
814 let mut cloud = PointCloud::new();
816
817 for i in 0..5 {
819 for j in 0..5 {
820 cloud.push(Point3f::new(i as f32, j as f32, 0.0));
821 }
822 }
823
824 let result1 = segment_plane_ransac(&cloud, 100, 0.1).unwrap();
825 let result2 = plane_segmentation_ransac(&cloud, 100, 0.1).unwrap();
826
827 assert!(result1.1.len() >= 20, "Should find most points as inliers");
829 assert!(result2.1.len() >= 20, "Should find most points as inliers");
830
831 let diff = (result1.1.len() as i32 - result2.1.len() as i32).abs();
833 assert!(
834 diff <= 5,
835 "Inlier counts should be similar: {} vs {}",
836 result1.1.len(),
837 result2.1.len()
838 );
839 }
840
841 #[test]
842 fn test_segment_plane_ransac_insufficient_points() {
843 let mut cloud = PointCloud::new();
844 cloud.push(Point3f::new(0.0, 0.0, 0.0));
845 cloud.push(Point3f::new(1.0, 0.0, 0.0));
846
847 let result = segment_plane_ransac(&cloud, 100, 0.1);
848 assert!(result.is_err(), "Should fail with insufficient points");
849 }
850
851 #[test]
852 fn test_segment_plane_ransac_invalid_threshold() {
853 let mut cloud = PointCloud::new();
854 cloud.push(Point3f::new(0.0, 0.0, 0.0));
855 cloud.push(Point3f::new(1.0, 0.0, 0.0));
856 cloud.push(Point3f::new(0.0, 1.0, 0.0));
857
858 let result = segment_plane_ransac(&cloud, 100, -0.1);
859 assert!(result.is_err(), "Should fail with negative threshold");
860 }
861
862 #[test]
863 fn test_segment_plane_ransac_zero_iterations() {
864 let mut cloud = PointCloud::new();
865 cloud.push(Point3f::new(0.0, 0.0, 0.0));
866 cloud.push(Point3f::new(1.0, 0.0, 0.0));
867 cloud.push(Point3f::new(0.0, 1.0, 0.0));
868
869 let result = segment_plane_ransac(&cloud, 0, 0.1);
870 assert!(result.is_err(), "Should fail with zero iterations");
871 }
872
873 fn make_sphere_cloud(center: Point3f, radius: f32, count: usize) -> Vec<Point3f> {
876 let mut rng = rng();
877 let mut pts = Vec::with_capacity(count);
878 while pts.len() < count {
879 let x: f32 = rng.random_range(-radius..radius);
880 let y: f32 = rng.random_range(-radius..radius);
881 let z: f32 = rng.random_range(-radius..radius);
882 if x * x + y * y + z * z <= radius * radius {
883 pts.push(Point3f::new(center.x + x, center.y + y, center.z + z));
884 }
885 }
886 pts
887 }
888
889 #[test]
890 fn test_euclidean_cluster_two_blobs() {
891 let mut cloud = PointCloud::new();
892 for p in make_sphere_cloud(Point3f::new(0.0, 0.0, 0.0), 0.3, 200) {
894 cloud.push(p);
895 }
896 for p in make_sphere_cloud(Point3f::new(10.0, 0.0, 0.0), 0.3, 150) {
898 cloud.push(p);
899 }
900
901 let config = EuclideanClusterConfig::new(0.5, 50, 10000);
902 let result = extract_euclidean_clusters(&cloud, &config).unwrap();
903
904 assert_eq!(result.num_clusters(), 2, "Should detect exactly 2 clusters");
905 assert!(result.clusters[0].len() >= result.clusters[1].len());
907 }
908
909 #[test]
910 fn test_euclidean_cluster_three_blobs() {
911 let mut cloud = PointCloud::new();
912 for p in make_sphere_cloud(Point3f::new(0.0, 0.0, 0.0), 0.4, 300) {
913 cloud.push(p);
914 }
915 for p in make_sphere_cloud(Point3f::new(5.0, 0.0, 0.0), 0.4, 200) {
916 cloud.push(p);
917 }
918 for p in make_sphere_cloud(Point3f::new(0.0, 5.0, 0.0), 0.4, 100) {
919 cloud.push(p);
920 }
921
922 let config = EuclideanClusterConfig::new(0.6, 50, 10000);
923 let result = extract_euclidean_clusters(&cloud, &config).unwrap();
924
925 assert_eq!(result.num_clusters(), 3);
926 }
927
928 #[test]
929 fn test_euclidean_cluster_min_size_filter() {
930 let mut cloud = PointCloud::new();
931 for p in make_sphere_cloud(Point3f::new(0.0, 0.0, 0.0), 0.4, 300) {
933 cloud.push(p);
934 }
935 for p in make_sphere_cloud(Point3f::new(10.0, 0.0, 0.0), 0.2, 5) {
937 cloud.push(p);
938 }
939
940 let config = EuclideanClusterConfig::new(0.5, 50, 10000);
941 let result = extract_euclidean_clusters(&cloud, &config).unwrap();
942
943 assert_eq!(result.num_clusters(), 1, "Small cluster should be filtered");
944 }
945
946 #[test]
947 fn test_euclidean_cluster_max_size_filter() {
948 let mut cloud = PointCloud::new();
949 for p in make_sphere_cloud(Point3f::new(0.0, 0.0, 0.0), 0.5, 500) {
950 cloud.push(p);
951 }
952
953 let config = EuclideanClusterConfig::new(0.6, 1, 100);
955 let result = extract_euclidean_clusters(&cloud, &config).unwrap();
956
957 assert_eq!(
959 result.num_clusters(),
960 0,
961 "Oversized cluster should be filtered"
962 );
963 }
964
965 #[test]
966 fn test_euclidean_cluster_get_cloud() {
967 let mut cloud = PointCloud::new();
968 for p in make_sphere_cloud(Point3f::new(0.0, 0.0, 0.0), 0.3, 200) {
969 cloud.push(p);
970 }
971 for p in make_sphere_cloud(Point3f::new(10.0, 0.0, 0.0), 0.3, 100) {
972 cloud.push(p);
973 }
974
975 let config = EuclideanClusterConfig::new(0.5, 50, 10000);
976 let result = extract_euclidean_clusters(&cloud, &config).unwrap();
977
978 let sub = result.get_cluster_cloud(&cloud, 0).unwrap();
979 assert_eq!(sub.len(), result.clusters[0].len());
980 }
981
982 #[test]
983 fn test_euclidean_cluster_parallel_matches_sequential() {
984 let mut cloud = PointCloud::new();
985 for p in make_sphere_cloud(Point3f::new(0.0, 0.0, 0.0), 0.3, 200) {
986 cloud.push(p);
987 }
988 for p in make_sphere_cloud(Point3f::new(5.0, 0.0, 0.0), 0.3, 150) {
989 cloud.push(p);
990 }
991
992 let config = EuclideanClusterConfig::new(0.5, 50, 10000);
993 let seq = extract_euclidean_clusters(&cloud, &config).unwrap();
994 let par = extract_euclidean_clusters_parallel(&cloud, &config).unwrap();
995
996 assert_eq!(seq.num_clusters(), par.num_clusters());
997 for (s, p) in seq.clusters.iter().zip(par.clusters.iter()) {
998 assert_eq!(s.len(), p.len());
999 }
1000 }
1001
1002 #[test]
1003 fn test_euclidean_cluster_invalid_config() {
1004 let mut cloud = PointCloud::new();
1005 cloud.push(Point3f::new(0.0, 0.0, 0.0));
1006
1007 assert!(
1008 extract_euclidean_clusters(&cloud, &EuclideanClusterConfig::new(-1.0, 1, 100)).is_err()
1009 );
1010 assert!(
1011 extract_euclidean_clusters(&cloud, &EuclideanClusterConfig::new(0.1, 0, 100)).is_err()
1012 );
1013 assert!(
1014 extract_euclidean_clusters(&cloud, &EuclideanClusterConfig::new(0.1, 10, 5)).is_err()
1015 );
1016 }
1017
1018 #[test]
1019 fn test_euclidean_cluster_empty_cloud() {
1020 let cloud: PointCloud<Point3f> = PointCloud::new();
1021 let config = EuclideanClusterConfig::default();
1022 assert!(extract_euclidean_clusters(&cloud, &config).is_err());
1023 }
1024}