image_debug_utils/
region_labelling.rs

1//! Tools for visualizing and analyzing connected components results from `imageproc::region_labelling`.
2
3use crate::colors::generate_contrasting_colors;
4use image::{ImageBuffer, Luma, Rgba, RgbaImage};
5use std::collections::HashMap;
6
7/// Draws the n largest connected components with contrasting colors.
8///
9/// # Arguments
10/// * `labelled_image` - The labelled image generated by a function like `imageproc::region_labelling::connected_components`.
11/// * `n` - The number of largest components to keep and color.
12/// * `background_color` - The color for the background and smaller, unselected components.
13///
14/// # Examples
15///
16/// ```
17/// use image::{ImageBuffer, Luma, Rgba};
18/// use image_debug_utils::region_labelling::draw_principal_connected_components;
19///
20/// let mut labelled_image = ImageBuffer::<Luma<u32>, Vec<u32>>::new(10, 10);
21/// // Simulate a large component (label 1) and a small one (label 2)
22/// labelled_image.put_pixel(0, 0, Luma([1]));
23/// labelled_image.put_pixel(0, 1, Luma([1]));
24/// labelled_image.put_pixel(5, 5, Luma([2]));
25///
26/// // Keep top 1 component, use transparent black for background
27/// let colored = draw_principal_connected_components(&labelled_image, 1, Rgba([0, 0, 0, 0]));
28/// ```
29///
30/// # Returns
31/// An `RgbaImage` where the `n` largest components are colored and the rest is background.
32pub fn draw_principal_connected_components(
33    labelled_image: &ImageBuffer<Luma<u32>, Vec<u32>>,
34    n: usize,
35    background_color: Rgba<u8>,
36) -> RgbaImage {
37    let mut counts = HashMap::new();
38    for label in labelled_image.pixels() {
39        if label[0] != 0 {
40            *counts.entry(label[0]).or_insert(0) += 1;
41        }
42    }
43
44    let mut sorted_counts: Vec<_> = counts.into_iter().collect();
45    sorted_counts.sort_by(|a, b| b.1.cmp(&a.1));
46
47    let principal_labels: Vec<u32> = sorted_counts
48        .iter()
49        .take(n)
50        .map(|(label, _)| *label)
51        .collect();
52
53    let colors = generate_contrasting_colors(n, 255);
54    let color_map: HashMap<u32, Rgba<u8>> = principal_labels.into_iter().zip(colors).collect();
55
56    let (width, height) = labelled_image.dimensions();
57    let mut output_image = RgbaImage::from_pixel(width, height, background_color);
58
59    for (x, y, pixel) in labelled_image.enumerate_pixels() {
60        let label = pixel[0];
61        if let Some(color) = color_map.get(&label) {
62            output_image.put_pixel(x, y, *color);
63        }
64    }
65
66    output_image
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn test_draw_principal_connected_components() {
75        // Arrange:
76        // Define a labelled image with multiple components of different sizes.
77        // This simulates the output of `connected_components`.
78        // 1. A large 4x4 square (16 pixels), label 1.
79        // 2. A medium 2x3 rectangle (6 pixels), label 2.
80        // 3. A small 1x2 line (2 pixels), label 3.
81        #[rustfmt::skip]
82        let labels_vec = vec![
83            0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
84            0, 1, 1, 1, 1, 0, 0, 0, 0, 0,
85            0, 1, 1, 1, 1, 0, 2, 2, 2, 0,
86            0, 1, 1, 1, 1, 0, 2, 2, 2, 0,
87            0, 1, 1, 1, 1, 0, 0, 0, 0, 0,
88            0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
89            0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
90            0, 3, 0, 0, 0, 0, 0, 0, 0, 0,
91            0, 3, 0, 0, 0, 0, 0, 0, 0, 0,
92            0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
93        ];
94
95        let labelled_image = ImageBuffer::<Luma<u32>, _>::from_raw(10, 10, labels_vec).unwrap();
96
97        // We want to draw the two largest components (n=2).
98        let n = 2;
99        let background = Rgba([0, 0, 0, 255]);
100
101        // Act:
102        let result_image = draw_principal_connected_components(&labelled_image, n, background);
103
104        // Assert:
105        // -- Create the expected output image --
106
107        // The function will identify the largest components.
108        // Counts: {1: 16, 2: 6, 3: 2}.
109        // The two largest labels are 1 and 2.
110
111        // Get the colors that will be generated for these two labels.
112        let colors = generate_contrasting_colors(n, 255);
113        let color_for_label1 = colors[0]; // The largest component gets the first color.
114        let color_for_label2 = colors[1]; // The second largest component gets the second color.
115
116        let mut expected_image = RgbaImage::from_pixel(10, 10, background);
117
118        // Draw the largest component (label 1).
119        for y in 1..=4 {
120            for x in 1..=4 {
121                expected_image.put_pixel(x, y, color_for_label1);
122            }
123        }
124
125        // Draw the second largest component (label 2).
126        for y in 2..=3 {
127            for x in 6..=8 {
128                expected_image.put_pixel(x, y, color_for_label2);
129            }
130        }
131
132        // The smallest component (label 3) should remain as the background color,
133        // so we don't need to draw it on the expected_image.
134
135        assert_eq!(result_image, expected_image);
136    }
137
138    #[test]
139    fn test_draw_principal_connected_components_empty() {
140        // All background image.
141        let labelled_image = ImageBuffer::<Luma<u32>, Vec<u32>>::new(5, 5);
142        let background = Rgba([255, 255, 255, 255]);
143
144        // n = 0
145        let result = draw_principal_connected_components(&labelled_image, 0, background);
146        assert_eq!(result.dimensions(), (5, 5));
147        assert!(result.pixels().all(|p| *p == background));
148    }
149}