Skip to main content

iris/contours/
mod.rs

1pub mod shape_analysis;
2
3pub use shape_analysis::{RotatedRect, ShapeAnalysis};
4
5use crate::core::types::Point;
6use crate::error::Result;
7use crate::image::Image;
8use burn::tensor::backend::Backend;
9
10type HierarchyEntry = [i32; 4];
11type ContourResult = (Vec<Vec<Point<usize>>>, Vec<HierarchyEntry>);
12
13/// Represents a contour outline as a list of points.
14#[derive(Clone, Debug, Default, PartialEq)]
15pub struct Contour {
16    pub points: Vec<Point<usize>>,
17}
18
19/// Image moments representing spatial distribution.
20#[derive(Clone, Copy, Debug, Default, PartialEq)]
21pub struct Moments {
22    pub m00: f64,
23    pub m10: f64,
24    pub m01: f64,
25    pub m20: f64,
26    pub m02: f64,
27    pub m11: f64,
28    pub m30: f64,
29    pub m03: f64,
30    pub m21: f64,
31    pub m12: f64,
32}
33
34/// A convexity defect: the deepest point between two contour points and its convex hull.
35#[derive(Clone, Copy, Debug, Default, PartialEq)]
36pub struct ConvexityDefect {
37    pub start: Point<f64>,
38    pub end: Point<f64>,
39    pub far_point: Point<f64>,
40    pub depth: f64,
41}
42
43/// Contour retrieval modes for hierarchy-based contour detection.
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum RetrievalMode {
46    /// Retrieves only the extreme outer contours.
47    External,
48    /// Retrieves all contours and creates a flat list.
49    List,
50    /// Retrieves all contours and organizes them into two-level hierarchies
51    /// (external and holes).
52    CComp,
53    /// Retrieves all contours and builds a full hierarchy tree.
54    Tree,
55    /// Flood fill retrieval mode.
56    FloodFill,
57}
58
59impl Moments {
60    /// Computes the centroid (center of mass) from the moments.
61    #[must_use]
62    pub fn centroid(&self) -> Option<Point<f64>> {
63        if self.m00.abs() < 1e-9 {
64            None
65        } else {
66            Some(Point::new(self.m10 / self.m00, self.m01 / self.m00))
67        }
68    }
69}
70
71impl Contour {
72    /// Creates a new Contour with given points.
73    #[must_use]
74    pub fn new(points: Vec<Point<usize>>) -> Self {
75        Self { points }
76    }
77
78    /// Finds convexity defects between a contour and its convex hull.
79    ///
80    /// A convexity defect is a region where the contour deviates inward from
81    /// its convex hull. For each defect we record the start point (hull vertex),
82    /// end point (next hull vertex), the farthest contour point from the hull
83    /// edge, and the perpendicular depth of that point.
84    pub fn convexity_defects(contour: &[Point<f64>], hull: &[Point<f64>]) -> Vec<ConvexityDefect> {
85        if contour.len() < 3 || hull.len() < 3 {
86            return Vec::new();
87        }
88
89        let mut defects = Vec::new();
90
91        for hi in 0..hull.len() {
92            let a = hull[hi];
93            let b = hull[(hi + 1) % hull.len()];
94
95            let abx = b.x - a.x;
96            let aby = b.y - a.y;
97            let ab_len2 = abx * abx + aby * aby;
98            if ab_len2 < 1e-12 {
99                continue;
100            }
101
102            let mut max_depth = 0.0;
103            let mut far_point = a;
104
105            for &p in contour {
106                let apx = p.x - a.x;
107                let apy = p.y - a.y;
108
109                // Project P onto line AB, clamped to segment
110                let t = ((apx * abx + apy * aby) / ab_len2).clamp(0.0, 1.0);
111                let proj_x = a.x + t * abx;
112                let proj_y = a.y + t * aby;
113
114                let dx = p.x - proj_x;
115                let dy = p.y - proj_y;
116                let depth = (dx * dx + dy * dy).sqrt();
117
118                if depth > max_depth {
119                    max_depth = depth;
120                    far_point = p;
121                }
122            }
123
124            if max_depth > 1e-6 {
125                defects.push(ConvexityDefect {
126                    start: a,
127                    end: b,
128                    far_point,
129                    depth: max_depth,
130                });
131            }
132        }
133
134        defects
135    }
136
137    /// Computes the convex hull of the contour using Andrew's Monotone Chain algorithm.
138    #[must_use]
139    pub fn convex_hull(&self) -> Self {
140        let mut pts = self.points.clone();
141        if pts.len() <= 3 {
142            return Self::new(pts);
143        }
144
145        // Sort points lexicographically by x, then y
146        pts.sort_by(|a, b| {
147            if a.x == b.x {
148                a.y.cmp(&b.y)
149            } else {
150                a.x.cmp(&b.x)
151            }
152        });
153
154        fn cross(o: &Point<usize>, a: &Point<usize>, b: &Point<usize>) -> isize {
155            (a.x as isize - o.x as isize) * (b.y as isize - o.y as isize)
156                - (a.y as isize - o.y as isize) * (b.x as isize - o.x as isize)
157        }
158
159        let mut lower = Vec::new();
160        for p in &pts {
161            while lower.len() >= 2
162                && cross(&lower[lower.len() - 2], &lower[lower.len() - 1], p) <= 0
163            {
164                lower.pop();
165            }
166            lower.push(*p);
167        }
168
169        let mut upper = Vec::new();
170        for p in pts.iter().rev() {
171            while upper.len() >= 2
172                && cross(&upper[upper.len() - 2], &upper[upper.len() - 1], p) <= 0
173            {
174                upper.pop();
175            }
176            upper.push(*p);
177        }
178
179        lower.pop();
180        upper.pop();
181        lower.extend(upper);
182
183        Self::new(lower)
184    }
185
186    /// Computes the polygon moments using the Shoelace formula and Green's Theorem.
187    #[must_use]
188    pub fn moments(&self) -> Moments {
189        let pts = &self.points;
190        let n = pts.len();
191        if n < 3 {
192            return Moments::default();
193        }
194
195        let mut m00 = 0.0;
196        let mut m10 = 0.0;
197        let mut m01 = 0.0;
198        let mut m20 = 0.0;
199        let mut m02 = 0.0;
200        let mut m11 = 0.0;
201        let mut m30 = 0.0;
202        let mut m03 = 0.0;
203        let mut m21 = 0.0;
204        let mut m12 = 0.0;
205
206        for i in 0..n {
207            let p0 = pts[i];
208            let p1 = pts[(i + 1) % n];
209
210            let xi = p0.x as f64;
211            let yi = p0.y as f64;
212            let xi1 = p1.x as f64;
213            let yi1 = p1.y as f64;
214
215            let cross = xi * yi1 - xi1 * yi;
216            m00 += cross;
217            m10 += (xi + xi1) * cross;
218            m01 += (yi + yi1) * cross;
219            m20 += (xi * xi + xi * xi1 + xi1 * xi1) * cross;
220            m02 += (yi * yi + yi * yi1 + yi1 * yi1) * cross;
221            m11 += (2.0 * xi * yi + xi * yi1 + xi1 * yi + 2.0 * xi1 * yi1) * cross;
222            m30 += (xi * xi * xi + xi * xi * xi1 + xi * xi1 * xi1 + xi1 * xi1 * xi1) * cross;
223            m03 += (yi * yi * yi + yi * yi * yi1 + yi * yi1 * yi1 + yi1 * yi1 * yi1) * cross;
224            m21 += (2.0 * xi * xi * yi
225                + xi * xi * yi1
226                + 2.0 * xi * xi1 * yi
227                + xi * xi1 * yi1
228                + xi1 * xi1 * yi
229                + 2.0 * xi1 * xi1 * yi1)
230                * cross;
231            m12 += (2.0 * yi * yi * xi
232                + yi * yi * xi1
233                + 2.0 * yi * yi1 * xi
234                + yi * yi1 * xi1
235                + yi1 * yi1 * xi
236                + 2.0 * yi1 * yi1 * xi1)
237                * cross;
238        }
239
240        m00 /= 2.0;
241        m10 /= 6.0;
242        m01 /= 6.0;
243        m20 /= 12.0;
244        m02 /= 12.0;
245        m11 /= 24.0;
246        m30 /= 20.0;
247        m03 /= 20.0;
248        m21 /= 60.0;
249        m12 /= 60.0;
250
251        Moments {
252            m00: m00.abs(),
253            m10: m10.abs(),
254            m01: m01.abs(),
255            m20: m20.abs(),
256            m02: m02.abs(),
257            m11: m11.abs(),
258            m30: m30.abs(),
259            m03: m03.abs(),
260            m21: m21.abs(),
261            m12: m12.abs(),
262        }
263    }
264}
265
266impl<B: Backend> Image<B> {
267    /// Scans a binary image (grayscale, thresholded) to find contours (connected components).
268    /// Uses a basic boundary-following algorithm to find contiguous shapes.
269    pub fn find_contours(&self) -> Result<Vec<Contour>> {
270        let gray = self.grayscale()?;
271        let dims = gray.tensor.dims();
272        let h = dims[1];
273        let w = dims[2];
274
275        let tensor_data = gray.tensor.clone().into_data();
276        let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
277
278        let mut visited = vec![false; h * w];
279        let mut contours = Vec::new();
280
281        // 8-neighbor offsets
282        let dx = [1, 1, 0, -1, -1, -1, 0, 1];
283        let dy = [0, 1, 1, 1, 0, -1, -1, -1];
284
285        for y in 1..(h - 1) {
286            for x in 1..(w - 1) {
287                let idx = y * w + x;
288                if flat_vals[idx] > 0.5 && !visited[idx] {
289                    // Start of a boundary
290                    let mut pts = Vec::new();
291                    let mut cx = x;
292                    let mut cy = y;
293                    let mut dir = 0;
294
295                    pts.push(Point::new(cx, cy));
296                    visited[idx] = true;
297
298                    // Trace boundary
299                    let mut loop_count = 0;
300                    loop {
301                        let mut found = false;
302                        for i in 0..8 {
303                            let ndir = (dir + i) % 8;
304                            let nx = cx as isize + dx[ndir];
305                            let ny = cy as isize + dy[ndir];
306
307                            if nx >= 0 && nx < w as isize && ny >= 0 && ny < h as isize {
308                                let nidx = (ny as usize) * w + (nx as usize);
309                                if flat_vals[nidx] > 0.5 {
310                                    cx = nx as usize;
311                                    cy = ny as usize;
312                                    visited[nidx] = true;
313                                    pts.push(Point::new(cx, cy));
314                                    dir = (ndir + 5) % 8; // Backtrack direction
315                                    found = true;
316                                    break;
317                                }
318                            }
319                        }
320
321                        if !found || (cx == x && cy == y) || loop_count > 10000 {
322                            break;
323                        }
324                        loop_count += 1;
325                    }
326
327                    if pts.len() >= 3 {
328                        contours.push(Contour::new(pts));
329                    }
330                }
331            }
332        }
333
334        Ok(contours)
335    }
336
337    /// Finds contours in a binary image with hierarchy information.
338    ///
339    /// Returns a tuple of `(contours, hierarchy)` where each hierarchy entry
340    /// is `[next, prev, child, parent]` using -1 as a sentinel for "none".
341    /// The hierarchy encodes the nesting relationship between external contours
342    /// and holes.
343    pub fn find_contours_with_hierarchy(&self, mode: RetrievalMode) -> Result<ContourResult> {
344        let gray = self.grayscale()?;
345        let dims = gray.tensor.dims();
346        let h = dims[1];
347        let w = dims[2];
348
349        let tensor_data = gray.tensor.clone().into_data();
350        let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
351
352        // Build a binary mask
353        let mut binary = vec![false; h * w];
354        for i in 0..(h * w) {
355            binary[i] = flat_vals[i] > 0.5;
356        }
357
358        // Label connected components with 4-connectivity flood fill
359        let mut labels = vec![0i32; h * w];
360        let mut label_count: i32 = 0;
361
362        let dx4 = [1, 0, -1, 0];
363        let dy4 = [0, 1, 0, -1];
364
365        for y in 0..h {
366            for x in 0..w {
367                let idx = y * w + x;
368                if binary[idx] && labels[idx] == 0 {
369                    label_count += 1;
370                    // BFS flood fill
371                    let mut stack = vec![(x, y)];
372                    labels[idx] = label_count;
373                    while let Some((cx, cy)) = stack.pop() {
374                        for d in 0..4 {
375                            let nx = cx as isize + dx4[d];
376                            let ny = cy as isize + dy4[d];
377                            if nx >= 0 && nx < w as isize && ny >= 0 && ny < h as isize {
378                                let nidx = ny as usize * w + nx as usize;
379                                if binary[nidx] && labels[nidx] == 0 {
380                                    labels[nidx] = label_count;
381                                    stack.push((nx as usize, ny as usize));
382                                }
383                            }
384                        }
385                    }
386                }
387            }
388        }
389
390        // Extract contours per label using boundary tracing
391        let dx8 = [1, 1, 0, -1, -1, -1, 0, 1];
392        let dy8 = [0, 1, 1, 1, 0, -1, -1, -1];
393
394        let mut all_contours: Vec<Vec<Point<usize>>> = Vec::new();
395        let mut visited = vec![false; h * w];
396
397        for y in 1..(h - 1) {
398            for x in 1..(w - 1) {
399                let idx = y * w + x;
400                if !binary[idx] || visited[idx] {
401                    continue;
402                }
403
404                // Check if this is a boundary pixel (has a background neighbor)
405                let mut is_boundary = false;
406                for d in 0..8 {
407                    let nx = x as isize + dx8[d];
408                    let ny = y as isize + dy8[d];
409                    if nx >= 0
410                        && nx < w as isize
411                        && ny >= 0
412                        && ny < h as isize
413                        && !binary[ny as usize * w + nx as usize]
414                    {
415                        is_boundary = true;
416                        break;
417                    }
418                }
419                if !is_boundary {
420                    visited[idx] = true;
421                    continue;
422                }
423
424                // Trace the boundary
425                let mut pts = Vec::new();
426                let mut cx = x;
427                let mut cy = y;
428                let mut dir = 0;
429
430                pts.push(Point::new(cx, cy));
431                visited[idx] = true;
432
433                let mut loop_count = 0;
434                loop {
435                    let mut found = false;
436                    for i in 0..8 {
437                        let ndir = (dir + i) % 8;
438                        let nx = cx as isize + dx8[ndir];
439                        let ny = cy as isize + dy8[ndir];
440
441                        if nx >= 0 && nx < w as isize && ny >= 0 && ny < h as isize {
442                            let nidx = ny as usize * w + nx as usize;
443                            if binary[nidx] {
444                                cx = nx as usize;
445                                cy = ny as usize;
446                                visited[nidx] = true;
447                                pts.push(Point::new(cx, cy));
448                                dir = (ndir + 5) % 8;
449                                found = true;
450                                break;
451                            }
452                        }
453                    }
454
455                    if !found || (cx == x && cy == y) || loop_count > 10000 {
456                        break;
457                    }
458                    loop_count += 1;
459                }
460
461                if pts.len() >= 3 {
462                    all_contours.push(pts);
463                }
464            }
465        }
466
467        // Build hierarchy based on point-in-polygon inclusion testing
468        let n = all_contours.len();
469
470        // Determine parent-child by containment:
471        // For each contour, find the smallest enclosing contour.
472        let mut parent_map: Vec<Option<usize>> = vec![None; n];
473
474        for i in 0..n {
475            let ci = &all_contours[i];
476            let center_i = {
477                let sx: f64 = ci.iter().map(|p| p.x as f64).sum::<f64>() / ci.len() as f64;
478                let sy: f64 = ci.iter().map(|p| p.y as f64).sum::<f64>() / ci.len() as f64;
479                Point::new(sx, sy)
480            };
481
482            let mut best_parent: Option<usize> = None;
483            let mut best_area = f64::MAX;
484
485            for j in 0..n {
486                if i == j {
487                    continue;
488                }
489                // Check if center of contour i is inside contour j
490                let cj = &all_contours[j];
491                if Self::point_inside_polygon(center_i, cj) {
492                    let area = Self::polygon_area(cj);
493                    if area < best_area {
494                        best_area = area;
495                        best_parent = Some(j);
496                    }
497                }
498            }
499            parent_map[i] = best_parent;
500        }
501
502        // Apply RetrievalMode filter
503        let include = |idx: usize, mode: RetrievalMode| -> bool {
504            match mode {
505                RetrievalMode::External => parent_map[idx].is_none(),
506                RetrievalMode::List | RetrievalMode::CComp | RetrievalMode::Tree => true,
507                RetrievalMode::FloodFill => true,
508            }
509        };
510
511        // Build filtered index mapping
512        let filtered_indices: Vec<usize> = (0..n).filter(|&i| include(i, mode)).collect();
513        let mut index_map: Vec<i32> = vec![-1; n];
514        for (new_idx, &old_idx) in filtered_indices.iter().enumerate() {
515            index_map[old_idx] = new_idx as i32;
516        }
517
518        let filtered_n = filtered_indices.len();
519        let mut filtered_hierarchy = vec![[-1i32; 4]; filtered_n];
520
521        for (new_idx, &old_idx) in filtered_indices.iter().enumerate() {
522            // next sibling
523            let next_sibling = {
524                let mut found = -1i32;
525                if let Some(parent) = parent_map[old_idx] {
526                    for k in (old_idx + 1)..n {
527                        if parent_map[k] == Some(parent) && index_map[k] >= 0 {
528                            found = index_map[k];
529                            break;
530                        }
531                    }
532                }
533                found
534            };
535
536            // prev sibling
537            let prev_sibling = {
538                let mut found = -1i32;
539                if let Some(parent) = parent_map[old_idx] {
540                    for k in (0..old_idx).rev() {
541                        if parent_map[k] == Some(parent) && index_map[k] >= 0 {
542                            found = index_map[k];
543                            break;
544                        }
545                    }
546                }
547                found
548            };
549
550            // first child
551            let first_child = {
552                let mut found = -1i32;
553                for k in 0..n {
554                    if parent_map[k] == Some(old_idx) && index_map[k] >= 0 {
555                        found = index_map[k];
556                        break;
557                    }
558                }
559                found
560            };
561
562            let parent = parent_map[old_idx].map(|p| index_map[p]).unwrap_or(-1);
563
564            filtered_hierarchy[new_idx] = [next_sibling, prev_sibling, first_child, parent];
565        }
566
567        let contours_out: Vec<Vec<Point<usize>>> = filtered_indices
568            .into_iter()
569            .map(|i| all_contours[i].clone())
570            .collect();
571
572        Ok((contours_out, filtered_hierarchy))
573    }
574
575    /// Ray-casting point-in-polygon test.
576    fn point_inside_polygon(point: Point<f64>, polygon: &[Point<usize>]) -> bool {
577        let n = polygon.len();
578        if n < 3 {
579            return false;
580        }
581        let mut inside = false;
582        let mut j = n - 1;
583        for i in 0..n {
584            let yi = polygon[i].y as f64;
585            let yj = polygon[j].y as f64;
586            let xi = polygon[i].x as f64;
587            let xj = polygon[j].x as f64;
588
589            if ((yi > point.y) != (yj > point.y))
590                && (point.x < (xj - xi) * (point.y - yi) / (yj - yi + 1e-9) + xi)
591            {
592                inside = !inside;
593            }
594            j = i;
595        }
596        inside
597    }
598
599    /// Signed area of a polygon using the Shoelace formula.
600    fn polygon_area(polygon: &[Point<usize>]) -> f64 {
601        let n = polygon.len();
602        if n < 3 {
603            return 0.0;
604        }
605        let mut area = 0.0;
606        for i in 0..n {
607            let j = (i + 1) % n;
608            area += polygon[i].x as f64 * polygon[j].y as f64;
609            area -= polygon[j].x as f64 * polygon[i].y as f64;
610        }
611        area.abs() / 2.0
612    }
613}
614
615#[cfg(test)]
616mod tests {
617    use super::*;
618    use crate::test_helpers::{TestBackend, test_device};
619    use burn::tensor::{Tensor, TensorData};
620
621    #[test]
622    fn test_contours_and_moments() {
623        let pts = vec![
624            Point::new(0, 0),
625            Point::new(10, 0),
626            Point::new(10, 10),
627            Point::new(0, 10),
628        ];
629        let contour = Contour::new(pts);
630        let hull = contour.convex_hull();
631        assert_eq!(hull.points.len(), 4);
632
633        let m = contour.moments();
634        assert!(m.m00 > 0.0);
635        let centroid = m.centroid().unwrap();
636        assert!(centroid.x > 0.0);
637
638        let device = test_device();
639        // Create an image with a single pixel set to 1.0 (binary mask)
640        let mut flat_data = vec![0.0f32; 10 * 10];
641        // Set a 3x3 block to 1.0
642        for y in 2..5 {
643            for x in 2..5 {
644                flat_data[y * 10 + x] = 1.0;
645            }
646        }
647        let tensor =
648            Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [1, 10, 10]), &device);
649        let img = Image::new(tensor);
650        let found = img.find_contours().unwrap();
651        assert!(!found.is_empty());
652    }
653
654    #[test]
655    fn test_convexity_defects() {
656        // Create a concave polygon: a square with a notch cut into one side.
657        // Points go clockwise: bottom-left -> bottom-right -> notch inward -> notch back -> top-right -> top-left
658        let contour = vec![
659            Point::new(0.0, 0.0),
660            Point::new(10.0, 0.0),
661            Point::new(10.0, 5.0),
662            Point::new(7.0, 3.0), // notch inward
663            Point::new(5.0, 5.0), // notch bottom
664            Point::new(5.0, 10.0),
665            Point::new(0.0, 10.0),
666        ];
667
668        // Convex hull of this shape
669        let hull = vec![
670            Point::new(0.0, 0.0),
671            Point::new(10.0, 0.0),
672            Point::new(10.0, 5.0),
673            Point::new(5.0, 10.0),
674            Point::new(0.0, 10.0),
675        ];
676
677        let defects = Contour::convexity_defects(&contour, &hull);
678        // There should be at least one defect where the contour goes inward
679        assert!(!defects.is_empty());
680        for d in &defects {
681            assert!(d.depth > 0.0);
682        }
683    }
684
685    #[test]
686    fn test_find_contours_with_hierarchy() {
687        let device = test_device();
688        // Create image with two separate blobs
689        let mut flat_data = vec![0.0f32; 20 * 20];
690        // Blob 1: 3x3 at (2,2)
691        for y in 2..5 {
692            for x in 2..5 {
693                flat_data[y * 20 + x] = 1.0;
694            }
695        }
696        // Blob 2: 3x3 at (12,12)
697        for y in 12..15 {
698            for x in 12..15 {
699                flat_data[y * 20 + x] = 1.0;
700            }
701        }
702        let tensor =
703            Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [1, 20, 20]), &device);
704        let img = Image::new(tensor);
705
706        let (contours, hierarchy) = img
707            .find_contours_with_hierarchy(RetrievalMode::External)
708            .unwrap();
709        assert!(!contours.is_empty());
710        assert_eq!(contours.len(), hierarchy.len());
711        // Hierarchy entries should have 4 elements each
712        for h in &hierarchy {
713            assert_eq!(h.len(), 4);
714        }
715    }
716}