From c13216ce08889bdec3a158aa3161db77dda5ad54 Mon Sep 17 00:00:00 2001 From: "MakKi (makki_d)" Date: Thu, 4 Oct 2018 18:24:38 +0900 Subject: [PATCH] Rewrite DataMatrix Detector (#1080) * rewrite datamatrix detector * comments * remove unused * modify test * fix CI errors --- .../zxing/datamatrix/detector/Detector.java | 488 ++++++++---------- .../DataMatrixBlackBox2TestCase.java | 8 +- 2 files changed, 219 insertions(+), 277 deletions(-) diff --git a/core/src/main/java/com/google/zxing/datamatrix/detector/Detector.java b/core/src/main/java/com/google/zxing/datamatrix/detector/Detector.java index f39330bdc..c9196fb16 100644 --- a/core/src/main/java/com/google/zxing/datamatrix/detector/Detector.java +++ b/core/src/main/java/com/google/zxing/datamatrix/detector/Detector.java @@ -21,17 +21,8 @@ import com.google.zxing.ResultPoint; import com.google.zxing.common.BitMatrix; import com.google.zxing.common.DetectorResult; import com.google.zxing.common.GridSampler; -import com.google.zxing.common.detector.MathUtils; import com.google.zxing.common.detector.WhiteRectangleDetector; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - /** *

Encapsulates logic that can detect a Data Matrix Code in an image, even if the Data Matrix Code * is rotated or skewed, or partially obscured.

@@ -57,267 +48,262 @@ public final class Detector { public DetectorResult detect() throws NotFoundException { ResultPoint[] cornerPoints = rectangleDetector.detect(); - ResultPoint pointA = cornerPoints[0]; - ResultPoint pointB = cornerPoints[1]; - ResultPoint pointC = cornerPoints[2]; - ResultPoint pointD = cornerPoints[3]; - // Point A and D are across the diagonal from one another, - // as are B and C. Figure out which are the solid black lines - // by counting transitions - List transitions = new ArrayList<>(4); - transitions.add(transitionsBetween(pointA, pointB)); - transitions.add(transitionsBetween(pointA, pointC)); - transitions.add(transitionsBetween(pointB, pointD)); - transitions.add(transitionsBetween(pointC, pointD)); - Collections.sort(transitions, new ResultPointsAndTransitionsComparator()); - - // Sort by number of transitions. First two will be the two solid sides; last two - // will be the two alternating black/white sides - ResultPointsAndTransitions lSideOne = transitions.get(0); - ResultPointsAndTransitions lSideTwo = transitions.get(1); - - // Figure out which point is their intersection by tallying up the number of times we see the - // endpoints in the four endpoints. One will show up twice. - Map pointCount = new HashMap<>(); - increment(pointCount, lSideOne.getFrom()); - increment(pointCount, lSideOne.getTo()); - increment(pointCount, lSideTwo.getFrom()); - increment(pointCount, lSideTwo.getTo()); - - ResultPoint maybeTopLeft = null; - ResultPoint bottomLeft = null; - ResultPoint maybeBottomRight = null; - for (Map.Entry entry : pointCount.entrySet()) { - ResultPoint point = entry.getKey(); - Integer value = entry.getValue(); - if (value == 2) { - bottomLeft = point; // this is definitely the bottom left, then -- end of two L sides - } else { - // Otherwise it's either top left or bottom right -- just assign the two arbitrarily now - if (maybeTopLeft == null) { - maybeTopLeft = point; - } else { - maybeBottomRight = point; - } - } - } - - if (maybeTopLeft == null || bottomLeft == null || maybeBottomRight == null) { + ResultPoint[] points = detectSolid1(cornerPoints); + points = detectSolid2(points); + points[3] = correctTopRight(points); + if (points[3] == null) { throw NotFoundException.getNotFoundInstance(); } + points = shiftToModuleCenter(points); - // Bottom left is correct but top left and bottom right might be switched - ResultPoint[] corners = { maybeTopLeft, bottomLeft, maybeBottomRight }; - // Use the dot product trick to sort them out - ResultPoint.orderBestPatterns(corners); - - // Now we know which is which: - ResultPoint bottomRight = corners[0]; - bottomLeft = corners[1]; - ResultPoint topLeft = corners[2]; - - // Which point didn't we find in relation to the "L" sides? that's the top right corner - ResultPoint topRight; - if (!pointCount.containsKey(pointA)) { - topRight = pointA; - } else if (!pointCount.containsKey(pointB)) { - topRight = pointB; - } else if (!pointCount.containsKey(pointC)) { - topRight = pointC; - } else { - topRight = pointD; - } - - // Next determine the dimension by tracing along the top or right side and counting black/white - // transitions. Since we start inside a black module, we should see a number of transitions - // equal to 1 less than the code dimension. Well, actually 2 less, because we are going to - // end on a black module: - - // The top right point is actually the corner of a module, which is one of the two black modules - // adjacent to the white module at the top right. Tracing to that corner from either the top left - // or bottom right should work here. - - int dimensionTop = transitionsBetween(topLeft, topRight).getTransitions(); - int dimensionRight = transitionsBetween(bottomRight, topRight).getTransitions(); + ResultPoint topLeft = points[0]; + ResultPoint bottomLeft = points[1]; + ResultPoint bottomRight = points[2]; + ResultPoint topRight = points[3]; + int dimensionTop = transitionsBetween(topLeft, topRight) + 1; + int dimensionRight = transitionsBetween(bottomRight, topRight) + 1; if ((dimensionTop & 0x01) == 1) { - // it can't be odd, so, round... up? - dimensionTop++; + dimensionTop += 1; } - dimensionTop += 2; - if ((dimensionRight & 0x01) == 1) { - // it can't be odd, so, round... up? - dimensionRight++; + dimensionRight += 1; } - dimensionRight += 2; - BitMatrix bits; - ResultPoint correctedTopRight; - - // Rectangular symbols are 6x16, 6x28, 10x24, 10x32, 14x32, or 14x44. If one dimension is more - // than twice the other, it's certainly rectangular, but to cut a bit more slack we accept it as - // rectangular if the bigger side is at least 7/4 times the other: - if (4 * dimensionTop >= 7 * dimensionRight || 4 * dimensionRight >= 7 * dimensionTop) { - // The matrix is rectangular - - correctedTopRight = - correctTopRightRectangular(bottomLeft, bottomRight, topLeft, topRight, dimensionTop, dimensionRight); - if (correctedTopRight == null) { - correctedTopRight = topRight; - } - - dimensionTop = transitionsBetween(topLeft, correctedTopRight).getTransitions(); - dimensionRight = transitionsBetween(bottomRight, correctedTopRight).getTransitions(); - - if ((dimensionTop & 0x01) == 1) { - // it can't be odd, so, round... up? - dimensionTop++; - } - - if ((dimensionRight & 0x01) == 1) { - // it can't be odd, so, round... up? - dimensionRight++; - } - - bits = sampleGrid(image, topLeft, bottomLeft, bottomRight, correctedTopRight, dimensionTop, dimensionRight); - - } else { + if (4 * dimensionTop < 7 * dimensionRight && 4 * dimensionRight < 7 * dimensionTop) { // The matrix is square - - int dimension = Math.min(dimensionRight, dimensionTop); - // correct top right point to match the white module - correctedTopRight = correctTopRight(bottomLeft, bottomRight, topLeft, topRight, dimension); - if (correctedTopRight == null) { - correctedTopRight = topRight; - } - - // Redetermine the dimension using the corrected top right point - int dimensionCorrected = Math.max(transitionsBetween(topLeft, correctedTopRight).getTransitions(), - transitionsBetween(bottomRight, correctedTopRight).getTransitions()); - dimensionCorrected++; - if ((dimensionCorrected & 0x01) == 1) { - dimensionCorrected++; - } - - bits = sampleGrid(image, - topLeft, - bottomLeft, - bottomRight, - correctedTopRight, - dimensionCorrected, - dimensionCorrected); + dimensionTop = dimensionRight = Math.max(dimensionTop, dimensionRight); } - return new DetectorResult(bits, new ResultPoint[]{topLeft, bottomLeft, bottomRight, correctedTopRight}); + BitMatrix bits = sampleGrid(image, + topLeft, + bottomLeft, + bottomRight, + topRight, + dimensionTop, + dimensionRight); + + return new DetectorResult(bits, new ResultPoint[]{topLeft, bottomLeft, bottomRight, topRight}); + } + + private ResultPoint shiftPoint(ResultPoint point, ResultPoint to, int div) { + float x = (to.getX() - point.getX()) / (div + 1); + float y = (to.getY() - point.getY()) / (div + 1); + return new ResultPoint(point.getX() + x, point.getY() + y); + } + + private ResultPoint moveAway(ResultPoint point, float fromX, float fromY) { + float x = point.getX(); + float y = point.getY(); + + if (x < fromX) { + x -= 1; + } else { + x += 1; + } + + if (y < fromY) { + y -= 1; + } else { + y += 1; + } + + return new ResultPoint(x, y); } /** - * Calculates the position of the white top right module using the output of the rectangle detector - * for a rectangular matrix + * Detect a solid side which has minimum transition. */ - private ResultPoint correctTopRightRectangular(ResultPoint bottomLeft, - ResultPoint bottomRight, - ResultPoint topLeft, - ResultPoint topRight, - int dimensionTop, - int dimensionRight) { + private ResultPoint[] detectSolid1(ResultPoint[] cornerPoints) { + // 0 2 + // 1 3 + ResultPoint pointA = cornerPoints[0]; + ResultPoint pointB = cornerPoints[1]; + ResultPoint pointC = cornerPoints[3]; + ResultPoint pointD = cornerPoints[2]; - float corr = distance(bottomLeft, bottomRight) / (float) dimensionTop; - int norm = distance(topLeft, topRight); - float cos = (topRight.getX() - topLeft.getX()) / norm; - float sin = (topRight.getY() - topLeft.getY()) / norm; + int trAB = transitionsBetween(pointA, pointB); + int trBC = transitionsBetween(pointB, pointC); + int trCD = transitionsBetween(pointC, pointD); + int trDA = transitionsBetween(pointD, pointA); - ResultPoint c1 = new ResultPoint(topRight.getX() + corr * cos, topRight.getY() + corr * sin); - - corr = distance(bottomLeft, topLeft) / (float) dimensionRight; - norm = distance(bottomRight, topRight); - cos = (topRight.getX() - bottomRight.getX()) / norm; - sin = (topRight.getY() - bottomRight.getY()) / norm; - - ResultPoint c2 = new ResultPoint(topRight.getX() + corr * cos, topRight.getY() + corr * sin); - - if (!isValid(c1)) { - if (isValid(c2)) { - return c2; - } - return null; + // 0..3 + // : : + // 1--2 + int min = trAB; + ResultPoint[] points = {pointD, pointA, pointB, pointC}; + if (min > trBC) { + min = trBC; + points[0] = pointA; + points[1] = pointB; + points[2] = pointC; + points[3] = pointD; } - if (!isValid(c2)) { - return c1; + if (min > trCD) { + min = trCD; + points[0] = pointB; + points[1] = pointC; + points[2] = pointD; + points[3] = pointA; + } + if (min > trDA) { + points[0] = pointC; + points[1] = pointD; + points[2] = pointA; + points[3] = pointB; } - int l1 = Math.abs(dimensionTop - transitionsBetween(topLeft, c1).getTransitions()) + - Math.abs(dimensionRight - transitionsBetween(bottomRight, c1).getTransitions()); - int l2 = Math.abs(dimensionTop - transitionsBetween(topLeft, c2).getTransitions()) + - Math.abs(dimensionRight - transitionsBetween(bottomRight, c2).getTransitions()); - - if (l1 <= l2) { - return c1; - } - - return c2; + return points; } /** - * Calculates the position of the white top right module using the output of the rectangle detector - * for a square matrix + * Detect a second solid side next to first solid side. */ - private ResultPoint correctTopRight(ResultPoint bottomLeft, - ResultPoint bottomRight, - ResultPoint topLeft, - ResultPoint topRight, - int dimension) { + private ResultPoint[] detectSolid2(ResultPoint[] points) { + // A..D + // : : + // B--C + ResultPoint pointA = points[0]; + ResultPoint pointB = points[1]; + ResultPoint pointC = points[2]; + ResultPoint pointD = points[3]; - float corr = distance(bottomLeft, bottomRight) / (float) dimension; - int norm = distance(topLeft, topRight); - float cos = (topRight.getX() - topLeft.getX()) / norm; - float sin = (topRight.getY() - topLeft.getY()) / norm; + // Transition detection on the edge is not stable. + // To safely detect, shift the points to the module center. + int tr = transitionsBetween(pointA, pointD); + ResultPoint pointBs = shiftPoint(pointB, pointC, (tr + 1) * 4); + ResultPoint pointCs = shiftPoint(pointC, pointB, (tr + 1) * 4); + int trBA = transitionsBetween(pointBs, pointA); + int trCD = transitionsBetween(pointCs, pointD); - ResultPoint c1 = new ResultPoint(topRight.getX() + corr * cos, topRight.getY() + corr * sin); + // 0..3 + // | : + // 1--2 + if (trBA < trCD) { + // solid sides: A-B-C + points[0] = pointA; + points[1] = pointB; + points[2] = pointC; + points[3] = pointD; + } else { + // solid sides: B-C-D + points[0] = pointB; + points[1] = pointC; + points[2] = pointD; + points[3] = pointA; + } - corr = distance(bottomLeft, topLeft) / (float) dimension; - norm = distance(bottomRight, topRight); - cos = (topRight.getX() - bottomRight.getX()) / norm; - sin = (topRight.getY() - bottomRight.getY()) / norm; + return points; + } - ResultPoint c2 = new ResultPoint(topRight.getX() + corr * cos, topRight.getY() + corr * sin); + /** + * Calculates the corner position of the white top right module. + */ + private ResultPoint correctTopRight(ResultPoint[] points) { + // A..D + // | : + // B--C + ResultPoint pointA = points[0]; + ResultPoint pointB = points[1]; + ResultPoint pointC = points[2]; + ResultPoint pointD = points[3]; - if (!isValid(c1)) { - if (isValid(c2)) { - return c2; + // shift points for safe transition detection. + int trTop = transitionsBetween(pointA, pointD); + int trRight = transitionsBetween(pointB, pointD); + ResultPoint pointAs = shiftPoint(pointA, pointB, (trRight + 1) * 4); + ResultPoint pointCs = shiftPoint(pointC, pointB, (trTop + 1) * 4); + + trTop = transitionsBetween(pointAs, pointD); + trRight = transitionsBetween(pointCs, pointD); + + ResultPoint candidate1 = new ResultPoint( + pointD.getX() + (pointC.getX() - pointB.getX()) / (trTop + 1), + pointD.getY() + (pointC.getY() - pointB.getY()) / (trTop + 1)); + ResultPoint candidate2 = new ResultPoint( + pointD.getX() + (pointA.getX() - pointB.getX()) / (trRight + 1), + pointD.getY() + (pointA.getY() - pointB.getY()) / (trRight + 1)); + + if (!isValid(candidate1)) { + if (isValid(candidate2)) { + return candidate2; } return null; } - if (!isValid(c2)) { - return c1; + if (!isValid(candidate2)) { + return candidate1; } - int l1 = Math.abs(transitionsBetween(topLeft, c1).getTransitions() - - transitionsBetween(bottomRight, c1).getTransitions()); - int l2 = Math.abs(transitionsBetween(topLeft, c2).getTransitions() - - transitionsBetween(bottomRight, c2).getTransitions()); + int sumc1 = transitionsBetween(pointAs, candidate1) + transitionsBetween(pointCs, candidate1); + int sumc2 = transitionsBetween(pointAs, candidate2) + transitionsBetween(pointCs, candidate2); - return l1 <= l2 ? c1 : c2; + if (sumc1 > sumc2) { + return candidate1; + } else { + return candidate2; + } + } + + /** + * Shift the edge points to the module center. + */ + private ResultPoint[] shiftToModuleCenter(ResultPoint[] points) { + // A..D + // | : + // B--C + ResultPoint pointA = points[0]; + ResultPoint pointB = points[1]; + ResultPoint pointC = points[2]; + ResultPoint pointD = points[3]; + + // calculate pseudo dimensions + int dimH = transitionsBetween(pointA, pointD) + 1; + int dimV = transitionsBetween(pointC, pointD) + 1; + + // shift points for safe dimension detection + ResultPoint pointAs = shiftPoint(pointA, pointB, dimV * 4); + ResultPoint pointCs = shiftPoint(pointC, pointB, dimH * 4); + + // calculate more precise dimensions + dimH = transitionsBetween(pointAs, pointD) + 1; + dimV = transitionsBetween(pointCs, pointD) + 1; + if ((dimH & 0x01) == 1) { + dimH += 1; + } + if ((dimV & 0x01) == 1) { + dimV += 1; + } + + // WhiteRectangleDetector returns points inside of the rectangle. + // I want points on the edges. + float centerX = (pointA.getX() + pointB.getX() + pointC.getX() + pointD.getX()) / 4; + float centerY = (pointA.getY() + pointB.getY() + pointC.getY() + pointD.getY()) / 4; + pointA = moveAway(pointA, centerX, centerY); + pointB = moveAway(pointB, centerX, centerY); + pointC = moveAway(pointC, centerX, centerY); + pointD = moveAway(pointD, centerX, centerY); + + ResultPoint pointBs; + ResultPoint pointDs; + + // shift points to the center of each modules + pointAs = shiftPoint(pointA, pointB, dimV * 4); + pointAs = shiftPoint(pointAs, pointD, dimH * 4); + pointBs = shiftPoint(pointB, pointA, dimV * 4); + pointBs = shiftPoint(pointBs, pointC, dimH * 4); + pointCs = shiftPoint(pointC, pointD, dimV * 4); + pointCs = shiftPoint(pointCs, pointB, dimH * 4); + pointDs = shiftPoint(pointD, pointC, dimV * 4); + pointDs = shiftPoint(pointDs, pointA, dimH * 4); + + return new ResultPoint[]{pointAs, pointBs, pointCs, pointDs}; } private boolean isValid(ResultPoint p) { return p.getX() >= 0 && p.getX() < image.getWidth() && p.getY() > 0 && p.getY() < image.getHeight(); } - private static int distance(ResultPoint a, ResultPoint b) { - return MathUtils.round(ResultPoint.distance(a, b)); - } - - /** - * Increments the Integer associated with a key by one. - */ - private static void increment(Map table, ResultPoint key) { - Integer value = table.get(key); - table.put(key, value == null ? 1 : value + 1); - } - private static BitMatrix sampleGrid(BitMatrix image, ResultPoint topLeft, ResultPoint bottomLeft, @@ -352,7 +338,7 @@ public final class Detector { /** * Counts the number of black/white transitions between two points, using something like Bresenham's algorithm. */ - private ResultPointsAndTransitions transitionsBetween(ResultPoint from, ResultPoint to) { + private int transitionsBetween(ResultPoint from, ResultPoint to) { // See QR Code Detector, sizeOfBlackWhiteBlackRun() int fromX = (int) from.getX(); int fromY = (int) from.getY(); @@ -390,51 +376,7 @@ public final class Detector { error -= dx; } } - return new ResultPointsAndTransitions(from, to, transitions); - } - - /** - * Simply encapsulates two points and a number of transitions between them. - */ - private static final class ResultPointsAndTransitions { - - private final ResultPoint from; - private final ResultPoint to; - private final int transitions; - - private ResultPointsAndTransitions(ResultPoint from, ResultPoint to, int transitions) { - this.from = from; - this.to = to; - this.transitions = transitions; - } - - ResultPoint getFrom() { - return from; - } - - ResultPoint getTo() { - return to; - } - - int getTransitions() { - return transitions; - } - - @Override - public String toString() { - return from + "/" + to + '/' + transitions; - } - } - - /** - * Orders ResultPointsAndTransitions by number of transitions, ascending. - */ - private static final class ResultPointsAndTransitionsComparator - implements Comparator, Serializable { - @Override - public int compare(ResultPointsAndTransitions o1, ResultPointsAndTransitions o2) { - return o1.getTransitions() - o2.getTransitions(); - } + return transitions; } } diff --git a/core/src/test/java/com/google/zxing/datamatrix/DataMatrixBlackBox2TestCase.java b/core/src/test/java/com/google/zxing/datamatrix/DataMatrixBlackBox2TestCase.java index 6edbe424d..2e3831420 100644 --- a/core/src/test/java/com/google/zxing/datamatrix/DataMatrixBlackBox2TestCase.java +++ b/core/src/test/java/com/google/zxing/datamatrix/DataMatrixBlackBox2TestCase.java @@ -27,10 +27,10 @@ public final class DataMatrixBlackBox2TestCase extends AbstractBlackBoxTestCase public DataMatrixBlackBox2TestCase() { super("src/test/resources/blackbox/datamatrix-2", new MultiFormatReader(), BarcodeFormat.DATA_MATRIX); - addTest(8, 8, 0, 1, 0.0f); - addTest(14, 14, 0, 1, 90.0f); - addTest(14, 14, 0, 1, 180.0f); - addTest(13, 13, 0, 1, 270.0f); + addTest(13, 13, 0, 1, 0.0f); + addTest(15, 15, 0, 1, 90.0f); + addTest(17, 16, 0, 1, 180.0f); + addTest(15, 15, 0, 1, 270.0f); } }