ddd08ae138970edf2c26a0dea08afbc52c63c1ab
[zxing.git] / core / src / com / google / zxing / datamatrix / detector / Detector.java
1 /*
2  * Copyright 2008 ZXing authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.google.zxing.datamatrix.detector;
18
19 import com.google.zxing.MonochromeBitmapSource;
20 import com.google.zxing.ReaderException;
21 import com.google.zxing.ResultPoint;
22 import com.google.zxing.BlackPointEstimationMethod;
23 import com.google.zxing.common.BitArray;
24 import com.google.zxing.common.BitMatrix;
25 import com.google.zxing.common.Collections;
26 import com.google.zxing.common.Comparator;
27 import com.google.zxing.common.DetectorResult;
28 import com.google.zxing.common.GenericResultPoint;
29 import com.google.zxing.common.GridSampler;
30
31 import java.util.Enumeration;
32 import java.util.Hashtable;
33 import java.util.Vector;
34
35 /**
36  * <p>Encapsulates logic that can detect a Data Matrix Code in an image, even if the Data Matrix Code
37  * is rotated or skewed, or partially obscured.</p>
38  *
39  * @author srowen@google.com (Sean Owen)
40  */
41 public final class Detector {
42
43   private static final int MAX_MODULES = 32;
44
45   // Trick to avoid creating new Integer objects below -- a sort of crude copy of
46   // the Integer.valueOf(int) optimization added in Java 5, not in J2ME
47   private static final Integer[] INTEGERS =
48       { new Integer(0), new Integer(1), new Integer(2), new Integer(3), new Integer(4) };
49
50   private final MonochromeBitmapSource image;
51
52   public Detector(MonochromeBitmapSource image) {
53     this.image = image;
54   }
55
56   /**
57    * <p>Detects a Data Matrix Code in an image.</p>
58    *
59    * @return {@link DetectorResult} encapsulating results of detecting a QR Code
60    * @throws ReaderException if no Data Matrix Code can be found
61    */
62   public DetectorResult detect() throws ReaderException {
63
64     if (!BlackPointEstimationMethod.TWO_D_SAMPLING.equals(image.getLastEstimationMethod())) {
65       image.estimateBlackPoint(BlackPointEstimationMethod.TWO_D_SAMPLING, 0);
66     }
67
68     int height = image.getHeight();
69     int width = image.getWidth();
70     int halfHeight = height >> 1;
71     int halfWidth = width >> 1;
72     int iSkip = Math.max(1, height / (MAX_MODULES << 3));
73     int jSkip = Math.max(1, width / (MAX_MODULES << 3));
74
75     int minI = 0;
76     int maxI = height;
77     int minJ = 0;
78     int maxJ = width;
79     ResultPoint pointA = findCornerFromCenter(halfHeight, -iSkip, minI, maxI, halfWidth,      0, minJ, maxJ, halfWidth >> 1);
80     minI = (int) pointA.getY() - 1;
81     ResultPoint pointB = findCornerFromCenter(halfHeight, 0,      minI, maxI, halfWidth, -jSkip, minJ, maxJ, halfHeight >> 1);
82     minJ = (int) pointB.getX() - 1;
83     ResultPoint pointC = findCornerFromCenter(halfHeight, 0,      minI, maxI, halfWidth,  jSkip, minJ, maxJ, halfHeight >> 1);
84     maxJ = (int) pointC.getX() + 1;
85     ResultPoint pointD = findCornerFromCenter(halfHeight,  iSkip, minI, maxI, halfWidth,      0, minJ, maxJ, halfWidth >> 1);
86     maxI = (int) pointD.getY() + 1;
87     // Go try to find point A again with better information -- might have been off at first.
88     pointA = findCornerFromCenter(halfHeight, -iSkip, minI, maxI, halfWidth,      0, minJ, maxJ, halfWidth >> 2);
89
90     // Point A and D are across the diagonal from one another,
91     // as are B and C. Figure out which are the solid black lines
92     // by counting transitions
93     Vector transitions = new Vector(4);
94     transitions.addElement(transitionsBetween(pointA, pointB));
95     transitions.addElement(transitionsBetween(pointA, pointC));
96     transitions.addElement(transitionsBetween(pointB, pointD));
97     transitions.addElement(transitionsBetween(pointC, pointD));
98     Collections.insertionSort(transitions, new ResultPointsAndTransitionsComparator());
99
100     // Sort by number of transitions. First two will be the two solid sides; last two
101     // will be the two alternating black/white sides
102     ResultPointsAndTransitions lSideOne = (ResultPointsAndTransitions) transitions.elementAt(0);
103     ResultPointsAndTransitions lSideTwo = (ResultPointsAndTransitions) transitions.elementAt(1);
104
105     // Figure out which point is their intersection by tallying up the number of times we see the
106     // endpoints in the four endpoints. One will show up twice.
107     Hashtable pointCount = new Hashtable();
108     increment(pointCount, lSideOne.getFrom());
109     increment(pointCount, lSideOne.getTo());
110     increment(pointCount, lSideTwo.getFrom());
111     increment(pointCount, lSideTwo.getTo());
112
113     ResultPoint maybeTopLeft = null;
114     ResultPoint bottomLeft = null;
115     ResultPoint maybeBottomRight = null;
116     Enumeration points = pointCount.keys();
117     while (points.hasMoreElements()) {
118       ResultPoint point = (ResultPoint) points.nextElement();
119       Integer value = (Integer) pointCount.get(point);
120       if (value.intValue() == 2) {
121         bottomLeft = point; // this is definitely the bottom left, then -- end of two L sides
122       } else {
123         // Otherwise it's either top left or bottom right -- just assign the two arbitrarily now
124         if (maybeTopLeft == null) {
125           maybeTopLeft = point;
126         } else {
127           maybeBottomRight = point;
128         }
129       }
130     }
131
132     if (maybeTopLeft == null || bottomLeft == null || maybeBottomRight == null) {
133       throw new ReaderException("Could not find three corners");
134     }
135
136     // Bottom left is correct but top left and bottom right might be switched
137     ResultPoint[] corners = new ResultPoint[] { maybeTopLeft, bottomLeft, maybeBottomRight };
138     // Use the dot product trick to sort them out
139     GenericResultPoint.orderBestPatterns(corners);
140
141     // Now we know which is which:
142     ResultPoint bottomRight = corners[0];
143     bottomLeft = corners[1];
144     ResultPoint topLeft = corners[2];
145
146     // Which point didn't we find in relation to the "L" sides? that's the top right corner
147     ResultPoint topRight;
148     if (!pointCount.containsKey(pointA)) {
149       topRight = pointA;
150     } else if (!pointCount.containsKey(pointB)) {
151       topRight = pointB;
152     } else if (!pointCount.containsKey(pointC)) {
153       topRight = pointC;
154     } else {
155       topRight = pointD;
156     }
157
158     // Next determine the dimension by tracing along the top or right side and counting black/white
159     // transitions. Since we start inside a black module, we should see a number of transitions
160     // equal to 1 less than the code dimension. Well, actually 2 less, because we are going to
161     // end on a black module:
162
163     // The top right point is actually the corner of a module, which is one of the two black modules
164     // adjacent to the white module at the top right. Tracing to that corner from either the top left
165     // or bottom right should work here, but, one will be more reliable since it's traced straight
166     // up or across, rather than at a slight angle. We use dot products to figure out which is
167     // better to use:
168     int dimension;
169     if (GenericResultPoint.crossProductZ(bottomLeft, bottomRight, topRight) <
170         GenericResultPoint.crossProductZ(topRight, topLeft, bottomLeft)) {
171       dimension = transitionsBetween(topLeft, topRight).getTransitions();
172     } else {
173       dimension = transitionsBetween(bottomRight, topRight).getTransitions();
174     }
175     dimension += 2;
176
177     BitMatrix bits = sampleGrid(image, topLeft, bottomLeft, bottomRight, dimension);
178     return new DetectorResult(bits, new ResultPoint[] {pointA, pointB, pointC, pointD});
179   }
180
181   /**
182    * Attempts to locate a corner of the barcode by scanning up, down, left or right from a center
183    * point which should be within the barcode.
184    *
185    * @param centerI center's i componennt (vertical)
186    * @param di change in i per step. If scanning up this is negative; down, positive; left or right, 0
187    * @param minI minimum value of i to search through (meaningless when di == 0)
188    * @param maxI maximum value of i
189    * @param centerJ center's j component (horizontal)
190    * @param dj same as di but change in j per step instead
191    * @param minJ see minI
192    * @param maxJ see minJ
193    * @param maxWhiteRun maximum run of white pixels that can still be considered to be within
194    *  the barcode
195    * @return a {@link ResultPoint} encapsulating the corner that was found
196    * @throws ReaderException if such a point cannot be found
197    */
198   private ResultPoint findCornerFromCenter(int centerI, int di, int minI, int maxI,
199                                            int centerJ, int dj, int minJ, int maxJ,
200                                            int maxWhiteRun) throws ReaderException {
201     int[] lastRange = null;
202     for (int i = centerI, j = centerJ;
203          i < maxI && i >= minI && j < maxJ && j >= minJ;
204          i += di, j += dj) {
205       int[] range;
206       if (dj == 0) {
207         // horizontal slices, up and down
208         range = blackWhiteRange(i, maxWhiteRun, minJ, maxJ, true);
209       } else {
210         // vertical slices, left and right
211         range = blackWhiteRange(j, maxWhiteRun, minI, maxI, false);
212       }
213       if (range == null) {
214         if (lastRange == null) {
215           throw new ReaderException("Center of image not within barcode");
216         }
217         // lastRange was found
218         if (dj == 0) {
219           int lastI = i - di;
220           if (lastRange[0] < centerJ) {
221             if (lastRange[1] > centerJ) {
222               // straddle, choose one or the other based on direction
223               return new GenericResultPoint(di > 0 ? lastRange[0] : lastRange[1], lastI);
224             }
225             return new GenericResultPoint(lastRange[0], lastI);
226           } else {
227             return new GenericResultPoint(lastRange[1], lastI);
228           }
229         } else {
230           int lastJ = j - dj;
231           if (lastRange[0] < centerI) {
232             if (lastRange[1] > centerI) {
233               return new GenericResultPoint(lastJ, dj < 0 ? lastRange[0] : lastRange[1]);
234             }
235             return new GenericResultPoint(lastJ, lastRange[0]);
236           } else {
237             return new GenericResultPoint(lastJ, lastRange[1]);
238           }
239         }
240       }
241       lastRange = range;
242     }
243     throw new ReaderException("Couldn't find an end to barcode");
244   }
245
246   /**
247    * Increments the Integer associated with a key by one.
248    */
249   private static void increment(Hashtable table, ResultPoint key) {
250     Integer value = (Integer) table.get(key);
251     table.put(key, value == null ? INTEGERS[1] : INTEGERS[value.intValue() + 1]);
252   }
253
254   /**
255    * Computes the start and end of a region of pixels, either horizontally or vertically, that could be
256    * part of a Data Matrix barcode.
257    *
258    * @param fixedDimension if scanning horizontally, this is the row (the fixed vertical location) where
259    *  we are scanning. If scanning vertically it's the colummn, the fixed horizontal location
260    * @param maxWhiteRun largest run of white pixels that can still be considered part of the barcode region
261    * @param minDim minimum pixel location, horizontally or vertically, to consider
262    * @param maxDim maximum pixel location, horizontally or vertically, to consider
263    * @param horizontal if true, we're scanning left-right, instead of up-down
264    * @return int[] with start and end of found range, or null if no such range is found (e.g. only white was found)
265    */
266   private int[] blackWhiteRange(int fixedDimension, int maxWhiteRun, int minDim, int maxDim, boolean horizontal) {
267
268     int center = (minDim + maxDim) / 2;
269
270     BitArray rowOrColumn = horizontal ? image.getBlackRow(fixedDimension, null, 0, image.getWidth())
271                                       : image.getBlackColumn(fixedDimension, null, 0, image.getHeight());
272
273     // Scan left/up first
274     int start = center;
275     while (start >= minDim) {
276       if (rowOrColumn.get(start)) {
277         start--;
278       } else {
279         int whiteRunStart = start;
280         do {
281           start--;
282         } while (start >= minDim && !rowOrColumn.get(start));
283         int whiteRunSize = whiteRunStart - start;
284         if (start < minDim || whiteRunSize > maxWhiteRun) {
285           start = whiteRunStart + 1; // back up
286           break;
287         }
288       }
289     }
290     start++;
291
292     // Then try right/down
293     int end = center;
294     while (end < maxDim) {
295       if (rowOrColumn.get(end)) {
296         end++;
297       } else {
298         int whiteRunStart = end;
299         do {
300           end++;
301         } while (end < maxDim && !rowOrColumn.get(end));
302         int whiteRunSize = end - whiteRunStart;
303         if (end >= maxDim || whiteRunSize > maxWhiteRun) {
304           end = whiteRunStart - 1;
305           break;
306         }
307       }
308     }
309     end--;
310
311     if (end > start) {
312       return new int[] { start, end };
313     } else {
314       return null;
315     }
316   }
317
318   private static BitMatrix sampleGrid(MonochromeBitmapSource image,
319                                       ResultPoint topLeft,
320                                       ResultPoint bottomLeft,
321                                       ResultPoint bottomRight,
322                                       int dimension) throws ReaderException {
323
324     // We make up the top right point for now, based on the others.
325     // TODO: we actually found a fourth corner above and figured out which of two modules
326     // it was the corner of. We could use that here and adjust for perspective distortion.
327     float topRightX = (bottomRight.getX() - bottomLeft.getX()) + topLeft.getX();
328     float topRightY = (bottomRight.getY() - bottomLeft.getY()) + topLeft.getY();
329
330     // Note that unlike in the QR Code sampler, we didn't find the center of modules, but the
331     // very corners. So there is no 0.5f here; 0.0f is right.
332     GridSampler sampler = GridSampler.getInstance();
333     return sampler.sampleGrid(
334         image,
335         dimension,
336         0.0f,
337         0.0f,
338         dimension,
339         0.0f,
340         dimension,
341         dimension,
342         0.0f,
343         dimension,
344         topLeft.getX(),
345         topLeft.getY(),
346         topRightX,
347         topRightY,
348         bottomRight.getX(),
349         bottomRight.getY(),
350         bottomLeft.getX(),
351         bottomLeft.getY());
352   }
353
354   /**
355    * Counts the number of black/white transitions between two points, using something like Bresenham's algorithm.
356    */
357   private ResultPointsAndTransitions transitionsBetween(ResultPoint from, ResultPoint to) {
358     // See QR Code Detector, sizeOfBlackWhiteBlackRun()
359     int fromX = (int) from.getX();
360     int fromY = (int) from.getY();
361     int toX = (int) to.getX();
362     int toY = (int) to.getY();
363     boolean steep = Math.abs(toY - fromY) > Math.abs(toX - fromX);
364     if (steep) {
365       int temp = fromX;
366       fromX = fromY;
367       fromY = temp;
368       temp = toX;
369       toX = toY;
370       toY = temp;
371     }
372
373     int dx = Math.abs(toX - fromX);
374     int dy = Math.abs(toY - fromY);
375     int error = -dx >> 1;
376     int ystep = fromY < toY ? 1 : -1;
377     int xstep = fromX < toX ? 1 : -1;
378     int transitions = 0;
379     boolean inBlack = image.isBlack(steep ? fromY : fromX, steep ? fromX : fromY);
380     for (int x = fromX, y = fromY; x != toX; x += xstep) {
381       boolean isBlack = image.isBlack(steep ? y : x, steep ? x : y);
382       if (isBlack == !inBlack) {
383         transitions++;
384         inBlack = isBlack;
385       }
386       error += dy;
387       if (error > 0) {
388         y += ystep;
389         error -= dx;
390       }
391     }
392     return new ResultPointsAndTransitions(from, to, transitions);
393   }
394
395   /**
396    * Simply encapsulates two points and a number of transitions between them.
397    */
398   private static class ResultPointsAndTransitions {
399     private final ResultPoint from;
400     private final ResultPoint to;
401     private final int transitions;
402     private ResultPointsAndTransitions(ResultPoint from, ResultPoint to, int transitions) {
403       this.from = from;
404       this.to = to;
405       this.transitions = transitions;
406     }
407     public ResultPoint getFrom() {
408       return from;
409     }
410     public ResultPoint getTo() {
411       return to;
412     }
413     public int getTransitions() {
414       return transitions;
415     }
416     public String toString() {
417       return from + "/" + to + '/' + transitions;
418     }
419   }
420
421   /**
422    * Orders ResultPointsAndTransitions by number of transitions, ascending.
423    */
424   private static class ResultPointsAndTransitionsComparator implements Comparator {
425     public int compare(Object o1, Object o2) {
426       return ((ResultPointsAndTransitions) o1).getTransitions() - ((ResultPointsAndTransitions) o2).getTransitions();
427     }
428   }
429
430 }