尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

zxing多二维码识别实战:批量提取与参数调优

zxing多二维码识别实战:批量提取与参数调优 简介这份源码工具面向需要在Java或Android项目中实现多二维码批量识别的开发者尤其适合已掌握基础图像处理、希望进一步理解ZXing解码流程的中级工程师。资源围绕「一幅图片中同时存在多个二维码」这一典型场景提供可运行的工程示例帮助解决逐个扫描、循环解码与异常处理等实际问题。压缩包共14个文件约12KB包含4个java源文件、4个class编译文件、3个prefs配置、1个classpath、1个project及1个xml覆盖源码、构建配置与IDE工程描述结构紧凑导入后即可对照调试。目前已有3789人学习下载说明该示例在同类需求中具有较高参考价值。读者可从中获取多二维码识别的完整代码骨架、MultiFormatReader的调用方式、图像预处理与结果循环处理思路以及针对识别失败场景的健壮性设计参考便于快速迁移到批量图片处理或扫码类应用中。1. 一张图里塞了十几个二维码zxing 默认只吐一个怎么办做过扫码登录、批量票据核销或者会议签到的人大概率遇到过这种场景运营甩过来一张拼接图上面密密麻麻排了二三十个二维码让你把里面的内容全部提取出来。你兴冲冲地写下MultiFormatReader.decode(image)结果它只返回了第一个命中的码剩下的像没看见一样。这不是 zxing 的 bug而是它的默认工作模式就是「找到一个就收工」。zxingZebra Crossing是 Google 开源的一维/二维条码处理库Java 生态里做二维码识别基本绕不开它Android 端、服务端批处理、桌面工具都有它的身影。它真正的多码识别能力藏在GenericMultipleBarcodeReader和QRCodeMultiReader这两个类里很多人用了好几年都没碰过。这篇笔记就围绕「一张图多个二维码」这个具体诉求把源码包里能直接跑的识别流程、参数调优和几个血泪坑拆开讲清楚适合已经会写单码识别、现在要批量提取的开发者。2. 多码识别的原理与选型为什么单码 Reader 会漏掉其余二维码2.1 zxing 的定位机制决定了它「见好就收」zxing 识别二维码的核心流程是先把彩色图转成BinaryBitmap二值化然后Detector在二值图里找定位图案二维码三个角的回字形方块找到一组就交给Decoder解码。MultiFormatReader的decode方法内部调用的是decodeWithState一旦Detector返回第一个成功定位的结果整个流程就结束了。它压根没打算继续找第二个。所以当你把一张多码图丢进去得到的永远是「最靠左上、最清晰、最先被扫描到」的那一个。这不是精度问题是设计目标问题——单码 Reader 面向的是「扫一个码」的交互场景不是「批量提取」的批处理场景。2.2 多码识别靠的是「切分 递归」而不是「一次全找」GenericMultipleBarcodeReader的思路很朴素先用底层 Reader 找到第一个码记录它的边界框然后把这张图按边界框切成若干块在剩下的区域里继续找直到找不到为止。它本质上是一个递归下降的搜索器。QRCodeMultiReader则更聪明一点它继承自MultiFormatReader重写了decodeMultiple方法能一次性返回Result[]数组内部对 QR 码的定位做了优化适合纯二维码场景。选型上我的经验是如果图里全是二维码优先用QRCodeMultiReader速度快、漏检少如果图里可能混着条形码Code128、EAN-13 之类那就用GenericMultipleBarcodeReader包一个MultiFormatReader通用性更强但慢一些。下面这张表是我实测两种方案在 20 个二维码拼接图上的表现差异方案识别耗时漏检情况适用场景QRCodeMultiReader约 380ms20/20 全中纯二维码批量提取GenericMultipleBarcodeReaderMultiFormatReader约 920ms19/20边缘一个被裁切混合码、边界不规整MultiFormatReader单码约 60ms只返回 1 个交互式扫码耗时数据来自我本地一台普通笔记本的 JMH 粗测不同图片差异很大但量级关系是稳定的多码识别比单码慢一个数量级因为要反复做二值化和定位。2.3 依赖引入与最小可运行骨架先把依赖配好。Maven 项目里加 core 就够了如果要处理图片文件再考虑 javasedependency groupIdcom.google.zxing/groupId artifactIdcore/artifactId version3.5.3/version /dependency dependency groupIdcom.google.zxing/groupId artifactIdjavase/artifactId version3.5.3/version /dependencycore提供识别算法javase提供BufferedImageLuminanceSource这类把BufferedImage转成 zxing 内部亮度源的桥接类。版本号建议用 3.5.x3.4 之前QRCodeMultiReader对低对比度图片的鲁棒性差一些。引入之后先写一个最小骨架确认环境通了再谈调优import com.google.zxing.*; import com.google.zxing.common.HybridBinarizer; import com.google.zxing.qrcode.QRCodeMultiReader; import com.google.zxing.client.j2se.BufferedImageLuminanceSource; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; import java.util.EnumMap; import java.util.Map; public class MultiQrDemo { public static void main(String[] args) throws Exception { BufferedImage image ImageIO.read(new File(codes.png)); // 把 BufferedImage 转成 zxing 能吃的亮度源 LuminanceSource source new BufferedImageLuminanceSource(image); // HybridBinarizer 对光照不均的图更友好GlobalHistogramBinarizer 适合高对比度 BinaryBitmap bitmap new BinaryBitmap(new HybridBinarizer(source)); MapDecodeHintType, Object hints new EnumMap(DecodeHintType.class); hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE); QRCodeMultiReader reader new QRCodeMultiReader(); Result[] results reader.decodeMultiple(bitmap, hints); for (Result r : results) { System.out.println(r.getText()); } } }这段代码的关键在decodeMultiple而不是decode返回的是数组。TRY_HARDER这个 hint 会让识别器在定位失败时多尝试几轮代价是耗时增加但对模糊、倾斜的码有明显帮助。HybridBinarizer和GlobalHistogramBinarizer的选择是个玄学点前者对局部光照变化适应好后者在整图对比度高时更快更准我一般先用 Hybrid漏检了再换 Global 对比。3. 从单张图到批量提取参数调优与完整落地代码3.1 二值化策略直接决定漏检率二维码识别的第一步是二值化把灰度图变成黑白图。这一步做砸了后面再调 hint 都是白搭。zxing 提供两个Binarizer实现GlobalHistogramBinarizer用整张图的直方图算一个全局阈值HybridBinarizer把图切成 8x8 的块每块单独算阈值。多码拼接图往往存在局部阴影、反光或者不同二维码底色不一致的情况全局阈值会把某些区域直接压成全黑或全白定位图案就没了。所以多码场景我默认用HybridBinarizer。但 Hybrid 也有翻车的时候如果图里二维码特别小比如每个只有 80x80 像素8x8 分块会切得太碎反而引入噪声。这时候可以先把图放大 2 倍再做二值化实测能救回不少小码。// 小码场景先放大再识别 BufferedImage scaled new BufferedImage( image.getWidth() * 2, image.getHeight() * 2, BufferedImage.TYPE_INT_RGB); scaled.createGraphics().drawImage(image, 0, 0, image.getWidth() * 2, image.getHeight() * 2, null); LuminanceSource source new BufferedImageLuminanceSource(scaled); BinaryBitmap bitmap new BinaryBitmap(new HybridBinarizer(source));放大用的是drawImage的双线性插值不是简单的像素复制这样边缘更平滑定位图案的角点更容易被Detector抓到。代价是内存和耗时都翻倍所以只在小码场景用。3.2 用 GenericMultipleBarcodeReader 处理混合码和裁切边界纯二维码用QRCodeMultiReader就够了但现实里的图经常是二维码和条形码混排或者二维码被裁掉了一个角。这时候GenericMultipleBarcodeReader更稳。它的工作方式是递归切分所以对边界不完整的码容忍度更高。用法是把它包在一个MultiFormatReader外面MultiFormatReader baseReader new MultiFormatReader(); MapDecodeHintType, Object hints new EnumMap(DecodeHintType.class); hints.put(DecodeHintType.POSSIBLE_FORMATS, java.util.Arrays.asList( BarcodeFormat.QR_CODE, BarcodeFormat.CODE_128, BarcodeFormat.EAN_13)); hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE); baseReader.setHints(hints); GenericMultipleBarcodeReader multiReader new GenericMultipleBarcodeReader(baseReader); Result[] results multiReader.decodeMultiple(bitmap, hints);POSSIBLE_FORMATS这个 hint 很重要。不指定的话MultiFormatReader会挨个尝试所有支持的格式耗时直接翻几倍。明确告诉它只找 QR、Code128、EAN-13速度能快 40% 以上。GenericMultipleBarcodeReader的递归深度默认是有限的如果图里码特别多超过 20 个可能会提前停止这时候需要手动循环每次识别完把已识别的区域涂白再重新识别直到返回空数组。3.3 完整批量提取脚本与结果去重把上面的点串起来写一个能直接跑的批量提取工具。核心逻辑是读图 → 二值化 → 多码识别 → 按内容去重 → 输出。去重是因为递归切分可能对同一个码重复识别尤其是码与码挨得很近的时候。import com.google.zxing.*; import com.google.zxing.common.HybridBinarizer; import com.google.zxing.multi.GenericMultipleBarcodeReader; import com.google.zxing.client.j2se.BufferedImageLuminanceSource; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; import java.util.*; public class BatchQrExtractor { public static ListString extract(File imageFile) throws Exception { BufferedImage image ImageIO.read(imageFile); LuminanceSource source new BufferedImageLuminanceSource(image); BinaryBitmap bitmap new BinaryBitmap(new HybridBinarizer(source)); MapDecodeHintType, Object hints new EnumMap(DecodeHintType.class); hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE); hints.put(DecodeHintType.CHARACTER_SET, UTF-8); MultiFormatReader base new MultiFormatReader(); base.setHints(hints); GenericMultipleBarcodeReader reader new GenericMultipleBarcodeReader(base); Result[] results reader.decodeMultiple(bitmap, hints); // 用 LinkedHashSet 保序去重 SetString unique new LinkedHashSet(); for (Result r : results) { unique.add(r.getText()); } return new ArrayList(unique); } public static void main(String[] args) throws Exception { File dir new File(./images); for (File f : Objects.requireNonNull(dir.listFiles())) { if (!f.getName().endsWith(.png) !f.getName().endsWith(.jpg)) continue; ListString codes extract(f); System.out.println(f.getName() - codes.size() 个码); codes.forEach(c - System.out.println( c)); } } }CHARACTER_SET设成 UTF-8 是必须的否则中文内容会乱码这是最常见的翻车点之一。LinkedHashSet保证输出顺序和识别顺序一致方便对照原图排查。整个脚本对单张 20 码的图耗时在 1 秒左右批量处理几百张图建议开线程池但注意 zxing 的 Reader 实例不是线程安全的每个线程要 new 一个。4. 避坑与排查多码识别最容易翻车的五个地方4.1 识别结果只有第一个码数组长度永远是 1现象调了decodeMultiple返回的Result[]长度还是 1。原因底层用的还是MultiFormatReader而不是GenericMultipleBarcodeReader或QRCodeMultiReader。MultiFormatReader本身没有decodeMultiple方法如果你强行转型或者用错类它只会走单码逻辑。解决确认实例类型纯二维码用QRCodeMultiReader混合码用GenericMultipleBarcodeReader包装。另外检查是不是误用了decode而不是decodeMultiple。4.2 中文内容乱码或显示问号现象识别出来的文本里中文变成???或者乱码。原因没有设置CHARACTER_SEThintzxing 默认用 ISO-8859-1 解码字节流。解决在 hints 里加hints.put(DecodeHintType.CHARACTER_SET, UTF-8)。如果二维码生成时用的不是 UTF-8比如 GBK那这里也要对应改成 GBK但这种情况很少见绝大多数现代二维码都是 UTF-8。4.3 边缘的二维码死活识别不出来现象图中间的几个码都能识别最右边或最下面的那个总是漏。原因GenericMultipleBarcodeReader递归切分时边缘区域可能被切得不完整或者原图本身在边缘处有裁切。解决先给图片加一圈白边padding让边缘的码有完整的静默区。二维码规范要求四周至少留 4 个模块宽度的空白很多拼接图为了省空间把静默区压没了。// 给图片加 20px 白边 int pad 20; BufferedImage padded new BufferedImage( image.getWidth() pad * 2, image.getHeight() pad * 2, BufferedImage.TYPE_INT_RGB); var g padded.createGraphics(); g.setColor(java.awt.Color.WHITE); g.fillRect(0, 0, padded.getWidth(), padded.getHeight()); g.drawImage(image, pad, pad, null); g.dispose();4.4 识别耗时爆炸一张图跑十几秒现象图里码一多识别时间从几百毫秒涨到十几秒。原因TRY_HARDER加上GenericMultipleBarcodeReader的递归切分复杂度是 O(n²) 级别的。每找到一个码就要重新二值化剩余区域。解决如果确定是纯二维码换QRCodeMultiReader它内部做了优化不会反复二值化。另外把POSSIBLE_FORMATS限定死别让它挨个试。如果还是慢考虑先把图缩小到合理尺寸比如最长边 2000px超过这个尺寸的图对识别精度没帮助只增加耗时。4.5 同一个码被识别出两次结果重复现象输出列表里同一个内容出现两遍。原因递归切分时切分边界可能让同一个码落在两个子区域里各识别一次。解决用Set去重就像 3.3 的代码那样。但要注意如果业务上确实允许重复内容比如两个码内容一样但代表不同票据那就不能简单去重得用Result.getResultPoints()拿到坐标来区分。坐标是ResultPoint[]包含二维码三个角点的位置可以用来判断是不是同一个物理码。5. 进阶技巧用坐标信息做可视化校验和精度兜底多码识别最让人不放心的就是「到底识别全了没有」。光看输出列表没法判断是图里只有 5 个码还是本来有 8 个但漏了 3 个。这时候Result.getResultPoints()就派上用场了。它返回二维码三个定位角点的坐标拿到坐标后可以在原图上画框生成一张标注图肉眼一比就知道漏没漏。这个技巧我在处理客户投诉「码没提全」的时候屡试不爽直接把标注图甩过去是图的问题还是代码的问题一目了然。import java.awt.*; import java.awt.image.BufferedImage; public static void drawBoxes(BufferedImage image, Result[] results) { Graphics2D g image.createGraphics(); g.setColor(Color.RED); g.setStroke(new BasicStroke(3)); for (Result r : results) { ResultPoint[] points r.getResultPoints(); if (points null || points.length 3) continue; // 用三个角点画多边形 int[] xs new int[points.length]; int[] ys new int[points.length]; for (int i 0; i points.length; i) { xs[i] (int) points[i].getX(); ys[i] (int) points[i].getY(); } g.drawPolygon(xs, ys, points.length); } g.dispose(); }画出来的框如果和原图里的二维码对不上说明定位有偏差通常是二值化参数不对。如果某个码没框那就是漏检回去调TRY_HARDER或者换Binarizer。这个校验步骤我建议固化成流程每次批量处理前先跑一张样图看看标注结果。另一个兜底手段是「分块识别」。如果一张图里二维码排列很规整比如 4x5 的网格与其让 zxing 自己递归找不如手动按网格切图每块单独识别。这样虽然代码多几行但漏检率极低而且可以并行处理。切图的时候注意每块之间留 10% 的重叠区域防止码正好压在切割线上。// 按 4x5 网格切图每块带 10% 重叠 int cols 4, rows 5; int w image.getWidth() / cols; int h image.getHeight() / rows; int overlap (int) (w * 0.1); for (int r 0; r rows; r) { for (int c 0; c cols; c) { int x Math.max(0, c * w - overlap); int y Math.max(0, r * h - overlap); int cw Math.min(image.getWidth() - x, w overlap * 2); int ch Math.min(image.getHeight() - y, h overlap * 2); BufferedImage tile image.getSubimage(x, y, cw, ch); // 对 tile 单独调用识别逻辑 } }分块识别的代价是可能把跨块的码切断所以重叠区域不能省。我一般会先用整图识别跑一遍漏检的码再用分块补一遍两边的结果合并去重。这套组合拳打下来基本没再遇到过「码提不全」的投诉。从那以后我每次做多码提取都强制先跑一遍标注图校验再决定要不要上分块兜底。希望帮到你。本文还有配套的精品资源点击获取
返回列表