imageproc/
region_labelling.rs

1//! Functions for finding and labelling connected components of an image.
2
3use std::cmp;
4
5use image::{GenericImage, GenericImageView, Luma};
6
7use crate::definitions::Image;
8use crate::union_find::DisjointSetForest;
9
10/// Determines which neighbors of a pixel we consider
11/// to be connected to it.
12#[derive(Debug, PartialEq, Eq, Copy, Clone)]
13pub enum Connectivity {
14    /// A pixel is connected to its N, S, E and W neighbors.
15    Four,
16    /// A pixel is connected to all of its neighbors.
17    Eight,
18}
19
20/// Returns an image of the same size as the input, where each pixel
21/// is labelled by the connected foreground component it belongs to,
22/// or 0 if it's in the background. Input pixels are treated as belonging
23/// to the background if and only if they are equal to the provided background pixel.
24///
25/// # Panics
26/// Panics if the image contains 2<sup>32</sup> or more pixels. If this limitation causes you
27/// problems then open an issue and we can rewrite this function to support larger images.
28///
29/// # Examples
30///
31/// ```
32/// # extern crate image;
33/// # #[macro_use]
34/// # extern crate imageproc;
35/// # fn main() {
36/// use image::Luma;
37/// use imageproc::region_labelling::{connected_components, Connectivity};
38///
39/// let background_color = Luma([0u8]);
40///
41/// let image = gray_image!(
42///     1, 0, 1, 1;
43///     0, 1, 1, 0;
44///     0, 0, 0, 0;
45///     0, 0, 0, 1);
46///
47/// // With four-way connectivity the foreground regions which
48/// // are only connected across diagonals belong to different
49/// // connected components.
50/// let components_four = gray_image!(type: u32,
51///     1, 0, 2, 2;
52///     0, 2, 2, 0;
53///     0, 0, 0, 0;
54///     0, 0, 0, 3);
55///
56/// assert_pixels_eq!(
57///     connected_components(&image, Connectivity::Four, background_color),
58///     components_four);
59///
60/// // With eight-way connectivity all foreground pixels in the top two rows
61/// // belong to the same connected component.
62/// let components_eight = gray_image!(type: u32,
63///     1, 0, 1, 1;
64///     0, 1, 1, 0;
65///     0, 0, 0, 0;
66///     0, 0, 0, 2);
67///
68/// assert_pixels_eq!(
69///     connected_components(&image, Connectivity::Eight, background_color),
70///     components_eight);
71/// # }
72/// ```
73///
74/// ```
75/// # extern crate image;
76/// # #[macro_use]
77/// # extern crate imageproc;
78/// # fn main() {
79/// // This example is like the first, except that not all of the input foreground
80/// // pixels are the same color. Pixels of different color are never counted
81/// // as belonging to the same connected component.
82///
83/// use image::Luma;
84/// use imageproc::region_labelling::{connected_components, Connectivity};
85///
86/// let background_color = Luma([0u8]);
87///
88/// let image = gray_image!(
89///     1, 0, 1, 1;
90///     0, 1, 2, 0;
91///     0, 0, 0, 0;
92///     0, 0, 0, 1);
93///
94/// let components_four = gray_image!(type: u32,
95///     1, 0, 2, 2;
96///     0, 3, 4, 0;
97///     0, 0, 0, 0;
98///     0, 0, 0, 5);
99///
100/// assert_pixels_eq!(
101///     connected_components(&image, Connectivity::Four, background_color),
102///     components_four);
103///
104/// // If this behaviour is not what you want then you can first
105/// // threshold the input image.
106/// use imageproc::contrast::{threshold, ThresholdType};
107///
108/// // Pixels equal to the threshold are treated as background.
109/// let thresholded = threshold(&image, 0,ThresholdType::Binary);
110///
111/// let thresholded_components_four = gray_image!(type: u32,
112///     1, 0, 2, 2;
113///     0, 2, 2, 0;
114///     0, 0, 0, 0;
115///     0, 0, 0, 3);
116///
117/// assert_pixels_eq!(
118///     connected_components(&thresholded, Connectivity::Four, background_color),
119///     thresholded_components_four);
120/// # }
121/// ```
122pub fn connected_components<I>(
123    image: &I,
124    conn: Connectivity,
125    background: I::Pixel,
126) -> Image<Luma<u32>>
127where
128    I: GenericImage,
129    I::Pixel: Eq,
130{
131    let (width, height) = image.dimensions();
132    let image_size = width as usize * height as usize;
133    if image_size >= 2usize.saturating_pow(32) {
134        panic!("Images with 2^32 or more pixels are not supported");
135    }
136
137    let mut out = Image::new(width, height);
138
139    // TODO: add macro to abandon early if either dimension is zero
140    if width == 0 || height == 0 {
141        return out;
142    }
143
144    let mut forest = DisjointSetForest::new(image_size);
145    let mut adj_labels = [0u32; 4];
146    let mut next_label = 1;
147
148    for y in 0..height {
149        for x in 0..width {
150            let current = unsafe { image.unsafe_get_pixel(x, y) };
151            if current == background {
152                continue;
153            }
154
155            let mut num_adj = 0;
156
157            if x > 0 {
158                // West
159                let pixel = unsafe { image.unsafe_get_pixel(x - 1, y) };
160                if pixel == current {
161                    let label = unsafe { out.unsafe_get_pixel(x - 1, y)[0] };
162                    adj_labels[num_adj] = label;
163                    num_adj += 1;
164                }
165            }
166
167            if y > 0 {
168                // North
169                let pixel = unsafe { image.unsafe_get_pixel(x, y - 1) };
170                if pixel == current {
171                    let label = unsafe { out.unsafe_get_pixel(x, y - 1)[0] };
172                    adj_labels[num_adj] = label;
173                    num_adj += 1;
174                }
175
176                if conn == Connectivity::Eight {
177                    if x > 0 {
178                        // North West
179                        let pixel = unsafe { image.unsafe_get_pixel(x - 1, y - 1) };
180                        if pixel == current {
181                            let label = unsafe { out.unsafe_get_pixel(x - 1, y - 1)[0] };
182                            adj_labels[num_adj] = label;
183                            num_adj += 1;
184                        }
185                    }
186                    if x < width - 1 {
187                        // North East
188                        let pixel = unsafe { image.unsafe_get_pixel(x + 1, y - 1) };
189                        if pixel == current {
190                            let label = unsafe { out.unsafe_get_pixel(x + 1, y - 1)[0] };
191                            adj_labels[num_adj] = label;
192                            num_adj += 1;
193                        }
194                    }
195                }
196            }
197
198            if num_adj == 0 {
199                unsafe {
200                    out.unsafe_put_pixel(x, y, Luma([next_label]));
201                }
202                next_label += 1;
203            } else {
204                let mut min_label = u32::MAX;
205                for n in 0..num_adj {
206                    min_label = cmp::min(min_label, adj_labels[n]);
207                }
208                unsafe {
209                    out.unsafe_put_pixel(x, y, Luma([min_label]));
210                }
211                for n in 0..num_adj {
212                    forest.union(min_label as usize, adj_labels[n] as usize);
213                }
214            }
215        }
216    }
217
218    // Make components start at 1
219    let mut output_labels = vec![0u32; image_size];
220    let mut count = 1;
221
222    unsafe {
223        for y in 0..height {
224            for x in 0..width {
225                let label = {
226                    if image.unsafe_get_pixel(x, y) == background {
227                        continue;
228                    }
229                    out.unsafe_get_pixel(x, y)[0]
230                };
231                let root = forest.root(label as usize);
232                let mut output_label = *output_labels.get_unchecked(root);
233                if output_label < 1 {
234                    output_label = count;
235                    count += 1;
236                }
237                *output_labels.get_unchecked_mut(root) = output_label;
238                out.unsafe_put_pixel(x, y, Luma([output_label]));
239            }
240        }
241    }
242
243    out
244}
245
246#[cfg(test)]
247mod tests {
248    extern crate wasm_bindgen_test;
249
250    use image::{GrayImage, Luma};
251    #[cfg(target_arch = "wasm32")]
252    use wasm_bindgen_test::*;
253
254    use crate::definitions::{HasBlack, HasWhite};
255
256    use super::Connectivity::{Eight, Four};
257    use super::connected_components;
258
259    #[cfg_attr(not(target_arch = "wasm32"), test)]
260    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
261    fn test_connected_components_eight_white_background() {
262        let image = gray_image!(
263              1, 255,   2,   1;
264            255,   1,   1, 255;
265            255, 255, 255, 255;
266            255, 255, 255,   1);
267
268        let expected = gray_image!(type: u32,
269            1, 0, 2, 1;
270            0, 1, 1, 0;
271            0, 0, 0, 0;
272            0, 0, 0, 3);
273
274        let labelled = connected_components(&image, Eight, Luma::white());
275        assert_pixels_eq!(labelled, expected);
276    }
277
278    // One huge component with eight-way connectivity, loads of
279    // isolated components with four-way connectivity.
280    pub(super) fn chessboard(width: u32, height: u32) -> GrayImage {
281        GrayImage::from_fn(width, height, |x, y| {
282            if (x + y) % 2 == 0 {
283                Luma([255u8])
284            } else {
285                Luma([0u8])
286            }
287        })
288    }
289
290    #[cfg_attr(not(target_arch = "wasm32"), test)]
291    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
292    fn test_connected_components_eight_chessboard() {
293        let image = chessboard(30, 30);
294        let components = connected_components(&image, Eight, Luma::black());
295        let max_component = components.pixels().map(|p| p[0]).max();
296        assert_eq!(max_component, Some(1u32));
297    }
298
299    #[cfg_attr(not(target_arch = "wasm32"), test)]
300    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
301    fn test_connected_components_four_chessboard() {
302        let image = chessboard(30, 30);
303        let components = connected_components(&image, Four, Luma::black());
304        let max_component = components.pixels().map(|p| p[0]).max();
305        assert_eq!(max_component, Some(450u32));
306    }
307}
308
309#[cfg(not(miri))]
310#[cfg(test)]
311mod benches {
312    use super::Connectivity::{Eight, Four};
313    use super::connected_components;
314    use super::tests::chessboard;
315    use crate::definitions::HasBlack;
316    use ::test;
317    use image::Luma;
318
319    #[bench]
320    fn bench_connected_components_eight_chessboard(b: &mut test::Bencher) {
321        let image = chessboard(300, 300);
322        b.iter(|| {
323            let components = connected_components(&image, Eight, Luma::black());
324            test::black_box(components);
325        });
326    }
327
328    #[bench]
329    fn bench_connected_components_four_chessboard(b: &mut test::Bencher) {
330        let image = chessboard(300, 300);
331        b.iter(|| {
332            let components = connected_components(&image, Four, Luma::black());
333            test::black_box(components);
334        });
335    }
336}