2 * Copyright 2008 ZXing authors
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.google.zxing.datamatrix.detector;
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;
31 import java.util.Enumeration;
32 import java.util.Hashtable;
33 import java.util.Vector;
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>
39 * @author srowen@google.com (Sean Owen)
41 public final class Detector {
43 private static final int MAX_MODULES = 32;
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) };
50 private final MonochromeBitmapSource image;
52 public Detector(MonochromeBitmapSource image) {
57 * <p>Detects a Data Matrix Code in an image.</p>
59 * @return {@link DetectorResult} encapsulating results of detecting a QR Code
60 * @throws ReaderException if no Data Matrix Code can be found
62 public DetectorResult detect() throws ReaderException {
64 if (!BlackPointEstimationMethod.TWO_D_SAMPLING.equals(image.getLastEstimationMethod())) {
65 image.estimateBlackPoint(BlackPointEstimationMethod.TWO_D_SAMPLING, 0);
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));
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);
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());
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);
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());
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
123 // Otherwise it's either top left or bottom right -- just assign the two arbitrarily now
124 if (maybeTopLeft == null) {
125 maybeTopLeft = point;
127 maybeBottomRight = point;
132 if (maybeTopLeft == null || bottomLeft == null || maybeBottomRight == null) {
133 throw new ReaderException("Could not find three corners");
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);
141 // Now we know which is which:
142 ResultPoint bottomRight = corners[0];
143 bottomLeft = corners[1];
144 ResultPoint topLeft = corners[2];
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)) {
150 } else if (!pointCount.containsKey(pointB)) {
152 } else if (!pointCount.containsKey(pointC)) {
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:
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
169 if (GenericResultPoint.crossProductZ(bottomLeft, bottomRight, topRight) <
170 GenericResultPoint.crossProductZ(topRight, topLeft, bottomLeft)) {
171 dimension = transitionsBetween(topLeft, topRight).getTransitions();
173 dimension = transitionsBetween(bottomRight, topRight).getTransitions();
177 BitMatrix bits = sampleGrid(image, topLeft, bottomLeft, bottomRight, dimension);
178 return new DetectorResult(bits, new ResultPoint[] {pointA, pointB, pointC, pointD});
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.
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
195 * @return a {@link ResultPoint} encapsulating the corner that was found
196 * @throws ReaderException if such a point cannot be found
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;
207 // horizontal slices, up and down
208 range = blackWhiteRange(i, maxWhiteRun, minJ, maxJ, true);
210 // vertical slices, left and right
211 range = blackWhiteRange(j, maxWhiteRun, minI, maxI, false);
214 if (lastRange == null) {
215 throw new ReaderException("Center of image not within barcode");
217 // lastRange was found
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);
225 return new GenericResultPoint(lastRange[0], lastI);
227 return new GenericResultPoint(lastRange[1], lastI);
231 if (lastRange[0] < centerI) {
232 if (lastRange[1] > centerI) {
233 return new GenericResultPoint(lastJ, dj < 0 ? lastRange[0] : lastRange[1]);
235 return new GenericResultPoint(lastJ, lastRange[0]);
237 return new GenericResultPoint(lastJ, lastRange[1]);
243 throw new ReaderException("Couldn't find an end to barcode");
247 * Increments the Integer associated with a key by one.
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]);
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.
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)
266 private int[] blackWhiteRange(int fixedDimension, int maxWhiteRun, int minDim, int maxDim, boolean horizontal) {
268 int center = (minDim + maxDim) / 2;
270 BitArray rowOrColumn = horizontal ? image.getBlackRow(fixedDimension, null, 0, image.getWidth())
271 : image.getBlackColumn(fixedDimension, null, 0, image.getHeight());
273 // Scan left/up first
275 while (start >= minDim) {
276 if (rowOrColumn.get(start)) {
279 int whiteRunStart = 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
292 // Then try right/down
294 while (end < maxDim) {
295 if (rowOrColumn.get(end)) {
298 int whiteRunStart = end;
301 } while (end < maxDim && !rowOrColumn.get(end));
302 int whiteRunSize = end - whiteRunStart;
303 if (end >= maxDim || whiteRunSize > maxWhiteRun) {
304 end = whiteRunStart - 1;
312 return new int[] { start, end };
318 private static BitMatrix sampleGrid(MonochromeBitmapSource image,
320 ResultPoint bottomLeft,
321 ResultPoint bottomRight,
322 int dimension) throws ReaderException {
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();
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(
355 * Counts the number of black/white transitions between two points, using something like Bresenham's algorithm.
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);
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;
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) {
392 return new ResultPointsAndTransitions(from, to, transitions);
396 * Simply encapsulates two points and a number of transitions between them.
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) {
405 this.transitions = transitions;
407 public ResultPoint getFrom() {
410 public ResultPoint getTo() {
413 public int getTransitions() {
416 public String toString() {
417 return from + "/" + to + '/' + transitions;
422 * Orders ResultPointsAndTransitions by number of transitions, ascending.
424 private static class ResultPointsAndTransitionsComparator implements Comparator {
425 public int compare(Object o1, Object o2) {
426 return ((ResultPointsAndTransitions) o1).getTransitions() - ((ResultPointsAndTransitions) o2).getTransitions();