97 个 OpenCV 实例(三十七):二维码识别,一张图四个码的陷阱

📋 本文概览:

学什么 |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

两个结论:

  1. aruco 全面快于 contour ,多码场景差 2.3×(27.6 vs 62.3 ms);
  2. 仅检测(detect)比检测+解码(detectAndDecode)快 2~3× ------ 说明解码的主要开销在 Reed-Solomon 纠错和模块采样 。如果只需要知道「码在哪里」(比如引导摄像头对准),用 detect 就够了,能省大半时间。

六、方法沉淀:这次学到的三件事

  1. API 语义要对上输入基数。 「单码」和「多码」是两套 API,用错不是数据问题,是调用问题。以后遇到「找不到」,先看 API 文档的「能找几个」。
  2. 同一模块的两个实现要对照着用。 contour 快照准、aruco 快且旋转稳------二维码识别优先 aruco,只有需要特定实现时才用 contour。
  3. 测试数据可以自己造,不依赖网络。 这次的测试图全是用 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 许可。

相关推荐
UIU1141 小时前
P16237 [蓝桥杯 2026 省 B] 应急布线
数据结构·c++·算法·蓝桥杯
raindayinrain1 小时前
c++设计模式
开发语言·c++·设计模式
旖旎夜光1 小时前
LeetCode 315: 算右侧小于当前元素的个数(归并排序) —— 题解
c++·学习·算法·leetcode·排序算法
D e re k ZH2 小时前
保序回归:从 PAVA 到可并堆的单调约束优化
c++·算法·数据挖掘·回归·kotlin
青少儿编程课堂2 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:字典树前缀树与单词检索统计
c++·python·算法·bfs·信息学竞赛
octopus_c2 小时前
C++ 运算符重载详解 && 日期类的实现
开发语言·c++
青少儿编程课堂3 小时前
2026蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:堆排序与优先队列解析
c++·python·算法·bfs·信息学竞赛
j7~3 小时前
【C++ 面试笔记】(篇二):类与数据抽象 & STL 容器与算法
开发语言·c++·面试·stl容器和算法·类和数据抽象
Fuyo_11193 小时前
深入解析 C++ unordered_set 与 unordered_map 的核心特性与性能实战
开发语言·c++