最近接手了一个自动化处理任务,每天会有上千张截图和照片需要自动识别其中的二维码并提取内容。我第一反应是直接用 Python 加 OpenCV,但现场的服务是纯 C/C++ 编译出来的,硬塞一个 Python 运行时实在不划算,维护成本直接飙升。于是静下心来在 Linux 的 C/C++ 环境里找解析图片二维码的方案,折腾了一圈,最后落到 Zxing-cpp 这个库上。这篇笔记就把我在 linux-c 环境下用 Zxing-cpp 解析图片中二维码的完整过程记录下来,从选型、编译到踩坑调参,尽量写细一点,给有同样需求的人少绕几个弯。
1. 为什么是 Zxing-cpp?一个 Linux C++ 识别方案的选型记录
1.1 场景倒逼:不是想用 Python,而是只能上 C/C++
很多后台服务在初期用脚本语言把功能跑通,但一旦涉及到性能、内存占用、部署环境和跨平台交付,C/C++ 往往才是最终归宿。我这个任务就是典型的"必须走编译型语言"的场景:目标机器资源紧张,没有 Python 解释器,也没有 Node.js,整个技术栈都是 C++。那就只能在 C++ 生态里找能解析二维码的库。
选型时我给自己列了几个硬性条件:必须能读取图片文件并识别二维码、库要跨平台且维护活跃、依赖不能太重、API 要足够简单。根据这些条件,我把候选方案锁定在 ZBar、OpenCV 内置的二维码模块、以及 Zxing-cpp 这三条路上。
1.2 三个候选方案对比后,我选它的理由
先说我真实的对比体验。
ZBar 是老牌的二维码/条形码识别库,网上教程非常多,但它的维护状态并不理想,多年没有活跃更新,而且构建体系比较老旧,在较新的编译器或较干净的 Linux 发行版上编译会遇到不少兼容性问题。如果你的二维码只是简单的黑白规则图案,ZBar 也许够用;但一旦图片有倾斜、模糊、光照变化,它的识别率明显不够稳。
OpenCV 自带 QRCodeDetector,功能确实方便,而且如果你项目里本来就有 OpenCV,几乎零成本接入。问题是 OpenCV 本身太"重",为了一个二维码功能引入整个庞大的依赖树,在资源受限的环境里很不划算。另外它的二维码识别模块在某些边缘场景下不够精细,比如多个二维码共存、需要获取二维码四角坐标时,处理起来比较麻烦。
Zxing-cpp 是开源二维码库 ZXing 的 C++ 移植版,社区维护比较活跃,底层算法覆盖了二维码、Data Matrix、Code128 等多种格式。它是个纯 C++ 库,没有对外部图像处理库的强依赖,内存占用小,解码精度高,还提供了 C++ 和 C 两套 API。实测下来,它对于倾斜、部分遮挡、光照不匀的图片容错性比 ZBar 好很多,而且我能明确控制识别哪些格式、是否尝试旋转、是否加大搜索强度。最终我选了 Zxing-cpp。
放个简单对比表,方便你根据自己情况判断:
| 方案 | 依赖重量 | 维护活跃度 | 解码精度 | 多码支持 | 适用场景 |
|---|---|---|---|---|---|
| ZBar | 中 | 低 | 一般 | 一般 | 简单条形码/旧项目改造 |
| OpenCV QRCodeDetector | 重 | 高 | 中上 | 弱 | 项目里已用 OpenCV 的场景 |
| Zxing-cpp | 轻 | 高 | 高 | 强 | 纯 C++ 后台、嵌入式、批量识别 |
实际选型时你别只看"谁能识别出来",要站在长期维护的角度看。我后来把识别模块封装成一个独立组件,Zxing-cpp 这种轻量、可静态编译、API 清晰的特点,对后续移植帮助非常大。
2. 编译与集成:从源码到 CMake 里能用
2.1 构建参数和一条龙命令
Zxing-cpp 用 CMake 构建,源码拉下来后编译过程非常简单。依赖条件主要是编译器支持 C++17,CMake 版本不要太老,我这边 Ubuntu 和 CentOS 环境都没遇到问题。
具体操作我总结成一套命令,直接抄就行:
git clone https://github.com/zxing-cpp/zxing-cpp.git cd zxing-cpp cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build -j$(nproc) cmake --install build这里有两个细节特别值得注意。
第一,构建类型一定记得设置成 Release。我一开始忘了加-DCMAKE_BUILD_TYPE=Release,默认的 Debug 版本在批量识别时速度慢到怀疑人生。Release 模式下编译器会做大量优化,二维码识别这种计算密集型操作,性能差距可能在三倍以上。
第二,Zxing-cpp 的 C++ API 从早期版本开始做过一次命名空间调整。老版本代码用的是小写的zxing命名空间,现在的版本统一用大写的ZXing。网上大量教程还是老 API 的写法,你照着抄编译直接报错。我建议你以仓库当前版本的include/ZXing目录和官方示例为准,不要在旧帖子里浪费时间。
安装完成后,头文件默认放到/usr/local/include/ZXing/下,动态库是libZXing.so。如果你不想让库污染系统目录,可以直接在 CMake 里用add_subdirectory把源码打进来,这样会编译一个ZXing::ZXing目标供项目链接。
2.2 装进系统还是跟随项目编译,两种集成方式
根据你的项目形态,我建议采取不同的集成方式。
第一种,工具类项目,库装进系统里就好。比如我写独立命令行工具,直接用find_package(ZXing REQUIRED)就能找到:
cmake_minimum_required(VERSION 3.15) project(qr_tool) find_package(ZXing REQUIRED) add_executable(qr_tool main.cpp) target_link_libraries(qr_tool PRIVATE ZXing::ZXing)第二种,业务服务项目,为了版本可控,我倾向于把 Zxing-cpp 源码放到项目third_party目录下,通过add_subdirectory跟随主项目一起编译。这样哪怕不同环境库版本有差异,整个服务也是确定性构建,出问题好排查。
我在实际项目里用的是第二种,原因很简单:服务端 C++ 程序往往不只一个人开发,如果依赖系统里的库,换台机器就得重装一遍环境,构建脚本就没法做到"一条命令出结果"。跟随项目编译虽然首次编译慢一点,但换环境时体验极其顺畅。
如果你是嵌入式环境,比如想把识别库交叉编译到 ARM 板子上,Zxing-cpp 的处理也很简单。只需要在 CMake 里配置对应的工具链文件,然后同样执行那套编译流程,生成静态库放到目标板上直接调。纯 C++ 实现的好处这时候就体现出来了:没有系统库绑架,交叉编译非常干净。
3. 读取图片到返回字符串:核心调用链路拆解
3.1 图像解码库这一环怎么选
这里必须强调一点:Zxing-cpp 只负责"识别二维码",它不管你有没有图片数据,更不管你图片是 PNG、JPEG 还是 BMP。库本身不带图像解码能力,所以在调用识别 API 之前,你得先自己完成"图片文件 → 像素数据"这一步。
我试过三条路径。
第一条是用 STB Image。这是一个单头文件库,把stb_image.h下载下来放到项目里就能用,没有任何外部依赖,加载 PNG/JPEG 都很方便。它的缺点是只负责解码,不提供缩放、滤波、旋转等图像处理能力。
第二条是用 OpenCV。如果你的项目里已经引用了 OpenCV,那直接用cv::imread读取图片再转成像素数据就行,后续做预处理也顺手。缺点是对于只需要识别二维码的小工具来说,引入 OpenCV 有点杀鸡用牛刀。
第三条是用 ImageMagick 的 Magick++ 接口。功能很全,但库比较重,接口风格也更厚重,没必要为了这个功能引入。
我做最终裁剪时选了 STB Image,理由就一个字:轻。识别库本身已经够轻了,图像解码再搞得乌央乌央的没意义。STB 的用法极为简单,两行代码就能把图片像素取出来:
#define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" int w = 0, h = 0, channels = 0; unsigned char* pixels = stbi_load(image_path, &w, &h, &channels, 4);最后一个参数传 4,是强制把图片统一解码为 RGBA 四通道像素,这样后续传给 Zxing-cpp 时格式固定,不用去判断每张图到底是三通道还是四通道。
3.2 构造 ImageView 与调用 ReadBarcode
拿到了像素数据,下一步就是构造ZXing::ImageView,再调用ReadBarcode识别。整个核心代码其实很短:
#include <stdio.h> #include <ZXing/DecodeHints.h> #include <ZXing/ImageView.h> #include <ZXing/ReadBarcode.h> int decode_qr_from_file(const char* image_path) { int w = 0, h = 0, channels = 0; unsigned char* pixels = stbi_load(image_path, &w, &h, &channels, 4); if (pixels == nullptr) { fprintf(stderr, "load image failed: %s\n", image_path); return -1; } using namespace ZXing; DecodeHints hints = DecodeHints() .setFormats(BarcodeFormat::QRCode) .setTryRotate(true) .setTryHarder(true); ImageView view(pixels, w, h, ImageFormat::RGBA); Barcode barcode = ReadBarcode(view, hints); if (barcode.isValid()) { printf("result: %s\n", barcode.text().c_str()); } else { printf("no qr found\n"); } stbi_image_free(pixels); return 0; }这里有几个参数值得展开讲。
ImageView是 Zxing-cpp 用来描述原始像素数据的对象。构造时要明确告诉它宽度、高度和像素格式。像素格式尤其重要,如果你把 BGR 数据说成 RGB,库在转换时会按错误的通道顺序处理,对某些图片可能影响不明显,但对低对比度或反色图片可能就是识别成功与失败的差别。
DecodeHints是配置识别行为的入口。setFormats可以用来限制只识别 QRCode,这会减少不必要的计算消耗;setTryRotate让库在图像旋转 90 度、180 度、270 度时也尝试识别;setTryHarder是更深度搜索的模式,会明显提升识别率,但耗时也会显著增加。按我的经验,平时用默认参数先随便跑一轮,失败后再单独开 TryHarder,是性价比最高的做法。
Barcode对象除了text()拿内容之外,还提供了isValid()判断是否识别成功、format()查看识别的具体格式、position()获取四角坐标等一系列接口。我后来做业务对接时还用到过bytes(),这个后面细说。
3.3 同一张图里多个二维码:换成读集合
如果你以为每张图只包含一个二维码,那就天真了。实际业务里,有的图片是用户把好几个二维码拼在一起截的图,或者是对着杂志页面拍的,页面上同时存在三四个二维码。这时候ReadBarcode只能返回第一个识别到的结果,不适合。
Zxing-cpp 提供了ReadBarcodes接口,一次调用返回所有识别到的结果集合:
std::vector<Barcode> results = ReadBarcodes(view, hints); for (const auto& barcode : results) { printf("find one: %s\n", barcode.text().c_str()); }这个功能在批量处理场景下特别实用。我后来把识别逻辑改成先调用ReadBarcodes,如果结果只有一个就正常返回;如果有多个,就按业务需求全部提取出来。
一个小提醒:ReadBarcodes在TryHarder开启时,耗时会比单码识别高不少,因为库要在大图里反复搜索多个符号。如果你明确知道图片里只可能有一个二维码,别用复数接口,用ReadBarcode就好,又快又稳。
另外一个技巧是,如果图片本来就是用cv::imread读进来的 OpenCV 的Mat,你也可以直接把它的数据给ImageView,不用特意转成 STB:
cv::Mat mat = cv::imread(image_path, cv::IMREAD_COLOR); cv::cvtColor(mat, mat, cv::COLOR_BGR2RGB); ZXing::ImageView view(mat.data, mat.cols, mat.rows, ZXing::ImageFormat::RGB);注意imread默认是 BGR 顺序,这里特意用cvtColor转成了 RGB。别小看这个转换,很多人直接拿mat.data塞进 ZXing,结果发现图片越是色彩复杂越识别不出来,问题就出在通道顺序上。
4. 踩坑记录:识别不到、慢、乱码背后的真正原因
4.1 "明明能扫出来的码,程序读不到"的完整排查链路
这是整个使用过程中最让人头疼的问题:手机一秒钟能扫出来的二维码,程序就是返回 invalid。一开始我以为 Zxing-cpp 识别能力弱,后来才意识到最好先怀疑自己。
我整理了一套完整的排查流程,按这个顺序走,能解决九成以上的"识别不到"问题。
第一步,确认图片本身没坏。用stbi_load后先检查w、h和pixels是否正常。再放手机上扫一下,确认图片里的二维码确实有效。如果手机也扫不出来,问题在图片生成端,不在识别端。
第二步,确认像素格式匹配。我当时最隐蔽的坑就在这里:某批 PNG 图片有透明通道,但我用stbi_load时传的是三通道,加载出来像素数据是紧密排列的 RGB,我却在构造ImageView时写成了ImageFormat::RGBA。库以为每个像素占四个字节,读的时候数据全错位了,结果当然识别不出来。后来强制统一用四通道加载,问题直接消失。
第三步,确认图片尺寸。如果图片是单反相机拍出来的,宽度可能到 6000 像素。这种大图上的二维码可能只占画面的一小部分,Zxing-cpp 默认参数下可能会忽略掉小目标。这时候可以先把图片长边缩放到 1000 到 1500 像素左右再识别,往往能很快命中。
第四步,尝试开启更强的识别参数。把setTryHarder(true)和setTryRotate(true)都打开。TryHarder会显著增加解码算法的时间投入,但某些复杂场景确实只有它能识别出来。
第五步,最后才考虑预处理。比如把彩色图转成灰度图、增强对比度、做二值化。注意顺序不能反,很多人一上来就预处理,结果把不是导致识别失败的原因处理了半天,耗时上去了,问题依然在。
我那次将近二十张图识别不出来,最后定位下来就是第二步的像素格式问题,跟 Zxing-cpp 本身毫无关系。所以遇到"识别不到"时,先耐心按上面顺序踩一遍,不要冲动地换库。
4.2 大图识别慢到怀疑人生:缩放阈值的人话解释
批量识别任务里除了精度问题,性能问题同样突出。我第一次拿生产环境的原图跑测试时,一张 5000 像素宽度的照片,开启 TryHarder 之后耗时可能到几秒,一千张图就是几十分钟,根本没法上线。
我的解决办法是"两阶段缩放"。
第一阶段,识别前把图片长边缩放到不超过 1200 像素。二维码本身是一种模块化图形,识别算法需要的分辨率是有限度的。一张二维码图片只要有 200 像素左右,内部的黑白模块就已经足够算法定位和读取。图片放大到几千像素,包含的只是大量冗余细节,对识别算法没有帮助,反而增加计算负担。
第二阶段是动态降级:先用关闭 TryHarder 的快速模式去识别,失败后只有对这张图单独开 TryHarder。这样大多数图片都在几十毫秒内跑完,只有个别难啃的图片才花几百毫秒。整体耗时降到了一个可以接受的水平。
这件事也侧面说明,工程上"识别不出来"和"识别太慢"经常是一对矛盾,你需要根据业务数据分布去设置合理的识别策略,而不是一把梭哈式地全开 TryHarder。
4.3 中文内容乱码:先检查 ECI,别急着怪库
二维码内容出现中文乱码是另一个高发问题。我第一次拿 Zxing-cpp 识别一个中文二维码时,输出内容是乱码,第一反应是用各种编码转换函数去转换。折腾了半天才弄明白,问题根源在二维码本身的编码声明上。
QR 码存储文字时,可以通过 ECI(Extended Channel Interpretation)来声明内容的字符编码。如果生成二维码的数据带有正确的 ECI 标记,ZXing 会按照标记把字节流转成 UTF-8。但很多二维码生成工具根本不打 ECI 标记,尤其是一些老式生成器,直接把字符串做完字节编码就塞进去。这时候 ZXing 只能按照早期 QR 规范默认的 ISO-8859-1 去解析字节,中文自然全乱。
这种场景的处理方式不是去"修复"ZXing,而是直接从二进制字节入手。ZXing 的Barcode对象提供了bytes()接口,返回的是二维码内原始字节序列。我后来遇到乱码就取bytes(),按照实际业务确认的编码方式自己处理,比如 GBK 的就自己转成 UTF-8。在代码里加一层编码兜底逻辑,比指望所有二维码生成端都规范要稳妥得多。
这里也有个反面教训:别一看到乱码就怀疑是终端问题。先用printf("bytes: %02x %02x %02x ...", ...)看看字节内容,再判断是编码转换没做,还是终端显示字体问题。一次排查就给你省下大量时间。
5. 识别率与性能的工程调优笔记
5.1 除了文本,Barcode 对象还能给你什么
很多教程只讲了text(),但实际做业务时,其余几个接口同样关键。
首先是position()。它返回二维码四个角点在原图像素坐标系中的坐标。我最初没在意这个能力,后来业务方提出"识别出二维码同时要定位它在图片哪个位置",position()直接解决了问题,不用自己去算轮廓。
其次是format()。批量识别场景里,图片里除了二维码可能还有条形码。通过format()我能精确知道结果属于哪种类型,业务侧可以分门别类处理。
然后是bytes()。这个在上面已经强调过,处理非 UTF-8 内容、或者要拿原始字节做签名校验时非常有用。我后来在数据对接时,就是用bytes()做哈希比对,保证内容未经篡改。
我把常用的接口整理一下:
| 接口 | 用途 | 使用时机 |
|---|---|---|
isValid() | 判断本次识别是否成功 | 每次调用后必查 |
text() | 获取解码后的文本内容 | 绝大多数业务场景 |
bytes() | 获取原始字节序列 | 编码不确定、哈希校验、非文本内容 |
format() | 获取识别到的码制类型 | 混合码制识别场景 |
position() | 获取四角点坐标 | 需要定位二维码在图片中的位置 |
5.2 预处理三件套:灰度、缩放、对比度
如果你已经按第 4 章的排查链路走完,图片依然识别不出来,预处理就得上场了。我个人经验是:预处理的目的不是"让图片更好看",而是"让二维码的模块边界更清晰"。
第一件事是转灰度。二维码识别算法在绝大多数情况下只需要亮度信息,彩色信息不仅是冗余,还可能干扰判断。Zxing-cpp 内部虽然会自动把输入转成亮度,但如果你自己能保证以ImageFormat::Lum格式输入单通道数据,内存占用和转换耗时都能降下来。
第二件事是缩放。这个在第 4 章已经详细说过,图片过大或过小都会影响识别率。二维码在图片中太小时,优先考虑把整图长边放大到 1000 像素左右再识别;二维码占画面明显很大时,反而可以缩小图片,把目标区域控制在合理范围内。
第三件事是增强对比度。实测下来,纸张扫描件、阳光下拍摄的照片经常会出现对比度不足的问题。简单的做法是把像素直方图做一下拉伸,让白色更白、黑色更黑。如果依赖 OpenCV,可以直接用cv::CLAHE做自适应直方图均衡,效果更稳。
要注意的是,预处理本身是有成本的,也会引入新的不确定性。我通常只在默认流程失败后进入预处理分支,不会对每张图都做全套处理。批量任务里做一个"先快速识别,失败再预处理"的双级流水线,比统一预处理再识别要高效得多。
5.3 多线程并发与编译优化:Release 和架构选项带来的惊喜
跨过识别率这道坎,下一个问题就是吞吐量。一千张图片,单线程逐张识别,哪怕每张 100 毫秒也要一百秒。我在工程上做了两处调整,效果非常明显。
第一处是并发。Zxing-cpp 的解码过程没有全局共享状态,ReadBarcode可以在多个线程里安全并行调用。我直接用std::async或者简单线程池把图片文件分配到多个线程处理,每个线程独立加载图片、独立解码、独立写结果。实测四线程并发时,总吞吐量基本接近线性提升。
第二处是编译优化。把 Zxing-cpp 的 Release 版本加上针对当前 CPU 的优化指令集,比如在 CMake 里配置-O3 -march=native,对二维码解码这种计算密集场景能带来可观收益。如果编译时发现性能不达标,先检查这两点,往往比在业务代码里抠细节更有效。
另外建议在业务代码层面做一次"输入输出分离":图片加载、解码、结果处理三个阶段用队列解耦。加载器负责读文件和像素解码,识别线程只负责跑 ZXing,结果线程负责写日志或数据库。这样每个阶段都能独立调整并发度,不会因为某个环节慢卡住整条流水线。
再分享一个小经验:我在日志里把识别结果和图片文件哈希一起打印。这样线上出问题时,能直接根据日志定位到具体是哪张图出了问题,把图片单独拎出来复现。这个习惯帮我省了大量排查时间,推荐你也试试。