2021SC@SDUSC
在之前的例子中,展示的都是针对单个码进行解码,但是在生活中会出现同时扫描两个二维码的情况,这种时候应该怎么解码呢?本节将对Zxing这一部分代码进行解读。
一、Zxing中的multi目录
为了方便介绍各类之间的关系,将Detector和Reader也加入到UML图中。可以大概了解到:对于多个码同时存在的情况,定位方法MultiDetector继承了Detector;但是解析方法是对Decode的调用;而解码器QRCodeMultiReader则是通过接口实现了多继承。我们解读这部分源码的重点是Zxing如何定位、存储多个二维码的。
- QRCodeMultiReader:检测和解码图像中的多个QR码
- GenericMultipleBarcodeReader:试图通过反复解码图像的一部分来定位图像中的多个条形码。找到一个条形码后,将递归扫描码ResultPoint的左、上、右和下区域。当试图在图像中查找多个二维条形码(如QR码)时,调用方还希望使用ByQuadrantReader。其中多个条形码的存在可能会阻止检测其中任何一个条形码。
- ByQuadrantReader:此类尝试从图像中解析码,不是通过扫描整个图像,而是通过扫描图像的子集。当图像中可能存在多个码时,检测一个条形码时可能会发现多个条形码的部分,并且无法解码(例如QR码)。取而代之的是扫描图像的四个象限——以及中央的“象限”,以覆盖在中央发现码的情况。
- MultipleBarcodeReader:此接口的实现尝试从一个图像读取多个条形码。
- MultiDetector:这个类封装了可以探测图像中一个或多个QR码的逻辑,即使QR码旋转、倾斜或部分模糊也可以尝试探测出来。
- MultiFinderPatternFinder:尝试在二维码中找到finder patterns(二维码个角上的方形标记),该类将返回图像中所有可能的QR码位置的数组。
二、扫码器QRCodeMultiReader
这个类继承了QRCodeReader(详见Zxing七),实现了MultipleBarcodeReader接口。类中只有3个方法。最主要的是decodeMultiple,可以类比于QRCodeReader中的decode。在具体解析方法上与QRCodeReader没有太大的区别,只是将result、DetectorResult、ResultPoint等信息变成了数组或链表进行存储。解析时,多个码彼此独立,单独解析,同图片中只要一个码的情况一致。通过下面的代码分析可以看出,该方法与QRCodeReader的decode方法是十分相似的。
@Override
public Result[] decodeMultiple(BinaryBitmap image, Map<DecodeHintType,?> hints) throws NotFoundException {
//定义Result类型的链表存放结果
List<Result> results = new ArrayList<>();
//封装解码位矩阵的结果,它包含获得的原始字节,以及这些字节的字符串解释。
DetectorResult[] detectorResults = new MultiDetector(image.getBlackMatrix()).detectMulti(hints);
//对于每个DetectorResult进行分析
for (DetectorResult detectorResult : detectorResults) {
try {
DecoderResult decoderResult = getDecoder().decode(detectorResult.getBits(), hints);
ResultPoint[] points = detectorResult.getPoints();
//如果代码被镜像:交换左下角点和右上角点
if (decoderResult.getOther() instanceof QRCodeDecoderMetaData) {
((QRCodeDecoderMetaData) decoderResult.getOther()).applyMirroredCorrection(points);
}
Result result = new Result(decoderResult.getText(), decoderResult.getRawBytes(), points,
BarcodeFormat.QR_CODE);
// 返回结果中的字节段列表。如果没有,返回null
List<byte[]> byteSegments = decoderResult.getByteSegments();
if (byteSegments != null) {
result.putMetadata(ResultMetadataType.BYTE_SEGMENTS, byteSegments);
}
// 使用的错误更正级别的名称
String ecLevel = decoderResult.getECLevel();
if (ecLevel != null) {
result.putMetadata(ResultMetadataType.ERROR_CORRECTION_LEVEL, ecLevel);
}
// 如果代码格式支持结构化追加,并且当前扫描的代码是其中的一部分:
if (decoderResult.hasStructuredAppend()) {
//序列号随附
result.putMetadata(ResultMetadataType.STRUCTURED_APPEND_SEQUENCE,
decoderResult.getStructuredAppendSequenceNumber());
//码与码之间关系平等
result.putMetadata(ResultMetadataType.STRUCTURED_APPEND_PARITY,
decoderResult.getStructuredAppendParity());
}
results.add(result);
} catch (ReaderException re) {
// 忽视并继续
}
}
//如果结果是空值
if (results.isEmpty()) {
return EMPTY_RESULT_ARRAY;
} else {
//对结果进行二次处理。
results = processStructuredAppend(results);
return results.toArray(EMPTY_RESULT_ARRAY);
}
}
但是在QRCodeMultiReader中,增加了对结果二次处理的操作。这是针对上述代码中提到的STRUCTURED_APPEND_SEQUENCE
执行的操作。由于是多个码同时出现,可能有前后顺序的约束,因此processStructuredAppend方法中执行的就是如果有序列化要求,就将结果进行排序,再返回排序后的newResults。为了实现这个方法,QRCodeMultiReader还实现了一个内部类,以方便使用语句“Collections.sort(saResults, new SAComparator());
”给result进行排序。
private static final class SAComparator implements Comparator<Result>, Serializable {
@Override
public int compare(Result a, Result b) {
//getResultMetadata实现将Map中ResultMetadataType的键映射到值。
int aNumber = (int) a.getResultMetadata().get(ResultMetadataType.STRUCTURED_APPEND_SEQUENCE);
int bNumber = (int) b.getResultMetadata().get(ResultMetadataType.STRUCTURED_APPEND_SEQUENCE);
return Integer.compare(aNumber, bNumber);
}
}
三、定位器MultiDetector
MultiDetector可以类比Detector,MultiFinderPatternFinder可以类比FinderPatternFinder (Detector、FinderPatternFinder详见Zxing十一)。此外,MultiFinderPatternFinder继承了FinderPatternFinder
public DetectorResult[] detectMulti(Map<DecodeHintType,?> hints) throws NotFoundException {
BitMatrix image = getImage();
//当找到可能的ResultPoint时,需要通过回调通知调用方。
ResultPointCallback resultPointCallback =
hints == null ? null : (ResultPointCallback) hints.get(DecodeHintType.NEED_RESULT_POINT_CALLBACK);
// 尝试在二维码中找到查找器模式。Finder图案是二维码三个角上的方形标记
MultiFinderPatternFinder finder = new MultiFinderPatternFinder(image, resultPointCallback);
//定义数组存放查询结果
FinderPatternInfo[] infos = finder.findMulti(hints);
//如果查询结果是空
if (infos.length == 0) {
//抛异常:在图像中找不到条形码时引发。可能已部分检测到,但无法确认。
throw NotFoundException.getNotFoundInstance();
}
//定义链表保存结果
List<DetectorResult> result = new ArrayList<>();
//对可能的定位点逐个操作
for (FinderPatternInfo info : infos) {
try {
//调用的方法在Detector的detect中
result.add(processFinderPatternInfo(info));
} catch (ReaderException e) {
// 忽视
}
}
if (result.isEmpty()) {
return EMPTY_DETECTOR_RESULTS;
} else {
return result.toArray(EMPTY_DETECTOR_RESULTS);
}
}
在Detector中,查找定位点的算法是FinderPatternFinder中的find(Map<DecodeHintType,?> hints)方法,而在这里,查定位点的算法是MultiFinderPatternFinder中的findMulti(Map<DecodeHintType,?> hints),基本思路完全一致,只是MultiFinderPatternFinder要多次操作,返回的是结果数组。
四、测试样例
我们通过Zxing的测试来展示对于多个二维码同时存在的扫码过程。这是Zxing官方给出的测试图片,我们要测试同时解析4个二维码的情况。在之前的扫码中,由于我们只针对一个码进行解析,其结果返回是一个result;但是现在有多个码,结果就是一个数组的result。
@Test
public void testMultiQRCodes() throws Exception {
//读取图片
Path testBase = AbstractBlackBoxTestCase.buildTestBase("src/test/resources/blackbox/multi-qrcode-1");
Path testImage = testBase.resolve("1.png");
//解码的套路操作,前面详细讲述了,这里不做赘述
BufferedImage image = ImageIO.read(testImage.toFile());
LuminanceSource source = new BufferedImageLuminanceSource(image);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
// 我们主要关注MultipleBarcodeReader
MultipleBarcodeReader reader = new QRCodeMultiReader();
// 在之前的解码中,result是单个的对象,但是在这里result是以数组的方式展示的
Result[] results = reader.decodeMultiple(bitmap);
//判断解析结果是否为空及是否长度为4
assertNotNull(results);
assertEquals(4, results.length);
Collection<String> barcodeContents = new HashSet<>();
for (Result result : results) {
//输出测试结果
System.out.println(result.getText());
System.out.println(result.getResultPoints()[0]+" "+result.getResultPoints()[1]+" "+result.getResultPoints()[2]+" "+result.getResultPoints()[3]);
//在HashSet中加入解析的文字结果
barcodeContents.add(result.getText());
assertEquals(BarcodeFormat.QR_CODE, result.getBarcodeFormat());
assertNotNull(result.getResultMetadata());
}
//判断解析结果是否正确
Collection<String> expectedContents = new HashSet<>();
expectedContents.add("You earned the class a 5 MINUTE DANCE PARTY!! Awesome! Way to go! Let's boogie!");
expectedContents.add("You earned the class 5 EXTRA MINUTES OF RECESS!! Fabulous!! Way to go!!");
expectedContents.add("You get to SIT AT MRS. SIGMON'S DESK FOR A DAY!! Awesome!! Way to go!! Guess I better clean up! :)");
expectedContents.add("You get to CREATE OUR JOURNAL PROMPT FOR THE DAY! Yay! Way to go! ");
assertEquals(expectedContents, barcodeContents);
}
打印一下文本结果和部分定位点信息,可以看到成功同时解析了4个二维码:
Zxing的测试代码和测试图片都很全面也很规范,包含了单元测试、集成测试等,对于理解源码很有帮助。
五、总结
本以为会很复杂的混合编码解码并不复杂,都是在之前已有的模块基础上进行编码的。通过这部分的代码分析,体会到了java代码复用的强大。在原有的模块基础上稍加改动就可以解决更加复杂的问题。
欢迎提出宝贵意见,感谢观看!
参考: ZxingAPI