View Javadoc

1   /**
2    * Copyright (c) 2011, The University of Southampton and the individual contributors.
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without modification,
6    * are permitted provided that the following conditions are met:
7    *
8    *   * 	Redistributions of source code must retain the above copyright notice,
9    * 	this list of conditions and the following disclaimer.
10   *
11   *   *	Redistributions in binary form must reproduce the above copyright notice,
12   * 	this list of conditions and the following disclaimer in the documentation
13   * 	and/or other materials provided with the distribution.
14   *
15   *   *	Neither the name of the University of Southampton nor the names of its
16   * 	contributors may be used to endorse or promote products derived from this
17   * 	software without specific prior written permission.
18   *
19   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20   * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21   * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22   * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
23   * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26   * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29   */
30  package org.openimaj.image.connectedcomponent;
31  
32  import gnu.trove.map.hash.TIntIntHashMap;
33  
34  import java.util.ArrayList;
35  import java.util.HashMap;
36  import java.util.LinkedHashSet;
37  import java.util.List;
38  import java.util.Map;
39  
40  import org.openimaj.image.FImage;
41  import org.openimaj.image.analyser.ImageAnalyser;
42  import org.openimaj.image.pixel.ConnectedComponent;
43  import org.openimaj.image.pixel.ConnectedComponent.ConnectMode;
44  import org.openimaj.image.pixel.Pixel;
45  
46  /**
47   * A connected component labeler.
48   *
49   * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
50   */
51  public class ConnectedComponentLabeler implements ImageAnalyser<FImage> {
52  	/**
53  	 * Different algorithms for finding {@link ConnectedComponent}s.
54  	 *
55  	 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
56  	 */
57  	public enum Algorithm {
58  		/**
59  		 * A single-pass algorithm
60  		 *
61  		 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
62  		 *
63  		 */
64  		SINGLE_PASS {
65  			@Override
66  			public List<ConnectedComponent> findComponents(FImage image, float bgThreshold, ConnectMode mode) {
67  				final List<ConnectedComponent> components = new ArrayList<ConnectedComponent>();
68  
69  				// Single pass method inspired by the wikipedia two-pass
70  				// technique
71  				// http://en.wikipedia.org/wiki/Connected_component_labeling
72  				for (int y = 0; y < image.height; y++) {
73  					for (int x = 0; x < image.width; x++) {
74  						final float element = image.pixels[y][x];
75  
76  						if (element > bgThreshold) {
77  							final List<Pixel> neighbours = mode.getNeighbours(image, x, y, bgThreshold);
78  
79  							ConnectedComponent currentComponent = null;
80  							for (final Pixel p : neighbours) {
81  								final ConnectedComponent cc = searchPixel(p, components);
82  								if (cc != null) {
83  									if (currentComponent == null) {
84  										currentComponent = cc;
85  									} else if (currentComponent != cc) {
86  										currentComponent.merge(cc);
87  										components.remove(cc);
88  									}
89  								}
90  							}
91  
92  							if (currentComponent == null) {
93  								currentComponent = new ConnectedComponent();
94  								components.add(currentComponent);
95  							}
96  
97  							currentComponent.addPixel(x, y);
98  						}
99  					}
100 				}
101 
102 				return components;
103 			}
104 
105 			private ConnectedComponent searchPixel(Pixel p, List<ConnectedComponent> components) {
106 				for (final ConnectedComponent c : components) {
107 					if (c.find(p))
108 						return c;
109 				}
110 				return null;
111 			}
112 		},
113 		/**
114 		 * The standard two-pass algorithm.
115 		 *
116 		 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
117 		 */
118 		TWO_PASS {
119 			@Override
120 			public List<ConnectedComponent> findComponents(FImage image, float bgThreshold, ConnectMode mode) {
121 				final List<ConnectedComponent> components = new ArrayList<ConnectedComponent>();
122 				final TIntIntHashMap linked = new TIntIntHashMap();
123 				final int[][] labels = new int[image.height][image.width];
124 				int nextLabel = 1;
125 
126 				// first pass
127 				for (int y = 0; y < image.height; y++) {
128 					for (int x = 0; x < image.width; x++) {
129 						final float element = image.pixels[y][x];
130 
131 						if (element > bgThreshold) {
132 							final List<Pixel> neighbours = mode.getNeighbours(image, x, y, bgThreshold);
133 							final List<Integer> L = new ArrayList<Integer>();
134 
135 							for (final Pixel p : neighbours)
136 								if (labels[p.y][p.x] != 0)
137 									L.add(labels[p.y][p.x]);
138 
139 							if (L.size() == 0) {
140 								linked.put(nextLabel, nextLabel);
141 								labels[y][x] = nextLabel;
142 								nextLabel++;
143 							} else {
144 								int min = Integer.MAX_VALUE;
145 								for (final int i : L)
146 									if (i < min)
147 										min = i;
148 								labels[y][x] = min;
149 
150 								for (final int i : L) {
151 									merge(linked, i, min);
152 								}
153 							}
154 						}
155 					}
156 				}
157 
158 				// second pass
159 				final Map<Integer, ConnectedComponent> comp = new HashMap<Integer, ConnectedComponent>();
160 
161 				for (int i = 1; i <= linked.size(); i++) {
162 					int min = linked.get(i);
163 
164 					while (true) {
165 						final int m = linked.get(min);
166 
167 						if (m == min)
168 							break;
169 						else
170 							min = m;
171 					}
172 					linked.put(i, min);
173 				}
174 
175 				for (int y = 0; y < image.height; y++) {
176 					for (int x = 0; x < image.width; x++) {
177 						if (labels[y][x] != 0) {
178 							final int min = linked.get(labels[y][x]);
179 							// labels[r][c] = min; //not needed
180 
181 							if (comp.containsKey(min)) {
182 								comp.get(min).addPixel(x, y);
183 							} else {
184 								final ConnectedComponent cc = new ConnectedComponent();
185 								cc.addPixel(x, y);
186 								comp.put(min, cc);
187 							}
188 						}
189 					}
190 				}
191 				components.addAll(comp.values());
192 
193 				return components;
194 			}
195 
196 			private void merge(TIntIntHashMap linked, int start, int target) {
197 				if (start == target)
198 					return;
199 
200 				final int old = linked.get(start);
201 
202 				if (old > target) {
203 					linked.put(start, target);
204 					merge(linked, old, target);
205 				} else {
206 					merge(linked, target, old);
207 				}
208 			}
209 		},
210 		/**
211 		 * The flood-fill algorithm
212 		 *
213 		 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
214 		 */
215 		FLOOD_FILL {
216 			@Override
217 			public List<ConnectedComponent> findComponents(FImage image, float bgThreshold, ConnectMode mode) {
218 				final List<ConnectedComponent> components = new ArrayList<ConnectedComponent>();
219 				final int[][] labels = new int[image.height][image.width];
220 				int nextColor = 1;
221 
222 				for (int y = 0; y < image.height; y++) {
223 					for (int x = 0; x < image.width; x++) {
224 						if (image.pixels[y][x] != 0 && labels[y][x] == 0) {
225 							components.add(floodFill(image, new Pixel(x, y), labels, nextColor));
226 							nextColor++;
227 						}
228 					}
229 				}
230 
231 				return components;
232 			}
233 
234 			protected ConnectedComponent floodFill(FImage image, Pixel start, int[][] output, int color) {
235 				final ConnectedComponent cc = new ConnectedComponent();
236 				// Flood-fill (node, target-color, replacement-color):
237 				// 1. Set Q to the empty queue.
238 				// Queue<Pixel> queue = new LinkedList<Pixel>();
239 				final LinkedHashSet<Pixel> queue = new LinkedHashSet<Pixel>();
240 
241 				// 2. If the color of node is not equal to target-color, return.
242 				if (image.pixels[start.y][start.x] == 0)
243 					return cc;
244 
245 				// 3. Add node to Q.
246 				queue.add(start);
247 
248 				// 4. For each element n of Q:
249 				while (queue.size() > 0) {
250 					// Pixel n = queue.poll();
251 					final Pixel n = queue.iterator().next();
252 					queue.remove(n);
253 
254 					// 5. If the color of n is equal to target-color:
255 					if (image.pixels[n.y][n.x] != 0 && output[n.y][n.x] != color) {
256 						// 6. Set w and e equal to n.
257 						int e = n.x, w = n.x;
258 						// 7. Move w to the west until the color of the node to
259 						// the west of w no longer matches target-color.
260 						while (w > 0 && image.pixels[n.y][w - 1] != 0)
261 							w--;
262 
263 						// 8. Move e to the east until the color of the node to
264 						// the east of e no longer matches target-color.
265 						while (e < image.width - 1 && image.pixels[n.y][e + 1] != 0)
266 							e++;
267 
268 						// 9. Set the color of nodes between w and e to
269 						// replacement-color.
270 						for (int i = w; i <= e; i++) {
271 							output[n.y][i] = color;
272 							cc.addPixel(i, n.y);
273 
274 							// 10. For each node n between w and e:
275 							final int north = n.y - 1;
276 							final int south = n.y + 1;
277 							// 11. If the color of the node to the north of n is
278 							// target-color, add that node to Q.
279 							if (north >= 0 && image.pixels[north][i] != 0 && output[north][i] != color)
280 								queue.add(new Pixel(i, north));
281 							// If the color of the node to the south of n is
282 							// target-color, add that node to Q.
283 							if (south < image.height && image.pixels[south][i] != 0 && output[south][i] != color)
284 								queue.add(new Pixel(i, south));
285 						}
286 						// 12. Continue looping until Q is exhausted.
287 					}
288 				}
289 				// 13. Return.
290 				return cc;
291 			}
292 		};
293 
294 		/**
295 		 * Find the connected components in an image.
296 		 *
297 		 * @param image
298 		 *            the image
299 		 * @param bgThreshold
300 		 *            the threshold below which pixels should be considered to
301 		 *            be background
302 		 * @param mode
303 		 *            the {@link ConnectMode}.
304 		 * @return the connected components
305 		 */
306 		public abstract List<ConnectedComponent> findComponents(FImage image, float bgThreshold, ConnectMode mode);
307 	}
308 
309 	protected float bgThreshold = 0;
310 	protected Algorithm algorithm = Algorithm.TWO_PASS;
311 	protected ConnectMode mode;
312 	protected List<ConnectedComponent> components;
313 
314 	/**
315 	 * Construct using the default (two-pass) algorithm, background pixels
316 	 * having a value of 0 or less, and the given {@link ConnectMode}.
317 	 *
318 	 * @param mode
319 	 *            the connection mode.
320 	 */
321 	public ConnectedComponentLabeler(ConnectMode mode) {
322 		this.mode = mode;
323 	}
324 
325 	/**
326 	 * Construct using the given algorithm, background pixels having a value of
327 	 * 0 or less, and the given {@link ConnectMode}.
328 	 *
329 	 * @param algorithm
330 	 *            the algorithm to use
331 	 * @param mode
332 	 *            the connection mode.
333 	 */
334 	public ConnectedComponentLabeler(Algorithm algorithm, ConnectMode mode) {
335 		this.algorithm = algorithm;
336 		this.mode = mode;
337 	}
338 
339 	/**
340 	 * Construct using the given algorithm, background pixel threshold, and the
341 	 * given {@link ConnectMode}.
342 	 *
343 	 * @param algorithm
344 	 *            the algorithm to use
345 	 * @param bgThreshold
346 	 *            threshold at which pixels with lower values are considered to
347 	 *            be the background
348 	 * @param mode
349 	 *            the connection mode.
350 	 */
351 	public ConnectedComponentLabeler(Algorithm algorithm, float bgThreshold, ConnectMode mode) {
352 		this.algorithm = algorithm;
353 		this.bgThreshold = bgThreshold;
354 		this.mode = mode;
355 	}
356 
357 	/**
358 	 * Syntactic sugar for calling {@link #analyseImage(FImage)} followed by
359 	 * {@link #getComponents()};
360 	 *
361 	 * @param image
362 	 *            the image to extract components from
363 	 * @return the extracted components.
364 	 */
365 	public List<ConnectedComponent> findComponents(FImage image) {
366 		analyseImage(image);
367 		return components;
368 	}
369 
370 	@Override
371 	public void analyseImage(FImage image) {
372 		components = algorithm.findComponents(image, bgThreshold, mode);
373 	}
374 
375 	/**
376 	 * @return the list of components found in the last call to
377 	 *         {@link #analyseImage(FImage)}.
378 	 */
379 	public List<ConnectedComponent> getComponents() {
380 		return components;
381 	}
382 }