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}