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}