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.camera.calibration;
31  
32  import java.io.IOException;
33  import java.net.MalformedURLException;
34  import java.util.ArrayList;
35  import java.util.Collections;
36  import java.util.List;
37  
38  import org.openimaj.image.FImage;
39  import org.openimaj.image.MBFImage;
40  import org.openimaj.image.analyser.ImageAnalyser;
41  import org.openimaj.image.colour.RGBColour;
42  import org.openimaj.image.contour.Contour;
43  import org.openimaj.image.contour.SuzukiContourProcessor;
44  import org.openimaj.image.processing.algorithm.FilterSupport;
45  import org.openimaj.image.processing.algorithm.MinMaxAnalyser;
46  import org.openimaj.image.typography.hershey.HersheyFont;
47  import org.openimaj.math.geometry.shape.RotatedRectangle;
48  import org.openimaj.util.pair.FloatIntPair;
49  import org.openimaj.video.VideoDisplay;
50  import org.openimaj.video.VideoDisplayAdapter;
51  import org.openimaj.video.capture.VideoCapture;
52  
53  /**
54   * Analyser for performing a fast check to see if a chessboard is in the input
55   * image.
56   * 
57   * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
58   */
59  public class FastChessboardDetector implements ImageAnalyser<FImage> {
60  	private static final float BLACK_LEVEL = 20.f / 255f;
61  	private static final float WHITE_LEVEL = 130.f / 255f;
62  	private static final float BLACK_WHITE_GAP = 70.f / 255f;
63  
64  	private static final float MIN_ASPECT_RATIO = 0.3f;
65  	private static final float MAX_ASPECT_RATIO = 3.0f;
66  	private static final float MIN_BOX_SIZE = 10.0f;
67  
68  	private int patternHeight;
69  	private int patternWidth;
70  	private boolean result;
71  
72  	/**
73  	 * * Construct with the given pattern size
74  	 * 
75  	 * @param patternWidth
76  	 *            the pattern width
77  	 * @param patternHeight
78  	 *            the pattern height
79  	 */
80  	public FastChessboardDetector(int patternWidth, int patternHeight) {
81  		this.patternWidth = patternWidth;
82  		this.patternHeight = patternHeight;
83  	}
84  
85  	private void quickThresh(FImage in, FImage out, float thresh, boolean inverse) {
86  		int low = 0;
87  		int high = 1;
88  
89  		if (inverse) {
90  			low = 1;
91  			high = 0;
92  		}
93  
94  		for (int y = 0; y < in.height; y++) {
95  			for (int x = 0; x < in.width; x++) {
96  				out.pixels[y][x] = in.pixels[y][x] > thresh ? low : high;
97  			}
98  		}
99  	}
100 
101 	@Override
102 	public void analyseImage(FImage src) {
103 		final FImage thresh = new FImage(src.width, src.height);
104 
105 		final MinMaxAnalyser mma = new MinMaxAnalyser(FilterSupport.BLOCK_3x3);
106 		src.analyseWith(mma);
107 
108 		final FImage white = mma.min;
109 		final FImage black = mma.max;
110 
111 		result = false;
112 		for (float threshLevel = BLACK_LEVEL; threshLevel < WHITE_LEVEL && !result; threshLevel += (20.0f / 255f))
113 		{
114 			final List<FloatIntPair> quads = new ArrayList<FloatIntPair>();
115 
116 			quickThresh(white, thresh, threshLevel + BLACK_WHITE_GAP, false);
117 			getQuadrangleHypotheses(SuzukiContourProcessor.findContours(thresh), quads, 1);
118 
119 			quickThresh(black, thresh, threshLevel, true);
120 			getQuadrangleHypotheses(SuzukiContourProcessor.findContours(thresh), quads, 0);
121 
122 			final int minQuadsCount = patternWidth * patternHeight / 2;
123 			Collections.sort(quads, FloatIntPair.FIRST_ITEM_ASCENDING_COMPARATOR);
124 
125 			// now check if there are many hypotheses with similar sizes
126 			// do this by floodfill-style algorithm
127 			final float sizeRelDev = 0.4f;
128 
129 			for (int i = 0; i < quads.size(); i++)
130 			{
131 				int j = i + 1;
132 				for (; j < quads.size(); j++)
133 				{
134 					if (quads.get(j).first / quads.get(i).first > 1.0f + sizeRelDev)
135 					{
136 						break;
137 					}
138 				}
139 
140 				if (j + 1 > minQuadsCount + i)
141 				{
142 					// check the number of black and white squares
143 					final int[] counts = new int[2];
144 					countClasses(quads, i, j, counts);
145 					final int blackCount = (int) Math.round(Math.ceil(patternWidth / 2.0)
146 							* Math.ceil(patternHeight / 2.0));
147 					final int whiteCount = (int) Math.round(Math.floor(patternWidth / 2.0)
148 							* Math.floor(patternHeight / 2.0));
149 					if (counts[0] < blackCount * 0.75 ||
150 							counts[1] < whiteCount * 0.75)
151 					{
152 						continue;
153 					}
154 					result = true;
155 					break;
156 				}
157 			}
158 		}
159 	}
160 
161 	void countClasses(List<FloatIntPair> pairs, int idx1, int idx2, int[] counts)
162 	{
163 		// counts.assign(2, 0);
164 		// counts[2] = 0; // why?
165 		for (int i = idx1; i != idx2; i++)
166 		{
167 			counts[pairs.get(i).second]++;
168 		}
169 	}
170 
171 	void getQuadrangleHypotheses(Contour contours, List<FloatIntPair> quads, int classId) {
172 		for (final Contour seq : contours.contourIterable()) {
173 			// can assume the the contour is simple, thus making convex hull
174 			// computation much faster
175 			final RotatedRectangle box = seq.minimumBoundingRectangle(true);
176 
177 			final float boxSize = Math.max(box.width, box.height);
178 			if (boxSize < MIN_BOX_SIZE)
179 			{
180 				continue;
181 			}
182 
183 			final float aspectRatio = box.width / Math.max(box.height, 1);
184 			if (aspectRatio < MIN_ASPECT_RATIO || aspectRatio > MAX_ASPECT_RATIO)
185 			{
186 				continue;
187 			}
188 			quads.add(new FloatIntPair(boxSize, classId));
189 		}
190 	}
191 
192 	/**
193 	 * Check whether the last image analysed with {@link #analyseImage(FImage)}
194 	 * was likely to contain a suitable chessboard pattern.
195 	 * 
196 	 * @return true if pattern detected; false otherwise
197 	 */
198 	public boolean chessboardDetected() {
199 		return this.result;
200 	}
201 
202 	/**
203 	 * Simple test program
204 	 * 
205 	 * @param args
206 	 * @throws MalformedURLException
207 	 * @throws IOException
208 	 */
209 	public static void main(String[] args) throws MalformedURLException, IOException {
210 		final FastChessboardDetector fcd = new FastChessboardDetector(9, 6);
211 		final VideoDisplay<MBFImage> vd = VideoDisplay.createVideoDisplay(new VideoCapture(640,
212 				480));
213 		vd.setCalculateFPS(true);
214 		vd.addVideoListener(new VideoDisplayAdapter<MBFImage>() {
215 			@Override
216 			public void beforeUpdate(MBFImage frame) {
217 				fcd.analyseImage(frame.flatten());
218 				frame.drawText(fcd.result + "", 100, 100, HersheyFont.FUTURA_LIGHT,
219 						20, RGBColour.RED);
220 				System.out.println(vd.getDisplayFPS());
221 			}
222 		});
223 	}
224 }