image_debug_utils/
contours.rs

1//! Specialized utilities for working with contours found by [`imageproc::contours`].
2//!
3//! This module provides functions for filtering, sorting, and analyzing the hierarchy of
4//! contours, complementing the core functionality in `imageproc`.
5
6use imageproc::{
7    contours::{BorderType, Contour},
8    geometry::min_area_rect,
9    point::Point,
10};
11use num::{Num, NumCast};
12use num_traits::AsPrimitive;
13
14/// Calculates the perimeter of each contour and sorts them in descending order.
15///
16/// This function takes a vector of `Contour<T>` objects, computes the perimeter for each one,
17/// and returns a new vector of tuples, where each tuple contains the original contour
18/// and its calculated perimeter as an `f64`. The returned vector is sorted based on the
19/// perimeter in descending order.
20///
21/// For performance, this function takes ownership of the input vector and uses an unstable sort.
22/// The perimeter is calculated as the sum of Euclidean distances between consecutive points,
23/// closing the loop by including the distance between the last and first point.
24///
25/// # Type Parameters
26///
27/// * `T`: The numeric type of the point coordinates within the contour. It must be a type
28///   that can be losslessly converted to `f64` for distance calculations, such as `i32` or `u32`.
29///
30/// # Arguments
31///
32/// * `contours`: A `Vec<Contour<T>>` which will be consumed by the function.
33///
34/// # Returns
35///
36/// A `Vec<(Contour<T>, f64)>` sorted by the perimeter in descending order.
37/// Contours with 0 or 1 point will have a perimeter of `0.0`.
38///
39/// # Examples
40///
41/// ```
42/// use imageproc::contours::{Contour, BorderType};
43/// use imageproc::point::Point;
44/// use image_debug_utils::contours::sort_by_perimeters_owned;
45///
46/// let c1 = Contour {
47///     parent: None,
48///     border_type: BorderType::Outer,
49///     points: vec![Point::new(0,0), Point::new(10,0), Point::new(10,10), Point::new(0,10)]
50/// }; // Perimeter 40.0
51///
52/// let c2 = Contour {
53///     parent: None,
54///     border_type: BorderType::Outer,
55///     points: vec![Point::new(0,0), Point::new(10,0)]
56/// }; // Perimeter 20.0 (10 + 10 back to start)
57///
58/// let contours = vec![c2.clone(), c1.clone()];
59///
60/// let sorted = sort_by_perimeters_owned(contours);
61/// assert_eq!(sorted[0].1, 40.0);
62/// assert_eq!(sorted[1].1, 20.0);
63/// ```
64///
65pub fn sort_by_perimeters_owned<T>(contours: Vec<Contour<T>>) -> Vec<(Contour<T>, f64)>
66where
67    T: Num + NumCast + Copy + PartialEq + Eq + AsPrimitive<f64>,
68{
69    let mut contours_with_perimeters: Vec<(Contour<T>, f64)> = contours
70        .into_iter()
71        .map(|contour| {
72            let perimeter: f64 = contour
73                .points
74                .iter()
75                .zip(contour.points.iter().cycle().skip(1))
76                .map(|(p1, p2)| {
77                    let dx: f64 = p2.x.as_() - p1.x.as_();
78                    let dy: f64 = p2.y.as_() - p1.y.as_();
79                    dx.hypot(dy)
80                })
81                .sum();
82            (contour, perimeter)
83        })
84        .collect();
85
86    contours_with_perimeters.sort_unstable_by(|a, b| b.1.total_cmp(&a.1));
87
88    contours_with_perimeters
89}
90
91/// Filters a vector of contours in-place based on shape properties.
92///
93/// This function removes contours that do not meet the specified criteria.
94/// The filtering is done based on two optional conditions:
95///
96/// 1.  **Border Type**: If `border_type` is `Some`, only contours with a matching
97///     `border_type` are kept.
98/// 2.  **Aspect Ratio**: Contours whose minimum area bounding rectangle has an
99///     aspect ratio (long side / short side) greater than or equal to
100///     `max_aspect_ratio` are removed.
101///
102/// # Arguments
103///
104/// * `contours`: A mutable reference to a `Vec<Contour>` to be filtered.
105/// * `max_aspect_ratio`: The maximum allowed aspect ratio. Must be a positive value.
106/// * `border_type`: An `Option<BorderType>` to filter contours by their border type.
107///
108/// # Examples
109///
110/// ```
111/// use imageproc::contours::{Contour, BorderType};
112/// use imageproc::point::Point;
113/// use image_debug_utils::contours::remove_hypotenuse_in_place;
114///
115/// let mut contours = vec![
116///     // Square, aspect ratio 1.0 (keep)
117///     Contour {
118///         parent: None,
119///         border_type: BorderType::Outer,
120///         points: vec![Point::new(0,0), Point::new(10,0), Point::new(10,10), Point::new(0,10)]
121///     },
122///     // Thin strip, high aspect ratio > 5.0 (remove)
123///     Contour {
124///         parent: None,
125///         border_type: BorderType::Outer,
126///         points: vec![Point::new(0,0), Point::new(100,0), Point::new(100,2), Point::new(0,2)]
127///     }
128/// ];
129///
130/// remove_hypotenuse_in_place(&mut contours, 5.0, None);
131/// assert_eq!(contours.len(), 1);
132/// ```
133///
134/// # Panics
135///
136/// # Type Parameters
137///
138/// * `T`: The numeric type of the point coordinates. Must implement `Num`, `Copy`, `AsPrimitive<f32>`,
139///   and be compatible with `min_area_rect`.
140///
141/// # Panics
142///
143/// Panics if `max_aspect_ratio` is not a positive finite number.
144pub fn remove_hypotenuse_in_place<T>(
145    contours: &mut Vec<Contour<T>>,
146    max_aspect_ratio: f32,
147    border_type: Option<BorderType>,
148) where
149    T: Num + NumCast + Copy + PartialEq + Eq + Ord + AsPrimitive<f32>,
150{
151    assert!(
152        max_aspect_ratio.is_finite() && max_aspect_ratio > 0.0,
153        "max_aspect_ratio must be a positive finite number"
154    );
155
156    let distance_squared = |p1: Point<T>, p2: Point<T>| -> f32 {
157        let dx: f32 = p1.x.as_() - p2.x.as_();
158        let dy: f32 = p1.y.as_() - p2.y.as_();
159        dx * dx + dy * dy
160    };
161
162    contours.retain(|contour| {
163        if let Some(required_type) = border_type
164            && contour.border_type != required_type
165        {
166            return false;
167        }
168
169        if contour.points.len() < 4 {
170            return false;
171        }
172
173        let rect_points = min_area_rect(&contour.points);
174
175        let side1_squared = distance_squared(rect_points[0], rect_points[1]);
176        let side2_squared = distance_squared(rect_points[1], rect_points[2]);
177
178        if side1_squared < 1e-6 || side2_squared < 1e-6 {
179            return false;
180        }
181
182        let aspect_ratio = if side1_squared > side2_squared {
183            (side1_squared / side2_squared).sqrt()
184        } else {
185            (side2_squared / side1_squared).sqrt()
186        };
187
188        aspect_ratio < max_aspect_ratio
189    });
190}
191
192/// Counts the number of direct child contours for each contour by consuming the input vector,
193/// and returns pairs of `(Contour, count)`, sorted by the count in descending order.
194///
195/// This function takes ownership of the `contours` vector. This is a highly performant
196/// approach as it avoids cloning the `Contour` objects. Instead, it moves them from the
197/// input vector into the result vector. This is ideal when the original `contours` vector
198/// is no longer needed after this call.
199///
200/// The time complexity is O(N log N), dominated by the final sort, where N is the number
201/// of contours. The memory overhead is minimal as no deep copies of contour data occur.
202///
203/// # Examples
204///
205/// ```
206/// use imageproc::contours::{Contour, BorderType};
207/// use image_debug_utils::contours::sort_by_direct_children_count_owned;
208///
209/// // Create a hierarchy where index 0 is parent of index 1
210/// let c0: Contour<i32> = Contour { parent: None, border_type: BorderType::Outer, points: vec![] };
211/// let c1: Contour<i32> = Contour { parent: Some(0), border_type: BorderType::Hole, points: vec![] };
212/// let contours = vec![c0, c1];
213///
214/// let sorted = sort_by_direct_children_count_owned(contours);
215/// // c0 (sorted[0]) has 1 child, c1 (sorted[1]) has 0
216/// assert_eq!(sorted[0].1, 1);
217/// assert_eq!(sorted[1].1, 0);
218/// ```
219///
220/// # Arguments
221///
222/// * `contours` - A `Vec<Contour<i32>>` which will be consumed by the function. The caller
223///   loses ownership of this vector.
224///
225/// # Returns
226///
227/// A `Vec<(Contour<T>, usize)>` where each tuple contains a contour moved from the input
228/// and its direct child count. The vector is sorted in descending order based on the count.
229///
230pub fn sort_by_direct_children_count_owned<T>(
231    contours: Vec<Contour<T>>,
232) -> Vec<(Contour<T>, usize)> {
233    if contours.is_empty() {
234        return Vec::new();
235    }
236
237    let mut child_counts = vec![0; contours.len()];
238
239    for contour in &contours {
240        if let Some(parent_index) = contour.parent
241            && let Some(count) = child_counts.get_mut(parent_index)
242        {
243            *count += 1;
244        }
245    }
246
247    let mut result: Vec<(Contour<T>, usize)> = contours
248        .into_iter()
249        .enumerate()
250        .map(|(i, contour)| (contour, child_counts[i]))
251        .collect();
252
253    result.sort_unstable_by(|a, b| b.1.cmp(&a.1));
254
255    result
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    fn assert_float_eq(a: f64, b: f64) {
263        assert!(
264            (a - b).abs() < 1e-9,
265            "Assertion failed: expected {}, got {}",
266            b,
267            a
268        );
269    }
270
271    fn make_contour(parent: Option<usize>, points: Vec<Point<i32>>) -> Contour<i32> {
272        Contour {
273            points,
274            parent,
275            border_type: BorderType::Hole,
276        }
277    }
278
279    fn make_simple_contour(parent: Option<usize>) -> Contour<i32> {
280        make_contour(parent, Vec::new())
281    }
282
283    #[test]
284    fn test_filter_contours() {
285        // A "good" contour with a 1:1 aspect ratio.
286        let good_contour = Contour {
287            points: vec![
288                Point::new(10, 10),
289                Point::new(20, 10),
290                Point::new(20, 20),
291                Point::new(10, 20),
292            ],
293            border_type: BorderType::Outer,
294            parent: None,
295        };
296
297        // A "bad" contour with a high 10:1 aspect ratio.
298        let bad_contour_high_ratio = Contour {
299            points: vec![
300                Point::new(0, 0),
301                Point::new(100, 0),
302                Point::new(100, 10),
303                Point::new(0, 10),
304            ],
305            border_type: BorderType::Outer,
306            parent: None,
307        };
308
309        // A contour with a different border type (Hole).
310        let hole_contour = Contour {
311            points: vec![
312                Point::new(30, 30),
313                Point::new(40, 30),
314                Point::new(40, 40),
315                Point::new(30, 40),
316            ],
317            border_type: BorderType::Hole,
318            parent: Some(0),
319        };
320
321        // A degenerate contour with too few points.
322        let degenerate_contour = Contour {
323            points: vec![Point::new(0, 0), Point::new(1, 1)],
324            border_type: BorderType::Outer,
325            parent: None,
326        };
327
328        // --- Test Case 1: Filter by aspect ratio only ---
329        let mut contours1 = vec![
330            good_contour.clone(),
331            bad_contour_high_ratio.clone(),
332            degenerate_contour.clone(),
333        ];
334        remove_hypotenuse_in_place(&mut contours1, 5.0, None);
335        assert_eq!(contours1.len(), 1);
336        assert_eq!(contours1[0].points, good_contour.points);
337
338        // --- Test Case 2: Filter by BorderType only (using a high aspect ratio threshold) ---
339        let mut contours2 = vec![
340            good_contour.clone(),
341            bad_contour_high_ratio.clone(),
342            hole_contour.clone(),
343        ];
344        remove_hypotenuse_in_place(&mut contours2, 100.0, Some(BorderType::Outer));
345        assert_eq!(contours2.len(), 2, "Should keep both 'Outer' contours");
346        // Check that the hole contour is removed.
347        assert!(!contours2.iter().any(|c| c.border_type == BorderType::Hole));
348
349        // --- Test Case 3: Filter by both aspect ratio and BorderType ---
350        let mut contours3 = vec![
351            good_contour.clone(),
352            bad_contour_high_ratio.clone(),
353            hole_contour.clone(),
354        ];
355        remove_hypotenuse_in_place(&mut contours3, 5.0, Some(BorderType::Outer));
356        assert_eq!(
357            contours3.len(),
358            1,
359            "Should keep only the 'good' outer contour"
360        );
361        assert_eq!(contours3[0].points, good_contour.points);
362
363        // --- Test Case 4: No contours removed ---
364        let mut contours4 = vec![good_contour.clone(), hole_contour.clone()];
365        remove_hypotenuse_in_place(&mut contours4, 10.0, None);
366        assert_eq!(contours4.len(), 2, "Should not remove any contours");
367
368        // --- Test Case 5: Filter everything out ---
369        let mut contours5 = vec![bad_contour_high_ratio.clone()];
370        remove_hypotenuse_in_place(&mut contours5, 5.0, None);
371        assert!(
372            contours5.is_empty(),
373            "Should filter out the high-ratio contour"
374        );
375
376        // --- Test Case 6: Degenerate contour (0-area) ---
377        let degenerate_contour_4pts = Contour {
378            points: vec![
379                Point::new(0, 0),
380                Point::new(0, 0),
381                Point::new(1, 0),
382                Point::new(1, 0),
383            ],
384            border_type: BorderType::Outer,
385            parent: None,
386        };
387        let mut contours6 = vec![degenerate_contour_4pts];
388        remove_hypotenuse_in_place(&mut contours6, 5.0, None);
389        assert!(contours6.is_empty(), "0-area contour should be removed");
390    }
391
392    #[test]
393    fn test_owned_nested_hierarchy_for_direct_children() {
394        // Hierarchy:
395        // 0 -> 1      (1 direct child)
396        // 1 -> 2      (1 direct child)
397        // 3 -> 4, 5   (2 direct children)
398        let contours = vec![
399            make_contour(None, vec![Point::new(1, 1)]), // 0, give unique points
400            make_contour(Some(0), vec![Point::new(2, 2)]), // 1
401            make_simple_contour(Some(1)),               // 2
402            make_contour(None, vec![Point::new(10, 10)]), // 3
403            make_simple_contour(Some(3)),               // 4
404            make_simple_contour(Some(3)),               // 5
405        ];
406        let contours_clone = contours.clone();
407
408        let result = sort_by_direct_children_count_owned(contours);
409
410        assert_eq!(result.len(), 6);
411
412        // First element must be contour 3 with 2 children.
413        // MODIFICATION: Compare the `points` field.
414        assert_eq!(result[0].0.points, contours_clone[3].points);
415        assert_eq!(result[0].1, 2);
416
417        // The next two elements must have 1 child each.
418        assert_eq!(result[1].1, 1);
419        assert_eq!(result[2].1, 1);
420        let one_child_contours_points: Vec<_> =
421            result[1..3].iter().map(|(c, _)| &c.points).collect();
422        // MODIFICATION: Check if the points vectors are present.
423        assert!(one_child_contours_points.contains(&&contours_clone[0].points));
424        assert!(one_child_contours_points.contains(&&contours_clone[1].points));
425
426        // The remaining must have 0 children.
427        assert!(result[3..].iter().all(|(_, count)| *count == 0));
428    }
429
430    #[test]
431    fn test_sort_by_direct_children_count_empty() {
432        let contours: Vec<Contour<i32>> = Vec::new();
433        let result = sort_by_direct_children_count_owned(contours);
434        assert!(result.is_empty());
435    }
436
437    #[test]
438    fn test_sort_by_direct_children_count_malformed_hierarchy() {
439        // Hierarchy with invalid parent index (out of bounds)
440        let contours = vec![Contour {
441            parent: Some(99), // Invalid index
442            border_type: BorderType::Outer,
443            points: vec![Point::new(1, 1)],
444        }];
445        let result = sort_by_direct_children_count_owned(contours);
446        assert_eq!(result.len(), 1);
447        assert_eq!(result[0].1, 0); // Invalid parent index should be ignored
448    }
449
450    #[test]
451    fn test_calculate_perimeters_and_sort_comprehensive() {
452        // 1. Test with an empty vector
453        let empty_contours: Vec<Contour<i32>> = vec![];
454        let result = sort_by_perimeters_owned(empty_contours);
455        assert!(
456            result.is_empty(),
457            "Should return an empty vec for empty input"
458        );
459
460        // 2. Test with a mix of contours, including edge cases
461        // Using the updated struct literal syntax for Contour
462        let square = Contour {
463            // Perimeter: 10 + 10 + 10 + 10 = 40.0
464            points: vec![
465                Point::new(0, 0),
466                Point::new(10, 0),
467                Point::new(10, 10),
468                Point::new(0, 10),
469            ],
470            border_type: BorderType::Outer,
471            parent: None,
472        };
473        let line = Contour {
474            // Perimeter: 10 (forward) + 10 (back) = 20.0
475            points: vec![Point::new(0, 0), Point::new(10, 0)],
476            border_type: BorderType::Outer,
477            parent: None,
478        };
479        let triangle = Contour {
480            // Perimeter: 3 + 4 + 5 = 12.0
481            points: vec![Point::new(0, 0), Point::new(3, 0), Point::new(0, 4)],
482            border_type: BorderType::Outer,
483            parent: None,
484        };
485        let single_point = Contour {
486            // Perimeter: 0.0
487            points: vec![Point::new(100, 100)],
488            border_type: BorderType::Hole,
489            parent: Some(0),
490        };
491        let empty_points = Contour {
492            // Perimeter: 0.0
493            points: vec![],
494            border_type: BorderType::Outer,
495            parent: None,
496        };
497
498        let contours = vec![triangle, single_point, square, empty_points, line];
499        let result = sort_by_perimeters_owned(contours);
500
501        // 3. Verify the results
502        assert_eq!(result.len(), 5, "Should return results for all 5 contours");
503
504        // Check order and values
505        // [0]: Square (40.0)
506        assert_eq!(result[0].0.points.len(), 4);
507        assert_float_eq(result[0].1, 40.0);
508
509        // [1]: Line (20.0)
510        assert_eq!(result[1].0.points.len(), 2);
511        assert_float_eq(result[1].1, 20.0);
512
513        // [2]: Triangle (12.0)
514        assert_eq!(result[2].0.points.len(), 3);
515        assert_float_eq(result[2].1, 12.0);
516
517        // [3] & [4]: Single Point (0.0) and Empty Points (0.0).
518        // Their relative order is not guaranteed due to unstable sort,
519        // so we just check that the last two perimeters are 0.0.
520        assert_float_eq(result[3].1, 0.0);
521        assert_float_eq(result[4].1, 0.0);
522        let last_two_point_counts: Vec<usize> =
523            result.iter().skip(3).map(|c| c.0.points.len()).collect();
524        assert!(last_two_point_counts.contains(&0));
525        assert!(last_two_point_counts.contains(&1));
526    }
527}