pasture_algorithms/
segmentation.rs

1use std::vec;
2
3use pasture_core::{
4    layout::attributes::POSITION_3D,
5    nalgebra::Vector3, containers::{BorrowedBuffer, BorrowedBufferExt},
6};
7use rand::Rng;
8use rayon::prelude::*;
9
10/// Represents a line between two points
11/// the ranking shows how many points of the pointcloud are inliers for this specific line
12#[derive(Debug)]
13pub struct Line {
14    first: Vector3<f64>,
15    second: Vector3<f64>,
16    ranking: usize,
17}
18
19/// Represents a plane in coordinate-form: ax + by + cz + d = 0
20/// the ranking shows how many points of the pointcloud are inliers for this specific plane
21#[derive(Debug)]
22pub struct Plane {
23    a: f64,
24    b: f64,
25    c: f64,
26    d: f64,
27    ranking: usize,
28}
29
30/// calculates the distance between a point and a plane
31fn distance_point_plane(point: &Vector3<f64>, plane: &Plane) -> f64 {
32    let d = (plane.a * point.x + plane.b * point.y + plane.c * point.z + plane.d).abs();
33    let e = (plane.a * plane.a + plane.b * plane.b + plane.c * plane.c).sqrt();
34    d / e
35}
36
37/// calculates the distance between a point and a line
38/// careful: seems to be slow in debug, but is really fast in release
39fn distance_point_line(point: &Vector3<f64>, line: &Line) -> f64 {
40    (line.second - line.first)
41        .cross(&(line.first - point))
42        .norm()
43        / (line.second - line.first).norm()
44}
45
46/// generates a random plane from three points of the buffer
47fn generate_rng_plane<'a, T: BorrowedBuffer<'a>>(buffer: &'a T) -> Plane {
48    // choose three random points from the pointcloud
49    let mut rng = rand::thread_rng();
50    let rand1 = rng.gen_range(0..buffer.len());
51    let mut rand2 = rng.gen_range(0..buffer.len());
52    while rand1 == rand2 {
53        rand2 = rng.gen_range(0..buffer.len());
54    }
55    let mut rand3 = rng.gen_range(0..buffer.len());
56    // make sure we have 3 unique random numbers to generate the plane model
57    while rand2 == rand3 || rand1 == rand3 {
58        rand3 = rng.gen_range(0..buffer.len());
59    }
60    let p_a: Vector3<f64> = buffer.view_attribute(&POSITION_3D).at(rand1);
61    let p_b: Vector3<f64> = buffer.view_attribute(&POSITION_3D).at(rand2);
62    let p_c: Vector3<f64> = buffer.view_attribute(&POSITION_3D).at(rand3);
63
64    // compute plane from the three positions
65    let vec1 = p_b - p_a;
66    let vec2 = p_c - p_a;
67    let normal = vec1.cross(&vec2);
68    let d = -normal.dot(&p_a);
69    Plane {
70        a: normal.x,
71        b: normal.y,
72        c: normal.z,
73        d,
74        ranking: 0,
75    }
76}
77
78/// generates a random line from two points of the buffer
79fn generate_rng_line<'a, T: BorrowedBuffer<'a>>(buffer: &'a T) -> Line {
80    // choose two random points from the pointcloud
81    let mut rng = rand::thread_rng();
82    let rand1 = rng.gen_range(0..buffer.len());
83    let mut rand2 = rng.gen_range(0..buffer.len());
84    // make sure we have two unique points
85    while rand1 == rand2 {
86        rand2 = rng.gen_range(0..buffer.len());
87    }
88    // generate line from the two points
89    Line {
90        first: buffer.view_attribute(&POSITION_3D).at(rand1),
91        second: buffer.view_attribute(&POSITION_3D).at(rand2),
92        ranking: 0,
93    }
94}
95
96fn generate_line_model<'a, T: BorrowedBuffer<'a>>(buffer: &'a T, distance_threshold: f64) -> (Line, Vec<usize>) {
97    // generate random line from three points in the buffer
98    let mut curr_hypo = generate_rng_line(buffer);
99    let mut curr_positions = vec![];
100    // find all points that belong to the line
101    for (index, p) in buffer
102        .view_attribute::<Vector3<f64>>(&POSITION_3D)
103        .into_iter()
104        .enumerate()
105    {
106        let distance = distance_point_line(&p, &curr_hypo);
107        if distance < distance_threshold {
108            // we found a point of the line
109            curr_positions.push(index);
110            curr_hypo.ranking += 1;
111        }
112    }
113    // return current line and positions
114    (curr_hypo, curr_positions)
115}
116
117fn generate_plane_model<'a, T: BorrowedBuffer<'a>>(buffer: &'a T,
118    distance_threshold: f64,
119) -> (Plane, Vec<usize>) {
120    // generate random plane from three points in the buffer
121    let mut curr_hypo = generate_rng_plane(buffer);
122    // find all points that belong to the plane
123    let mut curr_positions = vec![];
124
125    for (index, p) in buffer
126        .view_attribute::<Vector3<f64>>(&POSITION_3D)
127        .into_iter()
128        .enumerate()
129    {
130        let distance = distance_point_plane(&p, &curr_hypo);
131        if distance < distance_threshold {
132            // we found a point that belongs to the plane
133            curr_hypo.ranking += 1;
134            curr_positions.push(index);
135        }
136    }
137    (curr_hypo, curr_positions)
138}
139
140/// Ransac Plane Segmentation in parallel.
141/// Returns the plane with the highest rating/most inliers and the associated indices of the inliers.
142/// Iterates over all points in the `buffer`.
143/// The `distance_threshold` sets the maximum distance to the plane that a point is counted as an inlier from.
144/// With `num_of_iterations` the number of iterations that the algorithm performs can be chosen.
145///
146/// # Examples
147///
148/// ```
149/// # use pasture_core::nalgebra::Vector3;
150/// # use pasture_core::containers::*;
151/// # use pasture_core::layout::PointType;
152/// # use pasture_derive::PointType;
153/// # use pasture_algorithms::segmentation::ransac_plane_par;
154/// #[repr(C)]
155/// #[derive(PointType, Debug, Copy, Clone, bytemuck::AnyBitPattern, bytemuck::NoUninit)]
156/// struct SimplePoint {
157///     #[pasture(BUILTIN_POSITION_3D)]
158///    pub position: Vector3<f64>,
159/// }
160/// let mut points = vec![];
161/// // generate some inliers
162/// for i in 0..200{
163///     points.push(SimplePoint{position: Vector3::new(0.0, f64::from(i), f64::from(i*i))});
164/// }
165/// // generate an outlier
166/// points.push(SimplePoint{position: Vector3::new(9.0, 0.0, 0.0)});
167/// let buffer = points.into_iter().collect::<HashMapBuffer>();
168/// let plane_and_indices = ransac_plane_par(&buffer, 0.5, 10);
169/// for i in 0..199{
170///     // inliers are in the plane
171///     assert!(plane_and_indices.1.contains(&(i as usize)));
172/// }
173/// // outlier is not in the plane
174/// assert!(!plane_and_indices.1.contains(&200));
175/// ```
176///
177/// # Panics
178///
179/// If the size of the buffer is < 3.
180pub fn ransac_plane_par<'a, T: BorrowedBuffer<'a> + Sync>(buffer: &'a T,
181    distance_threshold: f64,
182    num_of_iterations: usize,
183) -> (Plane, Vec<usize>) {
184    if buffer.len() < 3 {
185        panic!("buffer needs to include at least 3 points to generate a plane.");
186    }
187    // iterate in parallel over num_of_iterations
188    (0..num_of_iterations)
189        .into_par_iter()
190        .map(|_x| {
191            // generate one model for the current iteration
192            generate_plane_model(buffer, distance_threshold)
193        })
194        // get the best plane-model from all iterations (highest ranking)
195        .max_by(|(x, _y), (a, _b)| x.ranking.cmp(&a.ranking))
196        .unwrap()
197}
198
199/// Ransac Plane Segmentation in serial (for maximum speed use ransac_plane_par).
200/// Returns the plane with the highest rating/most inliers and the associated indices of the inliers.
201/// Iterates over all points in the `buffer`.
202/// The `distance_threshold` sets the maximum distance to the plane that a point is counted as an inlier from.
203/// With `num_of_iterations` the number of iterations that the algorithm performs can be chosen.
204///
205/// # Examples
206///
207/// ```
208/// # use pasture_core::nalgebra::Vector3;
209/// # use pasture_core::containers::*;
210/// # use pasture_core::layout::PointType;
211/// # use pasture_derive::PointType;
212/// # use pasture_algorithms::segmentation::ransac_plane_serial;
213/// #[repr(C)]
214/// #[derive(PointType, Debug, Copy, Clone, bytemuck::AnyBitPattern, bytemuck::NoUninit)]
215/// struct SimplePoint {
216///     #[pasture(BUILTIN_POSITION_3D)]
217///    pub position: Vector3<f64>,
218/// }
219/// let mut points = vec![];
220/// // generate some inliers
221/// for i in 0..200{
222///     points.push(SimplePoint{position: Vector3::new(0.0, f64::from(i), f64::from(i*i))});
223/// }
224/// // generate an outlier
225/// points.push(SimplePoint{position: Vector3::new(9.0, 0.0, 0.0)});
226/// let buffer = points.into_iter().collect::<HashMapBuffer>();
227/// let plane_and_indices = ransac_plane_serial(&buffer, 0.5, 10);
228/// for i in 0..199{
229///     // inliers are in the plane
230///     assert!(plane_and_indices.1.contains(&(i as usize)));
231/// }
232/// // outlier is not in the plane
233/// assert!(!plane_and_indices.1.contains(&200));
234/// ```
235///
236/// # Panics
237///
238/// If the size of the buffer is < 3.
239pub fn ransac_plane_serial<'a, T: BorrowedBuffer<'a> + Sync>(buffer: &'a T,
240    distance_threshold: f64,
241    num_of_iterations: usize,
242) -> (Plane, Vec<usize>) {
243    if buffer.len() < 3 {
244        panic!("buffer needs to include at least 3 points to generate a plane.");
245    }
246    (0..num_of_iterations)
247        .map(|_x| 
248            // generate one model for the current iteration
249            generate_plane_model(buffer, distance_threshold))
250        // get the best plane-model from all iterations (highest ranking)
251        .max_by(|(x, _y), (a, _b)| x.ranking.cmp(&a.ranking))
252        .unwrap()
253}
254
255/// Ransac Line Segmentation in parallel.
256/// Returns the line with the highest rating/most inliers and the associated indices of the inliers.
257/// Iterates over all points in the `buffer`.
258/// The `distance_threshold` sets the maximum distance to the plane that a point is counted as an inlier from.
259/// With `num_of_iterations` the number of iterations that the algorithm performs can be chosen.
260///
261/// # Examples
262///
263/// ```
264/// # use pasture_core::nalgebra::Vector3;
265/// # use pasture_core::containers::*;
266/// # use pasture_core::layout::PointType;
267/// # use pasture_derive::PointType;
268/// # use pasture_algorithms::segmentation::ransac_line_par;
269/// #[repr(C)]
270/// #[derive(PointType, Debug, Copy, Clone, bytemuck::AnyBitPattern, bytemuck::NoUninit)]
271/// struct SimplePoint {
272///     #[pasture(BUILTIN_POSITION_3D)]
273///    pub position: Vector3<f64>,
274/// }
275/// let mut points = vec![];
276/// // generate some inliers
277/// for i in 0..200{
278///     points.push(SimplePoint{position: Vector3::new(0.0, 0.0, f64::from(i))});
279/// }
280/// // generate an outlier
281/// points.push(SimplePoint{position: Vector3::new(9.0, 0.0, 0.0)});
282/// let buffer = points.into_iter().collect::<HashMapBuffer>();
283/// let line_and_indices = ransac_line_par(&buffer, 0.5, 10);
284/// for i in 0..199{
285///     // inliers are in the plane
286///     assert!(line_and_indices.1.contains(&(i as usize)));
287/// }
288/// // outlier is not in the plane
289/// assert!(!line_and_indices.1.contains(&200));
290/// ```
291///
292/// # Panics
293///
294/// If the size of the buffer is < 2.
295pub fn ransac_line_par<'a, T: BorrowedBuffer<'a> + Sync>(buffer: &'a T,
296    distance_threshold: f64,
297    num_of_iterations: usize,
298) -> (Line, Vec<usize>) {
299    if buffer.len() < 2 {
300        panic!("buffer needs to include at least 2 points to generate a line.");
301    }
302    // iterate num_of_iterations in parallel
303    (0..num_of_iterations)
304        .into_par_iter()
305        .map(|_x| 
306            // generate one model for the current iteration
307            generate_line_model(buffer, distance_threshold))
308        // get the best line-model from all iterations (highest ranking)
309        .max_by(|(x, _y), (a, _b)| x.ranking.cmp(&a.ranking))
310        .unwrap()
311}
312
313/// Ransac Line Segmentation in serial (for maximum speed use ransac_line_par).
314/// Returns the line with the highest rating/most inliers and the associated indices of the inliers.
315/// Iterates over all points in the `buffer`.
316/// The `distance_threshold` sets the maximum distance to the plane that a point is counted as an inlier from.
317/// With `num_of_iterations` the number of iterations that the algorithm performs can be chosen.
318///
319/// # Examples
320///
321/// ```
322/// # use pasture_core::nalgebra::Vector3;
323/// # use pasture_core::containers::*;
324/// # use pasture_core::layout::PointType;
325/// # use pasture_derive::PointType;
326/// # use pasture_algorithms::segmentation::ransac_line_serial;
327/// #[repr(C)]
328/// #[derive(PointType, Debug, Copy, Clone, bytemuck::AnyBitPattern, bytemuck::NoUninit)]
329/// struct SimplePoint {
330///     #[pasture(BUILTIN_POSITION_3D)]
331///    pub position: Vector3<f64>,
332/// }
333/// let mut points = vec![];
334/// // generate some inliers
335/// for i in 0..200{
336///     points.push(SimplePoint{position: Vector3::new(0.0, 0.0, f64::from(i))});
337/// }
338/// // generate an outlier
339/// points.push(SimplePoint{position: Vector3::new(9.0, 0.0, 0.0)});
340/// let buffer = points.into_iter().collect::<HashMapBuffer>();
341/// let line_and_indices = ransac_line_serial(&buffer, 0.5, 10);
342/// for i in 0..199{
343///     // inliers are in the plane
344///     assert!(line_and_indices.1.contains(&(i as usize)));
345/// }
346/// // outlier is not in the plane
347/// assert!(!line_and_indices.1.contains(&200));
348/// ```
349///
350/// # Panics
351///
352/// If the size of the buffer is < 2.
353pub fn ransac_line_serial<'a, T: BorrowedBuffer<'a>>(buffer: &'a T,
354    distance_threshold: f64,
355    num_of_iterations: usize,
356) -> (Line, Vec<usize>) {
357    if buffer.len() < 2 {
358        panic!("buffer needs to include at least 2 points to generate a line.");
359    }
360
361    // iterate num_of_iterations in parallel
362    (0..num_of_iterations)
363        
364        .map(|_x| 
365            // generate one model for the current iteration
366            generate_line_model(buffer, distance_threshold))
367        // get the best line-model from all iterations (highest ranking)
368        .max_by(|(x, _y), (a, _b)| x.ranking.cmp(&a.ranking))
369        .unwrap()
370}
371
372#[cfg(test)]
373mod tests {
374
375    use pasture_core::{nalgebra::Vector3, containers::HashMapBuffer,
376    };
377    use pasture_derive::PointType;
378
379    use super::*;
380
381    #[repr(C)]
382    #[derive(PointType, Debug, Copy, Clone, bytemuck::AnyBitPattern, bytemuck::NoUninit)]
383    pub struct SimplePoint {
384        #[pasture(BUILTIN_POSITION_3D)]
385        pub position: Vector3<f64>,
386    }
387
388    fn setup_point_cloud() -> HashMapBuffer {
389        // generate random points for the pointcloud
390        (2..2002)
391            
392            .map(|p| {
393                // let mut rng = rand::thread_rng();
394                // generate plane points (along x- and y-axis)
395                let mut point = SimplePoint {
396                    // position: Vector3::new(rng.gen_range(0.0..100.0), rng.gen_range(0.0..100.0), 1.0),
397                    position: Vector3::new(p as f64, (p * p) as f64, 1.0),
398                };
399                // generate z-axis points for the line
400                if p % 5 == 0 {
401                    point.position = Vector3::new(0.0, 0.0, (p * p) as f64);
402                }
403                // generate outliers
404                if p % 50 == 0 {
405                    point.position.z = (p * p) as f64;
406                }
407                point
408            })
409            .collect()
410    }
411
412    #[test]
413    fn test_ransac_plane_par() {
414        let buffer = setup_point_cloud();
415        let (_plane, indices) = ransac_plane_par(&buffer, 0.1, 300);
416        assert!(indices.len() == 1600);
417        for i in 0..2000 {
418            if i % 5 != 3 {
419                assert!(indices.contains(&i));
420            }
421        }
422    }
423
424    #[test]
425    fn test_ransac_plane_serial() {
426        let buffer = setup_point_cloud();
427        let (_plane, indices) = ransac_plane_serial(&buffer, 0.1, 300);
428        assert!(indices.len() == 1600);
429        for i in 0..2000 {
430            if i % 5 != 3 {
431                assert!(indices.contains(&i));
432            }
433        }
434    }
435
436    #[test]
437    fn test_ransac_line_par() {
438        let buffer = setup_point_cloud();
439        let (_plane, indices) = ransac_line_par(&buffer, 0.1, 300);
440        assert!(indices.len() == 400);
441        for i in 0..2000 {
442            if i % 5 == 3 {
443                assert!(indices.contains(&i));
444            }
445        }
446    }
447
448    #[test]
449    fn test_ransac_line_serial() {
450        let buffer = setup_point_cloud();
451        let (_plane, indices) = ransac_line_serial(&buffer, 0.1, 300);
452        assert!(indices.len() == 400);
453        for i in 0..2000 {
454            if i % 5 == 3 {
455                assert!(indices.contains(&i));
456            }
457        }
458    }
459}