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 */
030/**
031 * 
032 */
033package org.openimaj.image.feature.local.detector.mser;
034
035import java.util.ArrayList;
036import java.util.LinkedList;
037import java.util.List;
038
039import org.openimaj.citation.annotation.Reference;
040import org.openimaj.citation.annotation.ReferenceType;
041import org.openimaj.image.analysis.watershed.Component;
042import org.openimaj.util.tree.TreeNode;
043
044/**
045 * Takes a merge tree from the watershed algorithm and detects MSERs. The
046 * {@link #detect()} method returns a list of MSER components. The components
047 * are also marked as MSERs in the merge tree, such that the tree can be used to
048 * determine the hierarchical nature of the MSERs. Use the
049 * {@link Component#isMSER} to determine if the component in the tree is an
050 * MSER.
051 * 
052 * <blockquote> (From http://www.vlfeat.org/overview/mser.html) The stability of
053 * an extremal region R is the inverse of the relative area variation of the
054 * region R when the intensity level is increased by d. Formally, the variation
055 * is defined as:
056 * 
057 * <pre>
058 * <code>
059 * |R(+d) - R|
060 * -----------
061 *      |R|
062 *  </code>
063 * </pre>
064 * 
065 * where |R| denotes the area of the extremal region R, R(+d) is the extremal
066 * region +d levels up which contains R and |R(+d) - R| is the area difference
067 * of the two regions.
068 * 
069 * A stable region has a small variation. The algorithm finds regions which are
070 * "maximally stable", meaning that they have a lower variation than the regions
071 * one level below or above. Note that due to the discrete nature of the image,
072 * the region below / above may be coincident with the actual region, in which
073 * case the region is still deemed maximal.
074 * 
075 * However, even if an extremal region is maximally stable, it might be rejected
076 * if:
077 * 
078 * it is too big (see the parameter maxArea); it is too small (see the parameter
079 * minArea); it is too unstable (see the parameter maxVariation); it is too
080 * similar to its parent MSER (see the parameter minDiversity). </blockquote>
081 * 
082 * @author David Dupplaw (dpd@ecs.soton.ac.uk)
083 * 
084 */
085@Reference(
086                type = ReferenceType.Article,
087                author = { "J Matas", "O Chum", "M Urban", "T Pajdla" },
088                title = "Robust wide-baseline stereo from maximally stable extremal regions",
089                year = "2004",
090                journal = "Image and Vision Computing",
091                pages = { "761 ", " 767" },
092                url = "http://www.sciencedirect.com/science/article/pii/S0262885604000435",
093                number = "10",
094                volume = "22",
095                customData = {
096                                "issn", "0262-8856",
097                                "doi", "10.1016/j.imavis.2004.02.006",
098                                "keywords", "Robust metric"
099                })
100public class MSERDetector {
101        /** The maximum area an MSER can take, in pixels */
102        private int maxArea = Integer.MAX_VALUE;
103
104        /** The minimum area an MSER can be, in pixels */
105        private int minArea = 1;
106
107        /** The minimum stability an MSER must have */
108        private float maxVariation = 1;
109
110        /** The minimum diversity with its parent than an MSER must have */
111        private float minDiversity = 0;
112
113        /** The stability delta */
114        private int delta = 10;
115
116        /** The tree to be processed */
117        private TreeNode<Component> mergeTree = null;
118
119        /**
120         * Constructor that takes the merge tree from the watershed algorithm.
121         * 
122         * @param mergeTree
123         *            The merge tree
124         */
125        public MSERDetector(TreeNode<Component> mergeTree) {
126                this.mergeTree = mergeTree;
127        }
128
129        /**
130         * Detect MSERs in the merge tree provided in the constructor.
131         * 
132         * @return a list of detected regions.
133         */
134        public List<Component> detect() {
135                final List<Component> detectedRegions = processTree(mergeTree);
136                return detectedRegions;
137        }
138
139        /**
140         * Traverses the tree and attempts to find the minima (in terms of the rate
141         * of change of size over intensity) in the chains that form from the
142         * branches of the tree.
143         * 
144         * @param mergeTree
145         *            The merge tree.
146         * @return A list of stable regions
147         */
148        private List<Component> processTree(TreeNode<Component> mergeTree) {
149                final List<Component> detectedRegions = new ArrayList<Component>();
150                if (mergeTree != null)
151                        processTreeAux(mergeTree, detectedRegions, new LinkedList<TreeNode<Component>>());
152                return detectedRegions;
153        }
154
155        /**
156         * Recursive tree-traversal method for the processTree function.
157         * 
158         * @param treeNode
159         *            The current tree node being processed
160         * @param components
161         *            The list of stable regions so far detected
162         * @param path
163         *            The path to treeNode that has been taken so far
164         */
165        private void processTreeAux(TreeNode<Component> treeNode, List<Component> components,
166                        LinkedList<TreeNode<Component>> path)
167        {
168                // Is there a branch or a chain?
169                // Also no need to iterate down the tree if the intensity level of
170                // this component is already less than the delta, because no regions
171                // below this point couldn't possibly be considered stable.
172                if (treeNode.getChildren() != null && treeNode.getChildren().size() > 0 &&
173                                treeNode.getValue().pivot.value >= delta)
174                {
175                        path.add(treeNode);
176
177                        // Recursive traversal of tree
178                        for (final TreeNode<Component> node : treeNode.getChildren())
179                                processTreeAux(node, components, path);
180                }
181
182                // Remove this node, if present
183                // (it won't be present for leaf nodes)
184                path.remove(treeNode);
185                final Component child = treeNode.getValue();
186
187                // The size of the component must be within the defined limits
188                if (child.size() < maxArea && child.size() > minArea) {
189                        // Now calculate whether this component could be an MSER
190                        final Component parent = getAppropriateParent(path, child.pivot.value, delta);
191
192                        // If there's no appropriate parent node (above delta) then
193                        // this node cannot be an MSER
194                        if (parent != null) {
195                                final int intensityDifference = Math.abs(parent.pivot.value - child.pivot.value);
196
197                                // MSERs can only occur if the difference is greater than the
198                                // delta.
199                                if (intensityDifference >= delta) {
200                                        float variation;
201
202                                        if (intensityDifference == delta)
203                                                variation = Math.abs(child.size() - parent.size()) / (float) parent.size();
204                                        else
205                                                variation = 0; // the virtual node at level-delta has
206                                                                                // the same size as the current
207
208                                        // The variation must be less than the defined maximum
209                                        if (variation < maxVariation) {
210                                                child.isMSER = true;
211                                                components.add(child);
212
213                                                // duplicate detection: if any children are mser of
214                                                // similar size, then remove them
215                                                for (final TreeNode<Component> childNode : treeNode.getChildren()) {
216                                                        if (childNode.getValue().isMSER) {
217                                                                final float div = (float) (child.size() - childNode.getValue().size())
218                                                                                / (float) child.size();
219
220                                                                if (div < minDiversity) {
221                                                                        childNode.getValue().isMSER = false;
222                                                                        components.remove(childNode.getValue());
223                                                                }
224                                                        }
225                                                }
226                                        }
227                                }
228                        }
229                }
230        }
231
232        /**
233         * Returns the next appropriate parent where the parent component has a
234         * greylevel that is at least delta more than the current greylevel
235         * 
236         * @param path
237         *            The path to search
238         * @param currentGL
239         *            The current grey level of the node
240         * @param delta
241         *            The delta to use
242         * @return The next component up the tree path that has an appropriate
243         *         greylevel, or NULL if there isn't one
244         */
245        private Component getAppropriateParent(LinkedList<TreeNode<Component>> path, int currentGL, int delta) {
246                // Iterate through the path backwards
247                for (int i = path.size() - 1; i >= 0; i--) {
248                        if (Math.abs(path.get(i).getValue().pivot.value - currentGL) >= delta)
249                                return path.get(i).getValue();
250                }
251
252                return null;
253        }
254
255        /**
256         * @return the maxArea
257         */
258        public int getMaxArea() {
259                return maxArea;
260        }
261
262        /**
263         * @param maxArea
264         *            the maxArea to set
265         */
266        public void setMaxArea(int maxArea) {
267                this.maxArea = maxArea;
268        }
269
270        /**
271         * @return the minArea
272         */
273        public int getMinArea() {
274                return minArea;
275        }
276
277        /**
278         * @param minArea
279         *            the minArea to set
280         */
281        public void setMinArea(int minArea) {
282                this.minArea = minArea;
283        }
284
285        /**
286         * @return the maxVariation
287         */
288        public float getMaxVariation() {
289                return maxVariation;
290        }
291
292        /**
293         * @param maxVariation
294         *            the maxVariation to set
295         */
296        public void setMaxVariation(float maxVariation) {
297                this.maxVariation = maxVariation;
298        }
299
300        /**
301         * @return the minDiversity
302         */
303        public float getMinDiversity() {
304                return minDiversity;
305        }
306
307        /**
308         * @param minDiversity
309         *            the minDiversity to set
310         */
311        public void setMinDiversity(float minDiversity) {
312                this.minDiversity = minDiversity;
313        }
314
315        /**
316         * @return the mergeTree
317         */
318        public TreeNode<Component> getMergeTree() {
319                return mergeTree;
320        }
321
322        /**
323         * @return the delta
324         */
325        public int getDelta() {
326                return delta;
327        }
328
329        /**
330         * @param delta
331         *            the delta to set
332         */
333        public void setDelta(int delta) {
334                this.delta = delta;
335        }
336}