Issue 1143: enable Codabar after improved decoding avoids false positives

git-svn-id: https://zxing.googlecode.com/svn/trunk@2158 59b500cc-1b3d-0410-9834-0bbf25fbcc57
This commit is contained in:
srowen 2012-02-04 14:44:05 +00:00
parent 5d89cc30fc
commit 2c84200f64
9 changed files with 274 additions and 146 deletions

View file

@ -21,7 +21,7 @@ Dave MacLachlan (Google)
David Phillip Oster (Google) David Phillip Oster (Google)
David Albert (Bug Labs) David Albert (Bug Labs)
David Olivier David Olivier
dawalker (Google) David Walker (Google)
Diego Pierotto Diego Pierotto
drejc83 drejc83
Eduardo Castillejo (University of Deusto) Eduardo Castillejo (University of Deusto)

View file

@ -43,7 +43,8 @@ final class DecodeFormatManager {
ONE_D_FORMATS = EnumSet.of(BarcodeFormat.CODE_39, ONE_D_FORMATS = EnumSet.of(BarcodeFormat.CODE_39,
BarcodeFormat.CODE_93, BarcodeFormat.CODE_93,
BarcodeFormat.CODE_128, BarcodeFormat.CODE_128,
BarcodeFormat.ITF); BarcodeFormat.ITF,
BarcodeFormat.CODABAR);
ONE_D_FORMATS.addAll(PRODUCT_FORMATS); ONE_D_FORMATS.addAll(PRODUCT_FORMATS);
} }

View file

@ -105,7 +105,7 @@ public final class MultiFormatReader implements Reader {
formats.contains(BarcodeFormat.UPC_E) || formats.contains(BarcodeFormat.UPC_E) ||
formats.contains(BarcodeFormat.EAN_13) || formats.contains(BarcodeFormat.EAN_13) ||
formats.contains(BarcodeFormat.EAN_8) || formats.contains(BarcodeFormat.EAN_8) ||
//formats.contains(BarcodeFormat.CODABAR) || formats.contains(BarcodeFormat.CODABAR) ||
formats.contains(BarcodeFormat.CODE_39) || formats.contains(BarcodeFormat.CODE_39) ||
formats.contains(BarcodeFormat.CODE_93) || formats.contains(BarcodeFormat.CODE_93) ||
formats.contains(BarcodeFormat.CODE_128) || formats.contains(BarcodeFormat.CODE_128) ||

View file

@ -29,119 +29,127 @@ import java.util.Map;
* <p>Decodes Codabar barcodes.</p> * <p>Decodes Codabar barcodes.</p>
* *
* @author Bas Vijfwinkel * @author Bas Vijfwinkel
* @author David Walker
*/ */
public final class CodaBarReader extends OneDReader { public final class CodaBarReader extends OneDReader {
private static final String ALPHABET_STRING = "0123456789-$:/.+ABCDTN"; // These values are critical for determining how permissive the decoding
// will be. All stripe sizes must be within the window these define, as
// compared to the average stripe size.
private static final int MAX_ACCEPTABLE = (int) (PATTERN_MATCH_RESULT_SCALE_FACTOR * 2.0f);
private static final int PADDING = (int) (PATTERN_MATCH_RESULT_SCALE_FACTOR * 1.5f);
private static final String ALPHABET_STRING = "0123456789-$:/.+ABCD";
static final char[] ALPHABET = ALPHABET_STRING.toCharArray(); static final char[] ALPHABET = ALPHABET_STRING.toCharArray();
/** /**
* These represent the encodings of characters, as patterns of wide and narrow bars. The 7 least-significant bits of * These represent the encodings of characters, as patterns of wide and narrow bars. The 7 least-significant bits of
* each int correspond to the pattern of wide and narrow, with 1s representing "wide" and 0s representing narrow. NOTE * each int correspond to the pattern of wide and narrow, with 1s representing "wide" and 0s representing narrow.
* : c is equal to the * pattern NOTE : d is equal to the e pattern
*/ */
static final int[] CHARACTER_ENCODINGS = { static final int[] CHARACTER_ENCODINGS = {
0x003, 0x006, 0x009, 0x060, 0x012, 0x042, 0x021, 0x024, 0x030, 0x048, // 0-9 0x003, 0x006, 0x009, 0x060, 0x012, 0x042, 0x021, 0x024, 0x030, 0x048, // 0-9
0x00c, 0x018, 0x045, 0x051, 0x054, 0x015, 0x01A, 0x029, 0x00B, 0x00E, // -$:/.+ABCD 0x00c, 0x018, 0x045, 0x051, 0x054, 0x015, 0x01A, 0x029, 0x00B, 0x00E, // -$:/.+ABCD
0x01A, 0x029 //TN
}; };
// minimal number of characters that should be present (inclusing start and stop characters) // minimal number of characters that should be present (inclusing start and stop characters)
// this check has been added to reduce the number of false positive on other formats // under normal circumstances this should be set to 3, but can be set higher
// until the cause for this behaviour has been determined // as a last-ditch attempt to reduce false positives.
// under normal circumstances this should be set to 3 private static final int MIN_CHARACTER_LENGTH = 3;
private static final int minCharacterLength = 6;
// multiple start/end patterns
// official start and end patterns // official start and end patterns
private static final char[] STARTEND_ENCODING = {'E', '*', 'A', 'B', 'C', 'D', 'T', 'N'}; private static final char[] STARTEND_ENCODING = {'A', 'B', 'C', 'D'};
// some codabar generator allow the codabar string to be closed by every character // some codabar generator allow the codabar string to be closed by every
//private static final char[] STARTEND_ENCODING = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '$', ':', '/', '.', '+', 'A', 'B', 'C', 'D', 'T', 'N'}; // character. This will cause lots of false positives!
// some industries use a checksum standard but this is not part of the original codabar standard // some industries use a checksum standard but this is not part of the original codabar standard
// for more information see : http://www.mecsw.com/specs/codabar.html // for more information see : http://www.mecsw.com/specs/codabar.html
// Keep some instance variables to avoid reallocations
private final StringBuilder decodeRowResult;
private int[] counters;
private int counterLength;
public CodaBarReader() {
decodeRowResult = new StringBuilder(20);
counters = new int[80];
counterLength = 0;
}
@Override @Override
public Result decodeRow(int rowNumber, BitArray row, Map<DecodeHintType,?> hints) public Result decodeRow(int rowNumber, BitArray row, Map<DecodeHintType,?> hints) throws NotFoundException {
throws NotFoundException {
int[] start = findAsteriskPattern(row);
start[1] = 0; // BAS: settings this to 0 improves the recognition rate somehow?
// Read off white space
int nextStart = row.getNextSet(start[1]);
int end = row.getSize();
StringBuilder result = new StringBuilder(); setCounters(row);
int[] counters = new int[7]; int startOffset = findStartPattern();
int lastStart; int nextStart = startOffset;
decodeRowResult.setLength(0);
do { do {
for (int i = 0; i < counters.length; i++) { int charOffset = toNarrowWidePattern(nextStart);
counters[i] = 0; if (charOffset == -1) {
}
recordPattern(row, nextStart, counters);
char decodedChar = toNarrowWidePattern(counters);
if (decodedChar == '!') {
throw NotFoundException.getNotFoundInstance(); throw NotFoundException.getNotFoundInstance();
} }
result.append(decodedChar); // Hack: We store the position in the alphabet table into a
lastStart = nextStart; // StringBuilder, so that we can access the decoded patterns in
for (int counter : counters) { // validatePattern. We'll translate to the actual characters later.
nextStart += counter; decodeRowResult.append((char)charOffset);
} nextStart += 8;
// Stop as soon as we see the end character.
// Read off white space if (decodeRowResult.length() > 1 &&
nextStart = row.getNextSet(nextStart); arrayContains(STARTEND_ENCODING, ALPHABET[charOffset])) {
} while (nextStart < end); // no fixed end pattern so keep on reading while data is available
// Look for whitespace after pattern:
int lastPatternSize = 0;
for (int counter : counters) {
lastPatternSize += counter;
}
int whiteSpaceAfterEnd = nextStart - lastStart - lastPatternSize;
// If 50% of last pattern size, following last pattern, is not whitespace, fail
// (but if it's whitespace to the very end of the image, that's OK)
if (nextStart != end && (whiteSpaceAfterEnd / 2 < lastPatternSize)) {
throw NotFoundException.getNotFoundInstance();
}
// valid result?
if (result.length() < 2) {
throw NotFoundException.getNotFoundInstance();
}
char startchar = result.charAt(0);
if (!arrayContains(STARTEND_ENCODING, startchar)) {
// invalid start character
throw NotFoundException.getNotFoundInstance();
}
// find stop character
for (int k = 1; k < result.length(); k++) {
if (result.charAt(k) == startchar) {
// found stop character -> discard rest of the string
if (k + 1 != result.length()) {
result.delete(k + 1, result.length() - 1);
break; break;
} }
} } while (nextStart < counterLength); // no fixed end pattern so keep on reading while data is available
// Look for whitespace after pattern:
int trailingWhitespace = counters[nextStart - 1];
int lastPatternSize = 0;
for (int i = -8; i < -1; i++) {
lastPatternSize += counters[nextStart + i];
} }
// remove stop/start characters character and check if a string longer than 5 characters is contained // We need to see whitespace equal to 50% of the last pattern size,
if (result.length() <= minCharacterLength) { // otherwise this is probably a false positive. The exception is if we are
// at the end of the row. (I.e. the barcode barely fits.)
if (nextStart < counterLength && trailingWhitespace < lastPatternSize / 2) {
throw NotFoundException.getNotFoundInstance();
}
validatePattern(startOffset);
// Translate character table offsets to actual characters.
for (int i = 0; i < decodeRowResult.length(); i++) {
decodeRowResult.setCharAt(i, ALPHABET[decodeRowResult.charAt(i)]);
}
// Ensure a valid start and end character
char startchar = decodeRowResult.charAt(0);
if (!arrayContains(STARTEND_ENCODING, startchar)) {
throw NotFoundException.getNotFoundInstance();
}
char endchar = decodeRowResult.charAt(decodeRowResult.length() - 1);
if (!arrayContains(STARTEND_ENCODING, endchar)) {
throw NotFoundException.getNotFoundInstance();
}
// remove stop/start characters character and check if a long enough string is contained
if (decodeRowResult.length() <= MIN_CHARACTER_LENGTH) {
// Almost surely a false positive ( start + stop + at least 1 character) // Almost surely a false positive ( start + stop + at least 1 character)
throw NotFoundException.getNotFoundInstance(); throw NotFoundException.getNotFoundInstance();
} }
result.deleteCharAt(result.length() - 1); decodeRowResult.deleteCharAt(decodeRowResult.length() - 1);
result.deleteCharAt(0); decodeRowResult.deleteCharAt(0);
float left = (float) (start[1] + start[0]) / 2.0f; int runningCount = 0;
float right = (float) (nextStart + lastStart) / 2.0f; for (int i = 0; i < startOffset; i++) {
runningCount += counters[i];
}
float left = (float) runningCount;
for (int i = startOffset; i < nextStart - 1; i++) {
runningCount += counters[i];
}
float right = (float) runningCount;
return new Result( return new Result(
result.toString(), decodeRowResult.toString(),
null, null,
new ResultPoint[]{ new ResultPoint[]{
new ResultPoint(left, (float) rowNumber), new ResultPoint(left, (float) rowNumber),
@ -149,41 +157,118 @@ public final class CodaBarReader extends OneDReader {
BarcodeFormat.CODABAR); BarcodeFormat.CODABAR);
} }
private static int[] findAsteriskPattern(BitArray row) throws NotFoundException { void validatePattern(int start) throws NotFoundException {
int width = row.getSize(); // First, sum up the total size of our four categories of stripe sizes;
int rowOffset = row.getNextSet(0); int[] sizes = {0, 0, 0, 0};
int[] counts = {0, 0, 0, 0};
int end = decodeRowResult.length() - 1;
int counterPosition = 0; // We break out of this loop in the middle, in order to handle
int[] counters = new int[7]; // inter-character spaces properly.
int patternStart = rowOffset; int pos = start;
boolean isWhite = false; for (int i = 0; true; i++) {
int patternLength = counters.length; int pattern = CHARACTER_ENCODINGS[decodeRowResult.charAt(i)];
for (int j = 6; j >= 0; j--) {
// Even j = bars, while odd j = spaces. Categories 2 and 3 are for
// long stripes, while 0 and 1 are for short stripes.
int category = (j & 1) + (pattern & 1) * 2;
sizes[category] += counters[pos + j];
counts[category]++;
pattern >>= 1;
}
if (i >= end) {
break;
}
// We ignore the inter-character space - it could be of any size.
pos += 8;
}
for (int i = rowOffset; i < width; i++) { // Calculate our allowable size thresholds using fixed-point math.
if (row.get(i) ^ isWhite) { int[] maxes = new int[4];
counters[counterPosition]++; int[] mins = new int[4];
// Define the threshold of acceptability to be the midpoint between the
// average small stripe and the average large stripe. No stripe lengths
// should be on the "wrong" side of that line.
for (int i = 0; i < 2; i++) {
mins[i] = 0; // Accept arbitrarily small "short" stripes.
mins[i + 2] = ((sizes[i] << INTEGER_MATH_SHIFT) / counts[i] +
(sizes[i + 2] << INTEGER_MATH_SHIFT) / counts[i + 2]) >> 1;
maxes[i] = mins[i + 2];
maxes[i + 2] = (sizes[i + 2] * MAX_ACCEPTABLE + PADDING) / counts[i + 2];
}
// Now verify that all of the stripes are within the thresholds.
pos = start;
for (int i = 0; true; i++) {
int pattern = CHARACTER_ENCODINGS[decodeRowResult.charAt(i)];
for (int j = 6; j >= 0; j--) {
// Even j = bars, while odd j = spaces. Categories 2 and 3 are for
// long stripes, while 0 and 1 are for short stripes.
int category = (j & 1) + (pattern & 1) * 2;
int size = counters[pos + j] << INTEGER_MATH_SHIFT;
if (size < mins[category] || size > maxes[category]) {
throw NotFoundException.getNotFoundInstance();
}
pattern >>= 1;
}
if (i >= end) {
break;
}
pos += 8;
}
}
/**
* Records the size of all runs of white and black pixels, starting with white.
* This is just like recordPattern, except it records all the counters, and
* uses our builtin "counters" member for storage.
* @param row row to count from
*/
private void setCounters(BitArray row) throws NotFoundException {
counterLength = 0;
// Start from the first white bit.
int i = row.getNextUnset(0);
int end = row.getSize();
if (i >= end) {
throw NotFoundException.getNotFoundInstance();
}
boolean isWhite = true;
int count = 0;
for (; i < end; i++) {
if (row.get(i) ^ isWhite) { // that is, exactly one is true
count++;
} else { } else {
if (counterPosition == patternLength - 1) { counterAppend(count);
try { count = 1;
if (arrayContains(STARTEND_ENCODING, toNarrowWidePattern(counters))) { isWhite = !isWhite;
}
}
counterAppend(count);
}
private void counterAppend(int e) {
counters[counterLength] = e;
counterLength++;
if (counterLength >= counterLength) {
int[] temp = new int[counterLength * 2];
System.arraycopy(counters, 0, temp, 0, counterLength);
counters = temp;
}
}
private int findStartPattern() throws NotFoundException {
for (int i = 1; i < counterLength; i += 2) {
int charOffset = toNarrowWidePattern(i);
if (charOffset != -1 && arrayContains(STARTEND_ENCODING, ALPHABET[charOffset])) {
// Look for whitespace before start pattern, >= 50% of width of start pattern // Look for whitespace before start pattern, >= 50% of width of start pattern
if (row.isRange(Math.max(0, patternStart - (i - patternStart) / 2), patternStart, false)) { // We make an exception if the whitespace is the first element.
return new int[]{patternStart, i}; int patternSize = 0;
for (int j = i; j < i + 7; j++) {
patternSize += counters[j];
} }
if (i == 1 || counters[i-1] >= patternSize / 2) {
return i;
} }
} catch (IllegalArgumentException re) {
// no match, continue
}
patternStart += counters[0] + counters[1];
System.arraycopy(counters, 2, counters, 0, patternLength - 2);
counters[patternLength - 2] = 0;
counters[patternLength - 1] = 0;
counterPosition--;
} else {
counterPosition++;
}
counters[counterPosition] = 1;
isWhite ^= true; // isWhite = !isWhite;
} }
} }
throw NotFoundException.getNotFoundInstance(); throw NotFoundException.getNotFoundInstance();
@ -200,45 +285,45 @@ public final class CodaBarReader extends OneDReader {
return false; return false;
} }
private static char toNarrowWidePattern(int[] counters) { // Assumes that counters[position] is a bar.
// BAS : I have changed the following part because some codabar images would fail with the original routine private int toNarrowWidePattern(int position) {
// I took from the Code39Reader.java file int end = position + 7;
// ----------- change start if (end >= counterLength) {
int numCounters = counters.length; return -1;
int maxNarrowCounter = 0;
int minCounter = Integer.MAX_VALUE;
for (int i = 0; i < numCounters; i++) {
if (counters[i] < minCounter) {
minCounter = counters[i];
} }
if (counters[i] > maxNarrowCounter) { // First element is for bars, second is for spaces.
maxNarrowCounter = counters[i]; int[] maxes = {0, 0};
int[] mins = {Integer.MAX_VALUE, Integer.MAX_VALUE};
int[] thresholds = {0, 0};
for (int i = 0; i < 2; i++) {
for (int j = position + i; j < end; j += 2) {
if (counters[j] < mins[i]) {
mins[i] = counters[j];
}
if (counters[j] > maxes[i]) {
maxes[i] = counters[j];
} }
} }
// ---------- change end thresholds[i] = (mins[i] + maxes[i]) / 2;
}
int bitmask = 1 << 7;
do {
int wideCounters = 0;
int pattern = 0; int pattern = 0;
for (int i = 0; i < numCounters; i++) { for (int i = 0; i < 7; i++) {
if (counters[i] > maxNarrowCounter) { int barOrSpace = i & 1;
pattern |= 1 << (numCounters - 1 - i); bitmask >>= 1;
wideCounters++; if (counters[position + i] > thresholds[barOrSpace]) {
pattern |= bitmask;
} }
} }
if ((wideCounters == 2) || (wideCounters == 3)) {
for (int i = 0; i < CHARACTER_ENCODINGS.length; i++) { for (int i = 0; i < CHARACTER_ENCODINGS.length; i++) {
if (CHARACTER_ENCODINGS[i] == pattern) { if (CHARACTER_ENCODINGS[i] == pattern) {
return ALPHABET[i]; return i;
} }
} }
} return -1;
maxNarrowCounter--;
} while (maxNarrowCounter > minCounter);
return '!';
} }
} }

View file

@ -71,12 +71,20 @@ public class CodaBarWriter extends OneDimensionalCodeWriter {
for (int index = 0; index < contents.length(); index++) { for (int index = 0; index < contents.length(); index++) {
char c = Character.toUpperCase(contents.charAt(index)); char c = Character.toUpperCase(contents.charAt(index));
if (index == contents.length() - 1) { if (index == contents.length() - 1) {
// Neither * nor E are in the CodaBarReader.ALPHABET. // The end chars are not in the CodaBarReader.ALPHABET.
// * is equal to the c pattern, and e is equal to the d pattern switch (c) {
if (c == '*') { case 'T':
c = 'A';
break;
case 'N':
c = 'B';
break;
case '*':
c = 'C'; c = 'C';
} else if (c == 'E') { break;
case 'E':
c = 'D'; c = 'D';
break;
} }
} }
int code = 0; int code = 0;

View file

@ -76,7 +76,7 @@ public final class MultiFormatOneDReader extends OneDReader {
if (readers.isEmpty()) { if (readers.isEmpty()) {
readers.add(new MultiFormatUPCEANReader(hints)); readers.add(new MultiFormatUPCEANReader(hints));
readers.add(new Code39Reader()); readers.add(new Code39Reader());
//readers.add(new CodaBarReader()); readers.add(new CodaBarReader());
readers.add(new Code93Reader()); readers.add(new Code93Reader());
readers.add(new Code128Reader()); readers.add(new Code128Reader());
readers.add(new ITFReader()); readers.add(new ITFReader());

View file

@ -1 +1 @@
294/685 294/586

View file

@ -1 +1 @@
123456789012 31117013206375

View file

@ -0,0 +1,34 @@
/*
* Copyright 2008 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.oned;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.common.AbstractBlackBoxTestCase;
/**
* @author Sean Owen
*/
public final class CodabarBlackBox1TestCase extends AbstractBlackBoxTestCase {
public CodabarBlackBox1TestCase() {
super("test/data/blackbox/codabar-1", new MultiFormatReader(), BarcodeFormat.CODABAR);
addTest(11, 11, 0.0f);
addTest(11, 11, 180.0f);
}
}