Skip to main content

iris/contours/
shape_analysis.rs

1use crate::contours::{Contour, Moments};
2use crate::core::types::{Point, Rect, Size};
3
4impl Contour {
5    /// Computes the perimeter/length of a contour.
6    #[must_use]
7    pub fn arc_length(&self, closed: bool) -> f64 {
8        let pts = &self.points;
9        let n = pts.len();
10        if n < 2 {
11            return 0.0;
12        }
13
14        let mut length = 0.0;
15        let limit = if closed { n } else { n - 1 };
16
17        for i in 0..limit {
18            let p0 = pts[i];
19            let p1 = pts[(i + 1) % n];
20            let dx = p1.x as f64 - p0.x as f64;
21            let dy = p1.y as f64 - p0.y as f64;
22            length += (dx * dx + dy * dy).sqrt();
23        }
24
25        length
26    }
27
28    /// Returns the area of the contour region.
29    #[must_use]
30    pub fn contour_area(&self) -> f64 {
31        self.moments().m00
32    }
33
34    /// Approximates a polygonal curve using the Douglas-Peucker algorithm.
35    #[must_use]
36    pub fn approx_poly_dp(&self, epsilon: f64, closed: bool) -> Self {
37        let pts = &self.points;
38        if pts.len() <= 2 {
39            return self.clone();
40        }
41
42        fn find_perpendicular_distance(
43            p: &Point<usize>,
44            line_start: &Point<usize>,
45            line_end: &Point<usize>,
46        ) -> f64 {
47            let dx = line_end.x as f64 - line_start.x as f64;
48            let dy = line_end.y as f64 - line_start.y as f64;
49            let len2 = dx * dx + dy * dy;
50            if len2 == 0.0 {
51                let px = p.x as f64 - line_start.x as f64;
52                let py = p.y as f64 - line_start.y as f64;
53                return (px * px + py * py).sqrt();
54            }
55
56            let t = ((p.x as f64 - line_start.x as f64) * dx
57                + (p.y as f64 - line_start.y as f64) * dy)
58                / len2;
59            let t_clamped = t.clamp(0.0, 1.0);
60            let proj_x = line_start.x as f64 + t_clamped * dx;
61            let proj_y = line_start.y as f64 + t_clamped * dy;
62
63            let rx = p.x as f64 - proj_x;
64            let ry = p.y as f64 - proj_y;
65            (rx * rx + ry * ry).sqrt()
66        }
67
68        #[allow(clippy::needless_range_loop)]
69        fn douglas_peucker(
70            pts: &[Point<usize>],
71            start: usize,
72            end: usize,
73            epsilon: f64,
74            keep: &mut [bool],
75        ) {
76            if end <= start + 1 {
77                return;
78            }
79
80            let mut max_dist = 0.0;
81            let mut index = 0;
82            let line_start = &pts[start];
83            let line_end = &pts[end];
84
85            for i in (start + 1)..end {
86                let dist = find_perpendicular_distance(&pts[i], line_start, line_end);
87                if dist > max_dist {
88                    max_dist = dist;
89                    index = i;
90                }
91            }
92
93            if max_dist > epsilon {
94                keep[index] = true;
95                douglas_peucker(pts, start, index, epsilon, keep);
96                douglas_peucker(pts, index, end, epsilon, keep);
97            }
98        }
99
100        let mut keep = vec![false; pts.len()];
101        keep[0] = true;
102        keep[pts.len() - 1] = true;
103
104        if closed {
105            // Find coordinates furthest apart to initialize splitting
106            let start = 0;
107            let end = pts.len() - 1;
108            douglas_peucker(pts, start, end, epsilon, &mut keep);
109        } else {
110            douglas_peucker(pts, 0, pts.len() - 1, epsilon, &mut keep);
111        }
112
113        let approx_pts: Vec<Point<usize>> = pts
114            .iter()
115            .enumerate()
116            .filter(|&(idx, _)| keep[idx])
117            .map(|(_, &p)| p)
118            .collect();
119
120        Self::new(approx_pts)
121    }
122
123    /// Computes the straight bounding rectangle of the contour.
124    #[must_use]
125    pub fn bounding_rect(&self) -> Rect<usize> {
126        if self.points.is_empty() {
127            return Rect::default();
128        }
129
130        let mut min_x = usize::MAX;
131        let mut min_y = usize::MAX;
132        let mut max_x = usize::MIN;
133        let mut max_y = usize::MIN;
134
135        for p in &self.points {
136            min_x = min_x.min(p.x);
137            min_y = min_y.min(p.y);
138            max_x = max_x.max(p.x);
139            max_y = max_y.max(p.y);
140        }
141
142        Rect::new(min_x, min_y, max_x - min_x, max_y - min_y)
143    }
144
145    /// Finds the minimum area bounding box (center, size, and rotation angle in degrees).
146    #[must_use]
147    pub fn min_area_rect(&self) -> (Point<f64>, Size<f64>, f64) {
148        let hull = self.convex_hull();
149        if hull.points.len() < 3 {
150            let br = self.bounding_rect();
151            return (
152                Point::new(
153                    br.x as f64 + br.width as f64 / 2.0,
154                    br.y as f64 + br.height as f64 / 2.0,
155                ),
156                Size::new(br.width as f64, br.height as f64),
157                0.0,
158            );
159        }
160
161        let mut min_area = f64::MAX;
162        let mut best_center = Point::new(0.0, 0.0);
163        let mut best_size = Size::new(0.0, 0.0);
164        let mut best_angle = 0.0;
165
166        // Iterates rotations around center to find the minimum area bounding box
167        for angle_deg in 0..90 {
168            let theta = f64::from(angle_deg).to_radians();
169            let cos_t = theta.cos();
170            let sin_t = theta.sin();
171
172            let mut min_u = f64::MAX;
173            let mut max_u = f64::MIN;
174            let mut min_v = f64::MAX;
175            let mut max_v = f64::MIN;
176
177            for p in &hull.points {
178                let px = p.x as f64;
179                let py = p.y as f64;
180                // Rotated coordinates
181                let u = px * cos_t + py * sin_t;
182                let v = -px * sin_t + py * cos_t;
183
184                min_u = min_u.min(u);
185                max_u = max_u.max(u);
186                min_v = min_v.min(v);
187                max_v = max_v.max(v);
188            }
189
190            let w_rot = max_u - min_u;
191            let h_rot = max_v - min_v;
192            let area = w_rot * h_rot;
193
194            if area < min_area {
195                min_area = area;
196                let uc = f64::midpoint(min_u, max_u);
197                let vc = f64::midpoint(min_v, max_v);
198                // Map center back
199                let cx = uc * cos_t - vc * sin_t;
200                let cy = uc * sin_t + vc * cos_t;
201
202                best_center = Point::new(cx, cy);
203                best_size = Size::new(w_rot, h_rot);
204                best_angle = f64::from(angle_deg);
205            }
206        }
207
208        (best_center, best_size, best_angle)
209    }
210
211    /// Checks if a point is inside, outside, or on the boundary of the contour.
212    /// Returns:
213    /// - Positive distance if inside.
214    /// - Negative distance if outside.
215    /// - Zero if on the edge.
216    #[must_use]
217    pub fn point_polygon_test(&self, pt: Point<f64>, measure_dist: bool) -> f64 {
218        let pts = &self.points;
219        let n = pts.len();
220        if n == 0 {
221            return -1.0;
222        }
223
224        // 1. Winding number / ray casting inside-outside check
225        let mut inside = false;
226        let mut min_dist2 = f64::MAX;
227
228        for i in 0..n {
229            let p0 = pts[i];
230            let p1 = pts[(i + 1) % n];
231
232            let x0 = p0.x as f64;
233            let y0 = p0.y as f64;
234            let x1 = p1.x as f64;
235            let y1 = p1.y as f64;
236
237            // Ray casting logic
238            if ((y0 > pt.y) != (y1 > pt.y))
239                && (pt.x < (x1 - x0) * (pt.y - y0) / (y1 - y0 + 1e-9) + x0)
240            {
241                inside = !inside;
242            }
243
244            // Shortest distance to line segment calculation
245            let dx = x1 - x0;
246            let dy = y1 - y0;
247            let len2 = dx * dx + dy * dy;
248            let dist2 = if len2 == 0.0 {
249                let rx = pt.x - x0;
250                let ry = pt.y - y0;
251                rx * rx + ry * ry
252            } else {
253                let t = ((pt.x - x0) * dx + (pt.y - y0) * dy) / len2;
254                let t_clamped = t.clamp(0.0, 1.0);
255                let proj_x = x0 + t_clamped * dx;
256                let proj_y = y0 + t_clamped * dy;
257                let rx = pt.x - proj_x;
258                let ry = pt.y - proj_y;
259                rx * rx + ry * ry
260            };
261
262            if dist2 < min_dist2 {
263                min_dist2 = dist2;
264            }
265        }
266
267        let dist = min_dist2.sqrt();
268        if inside {
269            if measure_dist { dist } else { 1.0 }
270        } else {
271            if measure_dist { -dist } else { -1.0 }
272        }
273    }
274}
275
276/// Rotated rect corner helper class.
277pub struct RotatedRect;
278
279impl RotatedRect {
280    /// Maps center, size, and angle (degrees) to 4 corner points.
281    #[must_use]
282    pub fn box_points(center: Point<f64>, size: Size<f64>, angle_degrees: f64) -> [Point<f64>; 4] {
283        let theta = angle_degrees.to_radians();
284        let cos_t = theta.cos();
285        let sin_t = theta.sin();
286
287        let hw = size.width / 2.0;
288        let hh = size.height / 2.0;
289
290        let local_corners = [
291            Point::new(-hw, -hh),
292            Point::new(hw, -hh),
293            Point::new(hw, hh),
294            Point::new(-hw, hh),
295        ];
296
297        let mut corners = [Point::default(); 4];
298        for i in 0..4 {
299            let lc = local_corners[i];
300            let rx = lc.x * cos_t - lc.y * sin_t;
301            let ry = lc.x * sin_t + lc.y * cos_t;
302            corners[i] = Point::new(center.x + rx, center.y + ry);
303        }
304
305        corners
306    }
307}
308
309/// Implements Hu Moments computation from spatial moments.
310pub struct ShapeAnalysis;
311
312impl ShapeAnalysis {
313    /// Computes the 7 Hu Moments from image spatial moments.
314    #[must_use]
315    pub fn hu_moments(m: &Moments) -> [f64; 7] {
316        if m.m00.abs() < 1e-9 {
317            return [0.0; 7];
318        }
319
320        let xc = m.m10 / m.m00;
321        let yc = m.m01 / m.m00;
322
323        let mu00 = m.m00;
324        let mu20 = m.m20 - xc * m.m10;
325        let mu02 = m.m02 - yc * m.m01;
326        let mu11 = m.m11 - xc * m.m01;
327        let mu30 = m.m30 - 3.0 * xc * m.m20 + 2.0 * xc * xc * m.m10;
328        let mu03 = m.m03 - 3.0 * yc * m.m02 + 2.0 * yc * yc * m.m01;
329        let mu21 = m.m21 - 2.0 * xc * m.m11 - yc * m.m20 + 2.0 * xc * xc * m.m01;
330        let mu12 = m.m12 - 2.0 * yc * m.m11 - xc * m.m02 + 2.0 * yc * yc * m.m10;
331
332        let inv = 1.0 / mu00;
333        let inv2 = inv * inv;
334        let inv3 = inv2 * inv;
335
336        let eta20 = mu20 * inv2;
337        let eta02 = mu02 * inv2;
338        let eta11 = mu11 * inv2;
339        let eta30 = mu30 * inv3;
340        let eta03 = mu03 * inv3;
341        let eta21 = mu21 * inv3;
342        let eta12 = mu12 * inv3;
343
344        let h1 = eta20 + eta02;
345        let h2 = (eta20 - eta02).powi(2) + 4.0 * eta11 * eta11;
346        let h3 = (eta30 - 3.0 * eta12).powi(2) + (3.0 * eta21 - eta03).powi(2);
347        let h4 = (eta30 + eta12).powi(2) + (eta21 + eta03).powi(2);
348        let h5 = (eta30 - 3.0 * eta12)
349            * (eta30 + eta12)
350            * ((eta30 + eta12).powi(2) - 3.0 * (eta21 + eta03).powi(2))
351            + (3.0 * eta21 - eta03)
352                * (eta21 + eta03)
353                * (3.0 * (eta30 + eta12).powi(2) - (eta21 + eta03).powi(2));
354        let h6 = (eta20 - eta02) * ((eta30 + eta12).powi(2) - (eta21 + eta03).powi(2))
355            + 4.0 * eta11 * (eta30 + eta12) * (eta21 + eta03);
356        let h7 = (3.0 * eta21 - eta03)
357            * (eta30 + eta12)
358            * ((eta30 + eta12).powi(2) - 3.0 * (eta21 + eta03).powi(2))
359            - (eta30 - 3.0 * eta12)
360                * (eta21 + eta03)
361                * (3.0 * (eta30 + eta12).powi(2) - (eta21 + eta03).powi(2));
362
363        [h1, h2, h3, h4, h5, h6, h7]
364    }
365
366    /// Matches two shapes based on their Hu moments.
367    #[must_use]
368    pub fn match_shapes(m1: &Moments, m2: &Moments) -> f64 {
369        let hu1 = Self::hu_moments(m1);
370        let hu2 = Self::hu_moments(m2);
371
372        let mut diff = 0.0;
373        for i in 0..7 {
374            diff += (hu1[i] - hu2[i]).abs();
375        }
376        diff
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    #[test]
385    fn test_shape_analysis() {
386        let pts = vec![
387            Point::new(0, 0),
388            Point::new(10, 0),
389            Point::new(10, 10),
390            Point::new(0, 10),
391        ];
392        let contour = Contour::new(pts);
393
394        let length = contour.arc_length(true);
395        assert!(length > 0.0);
396
397        let area = contour.contour_area();
398        assert!(area > 0.0);
399
400        let approx = contour.approx_poly_dp(1.0, true);
401        assert!(!approx.points.is_empty());
402
403        let br = contour.bounding_rect();
404        assert_eq!(br.width, 10);
405        assert_eq!(br.height, 10);
406
407        let (center, size, angle) = contour.min_area_rect();
408        assert!(size.width > 0.0);
409
410        let in_pt = Point::new(5.0, 5.0);
411        let out_pt = Point::new(15.0, 15.0);
412        assert!(contour.point_polygon_test(in_pt, false) > 0.0);
413        assert!(contour.point_polygon_test(out_pt, false) < 0.0);
414
415        let corners = RotatedRect::box_points(center, size, angle);
416        assert_eq!(corners.len(), 4);
417
418        let m = contour.moments();
419        let hu = ShapeAnalysis::hu_moments(&m);
420        assert!(hu[0] > 0.0);
421
422        let score = ShapeAnalysis::match_shapes(&m, &m);
423        assert!(score < 1e-9);
424    }
425}