1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
55
56
57
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
74
75
76
77
78
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
126
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
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
164
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
174
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
194
195
196
197
198 public boolean chessboardDetected() {
199 return this.result;
200 }
201
202
203
204
205
206
207
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 }