diff --git a/AUTHORS b/AUTHORS index 0eacff806..8159851c1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -8,6 +8,7 @@ Christian Brunschen (Google) Daniel Switkin (Google) David Albert (Bug Labs) Fred Lin (Anobiit) +Hannes Erven Isaac Potoczny-Jones John Connolly (Bug Labs) Joseph Wain (Google) diff --git a/core/src/com/google/zxing/multi/qrcode/QRCodeMultiReader.java b/core/src/com/google/zxing/multi/qrcode/QRCodeMultiReader.java new file mode 100644 index 000000000..a564766f6 --- /dev/null +++ b/core/src/com/google/zxing/multi/qrcode/QRCodeMultiReader.java @@ -0,0 +1,75 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.multi.qrcode; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.MonochromeBitmapSource; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.ResultMetadataType; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.DecoderResult; +import com.google.zxing.common.DetectorResult; +import com.google.zxing.multi.MultipleBarcodeReader; +import com.google.zxing.multi.qrcode.detector.MultiDetector; +import com.google.zxing.qrcode.QRCodeReader; + +import java.util.Hashtable; +import java.util.Vector; + +/** + * This implementation can detect and decode multiple QR Codes in an image. + * + * @author Sean Owen + * @author Hannes Erven + */ +public final class QRCodeMultiReader extends QRCodeReader implements MultipleBarcodeReader { + + private static final Result[] EMPTY_RESULT_ARRAY = new Result[0]; + + public Result[] decodeMultiple(MonochromeBitmapSource image) throws ReaderException { + return decodeMultiple(image, null); + } + + public Result[] decodeMultiple(MonochromeBitmapSource image, Hashtable hints) throws ReaderException { + Vector results = new Vector(); + DetectorResult[] detectorResult = new MultiDetector(image).detectMulti(hints); + for (int i = 0; i < detectorResult.length; i++) { + try { + DecoderResult decoderResult = getDecoder().decode(detectorResult[i].getBits()); + ResultPoint[] points = detectorResult[i].getPoints(); + Result result = new Result(decoderResult.getText(), decoderResult.getRawBytes(), points, BarcodeFormat.QR_CODE); + if (decoderResult.getByteSegments() != null) { + result.putMetadata(ResultMetadataType.BYTE_SEGMENTS, decoderResult.getByteSegments()); + } + results.addElement(result); + } catch (ReaderException re) { + // ignore and continue + } + } + if (results.isEmpty()) { + return EMPTY_RESULT_ARRAY; + } else { + Result[] resultArray = new Result[results.size()]; + for (int i = 0; i < results.size(); i++) { + resultArray[i] = (Result) results.elementAt(i); + } + return resultArray; + } + } + +} \ No newline at end of file diff --git a/core/src/com/google/zxing/multi/qrcode/detector/MultiDetector.java b/core/src/com/google/zxing/multi/qrcode/detector/MultiDetector.java new file mode 100644 index 000000000..c7da96612 --- /dev/null +++ b/core/src/com/google/zxing/multi/qrcode/detector/MultiDetector.java @@ -0,0 +1,76 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.multi.qrcode.detector; + +import com.google.zxing.BlackPointEstimationMethod; +import com.google.zxing.MonochromeBitmapSource; +import com.google.zxing.ReaderException; +import com.google.zxing.common.DetectorResult; +import com.google.zxing.qrcode.detector.Detector; +import com.google.zxing.qrcode.detector.FinderPatternInfo; + +import java.util.Hashtable; +import java.util.Vector; + +/** + *

Encapsulates logic that can detect one or more QR Codes in an image, even if the QR Code + * is rotated or skewed, or partially obscured.

+ * + * @author Sean Owen + * @author Hannes Erven + */ +public final class MultiDetector extends Detector { + + private static final DetectorResult[] EMPTY_DETECTOR_RESULTS = new DetectorResult[0]; + + public MultiDetector(MonochromeBitmapSource image) { + super(image); + } + + public DetectorResult[] detectMulti(Hashtable hints) throws ReaderException { + MonochromeBitmapSource image = getImage(); + if (!BlackPointEstimationMethod.TWO_D_SAMPLING.equals(image.getLastEstimationMethod())) { + image.estimateBlackPoint(BlackPointEstimationMethod.TWO_D_SAMPLING, 0); + } + + MultiFinderPatternFinder finder = new MultiFinderPatternFinder(image); + FinderPatternInfo[] info = finder.findMulti(hints); + + if (info == null || info.length == 0) { + throw ReaderException.getInstance(); + } + + Vector result = new Vector(); + for (int i = 0; i < info.length; i++) { + try { + result.addElement(processFinderPatternInfo(info[i])); + } catch (ReaderException e) { + // ignore + } + } + if (result.isEmpty()) { + return EMPTY_DETECTOR_RESULTS; + } else { + DetectorResult[] resultArray = new DetectorResult[result.size()]; + for (int i = 0; i < result.size(); i++) { + resultArray[i] = (DetectorResult) result.elementAt(i); + } + return resultArray; + } + } + +} diff --git a/core/src/com/google/zxing/multi/qrcode/detector/MultiFinderPatternFinder.java b/core/src/com/google/zxing/multi/qrcode/detector/MultiFinderPatternFinder.java new file mode 100644 index 000000000..c8766868b --- /dev/null +++ b/core/src/com/google/zxing/multi/qrcode/detector/MultiFinderPatternFinder.java @@ -0,0 +1,318 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.multi.qrcode.detector; + +import com.google.zxing.DecodeHintType; +import com.google.zxing.MonochromeBitmapSource; +import com.google.zxing.ReaderException; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitArray; +import com.google.zxing.common.Collections; +import com.google.zxing.common.Comparator; +import com.google.zxing.qrcode.detector.FinderPattern; +import com.google.zxing.qrcode.detector.FinderPatternFinder; +import com.google.zxing.qrcode.detector.FinderPatternInfo; + +import java.util.Hashtable; +import java.util.Vector; + +/** + *

This class attempts to find finder patterns in a QR Code. Finder patterns are the square + * markers at three corners of a QR Code.

+ * + *

This class is not thread-safe and should not be reused.

+ * + *

In contrast to {@link FinderPatternFinder}, this class will return an array of all possible + * QR code locations in the image.

+ * + *

Use the TRY_HARDER hint to ask for a more thorough detection.

+ * + * @author Sean Owen + * @author Hannes Erven + */ +final class MultiFinderPatternFinder extends FinderPatternFinder { + + private static final FinderPatternInfo[] EMPTY_RESULT_ARRAY = new FinderPatternInfo[0]; + + // TODO MIN_MODULE_COUNT and MAX_MODULE_COUNT would be great + // hints to ask the user for since it limits the number of regions to decode + private static final float MAX_MODULE_COUNT_PER_EDGE = 180; // max. legal count of modules per QR code edge (177) + private static final float MIN_MODULE_COUNT_PER_EDGE = 9; // min. legal count per modules per QR code edge (11) + + /** + * More or less arbitrary cutoff point for determining if two finder patterns might belong + * to the same code if they differ less than DIFF_MODSIZE_CUTOFF_PERCENT percent in their + * estimated modules sizes. + */ + private static final float DIFF_MODSIZE_CUTOFF_PERCENT = 0.05f; + + /** + * More or less arbitrary cutoff point for determining if two finder patterns might belong + * to the same code if they differ less than DIFF_MODSIZE_CUTOFF pixels/module in their + * estimated modules sizes. + */ + private static final float DIFF_MODSIZE_CUTOFF = 0.5f; + + + /** + * A comparator that orders FinderPatterns by their estimated module size. + */ + private static class ModuleSizeComparator implements Comparator { + public int compare(Object center1, Object center2) { + float value = ((FinderPattern) center2).getEstimatedModuleSize() - + ((FinderPattern) center1).getEstimatedModuleSize(); + return value < 0.0 ? -1 : value > 0.0 ? 1 : 0; + } + } + + /** + *

Creates a finder that will search the image for three finder patterns.

+ * + * @param image image to search + */ + MultiFinderPatternFinder(MonochromeBitmapSource image) { + super(image); + } + + /** + * @return the 3 best {@link FinderPattern}s from our list of candidates. The "best" are + * those that have been detected at least {@link #CENTER_QUORUM} times, and whose module + * size differs from the average among those patterns the least + * @throws ReaderException if 3 such finder patterns do not exist + */ + private FinderPattern[][] selectBestPatterns() throws ReaderException { + Vector possibleCenters = getPossibleCenters(); + int size = possibleCenters.size(); + + if (size < 3) { + // Couldn't find enough finder patterns + throw ReaderException.getInstance(); + } + + /* + * Begin HE modifications to safely detect multiple codes of equal size + */ + if (size == 3) { + return new FinderPattern[][]{ + new FinderPattern[]{ + (FinderPattern) possibleCenters.elementAt(0), + (FinderPattern) possibleCenters.elementAt(1), + (FinderPattern) possibleCenters.elementAt(2) + } + }; + } + + // Sort by estimated module size to speed up the upcoming checks + Collections.insertionSort(possibleCenters, new ModuleSizeComparator()); + + /* + * Now lets start: build a list of tuples of three finder locations that + * - feature similar module sizes + * - are placed in a distance so the estimated module count is within the QR specification + * - have similar distance between upper left/right and left top/bottom finder patterns + * - form a triangle with 90° angle (checked by comparing top right/bottom left distance with pythagoras) + * + * Note: we allow each point to be used for more than one code region: this might seem counterintuitive at first, + * but the performance penalty is not that big. At this point, we cannot make a good quality decision whether + * the three finders actually represent a QR code, or are just by chance layouted so it looks like there might + * be a QR code there. + * So, if the layout seems right, lets have the decoder try to decode. + */ + + Vector results = new Vector(); // holder for the results + + for (int i1 = 0; i1 < (size - 2); i1++) { + FinderPattern p1 = (FinderPattern) possibleCenters.elementAt(i1); + if (p1 == null) { + continue; + } + + for (int i2 = i1 + 1; i2 < (size - 1); i2++) { + FinderPattern p2 = (FinderPattern) possibleCenters.elementAt(i2); + if (p2 == null) { + continue; + } + + // Compare the expected module sizes; if they are really off, skip + float vModSize12 = (p1.getEstimatedModuleSize() - p2.getEstimatedModuleSize()) / + (Math.min(p1.getEstimatedModuleSize(), p2.getEstimatedModuleSize())); + float vModSize12A = Math.abs(p1.getEstimatedModuleSize() - p2.getEstimatedModuleSize()); + if (vModSize12A > DIFF_MODSIZE_CUTOFF && vModSize12 >= DIFF_MODSIZE_CUTOFF_PERCENT) { + // break, since elements are ordered by the module size deviation there cannot be + // any more interesting elements for the given p1. + break; + } + + for (int i3 = i2 + 1; i3 < size; i3++) { + FinderPattern p3 = (FinderPattern) possibleCenters.elementAt(i3); + if (p3 == null) { + continue; + } + + // Compare the expected module sizes; if they are really off, skip + float vModSize23 = (p2.getEstimatedModuleSize() - p3.getEstimatedModuleSize()) / + (Math.min(p2.getEstimatedModuleSize(), p3.getEstimatedModuleSize())); + float vModSize23A = Math.abs(p2.getEstimatedModuleSize() - p3.getEstimatedModuleSize()); + if (vModSize23A > DIFF_MODSIZE_CUTOFF && vModSize23 >= DIFF_MODSIZE_CUTOFF_PERCENT) { + // break, since elements are ordered by the module size deviation there cannot be + // any more interesting elements for the given p1. + break; + } + + FinderPattern[] test = {p1, p2, p3}; + ResultPoint.orderBestPatterns(test); + + // Calculate the distances: a = topleft-bottomleft, b=topleft-topright, c = diagonal + FinderPatternInfo info = new FinderPatternInfo(test); + float dA = ResultPoint.distance(info.getTopLeft(), info.getBottomLeft()); + float dC = ResultPoint.distance(info.getTopRight(), info.getBottomLeft()); + float dB = ResultPoint.distance(info.getTopLeft(), info.getTopRight()); + + // Check the sizes + float estimatedModuleCount = ((dA + dB) / p1.getEstimatedModuleSize()) / 2; + if (estimatedModuleCount > MAX_MODULE_COUNT_PER_EDGE || estimatedModuleCount < MIN_MODULE_COUNT_PER_EDGE) { + continue; + } + + // Calculate the difference of the edge lengths in percent + float vABBC = Math.abs(((dA - dB) / Math.min(dA, dB))); + if (vABBC >= 0.1f) { + continue; + } + + // Calculate the diagonal length by assuming a 90° angle at topleft + float dCpy = (float) Math.sqrt(dA * dA + dB * dB); + // Compare to the real distance in % + float vPyC = Math.abs(((dC - dCpy) / Math.min(dC, dCpy))); + + if (vPyC >= 0.1f) { + continue; + } + + // All tests passed! + results.addElement(test); + } // end iterate p3 + } // end iterate p2 + } // end iterate p1 + + if (!results.isEmpty()) { + FinderPattern[][] resultArray = new FinderPattern[results.size()][]; + for (int i = 0; i < results.size(); i++) { + resultArray[i] = (FinderPattern[]) results.elementAt(i); + } + return resultArray; + } + + // Nothing found! + throw ReaderException.getInstance(); + } + + public FinderPatternInfo[] findMulti(Hashtable hints) throws ReaderException { + boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER); + MonochromeBitmapSource image = getImage(); + int maxI = image.getHeight(); + int maxJ = image.getWidth(); + // We are looking for black/white/black/white/black modules in + // 1:1:3:1:1 ratio; this tracks the number of such modules seen so far + + // Let's assume that the maximum version QR Code we support takes up 1/4 the height of the + // image, and then account for the center being 3 modules in size. This gives the smallest + // number of pixels the center could be, so skip this often. When trying harder, look for all + // QR versions regardless of how dense they are. + int iSkip = (int) (maxI / (MAX_MODULES * 4.0f) * 3); + if (iSkip < MIN_SKIP || tryHarder) { + iSkip = MIN_SKIP; + } + + int[] stateCount = new int[5]; + for (int i = iSkip - 1; i < maxI; i += iSkip) { + BitArray blackRow = new BitArray(maxJ); + + // Get a row of black/white values + blackRow = image.getBlackRow(i, blackRow, 0, maxJ); + stateCount[0] = 0; + stateCount[1] = 0; + stateCount[2] = 0; + stateCount[3] = 0; + stateCount[4] = 0; + int currentState = 0; + for (int j = 0; j < maxJ; j++) { + if (blackRow.get(j)) { + // Black pixel + if ((currentState & 1) == 1) { // Counting white pixels + currentState++; + } + stateCount[currentState]++; + } else { // White pixel + if ((currentState & 1) == 0) { // Counting black pixels + if (currentState == 4) { // A winner? + if (foundPatternCross(stateCount)) { // Yes + boolean confirmed = handlePossibleCenter(stateCount, i, j); + if (!confirmed) { + do { // Advance to next black pixel + j++; + } while (j < maxJ && !blackRow.get(j)); + j--; // back up to that last white pixel + } + // Clear state to start looking again + currentState = 0; + stateCount[0] = 0; + stateCount[1] = 0; + stateCount[2] = 0; + stateCount[3] = 0; + stateCount[4] = 0; + } else { // No, shift counts back by two + stateCount[0] = stateCount[2]; + stateCount[1] = stateCount[3]; + stateCount[2] = stateCount[4]; + stateCount[3] = 1; + stateCount[4] = 0; + currentState = 3; + } + } else { + stateCount[++currentState]++; + } + } else { // Counting white pixels + stateCount[currentState]++; + } + } + } // for j=... + + if (foundPatternCross(stateCount)) { + handlePossibleCenter(stateCount, i, maxJ); + } // end if foundPatternCross + } // for i=iSkip-1 ... + FinderPattern[][] patternInfo = selectBestPatterns(); + Vector result = new Vector(); + for (int i = 0; i < patternInfo.length; i++) { + FinderPattern[] pattern = patternInfo[i]; + ResultPoint.orderBestPatterns(pattern); + result.addElement(new FinderPatternInfo(pattern)); + } + + if (result.isEmpty()) { + return EMPTY_RESULT_ARRAY; + } else { + FinderPatternInfo[] resultArray = new FinderPatternInfo[result.size()]; + for (int i = 0; i < result.size(); i++) { + resultArray[i] = (FinderPatternInfo) result.elementAt(i); + } + return resultArray; + } + } + +} diff --git a/core/src/com/google/zxing/qrcode/QRCodeReader.java b/core/src/com/google/zxing/qrcode/QRCodeReader.java index a8252301c..f3ddb5457 100644 --- a/core/src/com/google/zxing/qrcode/QRCodeReader.java +++ b/core/src/com/google/zxing/qrcode/QRCodeReader.java @@ -37,12 +37,16 @@ import java.util.Hashtable; * * @author Sean Owen */ -public final class QRCodeReader implements Reader { +public class QRCodeReader implements Reader { private static final ResultPoint[] NO_POINTS = new ResultPoint[0]; private final Decoder decoder = new Decoder(); + protected Decoder getDecoder() { + return decoder; + } + /** * Locates and decodes a QR code in an image. * diff --git a/core/src/com/google/zxing/qrcode/detector/Detector.java b/core/src/com/google/zxing/qrcode/detector/Detector.java index a933f069a..0f28ec9bc 100644 --- a/core/src/com/google/zxing/qrcode/detector/Detector.java +++ b/core/src/com/google/zxing/qrcode/detector/Detector.java @@ -33,7 +33,7 @@ import java.util.Hashtable; * * @author Sean Owen */ -public final class Detector { +public class Detector { private final MonochromeBitmapSource image; @@ -41,6 +41,10 @@ public final class Detector { this.image = image; } + protected MonochromeBitmapSource getImage() { + return image; + } + /** *

Detects a QR Code in an image, simply.

* @@ -68,6 +72,11 @@ public final class Detector { FinderPatternFinder finder = new FinderPatternFinder(image); FinderPatternInfo info = finder.find(hints); + return processFinderPatternInfo(info); + } + + protected DetectorResult processFinderPatternInfo(FinderPatternInfo info) throws ReaderException { + FinderPattern topLeft = info.getTopLeft(); FinderPattern topRight = info.getTopRight(); FinderPattern bottomLeft = info.getBottomLeft(); diff --git a/core/src/com/google/zxing/qrcode/detector/FinderPattern.java b/core/src/com/google/zxing/qrcode/detector/FinderPattern.java index c9200b4b1..7a9914d76 100644 --- a/core/src/com/google/zxing/qrcode/detector/FinderPattern.java +++ b/core/src/com/google/zxing/qrcode/detector/FinderPattern.java @@ -36,7 +36,7 @@ public final class FinderPattern extends ResultPoint { this.count = 1; } - float getEstimatedModuleSize() { + public float getEstimatedModuleSize() { return estimatedModuleSize; } diff --git a/core/src/com/google/zxing/qrcode/detector/FinderPatternFinder.java b/core/src/com/google/zxing/qrcode/detector/FinderPatternFinder.java index aa73ddc7e..fb7a7e84d 100755 --- a/core/src/com/google/zxing/qrcode/detector/FinderPatternFinder.java +++ b/core/src/com/google/zxing/qrcode/detector/FinderPatternFinder.java @@ -35,11 +35,11 @@ import java.util.Vector; * * @author Sean Owen */ -final class FinderPatternFinder { +public class FinderPatternFinder { private static final int CENTER_QUORUM = 2; - private static final int MIN_SKIP = 3; // 1 pixel/module times 3 modules/center - private static final int MAX_MODULES = 57; // support up to version 10 for mobile clients + protected static final int MIN_SKIP = 3; // 1 pixel/module times 3 modules/center + protected static final int MAX_MODULES = 57; // support up to version 10 for mobile clients private static final int INTEGER_MATH_SHIFT = 8; private final MonochromeBitmapSource image; @@ -52,12 +52,20 @@ final class FinderPatternFinder { * * @param image image to search */ - FinderPatternFinder(MonochromeBitmapSource image) { + public FinderPatternFinder(MonochromeBitmapSource image) { this.image = image; this.possibleCenters = new Vector(); this.crossCheckStateCount = new int[5]; } + protected MonochromeBitmapSource getImage() { + return image; + } + + protected Vector getPossibleCenters() { + return possibleCenters; + } + FinderPatternInfo find(Hashtable hints) throws ReaderException { boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER); int maxI = image.getHeight(); @@ -180,7 +188,7 @@ final class FinderPatternFinder { * @return true iff the proportions of the counts is close enough to the 1/1/3/1/1 ratios * used by finder patterns to be considered a match */ - private static boolean foundPatternCross(int[] stateCount) { + protected static boolean foundPatternCross(int[] stateCount) { int totalModuleSize = 0; for (int i = 0; i < 5; i++) { int count = stateCount[i]; @@ -370,7 +378,7 @@ final class FinderPatternFinder { * @param j end of possible finder pattern in row * @return true if a finder pattern candidate was found this time */ - private boolean handlePossibleCenter(int[] stateCount, + protected boolean handlePossibleCenter(int[] stateCount, int i, int j) { int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] + stateCount[4]; diff --git a/core/src/com/google/zxing/qrcode/detector/FinderPatternInfo.java b/core/src/com/google/zxing/qrcode/detector/FinderPatternInfo.java index 734f49ed4..3c3401085 100644 --- a/core/src/com/google/zxing/qrcode/detector/FinderPatternInfo.java +++ b/core/src/com/google/zxing/qrcode/detector/FinderPatternInfo.java @@ -22,27 +22,27 @@ package com.google.zxing.qrcode.detector; * * @author Sean Owen */ -final class FinderPatternInfo { +public final class FinderPatternInfo { private final FinderPattern bottomLeft; private final FinderPattern topLeft; private final FinderPattern topRight; - FinderPatternInfo(FinderPattern[] patternCenters) { + public FinderPatternInfo(FinderPattern[] patternCenters) { this.bottomLeft = patternCenters[0]; this.topLeft = patternCenters[1]; this.topRight = patternCenters[2]; } - FinderPattern getBottomLeft() { + public FinderPattern getBottomLeft() { return bottomLeft; } - FinderPattern getTopLeft() { + public FinderPattern getTopLeft() { return topLeft; } - FinderPattern getTopRight() { + public FinderPattern getTopRight() { return topRight; }