imageproc/
region_labelling.rs

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