28c727e0c3cb3311257d8447e3aa7fc3b9925752
[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     // Bottom left is correct but top left and bottom right might be switched
133     ResultPoint[] corners = new ResultPoint[] { maybeTopLeft, bottomLeft, maybeBottomRight };
134     // Use the dot product trick to sort them out
135     GenericResultPoint.orderBestPatterns(corners);
136
137     // Now we know which is which:
138     ResultPoint bottomRight = corners[0];
139     bottomLeft = corners[1];
140     ResultPoint topLeft = corners[2];
141
142     // Which point didn't we find in relation to the "L" sides? that's the top right corner
143     ResultPoint topRight;
144     if (!pointCount.containsKey(pointA)) {
145       topRight = pointA;
146     } else if (!pointCount.containsKey(pointB)) {
147       topRight = pointB;
148     } else if (!pointCount.containsKey(pointC)) {
149       topRight = pointC;
150     } else {
151       topRight = pointD;
152     }
153
154     // Next determine the dimension by tracing along the top or right side and counting black/white
155     // transitions. Since we start inside a black module, we should see a number of transitions
156     // equal to 1 less than the code dimension. Well, actually 2 less, because we are going to
157     // end on a black module:
158
159     // The top right point is actually the corner of a module, which is one of the two black modules
160     // adjacent to the white module at the top right. Tracing to that corner from either the top left
161     // or bottom right should work here, but, one will be more reliable since it's traced straight
162     // up or across, rather than at a slight angle. We use dot products to figure out which is
163     // better to use:
164     int dimension;
165     if (GenericResultPoint.crossProductZ(bottomLeft, bottomRight, topRight) <
166         GenericResultPoint.crossProductZ(topRight, topLeft, bottomLeft)) {
167       dimension = transitionsBetween(topLeft, topRight).getTransitions();
168     } else {
169       dimension = transitionsBetween(bottomRight, topRight).getTransitions();
170     }
171     dimension += 2;
172
173     BitMatrix bits = sampleGrid(image, topLeft, bottomLeft, bottomRight, dimension);
174     return new DetectorResult(bits, new ResultPoint[] {pointA, pointB, pointC, pointD});
175   }
176
177   /**
178    * Attempts to locate a corner of the barcode by scanning up, down, left or right from a center
179    * point which should be within the barcode.
180    *
181    * @param centerI center's i componennt (vertical)
182    * @param di change in i per step. If scanning up this is negative; down, positive; left or right, 0
183    * @param minI minimum value of i to search through (meaningless when di == 0)
184    * @param maxI maximum value of i
185    * @param centerJ center's j component (horizontal)
186    * @param dj same as di but change in j per step instead
187    * @param minJ see minI
188    * @param maxJ see minJ
189    * @param maxWhiteRun maximum run of white pixels that can still be considered to be within
190    *  the barcode
191    * @return a {@link ResultPoint} encapsulating the corner that was found
192    * @throws ReaderException if such a point cannot be found
193    */
194   private ResultPoint findCornerFromCenter(int centerI, int di, int minI, int maxI,
195                                            int centerJ, int dj, int minJ, int maxJ,
196                                            int maxWhiteRun) throws ReaderException {
197     int[] lastRange = null;
198     for (int i = centerI, j = centerJ;
199          i < maxI && i >= minI && j < maxJ && j >= minJ;
200          i += di, j += dj) {
201       int[] range;
202       if (dj == 0) {
203         // horizontal slices, up and down
204         range = blackWhiteRange(i, maxWhiteRun, minJ, maxJ, true);
205       } else {
206         // vertical slices, left and right
207         range = blackWhiteRange(j, maxWhiteRun, minI, maxI, false);
208       }
209       if (range == null) {
210         if (lastRange == null) {
211           throw new ReaderException("Center of image not within barcode");
212         }
213         // lastRange was found
214         if (dj == 0) {
215           int lastI = i - di;
216           if (lastRange[0] < centerJ) {
217             if (lastRange[1] > centerJ) {
218               // straddle, choose one or the other based on direction
219               return new GenericResultPoint(di > 0 ? lastRange[0] : lastRange[1], lastI);
220             }
221             return new GenericResultPoint(lastRange[0], lastI);
222           } else {
223             return new GenericResultPoint(lastRange[1], lastI);
224           }
225         } else {
226           int lastJ = j - dj;
227           if (lastRange[0] < centerI) {
228             if (lastRange[1] > centerI) {
229               return new GenericResultPoint(lastJ, dj < 0 ? lastRange[0] : lastRange[1]);
230             }
231             return new GenericResultPoint(lastJ, lastRange[0]);
232           } else {
233             return new GenericResultPoint(lastJ, lastRange[1]);
234           }
235         }
236       }
237       lastRange = range;
238     }
239     throw new ReaderException("Couldn't find an end to barcode");
240   }
241
242   /**
243    * Increments the Integer associated with a key by one.
244    */
245   private static void increment(Hashtable table, ResultPoint key) {
246     Integer value = (Integer) table.get(key);
247     table.put(key, value == null ? INTEGERS[1] : INTEGERS[value.intValue() + 1]);
248   }
249
250   /**
251    * Computes the start and end of a region of pixels, either horizontally or vertically, that could be
252    * part of a Data Matrix barcode.
253    *
254    * @param fixedDimension if scanning horizontally, this is the row (the fixed vertical location) where
255    *  we are scanning. If scanning vertically it's the colummn, the fixed horizontal location
256    * @param maxWhiteRun largest run of white pixels that can still be considered part of the barcode region
257    * @param minDim minimum pixel location, horizontally or vertically, to consider
258    * @param maxDim maximum pixel location, horizontally or vertically, to consider
259    * @param horizontal if true, we're scanning left-right, instead of up-down
260    * @return int[] with start and end of found range, or null if no such range is found (e.g. only white was found)
261    */
262   private int[] blackWhiteRange(int fixedDimension, int maxWhiteRun, int minDim, int maxDim, boolean horizontal) {
263
264     int center = (minDim + maxDim) / 2;
265
266     BitArray rowOrColumn = horizontal ? image.getBlackRow(fixedDimension, null, 0, image.getWidth())
267                                       : image.getBlackColumn(fixedDimension, null, 0, image.getHeight());
268
269     // Scan left/up first
270     int start = center;
271     while (start >= minDim) {
272       if (rowOrColumn.get(start)) {
273         start--;
274       } else {
275         int whiteRunStart = start;
276         do {
277           start--;
278         } while (start >= minDim && !rowOrColumn.get(start));
279         int whiteRunSize = whiteRunStart - start;
280         if (start < minDim || whiteRunSize > maxWhiteRun) {
281           start = whiteRunStart + 1; // back up
282           break;
283         }
284       }
285     }
286
287     // Then try right/down
288     int end = center;
289     while (end < maxDim) {
290       if (rowOrColumn.get(end)) {
291         end++;
292       } else {
293         int whiteRunStart = end;
294         do {
295           end++;
296         } while (end < maxDim && !rowOrColumn.get(end));
297         int whiteRunSize = end - whiteRunStart;
298         if (end >= maxDim || whiteRunSize > maxWhiteRun) {
299           end = whiteRunStart - 1;
300           break;
301         }
302       }
303     }
304
305     if (end > start) {
306       return new int[] { start, end };
307     } else {
308       return null;
309     }
310   }
311
312   private static BitMatrix sampleGrid(MonochromeBitmapSource image,
313                                       ResultPoint topLeft,
314                                       ResultPoint bottomLeft,
315                                       ResultPoint bottomRight,
316                                       int dimension) throws ReaderException {
317
318     // We make up the top right point for now, based on the others.
319     // TODO: we actually found a fourth corner above and figured out which of two modules
320     // it was the corner of. We could use that here and adjust for perspective distortion.
321     float topRightX = (bottomRight.getX() - bottomLeft.getX()) + topLeft.getX();
322     float topRightY = (bottomRight.getY() - bottomLeft.getY()) + topLeft.getY();
323
324     // Note that unlike in the QR Code sampler, we didn't find the center of modules, but the
325     // very corners. So there is no 0.5f here; 0.0f is right.
326     GridSampler sampler = GridSampler.getInstance();
327     return sampler.sampleGrid(
328         image,
329         dimension,
330         0.0f,
331         0.0f,
332         dimension,
333         0.0f,
334         dimension,
335         dimension,
336         0.0f,
337         dimension,
338         topLeft.getX(),
339         topLeft.getY(),
340         topRightX,
341         topRightY,
342         bottomRight.getX(),
343         bottomRight.getY(),
344         bottomLeft.getX(),
345         bottomLeft.getY());
346   }
347
348   /**
349    * Counts the number of black/white transitions between two points, using something like Bresenham's algorithm.
350    */
351   private ResultPointsAndTransitions transitionsBetween(ResultPoint from, ResultPoint to) {
352     // See QR Code Detector, sizeOfBlackWhiteBlackRun()
353     int fromX = (int) from.getX();
354     int fromY = (int) from.getY();
355     int toX = (int) to.getX();
356     int toY = (int) to.getY();
357     boolean steep = Math.abs(toY - fromY) > Math.abs(toX - fromX);
358     if (steep) {
359       int temp = fromX;
360       fromX = fromY;
361       fromY = temp;
362       temp = toX;
363       toX = toY;
364       toY = temp;
365     }
366
367     int dx = Math.abs(toX - fromX);
368     int dy = Math.abs(toY - fromY);
369     int error = -dx >> 1;
370     int ystep = fromY < toY ? 1 : -1;
371     int xstep = fromX < toX ? 1 : -1;
372     int transitions = 0;
373     boolean inBlack = image.isBlack(steep ? fromY : fromX, steep ? fromX : fromY);
374     for (int x = fromX, y = fromY; x != toX; x += xstep) {
375       boolean isBlack = image.isBlack(steep ? y : x, steep ? x : y);
376       if (isBlack == !inBlack) {
377         transitions++;
378         inBlack = isBlack;
379       }
380       error += dy;
381       if (error > 0) {
382         y += ystep;
383         error -= dx;
384       }
385     }
386     return new ResultPointsAndTransitions(from, to, transitions);
387   }
388
389   /**
390    * Simply encapsulates two points and a number of transitions between them.
391    */
392   private static class ResultPointsAndTransitions {
393     private final ResultPoint from;
394     private final ResultPoint to;
395     private final int transitions;
396     private ResultPointsAndTransitions(ResultPoint from, ResultPoint to, int transitions) {
397       this.from = from;
398       this.to = to;
399       this.transitions = transitions;
400     }
401     public ResultPoint getFrom() {
402       return from;
403     }
404     public ResultPoint getTo() {
405       return to;
406     }
407     public int getTransitions() {
408       return transitions;
409     }
410     public String toString() {
411       return from + "/" + to + '/' + transitions;
412     }
413   }
414
415   /**
416    * Orders ResultPointsAndTransitions by number of transitions, ascending.
417    */
418   private static class ResultPointsAndTransitionsComparator implements Comparator {
419     public int compare(Object o1, Object o2) {
420       return ((ResultPointsAndTransitions) o1).getTransitions() - ((ResultPointsAndTransitions) o2).getTransitions();
421     }
422   }
423
424 }