2021SC@SDUSC
一、Zxing中的一维码
在Zxing的目录结构中,所有的一维码有关代码被放到了同一个文件夹中oned 这是由于不同的一维码的编码解码逻辑比较简单并且关联性较大。这篇博客主要以CodaBar为例分析解码过程。
二、OneDReader
所有一维码的解码器都要继承OneDReader类。OneDReader是Reader的子类。在二维码中,解码方法都是decode,但是在一维码中,解码方法都命名为decodeRow。 各方法介绍如下:
方法 | 作用 |
---|
decode(BinaryBitmap image):Result | 重写方法。 | decode(BinaryBitmap image,Map<DecodeHintType,?> hints) :Result | 重写方法,外部调用的解码方法 | reset():void | 重写方法,方法里面是空的,也就是OneDreader不具体描述这个方法 | doDecode(BinaryBitmap image, Map<DecodeHintType,?> hints):Result | 内部实现的解码方法 | recordPattern(BitArray row, int start,int[] counters):void | 记录从给定点开始的一行中连续运行的白色和黑色像素的数量 | recordPatternInReverse(BitArray row, int start, int[] counters):void | 上一个方法是从前向后记录,这个方法是从后向前记录,只在rss中用到 | patternMatchVariance(int[] counters,int[] pattern,float maxIndividualVariance) :float | 确定一组观察到的黑白值运行计数与给定目标模式的匹配程度。是所有模式中,预期模式比例的总方差与模式长度的比率。 | decodeRow(int rowNumber, BitArray row, Map<DecodeHintType,?> hints): Result | 抽象方法,尝试对给定一行图像的一维条形码格式进行解码。需要每个码根据自己的特点实现。 |
docode
@Override
public Result decode(BinaryBitmap image,
Map<DecodeHintType,?> hints) throws NotFoundException, FormatException {
try {
return doDecode(image, hints);
} catch (NotFoundException nfe) {
boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER);
if (tryHarder && image.isRotateSupported()) {
BinaryBitmap rotatedImage = image.rotateCounterClockwise();
Result result = doDecode(rotatedImage, hints);
Map<ResultMetadataType,?> metadata = result.getResultMetadata();
int orientation = 270;
if (metadata != null && metadata.containsKey(ResultMetadataType.ORIENTATION)) {
orientation = (orientation +
(Integer) metadata.get(ResultMetadataType.ORIENTATION)) % 360;
}
result.putMetadata(ResultMetadataType.ORIENTATION, orientation);
ResultPoint[] points = result.getResultPoints();
if (points != null) {
int height = rotatedImage.getHeight();
for (int i = 0; i < points.length; i++) {
points[i] = new ResultPoint(height - points[i].getY() - 1, points[i].getX());
}
}
return result;
} else {
throw nfe;
}
}
}
doDecode decode交付给doDecode进行后续解码操作,而doDecode只是负责扫描定位,具体的解码方法在每个码的decodeRow中实现。
private Result doDecode(BinaryBitmap image,
Map<DecodeHintType,?> hints) throws NotFoundException {
int width = image.getWidth();
int height = image.getHeight();
BitArray row = new BitArray(width);
boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER);
int rowStep = Math.max(1, height >> (tryHarder ? 8 : 5));
int maxLines;
if (tryHarder) {
maxLines = height;
} else {
maxLines = 15;
}
int middle = height / 2;
for (int x = 0; x < maxLines; x++) {
int rowStepsAboveOrBelow = (x + 1) / 2;
boolean isAbove = (x & 0x01) == 0;
int rowNumber = middle + rowStep * (isAbove ? rowStepsAboveOrBelow : -rowStepsAboveOrBelow);
if (rowNumber < 0 || rowNumber >= height) {
break;
}
try {
row = image.getBlackRow(rowNumber, row);
} catch (NotFoundException ignored) {
continue;
}
for (int attempt = 0; attempt < 2; attempt++) {
if (attempt == 1) {
row.reverse();
if (hints != null && hints.containsKey(DecodeHintType.NEED_RESULT_POINT_CALLBACK)) {
Map<DecodeHintType,Object> newHints = new EnumMap<>(DecodeHintType.class);
newHints.putAll(hints);
newHints.remove(DecodeHintType.NEED_RESULT_POINT_CALLBACK);
hints = newHints;
}
}
try {
Result result = decodeRow(rowNumber, row, hints);
if (attempt == 1) {
result.putMetadata(ResultMetadataType.ORIENTATION, 180);
ResultPoint[] points = result.getResultPoints();
if (points != null) {
points[0] = new ResultPoint(width - points[0].getX() - 1, points[0].getY());
points[1] = new ResultPoint(width - points[1].getX() - 1, points[1].getY());
}
}
return result;
} catch (ReaderException re) {
}
}
}
三、一维码万能解码类——MultiFormatOneDReader
还记得在Reader中我们介绍的MultiFormatReader吗?我们称它是万能解码类,MultiFormatOneDReader和Reader、OneDReader、MultiFormatReader有着密切的关系,如下:所有一/二维码解码器都要实现Reader,所有一维码解码器都要继承OneDReader。为了调用方便,Zxing写了两个工厂类:MultiFormatOneDReade、MultiFormatReader;MultiFormatReader是面向一/二维码的,MultiFormatOneDReade是面向一维码的,在MultiFormatReader中对一维码的解析调用的就是MultiFormatOneDReader类。
if (addOneDReader && !tryHarder) {
readers.add(new MultiFormatOneDReader(hints));
}
四者关系如图: 作为工厂类,MultiFormatOneDReader的逻辑和MultiFormatReader基本一致:
- 判断hints是否为空?
是->尝试所有一维码解码器 否->尝试所有可能类型匹配的一维码解码器。
public MultiFormatOneDReader(Map<DecodeHintType,?> hints) {
@SuppressWarnings("unchecked")
Collection<BarcodeFormat> possibleFormats = hints == null ? null :
(Collection<BarcodeFormat>) hints.get(DecodeHintType.POSSIBLE_FORMATS);
boolean useCode39CheckDigit = hints != null &&
hints.get(DecodeHintType.ASSUME_CODE_39_CHECK_DIGIT) != null;
Collection<OneDReader> readers = new ArrayList<>();
if (possibleFormats != null) {
if (possibleFormats.contains(BarcodeFormat.EAN_13) ||
possibleFormats.contains(BarcodeFormat.UPC_A) ||
possibleFormats.contains(BarcodeFormat.EAN_8) ||
possibleFormats.contains(BarcodeFormat.UPC_E)) {
readers.add(new MultiFormatUPCEANReader(hints));
}
if (possibleFormats.contains(BarcodeFormat.CODE_39)) {
readers.add(new Code39Reader(useCode39CheckDigit));
}
if (possibleFormats.contains(BarcodeFormat.CODE_93)) {
readers.add(new Code93Reader());
}
if (possibleFormats.contains(BarcodeFormat.CODE_128)) {
readers.add(new Code128Reader());
}
if (possibleFormats.contains(BarcodeFormat.ITF)) {
readers.add(new ITFReader());
}
if (possibleFormats.contains(BarcodeFormat.CODABAR)) {
readers.add(new CodaBarReader());
}
if (possibleFormats.contains(BarcodeFormat.RSS_14)) {
readers.add(new RSS14Reader());
}
if (possibleFormats.contains(BarcodeFormat.RSS_EXPANDED)) {
readers.add(new RSSExpandedReader());
}
}
if (readers.isEmpty()) {
readers.add(new MultiFormatUPCEANReader(hints));
readers.add(new Code39Reader());
readers.add(new CodaBarReader());
readers.add(new Code93Reader());
readers.add(new Code128Reader());
readers.add(new ITFReader());
readers.add(new RSS14Reader());
readers.add(new RSSExpandedReader());
}
this.readers = readers.toArray(EMPTY_ONED_ARRAY);
}
四、CodaBarReader
一维码和二维码不同,Reader不需要将定位和解码的任务交给其他类别完成,所有的算法都在各自的Reader中(rss除外)。
认识CodaBar
- Codabar构成:Codabar具有4个条和3个空(共7个单元),每个窄或宽的宽度代表一个字符(字母)。
Codabar的基本构成如下: Zxing对起始终止字符的定义如下:
private static final char[] STARTEND_ENCODING = {'A', 'B', 'C', 'D'};
- Codabar字符的构成:Codabar可以用数字(0至9)、字母(A、B、C、D)以及符号(-、$、/、. 、+)来表示字符。
在Zxing中,定义了字符字母串,并用一个数组存储他们
private static final String ALPHABET_STRING = "0123456789-$:/.+ABCD";
static final char[] ALPHABET = ALPHABET_STRING.toCharArray();
规定这些表示字符的编码如下,表示宽条和窄条的模式。每个int的7个最低有效位对应于宽和窄的模式,1表示“宽”,0表示“窄”。
static final int[] CHARACTER_ENCODINGS = {
0x003, 0x006, 0x009, 0x060, 0x012, 0x042, 0x021, 0x024, 0x030, 0x048,
0x00c, 0x018, 0x045, 0x051, 0x054, 0x015, 0x01A, 0x029, 0x00B, 0x00E,
};
以“0”为例,0的编码为0x003,低7位为0000011,即,窄窄窄窄窄宽宽,与上图中0的条式图案一致。
- Codabar的特征:Codabar的遗漏读取比ITF的要少。同CODE 39相比,条码尺寸也较小。但这并不总意味着Codabar就不存在遗漏读取。如果条码的打印质量不好,往往在以下情形中会出现遗漏读取。
为了避免遗漏读取,推荐采用和ITF一样的办法,把条码读取仪设置在"数位指定"功能上,只读取规定位数的数字。例如,A––––A用于罗列价格,A––––C用于特别折扣价格而C––––C为大减价。 - Codabar的应用:应用于验血(标本)的试管上,以确定各个身份。
CodaBar解码方法——decodeRow
总的来说,所有一维码的解码思路都基本一致: 第一步:定位。条形码Reader扫描图像的整个宽度,试图识别是否有条形码候选对象——黑/白图。 第二步:解码。定位出条形码后开始解码。在这一步中,Reader它对图像像素进行计数和比较,以匹配开始和结束标识符。然后,它根据该代码类型的规范来解析开始标识符和结束标识符之间的模式,以解开编码的数据。
@Override
public Result decodeRow(int rowNumber, BitArray row, Map<DecodeHintType,?> hints) throws NotFoundException {
Arrays.fill(counters, 0);
setCounters(row);
int startOffset = findStartPattern();
int nextStart = startOffset;
decodeRowResult.setLength(0);
do {
int charOffset = toNarrowWidePattern(nextStart);
if (charOffset == -1) {
throw NotFoundException.getNotFoundInstance();
}
decodeRowResult.append((char) charOffset);
nextStart += 8;
if (decodeRowResult.length() > 1 &&
arrayContains(STARTEND_ENCODING, ALPHABET[charOffset])) {
break;
}
} while (nextStart < counterLength);
int trailingWhitespace = counters[nextStart - 1];
int lastPatternSize = 0;
for (int i = -8; i < -1; i++) {
lastPatternSize += counters[nextStart + i];
}
if (nextStart < counterLength && trailingWhitespace < lastPatternSize / 2) {
throw NotFoundException.getNotFoundInstance();
}
validatePattern(startOffset);
for (int i = 0; i < decodeRowResult.length(); i++) {
decodeRowResult.setCharAt(i, ALPHABET[decodeRowResult.charAt(i)]);
}
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();
}
if (decodeRowResult.length() <= MIN_CHARACTER_LENGTH) {
throw NotFoundException.getNotFoundInstance();
}
if (hints == null || !hints.containsKey(DecodeHintType.RETURN_CODABAR_START_END)) {
decodeRowResult.deleteCharAt(decodeRowResult.length() - 1);
decodeRowResult.deleteCharAt(0);
}
int runningCount = 0;
for (int i = 0; i < startOffset; i++) {
runningCount += counters[i];
}
float left = runningCount;
for (int i = startOffset; i < nextStart - 1; i++) {
runningCount += counters[i];
}
float right = runningCount;
Result result = new Result(
decodeRowResult.toString(),
null,
new ResultPoint[]{
new ResultPoint(left, rowNumber),
new ResultPoint(right, rowNumber)},
BarcodeFormat.CODABAR);
result.putMetadata(ResultMetadataType.SYMBOLOGY_IDENTIFIER, "]F0");
return result;
}
五、一维码和二维码区别
- 可以看到一维码没有纠错机制,如果一维码有破损,就不能被读取;对于二维码来说,即使有破损,也有可能可以正常读取。
- 一维码只能在水平方向单向的表达商品信息,而在垂直方向则不表达任何信息,它的一定高度通常是为了便于条码设备的对准,读取。而二维码在水平和垂直方向都可表达信息,也就是说它在二维空间内存储信息。
- 由于只有水平方向有信息,一维码扫码逻辑很简单。
欢迎提出宝贵意见,感谢观看! 参考: ZxingAPI Codabar介绍
|