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.contour;
31  
32  import java.util.ArrayList;
33  import java.util.HashMap;
34  import java.util.List;
35  import java.util.Map;
36  
37  import org.openimaj.citation.annotation.Reference;
38  import org.openimaj.citation.annotation.ReferenceType;
39  import org.openimaj.image.FImage;
40  import org.openimaj.image.analyser.ImageAnalyser;
41  import org.openimaj.image.pixel.Pixel;
42  import org.openimaj.math.geometry.shape.Rectangle;
43  import org.openimaj.util.function.Operation;
44  import org.openimaj.util.pair.IndependentPair;
45  
46  /**
47   * Given a binary image (1-connected and 0-connected regions) detect contours
48   * and provide both the contours and a hierarchy of contour membership.
49   * 
50   * @author Sina Samangooei (ss@ecs.soton.ac.uk)
51   * 
52   */
53  @Reference(
54  		type = ReferenceType.Article,
55  		author = { "Suzuki, S.", "Abe, K." },
56  		title = "Topological Structural Analysis of Digitized Binary Image by Border Following",
57  		year = "1985",
58  		journal = "Computer Vision, Graphics and Image Processing",
59  		pages = { "32", "46" },
60  		month = "January",
61  		number = "1",
62  		volume = "30")
63  public class SuzukiContourProcessor implements ImageAnalyser<FImage> {
64  	/**
65  	 * the root border detected
66  	 */
67  	public Contour root;
68  	private double minRelativeChildProp = -1;
69  
70  	@Override
71  	public void analyseImage(final FImage image) {
72  		this.root = findContours(image, this);
73  	}
74  
75  	/**
76  	 * Find contours in the given image and return the border
77  	 * 
78  	 * @param image
79  	 *            the image
80  	 * @return the border
81  	 */
82  	public static Contour findContours(final FImage image) {
83  		return findContours(image, new SuzukiContourProcessor());
84  	}
85  
86  	/**
87  	 * Detect borders hierarchically in this binary image. Note the image is
88  	 * changed while borders are found.
89  	 * 
90  	 * @param image
91  	 *            the image
92  	 * @param proc
93  	 *            the contour detector
94  	 * @return the detected border
95  	 */
96  	public static Contour findContours(final FImage image, SuzukiContourProcessor proc) {
97  		final float nbd[] = new float[] { 1 };
98  		final float lnbd[] = new float[] { 1 };
99  		// Prepare the special outer frame
100 		final Contour root = new Contour(ContourType.HOLE);
101 		final Rectangle bb = image.getBounds();
102 		root.points.addAll(bb.asPolygon().getVertices());
103 		root.finish();
104 
105 		final Map<Float, Contour> borderMap = new HashMap<Float, Contour>();
106 		borderMap.put(lnbd[0], root);
107 		final SuzukiNeighborStrategy borderFollow = new SuzukiNeighborStrategy();
108 
109 		for (int i = 0; i < image.height; i++) {
110 			lnbd[0] = 1; // Beggining of appendix 1, this is the beggining of a
111 							// scan
112 			for (int j = 0; j < image.width; j++) {
113 				final float fji = image.getPixel(j, i);
114 				final boolean isOuter = isOuterBorderStart(image, i, j); // check
115 																			// 1(a)
116 				final boolean isHole = isHoleBorderStart(image, i, j); // check
117 																		// 1(b)
118 				if (isOuter || isHole) { // check 1(c)
119 					final Contour border = new Contour(j, i);
120 					Contour borderPrime = null;
121 					final Pixel from = new Pixel(j, i);
122 					if (isOuter) {
123 						nbd[0] += 1; // in 1(a) we increment NBD
124 						from.x -= 1;
125 						border.type = ContourType.OUTER;
126 						borderPrime = borderMap.get(lnbd[0]);
127 						// the check of table 1
128 						switch (borderPrime.type) {
129 						case OUTER:
130 							border.setParent(borderPrime.parent);
131 							break;
132 						case HOLE:
133 							border.setParent(borderPrime);
134 							break;
135 						}
136 					}
137 					else {
138 						nbd[0] += 1; // in 1(b) we increment NBD
139 						// according to 1(b) we set lnbd to the pixel value if
140 						// it is greater than 1
141 						if (fji > 1)
142 							lnbd[0] = fji;
143 						borderPrime = borderMap.get(lnbd[0]);
144 						from.x += 1;
145 						border.type = ContourType.HOLE;
146 						// the check of table 1
147 						switch (borderPrime.type) {
148 						case OUTER:
149 							border.setParent(borderPrime);
150 							break;
151 						case HOLE:
152 							border.setParent(borderPrime.parent);
153 							break;
154 						}
155 					}
156 
157 					final Pixel ij = new Pixel(j, i);
158 					borderFollow.directedContour(image, ij, from,
159 							new Operation<IndependentPair<Pixel, boolean[]>>() {
160 
161 								@Override
162 								public void perform(IndependentPair<Pixel, boolean[]> object) {
163 									final Pixel p = object.firstObject();
164 									final boolean[] d = object.secondObject();
165 									border.points.add(p);
166 									if (crossesEastBorder(image, d, p)) {
167 										image.setPixel(p.x, p.y, -nbd[0]);
168 									} else if (image.getPixel(p) == 1f) {
169 										// only set if the pixel has not been
170 										// visited before 3.4(b)!
171 										image.setPixel(p.x, p.y, nbd[0]);
172 									} // otherwise leave it alone!
173 								}
174 
175 							});
176 					// this is 3.1, if no borders were given, this means this is
177 					// a pixel on its own, so we set it to -nbd
178 					if (border.points.size() == 0) {
179 						border.points.add(ij);
180 						image.setPixel(j, i, -nbd[0]);
181 					}
182 					border.finish();
183 					// if(thisborder.rect.calculateArea())
184 					borderMap.put(nbd[0], border);
185 				}
186 				// This is step (4)
187 				if (fji != 0 && fji != 1)
188 					lnbd[0] = Math.abs(fji);
189 
190 			}
191 		}
192 		if (proc.minRelativeChildProp > 0) {
193 			removeSmall(root, proc.minRelativeChildProp);
194 		}
195 		return root;
196 	}
197 
198 	private static void removeSmall(Contour root, double minRelativeChildProp) {
199 		final List<Contour> toSearch = new ArrayList<Contour>();
200 		toSearch.add(root);
201 		while (toSearch.size() != 0) {
202 			final Contour ret = toSearch.remove(0);
203 			if (ret.parent != null && ret.rect.calculateArea() / ret.parent.rect.calculateArea() < minRelativeChildProp) {
204 				ret.parent.children.remove(ret);
205 			} else {
206 				toSearch.addAll(ret.children);
207 			}
208 		}
209 	}
210 
211 	private static boolean crossesEastBorder(final FImage image, boolean[] checked, final Pixel p) {
212 		final boolean b = checked[Direction.fromTo(p, new Pixel(p.x + 1, p.y)).ordinal()];
213 		return image.getPixel(p) != 0 && (p.x == image.width - 1 || b);// this
214 																		// is
215 																		// 3.4(a)
216 																		// with
217 																		// an
218 																		// edge
219 																		// case
220 																		// check
221 	}
222 
223 	private static boolean isOuterBorderStart(FImage image, int i, int j) {
224 		return (image.pixels[i][j] == 1 && (j == 0 || image.pixels[i][j - 1] == 0));
225 	}
226 
227 	private static boolean isHoleBorderStart(FImage image, int i, int j) {
228 		return (image.pixels[i][j] >= 1 && (j == image.width - 1 || image.pixels[i][j + 1] == 0));
229 	}
230 
231 	/**
232 	 * Set the threshold at which small children (measured relative to their
233 	 * parent area) are removed.
234 	 * 
235 	 * @param d
236 	 *            the threshold
237 	 */
238 	public void setMinRelativeChildProp(double d) {
239 		this.minRelativeChildProp = d;
240 	}
241 
242 }