blob: 11baf65eb4b70ef7d4a281c7def2b7590c5b11c0 [file] [log] [blame]
/*
* 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.pdf417.detector;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.NotFoundException;
import com.google.zxing.ResultPoint;
import com.google.zxing.common.BitMatrix;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* <p>Encapsulates logic that can detect a PDF417 Code in an image, even if the
* PDF417 Code is rotated or skewed, or partially obscured.</p>
*
* @author SITA Lab (kevin.osullivan@sita.aero)
* @author dswitkin@google.com (Daniel Switkin)
* @author Guenther Grau
*/
public final class Detector {
private static final int[] INDEXES_START_PATTERN = {0, 4, 1, 5};
private static final int[] INDEXES_STOP_PATTERN = {6, 2, 7, 3};
private static final float MAX_AVG_VARIANCE = 0.42f;
private static final float MAX_INDIVIDUAL_VARIANCE = 0.8f;
// B S B S B S B S Bar/Space pattern
// 11111111 0 1 0 1 0 1 000
private static final int[] START_PATTERN = {8, 1, 1, 1, 1, 1, 1, 3};
// 1111111 0 1 000 1 0 1 00 1
private static final int[] STOP_PATTERN = {7, 1, 1, 3, 1, 1, 1, 2, 1};
private static final int MAX_PIXEL_DRIFT = 3;
private static final int MAX_PATTERN_DRIFT = 5;
// if we set the value too low, then we don't detect the correct height of the bar if the start patterns are damaged.
// if we set the value too high, then we might detect the start pattern from a neighbor barcode.
private static final int SKIPPED_ROW_COUNT_MAX = 25;
// A PDF471 barcode should have at least 3 rows, with each row being >= 3 times the module width.
// Therefore it should be at least 9 pixels tall. To be conservative, we use about half the size to
// ensure we don't miss it.
private static final int ROW_STEP = 5;
private static final int BARCODE_MIN_HEIGHT = 10;
private Detector() {
}
/**
* <p>Detects a PDF417 Code in an image. Checks 0, 90, 180, and 270 degree rotations.</p>
*
* @param image barcode image to decode
* @param hints optional hints to detector
* @param multiple if true, then the image is searched for multiple codes. If false, then at most one code will
* be found and returned
* @return {@link PDF417DetectorResult} encapsulating results of detecting a PDF417 code
* @throws NotFoundException if no PDF417 Code can be found
*/
public static PDF417DetectorResult detect(BinaryBitmap image, Map<DecodeHintType,?> hints, boolean multiple)
throws NotFoundException {
// TODO detection improvement, tryHarder could try several different luminance thresholds/blackpoints or even
// different binarizers
//boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER);
BitMatrix bitMatrix = image.getBlackMatrix();
List<ResultPoint[]> barcodeCoordinates = detect(multiple, bitMatrix);
// Try 180, 270, 90 degree rotations, in that order
for (int rotate = 0; barcodeCoordinates.isEmpty() && rotate < 3; rotate++) {
bitMatrix = bitMatrix.clone();
if (rotate != 1) {
bitMatrix.rotate180();
} else {
bitMatrix.rotate90();
}
barcodeCoordinates = detect(multiple, bitMatrix);
}
return new PDF417DetectorResult(bitMatrix, barcodeCoordinates);
}
/**
* Detects PDF417 codes in an image. Only checks 0 degree rotation
* @param multiple if true, then the image is searched for multiple codes. If false, then at most one code will
* be found and returned
* @param bitMatrix bit matrix to detect barcodes in
* @return List of ResultPoint arrays containing the coordinates of found barcodes
*/
private static List<ResultPoint[]> detect(boolean multiple, BitMatrix bitMatrix) {
List<ResultPoint[]> barcodeCoordinates = new ArrayList<>();
int row = 0;
int column = 0;
boolean foundBarcodeInRow = false;
while (row < bitMatrix.getHeight()) {
ResultPoint[] vertices = findVertices(bitMatrix, row, column);
if (vertices[0] == null && vertices[3] == null) {
if (!foundBarcodeInRow) {
// we didn't find any barcode so that's the end of searching
break;
}
// we didn't find a barcode starting at the given column and row. Try again from the first column and slightly
// below the lowest barcode we found so far.
foundBarcodeInRow = false;
column = 0;
for (ResultPoint[] barcodeCoordinate : barcodeCoordinates) {
if (barcodeCoordinate[1] != null) {
row = (int) Math.max(row, barcodeCoordinate[1].getY());
}
if (barcodeCoordinate[3] != null) {
row = Math.max(row, (int) barcodeCoordinate[3].getY());
}
}
row += ROW_STEP;
continue;
}
foundBarcodeInRow = true;
barcodeCoordinates.add(vertices);
if (!multiple) {
break;
}
// if we didn't find a right row indicator column, then continue the search for the next barcode after the
// start pattern of the barcode just found.
if (vertices[2] != null) {
column = (int) vertices[2].getX();
row = (int) vertices[2].getY();
} else {
column = (int) vertices[4].getX();
row = (int) vertices[4].getY();
}
}
return barcodeCoordinates;
}
/**
* Locate the vertices and the codewords area of a black blob using the Start
* and Stop patterns as locators.
*
* @param matrix the scanned barcode image.
* @return an array containing the vertices:
* vertices[0] x, y top left barcode
* vertices[1] x, y bottom left barcode
* vertices[2] x, y top right barcode
* vertices[3] x, y bottom right barcode
* vertices[4] x, y top left codeword area
* vertices[5] x, y bottom left codeword area
* vertices[6] x, y top right codeword area
* vertices[7] x, y bottom right codeword area
*/
private static ResultPoint[] findVertices(BitMatrix matrix, int startRow, int startColumn) {
int height = matrix.getHeight();
int width = matrix.getWidth();
ResultPoint[] result = new ResultPoint[8];
copyToResult(result, findRowsWithPattern(matrix, height, width, startRow, startColumn, START_PATTERN),
INDEXES_START_PATTERN);
if (result[4] != null) {
startColumn = (int) result[4].getX();
startRow = (int) result[4].getY();
}
copyToResult(result, findRowsWithPattern(matrix, height, width, startRow, startColumn, STOP_PATTERN),
INDEXES_STOP_PATTERN);
return result;
}
private static void copyToResult(ResultPoint[] result, ResultPoint[] tmpResult, int[] destinationIndexes) {
for (int i = 0; i < destinationIndexes.length; i++) {
result[destinationIndexes[i]] = tmpResult[i];
}
}
private static ResultPoint[] findRowsWithPattern(BitMatrix matrix,
int height,
int width,
int startRow,
int startColumn,
int[] pattern) {
ResultPoint[] result = new ResultPoint[4];
boolean found = false;
int[] counters = new int[pattern.length];
for (; startRow < height; startRow += ROW_STEP) {
int[] loc = findGuardPattern(matrix, startColumn, startRow, width, pattern, counters);
if (loc != null) {
while (startRow > 0) {
int[] previousRowLoc = findGuardPattern(matrix, startColumn, --startRow, width, pattern, counters);
if (previousRowLoc != null) {
loc = previousRowLoc;
} else {
startRow++;
break;
}
}
result[0] = new ResultPoint(loc[0], startRow);
result[1] = new ResultPoint(loc[1], startRow);
found = true;
break;
}
}
int stopRow = startRow + 1;
// Last row of the current symbol that contains pattern
if (found) {
int skippedRowCount = 0;
int[] previousRowLoc = {(int) result[0].getX(), (int) result[1].getX()};
for (; stopRow < height; stopRow++) {
int[] loc = findGuardPattern(matrix, previousRowLoc[0], stopRow, width, pattern, counters);
// a found pattern is only considered to belong to the same barcode if the start and end positions
// don't differ too much. Pattern drift should be not bigger than two for consecutive rows. With
// a higher number of skipped rows drift could be larger. To keep it simple for now, we allow a slightly
// larger drift and don't check for skipped rows.
if (loc != null &&
Math.abs(previousRowLoc[0] - loc[0]) < MAX_PATTERN_DRIFT &&
Math.abs(previousRowLoc[1] - loc[1]) < MAX_PATTERN_DRIFT) {
previousRowLoc = loc;
skippedRowCount = 0;
} else {
if (skippedRowCount > SKIPPED_ROW_COUNT_MAX) {
break;
} else {
skippedRowCount++;
}
}
}
stopRow -= skippedRowCount + 1;
result[2] = new ResultPoint(previousRowLoc[0], stopRow);
result[3] = new ResultPoint(previousRowLoc[1], stopRow);
}
if (stopRow - startRow < BARCODE_MIN_HEIGHT) {
Arrays.fill(result, null);
}
return result;
}
/**
* @param matrix row of black/white values to search
* @param column x position to start search
* @param row y position to start search
* @param width the number of pixels to search on this row
* @param pattern pattern of counts of number of black and white pixels that are
* being searched for as a pattern
* @param counters array of counters, as long as pattern, to re-use
* @return start/end horizontal offset of guard pattern, as an array of two ints.
*/
private static int[] findGuardPattern(BitMatrix matrix,
int column,
int row,
int width,
int[] pattern,
int[] counters) {
Arrays.fill(counters, 0, counters.length, 0);
int patternStart = column;
int pixelDrift = 0;
// if there are black pixels left of the current pixel shift to the left, but only for MAX_PIXEL_DRIFT pixels
while (matrix.get(patternStart, row) && patternStart > 0 && pixelDrift++ < MAX_PIXEL_DRIFT) {
patternStart--;
}
int x = patternStart;
int counterPosition = 0;
int patternLength = pattern.length;
for (boolean isWhite = false; x < width; x++) {
boolean pixel = matrix.get(x, row);
if (pixel != isWhite) {
counters[counterPosition]++;
} else {
if (counterPosition == patternLength - 1) {
if (patternMatchVariance(counters, pattern) < MAX_AVG_VARIANCE) {
return new int[] {patternStart, x};
}
patternStart += counters[0] + counters[1];
System.arraycopy(counters, 2, counters, 0, counterPosition - 1);
counters[counterPosition - 1] = 0;
counters[counterPosition] = 0;
counterPosition--;
} else {
counterPosition++;
}
counters[counterPosition] = 1;
isWhite = !isWhite;
}
}
if (counterPosition == patternLength - 1 &&
patternMatchVariance(counters, pattern) < MAX_AVG_VARIANCE) {
return new int[] {patternStart, x - 1};
}
return null;
}
/**
* Determines how closely a set of observed counts of runs of black/white
* values matches a given target pattern. This is reported as the ratio of
* the total variance from the expected pattern proportions across all
* pattern elements, to the length of the pattern.
*
* @param counters observed counters
* @param pattern expected pattern
* @return ratio of total variance between counters and pattern compared to total pattern size
*/
private static float patternMatchVariance(int[] counters, int[] pattern) {
int numCounters = counters.length;
int total = 0;
int patternLength = 0;
for (int i = 0; i < numCounters; i++) {
total += counters[i];
patternLength += pattern[i];
}
if (total < patternLength) {
// If we don't even have one pixel per unit of bar width, assume this
// is too small to reliably match, so fail:
return Float.POSITIVE_INFINITY;
}
// We're going to fake floating-point math in integers. We just need to use more bits.
// Scale up patternLength so that intermediate values below like scaledCounter will have
// more "significant digits".
float unitBarWidth = (float) total / patternLength;
float maxIndividualVariance = MAX_INDIVIDUAL_VARIANCE * unitBarWidth;
float totalVariance = 0.0f;
for (int x = 0; x < numCounters; x++) {
int counter = counters[x];
float scaledPattern = pattern[x] * unitBarWidth;
float variance = counter > scaledPattern ? counter - scaledPattern : scaledPattern - counter;
if (variance > maxIndividualVariance) {
return Float.POSITIVE_INFINITY;
}
totalVariance += variance;
}
return totalVariance / total;
}
}