pineapple_core/cv/
contours.rs

1// Copyright (c) 2025, Tom Ouellette
2// Licensed under the BSD 3-Clause License
3
4use std::cmp::Ordering;
5use std::collections::VecDeque;
6
7/// Find contours using 8-connectivity
8///
9/// # Arguments
10///
11/// * `width` - Width of mask
12/// * `height` - Height of mask
13/// * `buffer` - A row-major mask buffer
14/// * `threshold` - Threshold value for foreground/background pixels
15/// * `order` - An ordering specifying how a pixel will be compared to threshold
16///
17/// # References
18///
19/// Adapted/modified from: https://github.com/image-rs/imageproc
20///
21/// # Examples
22///
23/// ```
24/// use std::cmp::Ordering::Greater;
25/// use pineapple_core::cv::find_contours;
26///
27/// let width = 3;
28/// let height = 3;
29/// let buffer_one: Vec<u32> = vec![12, 12, 0, 12, 12, 0, 0, 0, 0];
30/// let contours_one = find_contours(width, height, &buffer_one, &0, Greater);
31///
32/// assert_eq!(contours_one, [[[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]]])
33/// ```
34pub fn find_contours(
35    width: u32,
36    height: u32,
37    pixels: &[u32],
38    threshold: &u32,
39    order: Ordering,
40) -> Vec<Vec<[f32; 2]>> {
41    let width = width as usize;
42    let height = height as usize;
43    let padded_width = width + 2;
44    let padded_height = height + 2;
45
46    let at = |x: usize, y: usize| x + padded_width * y;
47
48    let mut image_values = vec![0i32; padded_height * padded_width];
49
50    for y in 0..height {
51        for x in 0..width {
52            let pixel = pixels[y * width + x];
53            image_values[at(x + 1, y + 1)] = if pixel.cmp(threshold) == order { 1 } else { 0 };
54        }
55    }
56
57    let mut diffs = VecDeque::from(vec![
58        [-1, 0],  // West
59        [-1, -1], // Northwest
60        [0, -1],  // North
61        [1, -1],  // Northeast
62        [1, 0],   // East
63        [1, 1],   // Southeast
64        [0, 1],   // South
65        [-1, 1],  // Southwest
66    ]);
67
68    let mut contours: Vec<Contour> = Vec::new();
69    let mut curr_border_num = 1;
70    let mut parent_border_num = 1;
71
72    for y in 1..=height {
73        for x in 1..=width {
74            if image_values[at(x, y)] == 0 {
75                continue;
76            }
77
78            let curr = (x as i32, y as i32);
79
80            let (is_outer_border, adjacent_point) = if image_values[at(x, y)] == 1
81                && x > 0
82                && image_values[at(x - 1, y)] == 0
83            {
84                (true, (x as i32 - 1, y as i32))
85            } else if image_values[at(x, y)] > 0 && x + 1 < width && image_values[at(x + 1, y)] == 0
86            {
87                if image_values[at(x, y)] > 1 {
88                    parent_border_num = image_values[at(x, y)] as usize;
89                }
90                (false, (x as i32 + 1, y as i32))
91            } else {
92                continue;
93            };
94
95            curr_border_num += 1;
96
97            let border_type = if is_outer_border {
98                BorderType::Outer
99            } else {
100                BorderType::Hole
101            };
102
103            let parent = if parent_border_num > 1 {
104                let parent_index = parent_border_num - 2;
105                let parent_contour = &contours[parent_index];
106                if (border_type == BorderType::Outer)
107                    ^ (parent_contour.border_type == BorderType::Outer)
108                {
109                    Some(parent_index)
110                } else {
111                    parent_contour.parent
112                }
113            } else {
114                None
115            };
116
117            let mut contour_points: Vec<[f32; 2]> = Vec::new();
118            rotate_to_value(
119                &mut diffs,
120                [adjacent_point.0 - curr.0, adjacent_point.1 - curr.1],
121            );
122
123            let pos1_option = diffs.iter().find_map(|&diff| {
124                let nx = curr.0 + diff[0];
125                let ny = curr.1 + diff[1];
126                if nx >= 0
127                    && nx < padded_width as i32
128                    && ny >= 0
129                    && ny < padded_height as i32
130                    && image_values[at(nx as usize, ny as usize)] != 0
131                {
132                    Some((nx, ny))
133                } else {
134                    None
135                }
136            });
137
138            if let Some(pos1) = pos1_option {
139                let mut pos2 = pos1;
140                let mut pos3 = curr;
141
142                loop {
143                    contour_points.push([pos3.0 as f32 - 1.0, pos3.1 as f32 - 1.0]);
144                    rotate_to_value(&mut diffs, [pos2.0 - pos3.0, pos2.1 - pos3.1]);
145                    let pos4 = diffs
146                        .iter()
147                        .rev()
148                        .find_map(|&diff| {
149                            let nx = pos3.0 + diff[0];
150                            let ny = pos3.1 + diff[1];
151                            if nx >= 0
152                                && nx < padded_width as i32
153                                && ny >= 0
154                                && ny < padded_height as i32
155                                && image_values[at(nx as usize, ny as usize)] != 0
156                            {
157                                Some((nx, ny))
158                            } else {
159                                None
160                            }
161                        })
162                        .unwrap();
163
164                    let mut is_right_edge = false;
165                    for &diff in diffs.iter().rev() {
166                        if diff == [pos4.0 - pos3.0, pos4.1 - pos3.1] {
167                            break;
168                        }
169                        if diff == [1, 0] {
170                            is_right_edge = true;
171                            break;
172                        }
173                    }
174
175                    if pos3.0 as usize + 1 == padded_width || is_right_edge {
176                        image_values[at(pos3.0 as usize, pos3.1 as usize)] = -curr_border_num;
177                    } else if image_values[at(pos3.0 as usize, pos3.1 as usize)] == 1 {
178                        image_values[at(pos3.0 as usize, pos3.1 as usize)] = curr_border_num;
179                    }
180
181                    if pos4 == curr && pos3 == pos1 {
182                        break;
183                    }
184
185                    pos2 = pos3;
186                    pos3 = pos4;
187                }
188            } else {
189                contour_points.push([x as f32 - 1.0, y as f32 - 1.0]);
190                image_values[at(x, y)] = -curr_border_num;
191            }
192
193            contours.push(Contour::new(contour_points, border_type, parent));
194        }
195    }
196
197    contours
198        .into_iter()
199        .filter(|contour| contour.border_type() == &BorderType::Outer)
200        .map(|contour| contour.into_points())
201        .collect()
202}
203
204///  Contour for storiing outlines of segmented objects
205#[derive(Debug, Clone)]
206pub struct Contour {
207    points: Vec<[f32; 2]>,
208    border_type: BorderType,
209    parent: Option<usize>,
210}
211
212#[derive(Debug, Copy, Clone, PartialEq, Eq)]
213pub enum BorderType {
214    Outer,
215    Hole,
216}
217
218impl Contour {
219    pub fn new(points: Vec<[f32; 2]>, border_type: BorderType, parent: Option<usize>) -> Self {
220        Contour {
221            points,
222            border_type,
223            parent,
224        }
225    }
226
227    pub fn as_points(&self) -> &Vec<[f32; 2]> {
228        &self.points
229    }
230
231    pub fn into_points(self) -> Vec<[f32; 2]> {
232        self.points
233    }
234
235    pub fn border_type(&self) -> &BorderType {
236        &self.border_type
237    }
238
239    pub fn parent(&self) -> Option<usize> {
240        self.parent
241    }
242}
243
244fn rotate_to_value(values: &mut VecDeque<[i32; 2]>, value: [i32; 2]) {
245    if let Some(pos) = values.iter().position(|&v| v == value) {
246        values.rotate_left(pos);
247    }
248}
249
250/// Find contours incrementally for each object denoted by a unique non-zero integer
251///
252/// # Arguments
253///
254/// * `width` - Width of mask
255/// * `height` - Height of mask
256/// * `buffer` - A row-major mask buffer
257/// * `labels` - Positive non-zero integers specifying unique segmented objects
258///
259/// # Examples
260///
261/// ```
262/// use std::cmp::Ordering::Greater;
263/// use pineapple_core::cv::find_labeled_contours;
264///
265/// let width = 3;
266/// let height = 3;
267/// let buffer: Vec<u32> = vec![12, 12, 0, 12, 0, 10, 0, 10, 10];
268/// let (labels, contours) = find_labeled_contours(width, height, &buffer, &vec![10u32, 12u32]);
269///
270/// assert_eq!(contours, [[[2.0, 1.0], [1.0, 2.0], [2.0, 2.0]], [[0.0, 0.0], [0.0, 1.0], [1.0, 0.0]]]);
271/// ```
272pub fn find_labeled_contours(
273    width: u32,
274    height: u32,
275    pixels: &[u32],
276    labels: &Vec<u32>,
277) -> (Vec<u32>, Vec<Vec<[f32; 2]>>) {
278    let mut contours = Vec::with_capacity(labels.len());
279    let mut retained = Vec::with_capacity(labels.len());
280
281    for label in labels {
282        let contour = find_contours(width, height, pixels, label, Ordering::Equal)
283            .into_iter()
284            .max_by_key(|contour| contour.len());
285
286        if let Some(contour) = contour && contour.len() > 2 {
287            contours.push(contour);
288            retained.push(*label)
289        }
290    }
291
292    (retained, contours)
293}
294
295#[cfg(test)]
296mod test {
297
298    use super::*;
299
300    fn four_regions_small() -> (u32, u32, [u32; 9]) {
301        let mut buffer = [0u32; 9];
302
303        buffer[0] = 1u32;
304        buffer[2] = 1u32;
305        buffer[6] = 1u32;
306        buffer[8] = 1u32;
307
308        (3, 3, buffer)
309    }
310
311    fn four_regions_big() -> (u32, u32, [u32; 25]) {
312        let mut buffer = [0u32; 25];
313
314        buffer[0] = 1u32;
315        buffer[1] = 1u32;
316        buffer[5] = 1u32;
317        buffer[6] = 1u32;
318
319        buffer[3] = 1u32;
320        buffer[4] = 1u32;
321        buffer[8] = 1u32;
322        buffer[9] = 1u32;
323
324        buffer[15] = 2u32;
325        buffer[16] = 2u32;
326        buffer[20] = 2u32;
327        buffer[21] = 2u32;
328
329        buffer[18] = 3u32;
330        buffer[19] = 3u32;
331        buffer[23] = 3u32;
332        buffer[24] = 3u32;
333
334        (5, 5, buffer)
335    }
336
337    fn three_regions() -> (u32, u32, [u32; 9]) {
338        let mut buffer = [0u32; 9];
339
340        buffer[0] = 1u32;
341        buffer[2] = 1u32;
342        buffer[6] = 1u32;
343        buffer[7] = 1u32;
344        buffer[8] = 1u32;
345
346        (3, 3, buffer)
347    }
348
349    fn two_squares() -> (u32, u32, [u32; 100]) {
350        let mut buffer = [0u32; 100];
351
352        for i in 0..10 {
353            for j in 0..10 {
354                let idx = j * 10 + i;
355                if i < 4 && j < 4 {
356                    buffer[idx] = 1u32;
357                } else if i >= 6 && j >= 6 {
358                    buffer[idx] = 2u32;
359                } else {
360                    buffer[idx] = 0u32;
361                }
362            }
363        }
364
365        (10, 10, buffer)
366    }
367
368    fn two_squares_touching() -> (u32, u32, [u32; 100]) {
369        let mut buffer = [0u32; 100];
370
371        for i in 0..10 {
372            for j in 0..10 {
373                let idx = j * 10 + i;
374                if i < 5 {
375                    buffer[idx] = 1u32;
376                } else if i >= 5 {
377                    buffer[idx] = 2u32;
378                } else {
379                    buffer[idx] = 0u32;
380                }
381            }
382        }
383
384        (10, 10, buffer)
385    }
386
387    #[test]
388    fn test_four_regions_small() {
389        let (w, h, buffer) = four_regions_small();
390        let contours = find_contours(w, h, &buffer, &0, Ordering::Greater);
391
392        let n_contours = contours.len();
393        assert_eq!(n_contours, 4);
394
395        let p0 = &contours[0];
396        let p1 = &contours[1];
397        let p2 = &contours[2];
398        let p3 = &contours[3];
399
400        assert_eq!(*p0, vec![[0., 0.]]);
401        assert_eq!(*p1, vec![[2., 0.]]);
402        assert_eq!(*p2, vec![[0., 2.]]);
403        assert_eq!(*p3, vec![[2., 2.]]);
404    }
405
406    #[test]
407    fn test_four_regions_big() {
408        let (w, h, buffer) = four_regions_big();
409        let contours = find_contours(w, h, &buffer, &0, Ordering::Greater);
410
411        let n_contours = contours.len();
412        assert_eq!(n_contours, 4);
413
414        let p0 = &contours[0];
415        let p1 = &contours[1];
416        let p2 = &contours[2];
417        let p3 = &contours[3];
418
419        assert_eq!(*p0, vec![[0., 0.], [0., 1.], [1., 1.], [1., 0.]]);
420        assert_eq!(*p1, vec![[3., 0.], [3., 1.], [4., 1.], [4., 0.]]);
421        assert_eq!(*p2, vec![[0., 3.], [0., 4.], [1., 4.], [1., 3.]]);
422        assert_eq!(*p3, vec![[3., 3.], [3., 4.], [4., 4.], [4., 3.]]);
423    }
424
425    #[test]
426    fn test_three_regions() {
427        let (w, h, buffer) = three_regions();
428        let contours = find_contours(w, h, &buffer, &0, Ordering::Greater);
429
430        let n_contours = contours.len();
431        assert_eq!(n_contours, 3);
432
433        let p0 = &contours[0];
434        let p1 = &contours[1];
435        let p2 = &contours[2];
436
437        assert_eq!(*p0, vec![[0., 0.]]);
438        assert_eq!(*p1, vec![[2., 0.]]);
439        assert_eq!(*p2, vec![[0., 2.], [1., 2.], [2., 2.], [1., 2.]]);
440    }
441
442    #[test]
443    fn test_two_squares() {
444        let (w, h, buffer) = two_squares();
445        let contours = find_contours(w, h, &buffer, &0, Ordering::Greater);
446
447        let n_contours = contours.len();
448        assert_eq!(n_contours, 2);
449
450        let p0 = &contours[0];
451        let p1 = &contours[1];
452
453        assert_eq!(
454            *p0,
455            vec![
456                [0., 0.],
457                [0., 1.],
458                [0., 2.],
459                [0., 3.],
460                [1., 3.],
461                [2., 3.],
462                [3., 3.],
463                [3., 2.],
464                [3., 1.],
465                [3., 0.],
466                [2., 0.],
467                [1., 0.],
468            ]
469        );
470
471        assert_eq!(
472            *p1,
473            vec![
474                [6., 6.],
475                [6., 7.],
476                [6., 8.],
477                [6., 9.],
478                [7., 9.],
479                [8., 9.],
480                [9., 9.],
481                [9., 8.],
482                [9., 7.],
483                [9., 6.],
484                [8., 6.],
485                [7., 6.],
486            ]
487        );
488    }
489
490    #[test]
491    fn test_two_squares_touching() {
492        let (w, h, buffer) = two_squares_touching();
493        let contours = find_contours(w, h, &buffer, &0, Ordering::Greater);
494
495        assert_eq!(contours.len(), 1);
496
497        let (_, contours) = find_labeled_contours(w, h, &buffer, &vec![1u32, 2u32]);
498
499        let p0 = &contours[0];
500        let p1 = &contours[1];
501
502        assert_eq!(
503            *p0,
504            vec![
505                [0., 0.],
506                [0., 1.],
507                [0., 2.],
508                [0., 3.],
509                [0., 4.],
510                [0., 5.],
511                [0., 6.],
512                [0., 7.],
513                [0., 8.],
514                [0., 9.],
515                [1., 9.],
516                [2., 9.],
517                [3., 9.],
518                [4., 9.],
519                [4., 8.],
520                [4., 7.],
521                [4., 6.],
522                [4., 5.],
523                [4., 4.],
524                [4., 3.],
525                [4., 2.],
526                [4., 1.],
527                [4., 0.],
528                [3., 0.],
529                [2., 0.],
530                [1., 0.]
531            ]
532        );
533
534        assert_eq!(
535            *p1,
536            vec![
537                [5., 0.],
538                [5., 1.],
539                [5., 2.],
540                [5., 3.],
541                [5., 4.],
542                [5., 5.],
543                [5., 6.],
544                [5., 7.],
545                [5., 8.],
546                [5., 9.],
547                [6., 9.],
548                [7., 9.],
549                [8., 9.],
550                [9., 9.],
551                [9., 8.],
552                [9., 7.],
553                [9., 6.],
554                [9., 5.],
555                [9., 4.],
556                [9., 3.],
557                [9., 2.],
558                [9., 1.],
559                [9., 0.],
560                [8., 0.],
561                [7., 0.],
562                [6., 0.],
563            ]
564        );
565    }
566}