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.analysis.watershed;
31
32 import java.util.ArrayDeque;
33 import java.util.ArrayList;
34 import java.util.BitSet;
35 import java.util.List;
36
37 import org.openimaj.image.FImage;
38 import org.openimaj.image.analysis.watershed.event.ComponentStackMergeListener;
39 import org.openimaj.image.analysis.watershed.feature.ComponentFeature;
40 import org.openimaj.image.pixel.IntValuePixel;
41
42 /**
43 * Maximally Stable Extremal Region watershed algorithm, implemented as
44 * described in the Microsoft paper of Nister and Stewenius.
45 *
46 * @author David Dupplaw (dpd@ecs.soton.ac.uk)
47 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
48 *
49 */
50 public class WatershedProcessorAlgorithm
51 {
52 /**
53 * A sorted heap of pixels. When {@link #pop()} is called the lowest value
54 * pixel is returned first.
55 *
56 * @author David Dupplaw (dpd@ecs.soton.ac.uk)
57 * @author Jonathon Hare (dpd@ecs.soton.ac.uk)
58 *
59 */
60 private class BoundaryHeap
61 {
62 private BitSet availablePixels;
63 private ArrayDeque<IntValuePixel>[] stacks;
64
65 /**
66 * Construct a boundary heap object with a given number of levels (i.e.
67 * max value of a pixel).
68 *
69 * @param sz
70 * number of levels.
71 */
72 @SuppressWarnings("unchecked")
73 public BoundaryHeap(int sz) {
74 availablePixels = new BitSet(sz);
75 stacks = new ArrayDeque[sz];
76
77 for (int i = 0; i < sz; i++)
78 stacks[i] = new ArrayDeque<IntValuePixel>();
79 }
80
81 /**
82 * Pushes the pixel onto the heap.
83 *
84 * @param p
85 * The {@link IntValuePixel} to push onto the heap.
86 */
87 public void push(IntValuePixel p)
88 {
89 final ArrayDeque<IntValuePixel> l = stacks[p.value];
90 l.push(p);
91 availablePixels.set(p.value);
92 }
93
94 /**
95 * Returns the lowest available pixel off the heap (removing it from the
96 * heap). Pixels are returned in sorted order (lowest value first). The
97 * method will return null if the heap is empty.
98 *
99 * @return The lowest value available pixel or NULL if no pixels are
100 * available.
101 */
102 public IntValuePixel pop()
103 {
104 final int l = availablePixels.nextSetBit(0);
105 if (l == -1)
106 return null; // Null means no available pixels (empty heap)
107
108 final IntValuePixel xx = this.stacks[l].pop();
109 if (this.stacks[l].size() == 0)
110 availablePixels.set(l, false);
111 return xx; // lowest and newest pixel
112 }
113 }
114
115 /** The pixel where the pour will start */
116 private IntValuePixel startPixel = null;
117
118 /** The mask that shows which pixels have been visited */
119 private BitSet accessibleMask = null;
120
121 /** The current pixel being investigated */
122 private IntValuePixel currentPixel = null;
123
124 /** The stack of components during processing */
125 private ArrayDeque<Component> componentStack = null;
126
127 /** The boundary heap during processing */
128 private BoundaryHeap boundaryHeap = null;
129
130 /** The image being processed */
131 private int[][] greyscaleImage = null;
132
133 /**
134 * The listeners for this watershed process. They will be called as regions
135 * are detected
136 */
137 private List<ComponentStackMergeListener> csmListeners = null;
138
139 private Class<? extends ComponentFeature>[] featureClasses;
140
141 /**
142 * Default constructor
143 *
144 * @param greyscaleImage
145 * the image as a 2d array of integer values
146 * @param startPixel
147 * The pixel to start the process at
148 * @param featureClasses
149 * the features that should be created for each detected
150 * component
151 */
152 @SafeVarargs
153 public WatershedProcessorAlgorithm(int[][] greyscaleImage, IntValuePixel startPixel,
154 Class<? extends ComponentFeature>... featureClasses)
155 {
156 this.greyscaleImage = greyscaleImage;
157 this.startPixel = startPixel;
158 this.csmListeners = new ArrayList<ComponentStackMergeListener>();
159
160 this.featureClasses = featureClasses;
161 }
162
163 /**
164 * Default constructor
165 *
166 * @param bGreyscaleImage
167 * the image to apply the watershed transform too
168 * @param startPixel
169 * The pixel to start the process at
170 * @param featureClasses
171 * the features that should be created for each detected
172 * component
173 */
174 @SafeVarargs
175 public WatershedProcessorAlgorithm(FImage bGreyscaleImage, IntValuePixel startPixel,
176 Class<? extends ComponentFeature>... featureClasses)
177 {
178 this(new int[bGreyscaleImage.getHeight()][bGreyscaleImage.getWidth()], startPixel, featureClasses);
179
180 for (int j = 0; j < bGreyscaleImage.getHeight(); j++) {
181 for (int i = 0; i < bGreyscaleImage.getWidth(); i++) {
182 greyscaleImage[j][i] = (int) (bGreyscaleImage.pixels[j][i] * 255);
183 }
184 }
185 }
186
187 /**
188 * Start the detection process by pouring on water at the pour point. (part
189 * 1 and 2)
190 *
191 */
192 public void startPour()
193 {
194 // For each step on the downhill stream is created as
195 // a component.
196 this.currentPixel = startPixel;
197
198 // Store the grey level of the current pixel
199 this.currentPixel.value = greyscaleImage[this.startPixel.y][this.startPixel.x];
200
201 // Create the mask the shows where the water has access to
202 this.accessibleMask = new BitSet(this.greyscaleImage.length * this.greyscaleImage[0].length);
203
204 // Create the stack of components
205 this.componentStack = new ArrayDeque<Component>();
206
207 // Create the heap of boundary pixels
208 this.boundaryHeap = new BoundaryHeap(256);
209
210 // Create a dummy component with a greylevel higher than
211 // any allowed and push it onto the stack
212 final Component dummyComponent = new Component(new IntValuePixel(-1, -1, Integer.MAX_VALUE), featureClasses);
213 this.componentStack.push(dummyComponent);
214
215 // Continue the processing at the first pixel
216 this.processNeighbours();
217
218 // System.err.println("Component Stack: "+componentStack );
219 }
220
221 /**
222 * Process the current pixel's neighbours (part 4, 5, 6 and 7).
223 */
224 private void processNeighbours()
225 {
226 // Push an empty component with the current level
227 // onto the component stack
228 Component currentComponent = new Component(this.currentPixel, featureClasses);
229 componentStack.push(currentComponent);
230
231 // System.err.println( "Processing neighbours of "+currentPixel );
232
233 final boolean processNeighbours = true;
234 while (processNeighbours)
235 {
236 boolean toContinue = false;
237
238 // Get all the neighbours of the current pixel
239 final IntValuePixel[] neighbours = getNeighbourPixels_4(this.currentPixel);
240 // System.err.println("Neighbours: "+outputArray( neighbours ) );
241
242 // For each of the neighbours, check if the the neighbour
243 // is already accessible.
244 for (final IntValuePixel neighbour : neighbours)
245 {
246 if (neighbour == null)
247 break; // neighbours array is packed, so nulls only occur at
248 // the end
249
250 final int idx = neighbour.x + neighbour.y * this.greyscaleImage[0].length;
251
252 // If the neighbour is not accessible...
253 if (!this.accessibleMask.get(idx))
254 {
255 // Mark it as accessible...
256 this.accessibleMask.set(idx);
257 // System.err.println("Making "+neighbour+" accessible" );
258
259 // If its greylevel is not lower than the current one...
260 if (neighbour.value >= currentPixel.value)
261 {
262 // Push it onto the heap of boundary pixels
263 this.boundaryHeap.push(neighbour);
264 // System.err.println("1. Push "+neighbour+", = "+boundaryHeap
265 // );
266 }
267 // If, on the other hand, the greylevel is lower
268 // than the current one, enter the current pixel
269 // back into the queue of boundary pixels for later
270 // processing (with the next edge number), consider
271 // the new pixel and process it
272 // (this is the water pouring into the local minimum)
273 else
274 {
275 this.boundaryHeap.push(currentPixel);
276 // System.err.println("2. Push "+currentPixel+", = "+boundaryHeap
277 // );
278 this.currentPixel = neighbour;
279 currentComponent = new Component(this.currentPixel, featureClasses);
280 componentStack.push(currentComponent);
281 toContinue = true;
282 break;
283 }
284 }
285 }
286
287 if (toContinue)
288 continue;
289
290 // Accumulate the current pixel to the component at the top of the
291 // stack. (part 5)
292 this.componentStack.peek().accumulate(this.currentPixel);
293 // System.err.println("Added "+currentPixel+" to top component "+componentStack.peek()
294 // );
295
296 // Pop the heap of boundary pixels. (part 6)
297 final IntValuePixel p = this.boundaryHeap.pop();
298 // System.err.println("Popped "+p+", = "+boundaryHeap );
299
300 // If the heap is empty, then we're done
301 if (p == null)
302 return;
303
304 // If it's at the same grey-level we process its neighbours (part 6)
305 if (p.value == currentPixel.value)
306 {
307 this.currentPixel = p;
308 }
309 // If it's at a higher grey-level we must process the components in
310 // the stack (part 7)
311 else
312 {
313 this.currentPixel = p;
314 processComponentStack();
315 }
316 }
317 }
318
319 private void processComponentStack()
320 {
321 while (this.currentPixel.value > this.componentStack.peek().pivot.value)
322 {
323 // System.err.println( "Processing stack: "+componentStack );
324
325 // If the second component on the stack has a greater
326 // grey-level than the pixel, we set the component's grey-level
327 // to that of the pixel and quit...
328 final Component topOfStack = this.componentStack.pop();
329
330 // System.err.println( "Top of stack gl: "+topOfStack.greyLevel );
331 // System.err.println(
332 // "Second stack gl: "+componentStack.peek().greyLevel );
333 // System.err.println( "Pixel greylevel: "+currentPixel.value );
334
335 if (this.currentPixel.value < this.componentStack.peek().pivot.value)
336 {
337 topOfStack.pivot = this.currentPixel;
338 this.componentStack.push(topOfStack);
339
340 fireComponentStackMergeListener(componentStack.peek());
341
342 return;
343 }
344
345 fireComponentStackMergeListener(componentStack.peek(), topOfStack);
346
347 // Otherwise...
348 // Join the pixel lists
349 this.componentStack.peek().merge(topOfStack);
350
351 // TODO: histories of components
352 }
353 }
354
355 /**
356 * Returns the neighbouring pixels for a given pixel with 4-connectedness.
357 * If the pixel lies outside of the image the result will be null.
358 *
359 * @param pixel
360 * The pixel to find the neighbours of
361 * @return An array of pixels some of which may be null if they lie outside
362 * of the image boundary.
363 */
364 private IntValuePixel[] getNeighbourPixels_4(IntValuePixel pixel)
365 {
366 final IntValuePixel[] p = new IntValuePixel[4];
367 final int x = pixel.x;
368 final int y = pixel.y;
369
370 final int height = this.greyscaleImage.length;
371 final int width = this.greyscaleImage[0].length;
372
373 // Find the pixels
374 int c = 0;
375
376 if (x < width - 1)
377 p[c++] = new IntValuePixel(x + 1, y, greyscaleImage[y][x + 1]);
378
379 if (x > 0)
380 p[c++] = new IntValuePixel(x - 1, y, greyscaleImage[y][x - 1]);
381
382 if (y < height - 1)
383 p[c++] = new IntValuePixel(x, y + 1, greyscaleImage[y + 1][x]);
384
385 if (y > 0)
386 p[c++] = new IntValuePixel(x, y - 1, greyscaleImage[y - 1][x]);
387
388 return p;
389 }
390
391 /**
392 * Add a component stack merge listener
393 *
394 * @param csml
395 * The {@link ComponentStackMergeListener} to add
396 */
397 public void addComponentStackMergeListener(ComponentStackMergeListener csml)
398 {
399 csmListeners.add(csml);
400 }
401
402 /**
403 * Removes the given {@link ComponentStackMergeListener} from the listeners
404 * list.
405 *
406 * @param csml
407 * The {@link ComponentStackMergeListener} to remove
408 */
409 public void removeComponentStackMergeListener(ComponentStackMergeListener csml)
410 {
411 csmListeners.remove(csml);
412 }
413
414 /**
415 * Fire the component stack merge listener event for the merging of two
416 * components.
417 *
418 * @param c1
419 * The first component
420 * @param c2
421 * The second component
422 */
423 private void fireComponentStackMergeListener(Component c1, Component c2)
424 {
425 for (final ComponentStackMergeListener csm : csmListeners)
426 csm.componentsMerged(c1, c2);
427 }
428
429 /**
430 * Fire the component stack merge listener event for the upward merge of a
431 * component.
432 *
433 * @param c1
434 * The component that has been promoted to a higher intensity
435 * level
436 */
437 private void fireComponentStackMergeListener(Component c1)
438 {
439 for (final ComponentStackMergeListener csm : csmListeners)
440 csm.componentPromoted(c1);
441 }
442
443 /**
444 * Helper function for debugging arrays
445 *
446 * @param o
447 * @return
448 */
449 @SuppressWarnings("unused")
450 private String outputArray(Object[] o)
451 {
452 final StringBuilder sb = new StringBuilder();
453 sb.append("[");
454 boolean first = true;
455 for (final Object obj : o)
456 {
457 if (!first)
458 sb.append(",");
459 if (obj == null)
460 sb.append("null");
461 else
462 sb.append(obj.toString());
463 first = false;
464 }
465 sb.append("]");
466 return sb.toString();
467 }
468 }