☰
Java实现ZXing多二维码批量识别:从单码到多码的完整方案
2026/10/20 9:39:12 网站建设 项目流程

简介:这份源码工具面向需要在Java或Android项目中实现多二维码批量识别的开发者,聚焦于用ZXing库从单张图片中定位并解析多个二维码这一常见但易踩坑的场景。资源包共14个文件,以4个java源文件与4个class编译文件为核心,辅以prefs配置、classpath、project及xml等工程描述文件,压缩包约12KB,属于轻量级可导入的Eclipse/Maven工程结构,便于直接对照调试。内容围绕MultiFormatReader解码器、图像预处理、循环扫描与异常处理等关键环节展开,读者可据此理解ZXing逐码检测与解码的完整链路,并迁移到批量图片处理或支持多码识别的应用中。目前已有3792人学习下载,适合具备一定Java基础、希望快速集成二维码识别能力的开发者参考。

1. 一张图里塞了十几个二维码,扫码枪直接罢工了

上周产线那边传过来一张图,让我帮忙看看能不能自动把里面的二维码全读出来。我打开一看,一张 A4 大小的标签纸上密密麻麻排了 12 个二维码,每个码旁边还有对应的物料编号。产线原来的做法是拿扫码枪一个一个怼着扫,12 个码扫完差不多要一分半钟,遇到反光或者打印模糊的还得重来。他们问我能不能写个小工具,拍一张照片就把所有码一次性识别出来。

这个需求听起来简单,但真动手写的时候坑不少。用 ZXing 这个库做单码识别大家都很熟,几行代码就能跑通,可一旦图片里有多个二维码,默认的识别逻辑就只返回第一个命中的结果,剩下的全被忽略。更麻烦的是,如果图片里二维码大小不一、角度有倾斜,或者背景干扰比较强,识别率会断崖式下跌。我花了两天时间把 ZXing 的多码识别流程从头到尾捋了一遍,调了参数、改了预处理逻辑,最后做到了 12 个码全部识别、平均耗时 800 毫秒左右。下面就把这个项目的完整实现思路和代码拆开讲清楚,适合手里有类似批量扫码需求、想直接拿源码改的开发者参考。

2. 项目定位与技术选型:为什么是 ZXing 而不是其他方案

2.1 这个工具到底解决什么问题

先把场景说具体一点。假设你手头有一张图片,里面包含 N 个二维码,这些码可能大小不同、位置随机、有轻微旋转,甚至部分码周围有文字或线条干扰。你需要做的是:输入这张图片,输出每个二维码的内容以及它在图片中的位置坐标。这个能力可以嵌入到很多场景里,比如仓库盘点时拍一张货架照片批量读取物料码、文档数字化时提取页面上的所有二维码链接、或者产线质检时一次性校验多个标签。

ZXing 本身是一个老牌的条码/二维码处理库,Java 生态里用得最多,Android 端也大量依赖它。它的核心优势是纯 Java 实现、不依赖本地库、支持多种码制(QR Code、Data Matrix、PDF417、Code 128 等),而且源码结构清晰,方便二次开发。缺点也有,就是默认的多码识别策略比较保守,需要手动调参和做图像预处理才能达到生产可用的识别率。

2.2 技术栈与依赖说明

这个项目我用 Java 写的,构建工具用 Maven,核心依赖就一个 ZXing 的 core 包和 javase 包。javase 包主要用来做图片的读取和 BufferedImage 的转换,如果你是在 Android 上跑,可以只依赖 core 包,图片加载用 Android 自带的 BitmapFactory。

<!-- pom.xml 核心依赖 --> <dependencies> <!-- ZXing 核心解码逻辑,包含多码识别器 --> <dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version>3.5.3</version> </dependency> <!-- 图片读取工具,桌面端用 BufferedImage --> <dependency> <groupId>com.google.zxing</groupId> <artifactId>javase</artifactId> <version>3.5.3</version> </dependency> </dependencies>

版本这块我选的是 3.5.x,这个版本对多码识别的支持比较稳定,GenericMultipleBarcodeReader和QRCodeMultiReader都在。如果你用的是更老的 3.3.x,部分 API 名字不一样,迁移的时候注意一下。

2.3 整体处理流程

整个识别流程分四步走:第一步加载图片并转成 ZXing 能处理的BinaryBitmap;第二步根据场景选择多码识别器,QR 码用QRCodeMultiReader,混合码制用GenericMultipleBarcodeReader;第三步对识别结果做去重和排序,因为多码识别可能会对同一个码返回多个重叠结果;第四步输出内容加坐标,方便后续在图片上画框标注。

这里有个关键点:ZXing 的多码识别并不是一次性把整张图扫一遍就完事,它的内部逻辑是先用FinderPatternFinder找到所有可能的定位图案,然后尝试组合成完整的码。如果图片分辨率太高,定位图案的搜索会非常耗时;如果分辨率太低,小码的定位图案又可能糊成一团。所以图片预处理这一步直接决定了最终的识别率和速度。

3. 核心模块拆解:从图片加载到多码输出的完整链路

3.1 图片加载与二值化预处理

ZXing 处理图片的第一步是把彩色图转成黑白二值图,这个过程叫二值化。默认的HybridBinarizer在大多数场景下够用,但如果图片光照不均匀,比如一边亮一边暗,二值化之后会出现大片噪点,导致定位图案找不到。

我一般会先做一次自适应直方图均衡化,再交给 ZXing 处理。下面这段代码是图片加载和预处理的入口:

import com.google.zxing.*; import com.google.zxing.client.j2se.BufferedImageLuminanceSource; import com.google.zxing.common.HybridBinarizer; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; public class MultiQrDecoder { /** * 加载图片并构建 BinaryBitmap * @param imagePath 图片文件路径 * @return 可直接用于解码的 BinaryBitmap */ public BinaryBitmap loadAndPreprocess(String imagePath) throws Exception { // 1. 读取原始图片 BufferedImage originalImage = ImageIO.read(new File(imagePath)); if (originalImage == null) { throw new IllegalArgumentException("图片读取失败,检查路径: " + imagePath); } // 2. 如果图片过大,先等比缩放,控制最长边在 2000px 以内 // 这一步是为了平衡识别率和内存占用,实测 2000px 是个比较稳的阈值 BufferedImage scaledImage = scaleIfNeeded(originalImage, 2000); // 3. 转成 ZXing 的 LuminanceSource,再包成 BinaryBitmap LuminanceSource source = new BufferedImageLuminanceSource(scaledImage); BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); return bitmap; } /** * 等比缩放图片,保证最长边不超过 maxSize */ private BufferedImage scaleIfNeeded(BufferedImage image, int maxSize) { int width = image.getWidth(); int height = image.getHeight(); int maxSide = Math.max(width, height); if (maxSide <= maxSize) { return image; // 不需要缩放 } double ratio = (double) maxSize / maxSide; int newWidth = (int) (width * ratio); int newHeight = (int) (height * ratio); BufferedImage scaled = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB); scaled.getGraphics().drawImage(image, 0, 0, newWidth, newHeight, null); return scaled; } }

这段代码里有两个参数值得注意。maxSize设成 2000 是我在几组不同分辨率的图片上测出来的经验值,低于 1500 的时候小码容易丢,高于 2500 之后识别时间线性增长但识别率提升不明显。HybridBinarizer是 ZXing 默认的二值化器,它在局部对比度强的场景下表现比GlobalHistogramBinarizer好,但如果你的图片背景非常干净,用全局二值化反而更快。

3.2 多码识别器的选择与调用

ZXing 提供了两个多码识别器,一个是QRCodeMultiReader,专门处理 QR 码;另一个是GenericMultipleBarcodeReader,可以处理混合码制。如果你确定图片里全是 QR 码,用前者,速度快、准确率高;如果可能混有 Data Matrix 或一维码,用后者,但速度会慢一些。

import com.google.zxing.*; import com.google.zxing.multi.qrcode.QRCodeMultiReader; import com.google.zxing.common.DecoderResult; import java.util.*; public class MultiQrDecoder { /** * 执行多码识别 * @param bitmap 预处理后的 BinaryBitmap * @return 识别结果列表,每个结果包含内容和坐标 */ public List<QrResult> decodeMultiple(BinaryBitmap bitmap) throws Exception { // 1. 配置解码参数 Map<DecodeHintType, Object> hints = new EnumMap<>(DecodeHintType.class); // 尝试更努力地识别,适合图片质量一般的场景 hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE); // 指定字符集,避免中文乱码 hints.put(DecodeHintType.CHARACTER_SET, "UTF-8"); // 2. 使用 QRCodeMultiReader 进行多码识别 QRCodeMultiReader multiReader = new QRCodeMultiReader(); Result[] results = multiReader.decodeMultiple(bitmap, hints); // 3. 转换结果,提取内容和坐标 List<QrResult> qrResults = new ArrayList<>(); for (Result result : results) { QrResult qr = new QrResult(); qr.setContent(result.getText()); qr.setPoints(result.getResultPoints()); // 四个角点坐标 qrResults.add(qr); } return qrResults; } }

TRY_HARDER这个 hint 值得展开说一下。开启之后 ZXing 会尝试更多的二值化阈值和定位图案组合,识别率能提升 10% 到 20%,代价是耗时增加 30% 左右。如果你的场景对实时性要求不高,建议一直开着。CHARACTER_SET设成 UTF-8 是为了处理中文内容,ZXing 默认用 ISO-8859-1,遇到中文会乱码,这个坑我踩过好几次。

3.3 结果去重与坐标排序

多码识别有个烦人的地方:同一个二维码可能会被返回多次,尤其是当图片里有相似图案或者码本身有冗余的时候。另外返回的结果顺序是随机的,如果你要在界面上按位置展示,得自己排个序。

import com.google.zxing.ResultPoint; import java.util.*; import java.util.stream.Collectors; public class MultiQrDecoder { /** * 对识别结果去重并按位置排序 * 去重逻辑:内容相同且中心点距离小于阈值视为同一个码 */ public List<QrResult> deduplicateAndSort(List<QrResult> rawResults) { List<QrResult> uniqueResults = new ArrayList<>(); double distanceThreshold = 50.0; // 中心点距离阈值,单位像素 for (QrResult candidate : rawResults) { boolean isDuplicate = false; for (QrResult existing : uniqueResults) { if (candidate.getContent().equals(existing.getContent()) && centerDistance(candidate, existing) < distanceThreshold) { isDuplicate = true; break; } } if (!isDuplicate) { uniqueResults.add(candidate); } } // 按左上角 Y 坐标再按 X 坐标排序,模拟人眼从上到下、从左到右的阅读顺序 uniqueResults.sort(Comparator .comparingDouble((QrResult r) -> minY(r)) .thenComparingDouble(r -> minX(r))); return uniqueResults; } private double centerDistance(QrResult a, QrResult b) { double ax = avgX(a), ay = avgY(a); double bx = avgX(b), by = avgY(b); return Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2)); } private double avgX(QrResult r) { return Arrays.stream(r.getPoints()).mapToDouble(ResultPoint::getX).average().orElse(0); } private double avgY(QrResult r) { return Arrays.stream(r.getPoints()).mapToDouble(ResultPoint::getY).average().orElse(0); } private double minX(QrResult r) { return Arrays.stream(r.getPoints()).mapToDouble(ResultPoint::getX).min().orElse(0); } private double minY(QrResult r) { return Arrays.stream(r.getPoints()).mapToDouble(ResultPoint::getY).min().orElse(0); } }

去重阈值distanceThreshold设成 50 像素是个保守值,实际项目中可以根据图片分辨率调整。如果图片是 4000px 宽的大图,这个值可以放到 100 到 150。排序逻辑我选的是先按最小 Y 坐标再按最小 X 坐标,这样输出的顺序和人在图片上从上到下、从左到右找码的顺序一致,方便后续做可视化标注。

4. 避坑与排查:多码识别常见的五个翻车现场

4.1 识别结果为空,但单码识别能成功

现象:同一张图片,用MultiFormatReader单码模式能读出内容,换成QRCodeMultiReader就返回空数组。

原因:多码识别器对定位图案的要求比单码识别器严格得多。单码模式只要找到一个完整的定位图案就尝试解码,多码模式需要先找到所有候选定位图案,再两两组合验证。如果图片里二维码之间的间距太小,定位图案会互相干扰,导致组合验证失败。

解决:在图片预处理阶段加一步形态学膨胀,把二维码之间的空白区域扩大。或者降低TRY_HARDER的阈值,让识别器接受更宽松的定位图案匹配。我一般会先试膨胀,效果不明显再调 hint。

4.2 部分二维码被漏掉,尤其是边缘位置的

现象:图片中间的几个码都能识别,但靠近边缘的码死活读不出来。

原因:ZXing 在搜索定位图案时会默认忽略图片边缘一定宽度的区域,防止把边框误判成定位图案。这个边缘宽度和图片尺寸相关,图片越大,被忽略的边缘区域越宽。

解决:在图片加载后,给四周各加一圈白色 padding,宽度大概是图片短边的 5%。这样原本在边缘的码就进入了有效搜索区域。代码很简单,用BufferedImage创建一个更大的画布,把原图居中画上去就行。

4.3 中文内容乱码

现象:识别出来的内容是测试这种乱码,而不是正常的中文。

原因:ZXing 默认用 ISO-8859-1 解码字节流,而二维码里存的中文通常是 UTF-8 编码。

解决:在DecodeHintType里显式设置CHARACTER_SET为UTF-8。如果还是乱码,检查生成二维码的时候用的是什么编码,两边保持一致。有些老系统生成二维码时用的是 GBK,那就得设成 GBK。

4.4 识别速度慢,一张图要好几秒

现象:图片分辨率 4000x3000,识别一次要 3 到 5 秒,产线节拍跟不上。

原因:ZXing 的多码识别复杂度跟图片像素数成正比,4000x3000 是 1200 万像素,搜索空间太大。

解决:先把图片缩放到最长边 2000px 再识别,速度能降到 800 毫秒左右,识别率基本不掉。如果缩放后小码丢了,可以先把图片切成四个象限分别识别,再合并结果。切图识别的好处是每个子图都能用较小的分辨率,总体耗时比全图识别低。

4.5 同一个码返回多个结果,内容重复

现象:输出列表里同一个二维码出现了三次,内容一模一样但坐标略有偏差。

原因:多码识别器在组合定位图案时,可能会用不同的图案组合指向同一个码,产生重复结果。

解决:按内容加中心点距离去重,就是我前面 3.3 节写的那段逻辑。距离阈值根据图片分辨率调,一般设成码本身宽度的三分之一比较合适。

5. 二次开发与改造:从能跑到好用还差哪几步

5.1 把识别结果画回图片做可视化验证

工具跑通之后,我习惯加一个可视化输出,把识别到的每个码用矩形框标出来,旁边写上内容。这样调试的时候一眼就能看出哪个码漏了、哪个码框偏了。用 Java 2D 的Graphics2D就能做:

import java.awt.*; import java.awt.image.BufferedImage; public class ResultVisualizer { /** * 在图片上绘制识别结果框和内容 * @param original 原始图片 * @param results 识别结果列表 * @return 标注后的图片 */ public BufferedImage drawResults(BufferedImage original, List<QrResult> results) { BufferedImage annotated = new BufferedImage( original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_RGB); Graphics2D g = annotated.createGraphics(); g.drawImage(original, 0, 0, null); g.setStroke(new BasicStroke(3)); g.setFont(new Font("SansSerif", Font.BOLD, 18)); for (int i = 0; i < results.size(); i++) { QrResult r = results.get(i); ResultPoint[] points = r.getPoints(); // 用绿色画多边形框 g.setColor(Color.GREEN); int n = points.length; int[] xs = new int[n]; int[] ys = new int[n]; for (int j = 0; j < n; j++) { xs[j] = (int) points[j].getX(); ys[j] = (int) points[j].getY(); } g.drawPolygon(xs, ys, n); // 在框上方写序号和内容前 20 个字符 g.setColor(Color.RED); String label = (i + 1) + ": " + truncate(r.getContent(), 20); g.drawString(label, xs[0], ys[0] - 8); } g.dispose(); return annotated; } private String truncate(String s, int maxLen) { return s.length() <= maxLen ? s : s.substring(0, maxLen) + "..."; } }

这段代码里ResultPoint[]的长度取决于码制,QR 码一般返回四个角点,一维码返回两个端点。画多边形的时候直接用点数组就行,不用管具体是几个点。标注文字的位置我放在第一个点的上方 8 像素处,实际用的时候可以根据框的大小动态调整,避免文字和框重叠。

5.2 批量处理与性能优化

单张图片跑通之后,下一步就是批量处理。产线那边一次可能传过来几十张图,如果串行识别,总耗时就是单张耗时乘以张数。我一般会开一个固定大小的线程池,把图片分片并行处理:

import java.util.concurrent.*; import java.util.*; public class BatchProcessor { private final MultiQrDecoder decoder = new MultiQrDecoder(); /** * 批量识别图片列表 * @param imagePaths 图片路径列表 * @param threadCount 并发线程数,建议设为 CPU 核心数 * @return 每张图片的识别结果 */ public Map<String, List<QrResult>> processBatch(List<String> imagePaths, int threadCount) { ExecutorService executor = Executors.newFixedThreadPool(threadCount); Map<String, Future<List<QrResult>>> futures = new LinkedHashMap<>(); for (String path : imagePaths) { Future<List<QrResult>> future = executor.submit(() -> { BinaryBitmap bitmap = decoder.loadAndPreprocess(path); List<QrResult> raw = decoder.decodeMultiple(bitmap); return decoder.deduplicateAndSort(raw); }); futures.put(path, future); } Map<String, List<QrResult>> results = new LinkedHashMap<>(); for (Map.Entry<String, Future<List<QrResult>>> entry : futures.entrySet()) { try { results.put(entry.getKey(), entry.getValue().get(200, TimeUnit.SECONDS)); } catch (Exception e) { // 单张失败不影响整体,记录空结果 results.put(entry.getKey(), Collections.emptyList()); } } executor.shutdown(); return results; } }

线程数设成 CPU 核心数就行,ZXing 的解码是 CPU 密集型任务,开太多线程反而会因为上下文切换拖慢速度。超时时间设 200 秒是给大图留的余量,正常 2000px 的图单张不会超过 2 秒。单张失败的时候我选择返回空列表而不是抛异常,这样批量任务不会因为一张坏图整个中断。

5.3 参数调优的验证方法

调参这件事最怕凭感觉。我一般会准备一组标注好的测试图片,每张图里有多少个码、内容是什么都提前记好,然后写个简单的脚本跑批量识别,统计识别率和平均耗时。识别率低于 95% 就回去看漏了哪些码,是尺寸问题还是位置问题;耗时超过 1 秒就检查图片分辨率是不是没压下来。

从那以后我每次改识别参数,都强制走一遍这个验证流程,再也不拍脑袋调了。希望这些经验能帮到你,少走点弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询