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}