📋 本文概览:
学什么 |
qrcode------ OpenCV 的GraphicalCodeDetector,一个接口搞定二维码的「检测 + 解码」,支持两种检测器、四种调用模式。踩了什么坑 |我在一张图里放了 4 个二维码,用最熟悉的
detectAndDecode去读------返回空字符串 。换成QRCodeDetectorAruco也只认 1 个 。查了半天图片质量,最后发现:根本不是图的问题,是 API 用错了。单码 API 在多码图上「全军覆没」是设计,不是 bug。核心数据 |单码 vs 多码 API 在四码图上的检出对照(0 / 1 / 4);aruco 检测器比 contour 快 2.3× ;仅检测比检测+解码快 2~3×。
适合谁 |做二维码扫码、票务核验、工业追溯的工程师------尤其是第一次用 OpenCV 读二维码的人。
一、效果先行:先看它检出了什么
先摆结果。一张 500×500 的图,里面塞了 4 个二维码 ,用 detectAndDecodeMulti 跑:

四个码全部框住,四个角点都标出来了。 解码文本分别是 OPENCV-4.14、1234567890、HELLO-QR、DAY38------一字不差。
看起来很简单对吧?但我在这条路上摔了一跤,而且摔得挺典型。
二、QR 码是什么:三个定位图案决定一切
先说 QR 码的结构,因为检测算法的一切都围绕它转:

| 结构 | 作用 |
|---|---|
| 三个定位图案(finder pattern) | 左上、右上、左下的「回」字形 7×7 方块,检测全靠它 |
| 对齐图案(alignment pattern) | 右下的小回字,用来纠正拍摄形变 |
| 定时图案(timing pattern) | 定位图案间的黑白交替线,确定模块大小 |
| 数据区 + 纠错码 | 实际内容 + Reed-Solomon 纠错码 |
OpenCV 提供了两种底层检测器:
- 轮廓法
QRCodeDetector:找三个定位图案 → 拟合四边形 → 透视校正 → 采样解码; - ArUco 法
QRCodeDetectorAruco:把 QR 当作 ArUco 方形标记,复用成熟的亚像素角点细化。
两者通过 GraphicalCodeDetector 这个统一接口暴露,可以互换使用。
三、坑:一张图四个码,为什么只读出来 0 个?
这是本文的核心。我手上有三张测试图,其中 qr_multi.png 里有 4 个码。最自然的写法是:
cpp
String info = qr.detectAndDecode(img, corners); // 「检测并解码」,一步到位
结果:contour 检测器返回空字符串 ,corners 也是空的。
我第一反应是「图是不是有问题?」------放大了看,四个码清清楚楚,一点没坏。
换了 aruco 检测器,还是只读出来一个 (DAY38)。
于是我把四种模式全跑了一遍,画了张对照表:

| 检测器 | detectAndDecode(单码) |
detectAndDecodeMulti(多码) |
|---|---|---|
| contour | ❌ 0 码 | ✅ 4 码全中 |
| aruco | ⚠️ 只认 1 个 | ✅ 4 码全中 |
真相大白:
detect/detectAndDecode的契约就是「找到一个问题码」 。contour 内部假定全图只有一个定位图案组,多个码时四边形拟合失败就返回空;aruco 则返回它找到的第一个 码。这是 API 语义,不是图片问题。
多码场景必须显式用 detectAndDecodeMulti。
这个坑的教训很值钱:遇到「找不到目标」,先检查 API 语义是否匹配输入的基数(1 个 vs N 个),而不是先怀疑数据质量。
四、意外收获:contour 在旋转图上会漏检
跑 qr_rotated.png(把码转了 90°)时,又发现一个反常:
| 检测器 | detectAndDecode(单码) |
detectAndDecodeMulti(多码) |
|---|---|---|
| contour | ✅ 正常读出 | ❌ 漏检 |
| aruco | ✅ 正常读出 | ✅ 正常读出 |
同一个 contour 检测器,单码模式能读,多码模式反而漏了。 这说明这两层 API 走的是不同的代码路径 ------*Multi 对轮廓筛选更严格,旋转后定位图案的形态变了就过不了筛选。
aruco 检测器两种模式都稳。这就是「把 QR 当 ArUco 标记」的好处:ArUco 的角点检测天生对旋转鲁棒。
五、速度实测:aruco 全面快于 contour
我顺便做了速度基准(单码 420×420,Release 构建):

| 检测器 | detect(仅检测) |
detectAndDecode |
detectAndDecodeMulti |
|---|---|---|---|
| contour | 34.2 ms | 75.4 ms | 62.3 ms |
| aruco | 17.9 ms | 59.1 ms | 27.6 ms |
两个结论:
- aruco 全面快于 contour ,多码场景差 2.3×(27.6 vs 62.3 ms);
- 仅检测(
detect)比检测+解码(detectAndDecode)快 2~3× ------ 说明解码的主要开销在 Reed-Solomon 纠错和模块采样 。如果只需要知道「码在哪里」(比如引导摄像头对准),用detect就够了,能省大半时间。
六、方法沉淀:这次学到的三件事
- API 语义要对上输入基数。 「单码」和「多码」是两套 API,用错不是数据问题,是调用问题。以后遇到「找不到」,先看 API 文档的「能找几个」。
- 同一模块的两个实现要对照着用。 contour 快照准、aruco 快且旋转稳------二维码识别优先 aruco,只有需要特定实现时才用 contour。
- 测试数据可以自己造,不依赖网络。 这次的测试图全是用 OpenCV 自带的
QRCodeEncoder现生成的,断网也能复现。
七、其他值得记的细节
① QRCodeEncoder 产出的是单通道灰度图,必须转色:
cpp
Ptr<QRCodeEncoder> enc = QRCodeEncoder::create();
Mat qr; // ★ 单通道 CV_8UC1
enc->encode("https://opencv.org", qr);
Mat canvas(420, 420, CV_8UC3, Scalar::all(255)); // 画布是三通道
cvtColor(qr, qrBGR, COLOR_GRAY2BGR); // ★ 先转成 BGR 再贴
qrBGR.copyTo(canvas(Rect(x, y, qr.cols, qr.rows)));
② 多码结果的 corners 是「扁平化」的,不是 vector< vector<Point> >:
cpp
vector<Point> corners; // ★ 长度 = 4 × 码数,不是嵌套结构
vector<String> infos;
qr.detectAndDecodeMulti(input, infos, corners);
for (size_t i = 0; i + 4 <= corners.size(); i += 4) { // 自己按 4 个一组切
vector<Point> c(corners.begin() + i, corners.begin() + i + 4);
// c 就是第 i/4 个码的四角
}
这是本示例最容易踩的坑------官网 API 里 corners 类型是 OutputArray,实际是扁平的 vector<Point>。
③ imwrite 失败不抛异常,返回 false。 我第一次生成测试图时路径写错成 data/data/,图片全丢却毫无提示。加上 if (!imwrite(...)) printf("写失败") 才定位到。
八、AI 与 LLM Wiki:这一课沉淀了什么
- 把「单码 API 在多码图上失败是设计」写进了验收清单 ------ 以后任何「找不到码」的问题,第一反应是检查 API 语义是否匹配输入基数,而不是怀疑图片质量;
- 建立了
GraphicalCodeDetector选型表 :二维码识别优先 aruco(快 + 旋转稳),只有需要特定实现时才用 contour; - 测试图自生成 (
QRCodeEncoder):以后所有条码/二维码类实验都可离线复现,不再受网络图片源限制 ------ 这条经验已写进工程 README 的踩坑节; - 把「同一模块多实现要对照着用」升级成方法论 :这次 contour vs aruco 的对照,和 Day37 的 Default vs Daimler、Day34 的 Haar vs LBP 是同一种模式 ------OpenCV 很多模块都提供了多个内置实现,默认的那个往往不是最优的。
运行环境:Ubuntu 24.04 / OpenCV 4.14.0(源码编译)/ g++ 13.3.0
官方源码 :samples/cpp/qrcode.cpp(原样复制,零改动),代码引自 OpenCV 官方样例,遵循 Apache-2.0 许可。