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}