001/** 002 * Copyright (c) 2011, The University of Southampton and the individual contributors. 003 * All rights reserved. 004 * 005 * Redistribution and use in source and binary forms, with or without modification, 006 * are permitted provided that the following conditions are met: 007 * 008 * * Redistributions of source code must retain the above copyright notice, 009 * this list of conditions and the following disclaimer. 010 * 011 * * Redistributions in binary form must reproduce the above copyright notice, 012 * this list of conditions and the following disclaimer in the documentation 013 * and/or other materials provided with the distribution. 014 * 015 * * Neither the name of the University of Southampton nor the names of its 016 * contributors may be used to endorse or promote products derived from this 017 * software without specific prior written permission. 018 * 019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 029 */ 030package org.openimaj.image.analysis.watershed; 031 032import java.util.ArrayDeque; 033import java.util.ArrayList; 034import java.util.BitSet; 035import java.util.List; 036 037import org.openimaj.image.FImage; 038import org.openimaj.image.analysis.watershed.event.ComponentStackMergeListener; 039import org.openimaj.image.analysis.watershed.feature.ComponentFeature; 040import org.openimaj.image.pixel.IntValuePixel; 041 042/** 043 * Maximally Stable Extremal Region watershed algorithm, implemented as 044 * described in the Microsoft paper of Nister and Stewenius. 045 * 046 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 047 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk) 048 * 049 */ 050public class WatershedProcessorAlgorithm 051{ 052 /** 053 * A sorted heap of pixels. When {@link #pop()} is called the lowest value 054 * pixel is returned first. 055 * 056 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 057 * @author Jonathon Hare (dpd@ecs.soton.ac.uk) 058 * 059 */ 060 private class BoundaryHeap 061 { 062 private BitSet availablePixels; 063 private ArrayDeque<IntValuePixel>[] stacks; 064 065 /** 066 * Construct a boundary heap object with a given number of levels (i.e. 067 * max value of a pixel). 068 * 069 * @param sz 070 * number of levels. 071 */ 072 @SuppressWarnings("unchecked") 073 public BoundaryHeap(int sz) { 074 availablePixels = new BitSet(sz); 075 stacks = new ArrayDeque[sz]; 076 077 for (int i = 0; i < sz; i++) 078 stacks[i] = new ArrayDeque<IntValuePixel>(); 079 } 080 081 /** 082 * Pushes the pixel onto the heap. 083 * 084 * @param p 085 * The {@link IntValuePixel} to push onto the heap. 086 */ 087 public void push(IntValuePixel p) 088 { 089 final ArrayDeque<IntValuePixel> l = stacks[p.value]; 090 l.push(p); 091 availablePixels.set(p.value); 092 } 093 094 /** 095 * Returns the lowest available pixel off the heap (removing it from the 096 * heap). Pixels are returned in sorted order (lowest value first). The 097 * method will return null if the heap is empty. 098 * 099 * @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 public WatershedProcessorAlgorithm(int[][] greyscaleImage, IntValuePixel startPixel, 153 Class<? extends ComponentFeature>... featureClasses) 154 { 155 this.greyscaleImage = greyscaleImage; 156 this.startPixel = startPixel; 157 this.csmListeners = new ArrayList<ComponentStackMergeListener>(); 158 159 this.featureClasses = featureClasses; 160 } 161 162 /** 163 * Default constructor 164 * 165 * @param bGreyscaleImage 166 * the image to apply the watershed transform too 167 * @param startPixel 168 * The pixel to start the process at 169 * @param featureClasses 170 * the features that should be created for each detected 171 * component 172 */ 173 public WatershedProcessorAlgorithm(FImage bGreyscaleImage, IntValuePixel startPixel, 174 Class<? extends ComponentFeature>... featureClasses) 175 { 176 this(new int[bGreyscaleImage.getHeight()][bGreyscaleImage.getWidth()], startPixel, featureClasses); 177 178 for (int j = 0; j < bGreyscaleImage.getHeight(); j++) { 179 for (int i = 0; i < bGreyscaleImage.getWidth(); i++) { 180 greyscaleImage[j][i] = (int) (bGreyscaleImage.pixels[j][i] * 255); 181 } 182 } 183 } 184 185 /** 186 * Start the detection process by pouring on water at the pour point. (part 187 * 1 and 2) 188 * 189 */ 190 public void startPour() 191 { 192 // For each step on the downhill stream is created as 193 // a component. 194 this.currentPixel = startPixel; 195 196 // Store the grey level of the current pixel 197 this.currentPixel.value = greyscaleImage[this.startPixel.y][this.startPixel.x]; 198 199 // Create the mask the shows where the water has access to 200 this.accessibleMask = new BitSet(this.greyscaleImage.length * this.greyscaleImage[0].length); 201 202 // Create the stack of components 203 this.componentStack = new ArrayDeque<Component>(); 204 205 // Create the heap of boundary pixels 206 this.boundaryHeap = new BoundaryHeap(256); 207 208 // Create a dummy component with a greylevel higher than 209 // any allowed and push it onto the stack 210 final Component dummyComponent = new Component(new IntValuePixel(-1, -1, Integer.MAX_VALUE), featureClasses); 211 this.componentStack.push(dummyComponent); 212 213 // Continue the processing at the first pixel 214 this.processNeighbours(); 215 216 // System.err.println("Component Stack: "+componentStack ); 217 } 218 219 /** 220 * Process the current pixel's neighbours (part 4, 5, 6 and 7). 221 */ 222 private void processNeighbours() 223 { 224 // Push an empty component with the current level 225 // onto the component stack 226 Component currentComponent = new Component(this.currentPixel, featureClasses); 227 componentStack.push(currentComponent); 228 229 // System.err.println( "Processing neighbours of "+currentPixel ); 230 231 final boolean processNeighbours = true; 232 while (processNeighbours) 233 { 234 boolean toContinue = false; 235 236 // Get all the neighbours of the current pixel 237 final IntValuePixel[] neighbours = getNeighbourPixels_4(this.currentPixel); 238 // System.err.println("Neighbours: "+outputArray( neighbours ) ); 239 240 // For each of the neighbours, check if the the neighbour 241 // is already accessible. 242 for (final IntValuePixel neighbour : neighbours) 243 { 244 if (neighbour == null) 245 break; // neighbours array is packed, so nulls only occur at 246 // the end 247 248 final int idx = neighbour.x + neighbour.y * this.greyscaleImage[0].length; 249 250 // If the neighbour is not accessible... 251 if (!this.accessibleMask.get(idx)) 252 { 253 // Mark it as accessible... 254 this.accessibleMask.set(idx); 255 // System.err.println("Making "+neighbour+" accessible" ); 256 257 // If its greylevel is not lower than the current one... 258 if (neighbour.value >= currentPixel.value) 259 { 260 // Push it onto the heap of boundary pixels 261 this.boundaryHeap.push(neighbour); 262 // System.err.println("1. Push "+neighbour+", = "+boundaryHeap 263 // ); 264 } 265 // If, on the other hand, the greylevel is lower 266 // than the current one, enter the current pixel 267 // back into the queue of boundary pixels for later 268 // processing (with the next edge number), consider 269 // the new pixel and process it 270 // (this is the water pouring into the local minimum) 271 else 272 { 273 this.boundaryHeap.push(currentPixel); 274 // System.err.println("2. Push "+currentPixel+", = "+boundaryHeap 275 // ); 276 this.currentPixel = neighbour; 277 currentComponent = new Component(this.currentPixel, featureClasses); 278 componentStack.push(currentComponent); 279 toContinue = true; 280 break; 281 } 282 } 283 } 284 285 if (toContinue) 286 continue; 287 288 // Accumulate the current pixel to the component at the top of the 289 // stack. (part 5) 290 this.componentStack.peek().accumulate(this.currentPixel); 291 // System.err.println("Added "+currentPixel+" to top component "+componentStack.peek() 292 // ); 293 294 // Pop the heap of boundary pixels. (part 6) 295 final IntValuePixel p = this.boundaryHeap.pop(); 296 // System.err.println("Popped "+p+", = "+boundaryHeap ); 297 298 // If the heap is empty, then we're done 299 if (p == null) 300 return; 301 302 // If it's at the same grey-level we process its neighbours (part 6) 303 if (p.value == currentPixel.value) 304 { 305 this.currentPixel = p; 306 } 307 // If it's at a higher grey-level we must process the components in 308 // the stack (part 7) 309 else 310 { 311 this.currentPixel = p; 312 processComponentStack(); 313 } 314 } 315 } 316 317 private void processComponentStack() 318 { 319 while (this.currentPixel.value > this.componentStack.peek().pivot.value) 320 { 321 // System.err.println( "Processing stack: "+componentStack ); 322 323 // If the second component on the stack has a greater 324 // grey-level than the pixel, we set the component's grey-level 325 // to that of the pixel and quit... 326 final Component topOfStack = this.componentStack.pop(); 327 328 // System.err.println( "Top of stack gl: "+topOfStack.greyLevel ); 329 // System.err.println( 330 // "Second stack gl: "+componentStack.peek().greyLevel ); 331 // System.err.println( "Pixel greylevel: "+currentPixel.value ); 332 333 if (this.currentPixel.value < this.componentStack.peek().pivot.value) 334 { 335 topOfStack.pivot = this.currentPixel; 336 this.componentStack.push(topOfStack); 337 338 fireComponentStackMergeListener(componentStack.peek()); 339 340 return; 341 } 342 343 fireComponentStackMergeListener(componentStack.peek(), topOfStack); 344 345 // Otherwise... 346 // Join the pixel lists 347 this.componentStack.peek().merge(topOfStack); 348 349 // TODO: histories of components 350 } 351 } 352 353 /** 354 * Returns the neighbouring pixels for a given pixel with 4-connectedness. 355 * If the pixel lies outside of the image the result will be null. 356 * 357 * @param pixel 358 * The pixel to find the neighbours of 359 * @return An array of pixels some of which may be null if they lie outside 360 * of the image boundary. 361 */ 362 private IntValuePixel[] getNeighbourPixels_4(IntValuePixel pixel) 363 { 364 final IntValuePixel[] p = new IntValuePixel[4]; 365 final int x = pixel.x; 366 final int y = pixel.y; 367 368 final int height = this.greyscaleImage.length; 369 final int width = this.greyscaleImage[0].length; 370 371 // Find the pixels 372 int c = 0; 373 374 if (x < width - 1) 375 p[c++] = new IntValuePixel(x + 1, y, greyscaleImage[y][x + 1]); 376 377 if (x > 0) 378 p[c++] = new IntValuePixel(x - 1, y, greyscaleImage[y][x - 1]); 379 380 if (y < height - 1) 381 p[c++] = new IntValuePixel(x, y + 1, greyscaleImage[y + 1][x]); 382 383 if (y > 0) 384 p[c++] = new IntValuePixel(x, y - 1, greyscaleImage[y - 1][x]); 385 386 return p; 387 } 388 389 /** 390 * Add a component stack merge listener 391 * 392 * @param csml 393 * The {@link ComponentStackMergeListener} to add 394 */ 395 public void addComponentStackMergeListener(ComponentStackMergeListener csml) 396 { 397 csmListeners.add(csml); 398 } 399 400 /** 401 * Removes the given {@link ComponentStackMergeListener} from the listeners 402 * list. 403 * 404 * @param csml 405 * The {@link ComponentStackMergeListener} to remove 406 */ 407 public void removeComponentStackMergeListener(ComponentStackMergeListener csml) 408 { 409 csmListeners.remove(csml); 410 } 411 412 /** 413 * Fire the component stack merge listener event for the merging of two 414 * components. 415 * 416 * @param c1 417 * The first component 418 * @param c2 419 * The second component 420 */ 421 private void fireComponentStackMergeListener(Component c1, Component c2) 422 { 423 for (final ComponentStackMergeListener csm : csmListeners) 424 csm.componentsMerged(c1, c2); 425 } 426 427 /** 428 * Fire the component stack merge listener event for the upward merge of a 429 * component. 430 * 431 * @param c1 432 * The component that has been promoted to a higher intensity 433 * level 434 */ 435 private void fireComponentStackMergeListener(Component c1) 436 { 437 for (final ComponentStackMergeListener csm : csmListeners) 438 csm.componentPromoted(c1); 439 } 440 441 /** 442 * Helper function for debugging arrays 443 * 444 * @param o 445 * @return 446 */ 447 @SuppressWarnings("unused") 448 private String outputArray(Object[] o) 449 { 450 final StringBuilder sb = new StringBuilder(); 451 sb.append("["); 452 boolean first = true; 453 for (final Object obj : o) 454 { 455 if (!first) 456 sb.append(","); 457 if (obj == null) 458 sb.append("null"); 459 else 460 sb.append(obj.toString()); 461 first = false; 462 } 463 sb.append("]"); 464 return sb.toString(); 465 } 466}