核心课程 / 视觉入门课 / EXP-03
EXP-03 · 第 1 阶段 · 传统视觉 · 验证性 · 4 学时

二维码:自己生成一张,再让相机读回来

二维码是"机器写给机器看"的图案。本实验先让你用几行代码生成一张二维码,再让相机把它读回来 —— 一写一读,闭环。中间你会看到二维码的三个"回"字角标是怎么让机器一眼定位的,以及为什么同一个内容换个纠错等级,码的密度和版本就完全变了。

难度 ⭐ · 4 学时 qrcode 库生成OpenCV 检测 纠错等级
01 颜色02 轮廓03 二维码04 检测05 分类06 分割07 跟踪08 人脸09 骨骼10 3D11 体感12 手势

01目标:生成二维码再读回,文字一字不差

🎯 实验目标 · 学完你将能
  • 说出二维码三大件(定位角标 / 数据格子 / 纠错码)各干什么活
  • 用 --generate 生成自己的二维码,再用 --detect 把内容原样读回来(写入→读出闭环)
  • 实时模式下对着屏幕扫码,解释三个角标如何帮机器定位置和角度

说清这页的起点和终点 —— 整页的八节都在回答"怎么从左边走到右边":

你手里有什么
一张图 / 一帧相机画面

来源随意:自己生成的 PNG、打印出来的纸、手机屏幕上的码;斜的、暗的、被挡一角都算

→
你要拿到什么
[QR] https://example.com

终端上一行字节级精确的文本 + 一张画了绿框和文字的图。本页全部内容,都是为了稳定地吐出这行字

本实验的目标:自己生成一张二维码 → 打印或用手机显示 → 相机对着它,读出里面的文字。

02问题:一堆黑白格子,机器怎么读出文字

"嘀"一声就出字,可这活从头到尾是机器在干 —— 人只负责把码对准镜头。机器人更苛刻:它拿到的只是一帧像素画面,码可能歪着、被光打白、还被夹爪挡住一角。一堆黑白格子,机器凭什么把文字读出来?

扫码的两面:左边平时应用——手机对准二维码读出文字只看结果,右边打开黑盒——摄像头拿到的是歪斜的像素画面并带着问号
图 2-1|平时扫码只看到"对准、出字";打开黑盒,机器得从像素画面里先找到码、再摆正、再切格子 —— 每一步都得自己算
现实的问题平时你感受到的机器实际要做的
码到底在哪?"框一亮",好像天生就知道码在哪? —— 画面里是 640×480 个像素,先得在噪声、反光里把"回"字角标找出来
拍歪了怎么办?看到的画面永远是正的 —— 程序悄悄摆好了? —— 斜着拍格子会变梯形,得靠三个角标把透视拉回正方形
挡住一角还能读吗?挪个角度重新扫挪不了 —— 得靠纠错码从剩下的格子把内容推回来
最后读出来的到底是什么?屏幕上显示一行字一行文字字符串(终端打印),不是模糊位置 —— 字节级精确,直接拿去当库位号、工序号

把标题拆开 —— 「一堆黑白格子,机器怎么读出文字」,其实是三问:

  • ① 一堆黑白格子 —— 凭什么是码,码在哪?画面里几百×几百个像素,凭什么说中间这块是二维码、不是别的黑白花纹?(对应上表第 1 行)
  • ② 怎么读 —— 歪了、暗了、挡住了还读得出吗?斜着拍、屏幕反光、灯光偏暗、被夹爪挡住一角 —— 哪些情况还能救,哪些直接读不出?(对应上表第 2、3 行)
  • ③ 文字 —— 格子怎么变回一行字?错了怎么办?解出来的是一字不差的字符串,还是可能出错的猜测?出错了靠什么兜底?(对应上表第 4 行)

这三问不是凭空来的 —— 二维码是今天机器人最便宜、最可靠的"身份标签",到处都在用,所以"怎么读"必须搞得懂:

  • 贴在货架上 → 机器人一扫就知道"这是 3 号库位的零件 A"
  • 贴在工位上 → 相机一扫就知道"现在该执行哪道工序"
  • 贴在门上 → 巡检机器人一扫就知道"我到 2 楼了"

成本?一张纸。可靠性?被遮住一部分还能读出来。

把刚才那三问落到工程上,就是下面四个子问题 —— 注意它们"有先后":顺序不能换,每一步都卡着下一步。

#要解的子问题为什么必须先解它
1找到三个"回"字角标先得知道码在哪、歪了多少。找不到位置,后面全是空谈
2把歪掉的画面扶正斜着拍时格子是梯形,不扶正就没法按格读 —— 连"一格多大"都说不准
3把每格的黑白读成数据扶正之后才谈得上"这格是 0 还是 1",再按规则拼成字节、拼回文字
4处理脏了、缺角了的情况现场不会永远干净:贴纸、反光、手指。得有几个校验点(沿数据曲线多算出来的点),把读错的格子反推回来

四步一路解下来,才拿到最开始要的那行 [QR] https://example.com。这就是本节给你的结论:不是"机器很神奇",而是四个具体问题被逐个解决。

03现象:写一句话,30 秒读回一字不差

🎬 现象速览 · 30 秒先看效果

你会看到:自己写一句话 → 程序画出一张二维码 → 再让程序把它读回来,打印出的文字和你写进去的一字不差。全程不用相机,30 秒闭环。

程序生成的 my_qr.png:一张 165x165 的二维码,内容是 https://example.com
① my_qr.png —— 写进去的码
读回结果 out.png:绿框套住二维码,框上方是解析出的文字 https://example.com
② out.png —— 读回来的一行字
图 3-1|下面两条命令的全部产物:左边是你写进去的码,右边是程序读回来的结果 —— 框上方那行 https://example.com 与写进去的一字不差

三步跑出这个画面(先照做,每步为什么下一节拆讲):

① 进目录,生成你自己的码。把引号里的文字换成你自己的(学号、名字都行):

cd ~/ros2_ws/src/xflz_eai_car1/vision/04_qrcode
python src/app.py --generate "https://example.com" --save my_qr.png

② 喂给检测器读回来。绿框套住整张码、终端打印出 [QR] https://example.com 就算成功:

python src/app.py --detect --image my_qr.png --save out.png

③ 换你自己的字符串再来一遍。读回必须一字不差 —— 写进去什么、读出来就是什么,这就是闭环。

🔍 看什么:打开 out.png,绿框是不是套住整张码?码上三个「回」字角标在哪 —— 记住它们的位置,下一节讲机器怎么靠这三个角把歪着的码扶正。

04原理:二维码是怎么被认出来的

CORE IDEA · 一句话原理

一张黑白格子图变成一行字,中间只有三件事:找到并摆正(靠三个角标)、把每格读成 0 或 1、按编码规则拼回文字(脏了还有校验点兜底)。

打个比方:像把一张剪碎又打乱的纸条拼回来 —— 先认出哪几片带边框(知道纸在哪、有没有拿歪),再把碎片对齐(摆正),最后照着字母表一片片读成句子(解码)。

这一节就按 02 节拆出来的四个子问题走 ——四个子问题 → 四个步骤,一步解一层,走完就答完了:

步骤对应子问题这一步解决什么你会得到
步骤 1子问题 1
找到角标
机器凭什么知道"这是个码、它在哪"看懂一张码的四块"记号",以及三个"回"字的秘密
步骤 2子问题 2
扶正
斜着拍的梯形,怎么变回能按格读的正方形(顺带看完整条链)六格真图:一张画面到一行字的全过程
步骤 3子问题 3
读黑白
每格的黑白,怎么变成 0/1、字节和文字两张图解:一格 = 一位、八格 = 一字节;还有掩码为什么必须打散
步骤 4子问题 4
纠错
脏了、缺角了,靠什么把内容救回来一张图解:多算的校验点怎么把被遮的格子反推回来(校验点自己坏了也能解)

1机器凭什么先找到这张码?

它不认识"二维码",只认两件事:一种黑白比例、三个"回"字的摆法。一张码拆开是这四块:

一张真实二维码的解剖图(v2-M,25×25 格)① 定位角标 ×3“回”字形,三个角各一个 → 找到码、定方向② 校正图形 Alignment纸面弯了、镜头畸变时校准格子位置(版本 ≥2 才有)③ 时序线 ×2一横一竖,第 7 行 + 第 7 列,黑白交替 → 当尺子量格子多大④ 数据区 + 纠错码淡绿底的每一格都是它 —— 散落全码、被切碎,不是中间一块中间放 logo,盖的就是这些格 → 所以要用高纠错等级兜底外面那圈白边叫“静区”,不能裁掉 —— 没它机器容易找不到边界
图 4-1|这张图不是画的 —— 它就是 --generate 出来的真实 v2-M 矩阵,逐格渲染后加框标注。①②③ 是辅助图形;④ 的淡绿底才是数据 + 纠错格 —— 它们散落在整张码里、被切碎,所以中间也能放 logo:盖的就是这些格

还有两块不显眼但缺一不可:格式信息(第 8 行与第 8 列交叉处那圈黑白点)记着纠错等级和掩码编号;版本信息只有版本 7 以上才有 —— 本页这只码是版本 2,所以没有。

① 凭什么"认得出来" —— 一条线扫过去,黑白正好是 1 : 1 : 3 : 1 : 1

扫描器只做一件事:横着扫过去,量黑白有多长扫这条线113111 : 1 : 3 : 1 : 1数据区再扫一次2 : 1 : 3 : 1 …比例对不上 → 不是角标数据区偶尔也会撞上这个比例 —— 所以扫描器要求三个这样的位置同时出现(下一张图),掩码也是为了压低撞脸概率
图 4-2|扫描器逐行扫过画面,量连续五段黑白的长度。角标给的是 1:1:3:1:1,数据区给的是乱的比例 —— 这就是"为什么偏偏是回字":它的横截面比例极难被别的东西凑出来。换尺寸、换角度、换光照,比例都不变,所以斜拍、远近都扛得住

② 凭什么知道"哪边是上" —— 三个角标摆成 L,缺的那角在右下

三个“回”字占住三个角,缺的那一角在哪儿,就知道码转了多少度0°✓ 正的:缺角在右下90°缺角在左下 → 转 90°180°缺角在左上 → 转 180°270°缺角在右上 → 转 270°定位的产物是两样:码在画面里的位置(四个角点)+ 它转了多少度。把度数转回去,图就正了。
图 4-3|同一个码转四个角度,三个"回"字就落在不同角落。缺的那角在右下 = 正的;在左下 = 转了 90°;在左上 = 180°;在右上 = 270°。所以"定位"的产物不只是坐标,还有方向

正因为缺的那一角本身就带着方向信息,第四个"回"字绝不能出现 —— 哪怕是数据区凑巧拼出来的。生成时把数据打散(步骤 3 的掩码),压的就是这个概率。

2画面歪了,怎么扶正?

先说清"扶正"在链条里的位置。下面六格是真机拍到的全过程,顺序就是识别的顺序 —— 你要解的"扶正",就是第 ③④ 格;前后几格是它的上下文,顺便一起看:

识别六环节:640×480 的倾斜画面、缩到 0.5、detect 得到四个角点、透视摆正、二值化后逐格采样成 25×25、解码并回画原文
图 4-4|倾斜的相机画面(①)→ 缩到一半(②)→ detect() 只给四个角点(③)→ 按角点透视摆正(④)→ 二值化后逐格采样成 25×25 = 625 个 0/1(⑤)→ 纠错解码并把原文画回画面(⑥)

只看中间那列,六句话看完:

#画面变成什么为什么非它不可 / 少了会怎样
1相机给出一帧 640×480 彩色画面一切从这里开始;画面越糊,后面每一步都更难
2缩成 320×240,码还在,只是格子少了一半像素计算量降到 1/4;缩太小 → 远处的小码直接糊掉
3画面里画出四个角点(还不知道内容)先定位才能谈内容;斜太狠、角标被挡 → 一个点都给不出
4梯形被拉成正方形,格子大小一致了不摆正,格子间距不均,后面没法按格切;拍歪了也读得出,全靠这一步
5变成 625 个 0/1(25×25,每格只留黑或白)把"图像问题"变成"数字问题";反光、阴影会让个别格子判反
6画面被画上绿框 + 那行文字这是你要的产物;此时解码已经完成

回到这一步要解的"扶正":第 ③ 格给出四个角点,第 ④ 格按这四个点做透视 —— 梯形就变回了正方形。 这一步不改变码的内容,只改变"形状";但它决定了后面能不能按格均匀采样:格子间距不齐,"一格多大"就无从算起。

这 6 步在代码里由一个函数全包了 —— 具体是谁、参数在哪改,05 节的 图 5-1 流程图和它下面那张环节表里有。

3一格格黑白,怎么变成文字?

位置有了、画面也正了,真正的难题才开始:这一格黑、那一格白,到底代表什么?它们代表 0 和 1。但只有 0/1 还不够,还得知道怎么打包成字。下面两张图就是答案。

问题一:黑白凭什么变成 0 和 1?

① 一个格子 = 一位(1 bit) 白 = 0 黑 = 1 ② 连着读八个格子 = 一个字节 01101000 → 01101000 二进制 → 0x68 十六进制 → h 字母
图 4-5|一个格子就是一位(bit):白=0、黑=1。连着读八个格子就是一个字节,比如 01101000 就是 0x68,也就是字母 h。整串网址,就是十九个这样的字节排下来

问题二:这些格子看起来毫无规律,为什么?因为写进去之前,数据被刻意打散过 —— 这层"格子布"叫掩码:

③ 掩码:给数据盖一层"格子布",逐格做异或 数据原样(示意)⊕掩码图样(这只码的真图样)=盖上去之后 → 均匀了 黑⊕黑=白、白⊕黑=黑:同色相消、异色成黑。生生把连成一片的黑块,打成均匀的麻点。
图 4-6|掩码做的事:把数据图案和一张"格子布"逐格做异或。不打散会怎样:数据容易连成大片同色,甚至不小心拼出一个像模像样的"回"字,把扫描器骗过去(步骤 1 说过,只有三个角标才算数)。解码时按同一张布再盖一次,数据就回来了 —— 这一步叫「解掩码」,是解码的第一件事

4脏了、缺角了,怎么救回来?

上面读出来的是"干净的理想情况"。可现场从来不是:贴纸盖住一块、纸角撕了、手指压住半边。为什么有的还能读、有的直接废?因为码里的数据不是随便填的 —— 它和"多算的那几个点"一起,落在同一条曲线上:

纠错的真相:数据是曲线上的点,校验点也是 —— 所有点地位平等① 数据 = 曲线上的几个点3 个点就定死了这条曲线② 沿曲线多算 4 个点存进去(校验点)实心 = 数据点 空心 = 校验点③ 涂坏两个点:一个数据点 + 一个校验点两个都离开了曲线 —— 谁坏都一样④ 剩下的点重画曲线 → 两个都回来校验点不需要"内容",它只是曲线上的一个采样
图 4-7|数据不是随便填的格子,而是一条曲线上的几个点;"校验点"也是 —— 它只是沿同一条曲线多算出来的采样,地位和数据点完全平等。第 ③ 格故意涂坏两个:一个数据点、一个校验点,两个都"离开了曲线";第 ④ 格里,机器用剩下的点把曲线重画回来,两个都补上。这套数学和当年深空探测器传回照片用的是同一套

那到底能救多少?因为补回一个错,要花掉两个校验点 —— 这是它的预算:

补回一个错,要花掉两个校验点 —— 这是它的“预算”① 不知道错在哪(QR 的常态)✓3 个校验点 → 只能救 1 个错② 校验点多一点✓✓✓6 个校验点 → 能救 3 个错③ 如果已经知道坏在哪(擦除解码)✓✓✓同样 3 个 → 也能救 3 个为什么是 ÷2?一个错点有 2 个未知:① 它在曲线的哪儿(位置)② 它偏了多少(值)一个校验点只带 1 条信息→ 所以两个换一个知道位置时省掉第 ① 条 → 一个换一个所以 QR 的等级是这么来的校验点越多 → 能救的错越多但数据格就得让位(码更密)能救 ≈ 校验点 ÷ 27 / 15 / 25 / 30% 是粗略平均解码时不知道坏在哪,按 ① 算→ 所以 H 级是 30%,不是 60%
图 4-8|补回一个错点,要花掉两个校验点:一个用来指认它在曲线的哪儿,一个用来算出它偏了多少 —— 所以能救的错 ≈ 校验点 ÷ 2。如果已经知道坏在哪(整块撕掉那种),位置就不用猜了,同样的校验点能救两倍,这条路叫擦除解码。QR 平时拿不到"坏在哪",所以按第一行算 —— 这也是 H 级只有 30% 而不是 60% 的原因

校验点多留几个,是可以选的 —— 这就是"纠错等级":

等级能救回格子密度什么时候用
L(低)约 7%最疏图很干净、要塞很多字
M(中)约 15%中等默认值,绝大多数场景够用
Q(较高)约 25%较密可能被弄脏、贴在户外
H(高)约 30%最密要在中间放 logo

代价看第二列和第三列:校验越多,格子越密 —— 同样的内容会被顶到更大的版本,打印和拍摄的要求也跟着变高。06 节的实验台可以拖着看这个取舍。

🔁 交账:四个子问题,逐条对上
1 找到角标 —— 三个"回"字 + 1:1:3:1:1 的比例,告诉机器码在哪、歪了多少;
2 扶正 —— 四个角点做透视,把梯形拉回能按格读的正方形;
3 读黑白 —— 每格一位、八位一字节、掩码还原,最后拼成文字;
4 纠错 —— 多算的校验点把被遮的格子反推回来;坏得太多、点不够用才真的读不出。
四条都解完,才有 03 节那行 [QR] …。现在再看那张歪着的码,你应该能一步步说出它经历了什么。
补看:这张码当初是怎么造出来的?选读 2 分钟

造和读是一对逆操作:读是"格子 → 文字",造是"文字 → 格子"。看懂造,读会更顺 —— 因为你会知道那些"奇怪的格子"当初是怎么摆进去的。

生成三环节:一段文字 https://example.com 经 add_data 编码成 19 字节,make 排成 25×25 矩阵,make_image 每格渲染 5 像素并加 4 格静区,得到 165×165 的 PNG
图 4-9|文字(①)→ 排成 25×25 矩阵(②)→ 每格 5px、四周 4 格白边,得到 165×165 的 PNG(③)。165 = (25 + 4×2) × 5 —— 三个参数各占一个因子

三步就完了:文字打包成字节(编码)→ 排成 25×25 矩阵(含角标、时序线、校验位、掩码)→ 每格染成 5 像素、四周留 4 格白边,得到 165×165 的 PNG。中间那条公式值得记:165 = (25 + 4×2) × 5。

先认识 4 个词

版本 version

二维码的大小规格,从 1 到 40。数字越大格子越多、能存越多字。设 fit=True 会让它自动选够用的最小版本。

纠错等级

L/M/Q/H,代表"能修复多少破损"。越高级别码越密,但越抗造。

box_size

每个小格子画多少像素。设 5 就是每格 5×5 像素 —— 太小打印出来扫不出来。

border

外面那圈白边(静区)有几格宽。标准最小是 4,别改成 0。

它吃什么、吐什么

生成模式 · INPUT
一段文字

任意字符串,比如 "https://example.com"。中文也可以,编码和清晰度才是限制。

→
生成模式 · OUTPUT
一张二维码图片

黑白 PNG,可以直接打印,或用手机显示给相机看。

识别模式 · INPUT
相机画面 / 图片

画面上有一个或多个二维码,可以有倾斜、可以有缺损。

→
识别模式 · OUTPUT
绿框 + 解析出的文字

终端打印内容,画面上框住二维码并标出文字(太长会截断显示)。

# 配置里的生成参数
generate:
  version: 1              # 1-40,fit=True 时自动扩版
  error_correction: M     # L/M/Q/H — 本板实测别用 H
  box_size: 5             # 每格 5 像素
  border: 4               # 白边 4 格

detect:
  scale: 0.5              # 检测前先缩一半,提速

05实操:三条命令:生成 / 图片读回 / 实时扫

这一节把原理那四步落到具体函数上。先看全貌:两条线、每一步在哪个文件、吃哪个参数、对应原理的第几步 —— 下面再挨个拆。

一条命令跑过谁:环节 → 函数 → 文件 → 参数识别线:--detect --image my_qr.png① 读进来cv2.imreadapp.py :75路径 → BGR 图② 缩一半cv2.resizeqr.py :54detect.scale=0.5③ 一条龙detectAndDecodeqr.py :60黑盒,见下图原理 1~4④ 回算+多码×factor / 抹白qr.py :66-78MAX_DETECT_PASSES=8⑤ 画框标字cv2.rectangleqr.py :86-89putText 40 字符截断生成线:--generate "一段文字" --save my_qr.png (就是原理第 3 步的反向操作)① 建对象qrcode.QRCode(...)qr.py :35version / ec / box_size / border② 塞数据add_dataqr.py :41字符串 → 字节③ 排矩阵make(fit=True)qr.py :42自动选最小版本④ 染像素make_imageqr.py :43fill/back + cvtColor你自己写的只有 3 个函数generate_qr / detect_qr / draw_qr全在 src/qr.py,加起来不到 90 行原理那四步,全在 ③ 这一个函数里OpenCV 把“找角标→扶正→采样→纠错”封成了一次调用,我们只能在外面调 scale,进不去(下一张图拆开看)
图 5-1|识别线(绿)和生成线(紫)各五步/四步;每格从上到下是:环节名、函数、所在文件与行号、吃哪个参数。右侧青色小字是它对应原理的哪一步 —— 只有 ③ 那格包住了整整四步

先把 ③ 那格拆开看一眼 —— 它是整条识别线里唯一"看不见里面"的函数,而原理讲的四步,正好全在它里面:

detectAndDecode 里面:原理那四步,一步没少cv2.QRCodeDetector().detectAndDecode(work) ← OpenCV 自带,我们只写了这一行① 找三个角标1:1:3:1:1 扫描QRDetect内部类 · 无公开 API② 透视扶正梯形 → 正方形getPerspectiveTransform+ warpPerspective公开函数 · 可自己调③ 二值化采样每格判黑 / 白QRDecode内部类 · 无公开 API④ 纠错解码解掩码 + RS 纠错QRDecode内部类 · 无公开 API外面·进之前 cv2.resize你唯一改的识别参数 detect.scale0.5 = 缩一半 → 快 4 倍外面·出来之后 ×factor角点在缩小图上量的,乘回 1/scale不乘回去,框就画错位置反方向 draw_qrrectangle 画框 + putText 写字超 40 字符截成 37 + ...源码里那两个类名(QRDetect / QRDecode)你能在这份 OpenCV 的二进制里搜到,但没有 Python 接口;想拆开只能退一步用公开的 detect() + decode(img, points),或者自己 getPerspectiveTransform 再 warpPerspective。
图 5-2|虚线框里是 detectAndDecode 一次调用的内部:找角标、透视扶正、二值化采样、纠错解码,一步没少。注意每格最下面那行灰字 —— 只有 ② 里的 getPerspectiveTransform / warpPerspective 是公开函数,①③④ 全在 OpenCV 的内部类里,Python 这边调不到

看清楚了就动手:下面三条命令依次跑一遍,跑完再回来对参数。

  1. 生成一个二维码约 1 分钟

    先站到源码目录,再让程序把 --generate 后面的文字画成码。文字换成你自己的也行(学号、名字都能写,中文也可以):

    cd ~/ros2_ws/src/xflz_eai_car1/vision/04_qrcode
    python src/app.py --generate "https://example.com" --save my_qr.png
    QR 已保存: /home/iflytek/ros2_ws/src/xflz_eai_car1/vision/04_qrcode/my_qr.png

    这行命令背后发生了什么:src/app.py 先 load_config()(读 config/qrcode_config.yaml),再把文字交给 src/qr.py 的 generate_qr(),拿回一张 numpy 图,最后 cv2.imwrite 存盘。三个环节全在 04 节的图 4-9 里,你现在把它跑了一遍。

    参数都在 yaml 的 generate: 下面:version(起点版本)、error_correction(纠错等级)、box_size(每格几像素)、border(静区几格)。不要去改源码里的默认值,改配置就够了 —— 源码只是把配置读进来用。

    ⚠️ 打印出来的这一行是绝对路径:程序把 --save 的值 resolve 成了完整路径。图片默认落在你当前所在目录(所以必须先 cd 进去,否则会散落在别处)。

  2. 解析一个二维码约 1 分钟

    不用相机,直接把刚生成的图喂给检测器 —— 这是最快的自检,也正好对应 04 节图 4-4 的六步:

    python src/app.py --detect --image my_qr.png --save out.png
    [QR] https://example.com saved -> out.png

    两行回执分工明确:第一行 [QR] 后面是解出来的原文(src/app.py 里 print(f"[QR] {r.text}")),第二行是保存标注图的回执(saved -> {args.save})。读回的文字和写进去的一模一样,闭环就成立了。

    这一步只有一个可调参数:detect.scale(默认 0.5)。它决定"检测前把画面缩小多少倍",也就是图 4-4 的第 2 步。另外两个写死在 src/qr.py 里的数值值得记住:MAX_DETECT_PASSES = 8(一张图最多找 8 个码)和标注文字 40 字符的截断(超过只显示前 37 个加 ...)。

    打开 out.png 看一眼:绿框套在码上、文字画在框上方 —— 这就是图 4-4 ⑥。

  3. 接上相机实时扫约 3 分钟

    两句 source 必须都执行,缺一句 ros2 就找不到包:

    终端一 · 起相机驱动

    source /opt/ros/humble/setup.bash && source ~/ros2_ws/install/setup.bash
    ros2 launch ascamera hp60c.launch.py

    日志里出现 publish color(rgb) info 就说明相机出来了。

    把 my_qr.png 在电脑或手机上打开,举到相机前:

    终端二 · 开识别

    cd ~/ros2_ws/src/xflz_eai_car1/vision/04_qrcode
    python src/app.py --detect --ros
    Streams index: http://<板子IP>:8080 Available streams: http://<板子IP>:8080/qrcode.mjpg -------------------------------- Press Ctrl+C to stop the server [QR] https://example.com 30帧平均帧率: 33.4

    判据:先出现 Streams index: 那几行(它把流地址直接打给你了),然后每检测到一次码就打印一行 [QR],每 30 帧打一次帧率(这块板子 640×480、scale: 0.5 实测约 31~33 fps)。浏览器打开输出里那个 …/qrcode.mjpg 地址,就是实时画面,绿框和文字都在上面。

    实时模式多了两个模块:取帧换成 src/ros2_source.py 的 Ros2Camera.read()(从 ROS 话题拿图),输出多了 src/mjpeg_server.py 的 MjpegServer.set_frame()(把画好的帧推成网页视频流)。识别那段代码一行没变 —— 还是 detect_qr 和 draw_qr。

    ⚠️ 板上别用 --camera 0。这块板的相机是 Orbbec HP60C 深度相机,不走 V4L2 索引,实测会报 can't open camera by index 后退出。要用相机就走 --ros。

    试试这几个动作,观察识别率怎么变:正对 → 斜着、近 → 远、把码挡掉一个角。第三个特别有意思 —— 挡一个角它还能读出来,这就是纠错码在工作。

这个实验是整门课里唯一能"一写一读"闭环的 —— 你亲手造了一个机器可读的标签,又亲手把它读回来。这个感觉很值得体验一次。
🔁 现象呼应这三步跑完出现的画面,就是第 03 节「🎬 现象速览」里的那个:终端读回 https://example.com、绿框套住整张码。两处对上了,说明代码框架、流程、步骤你已完整走通。

逐环节对参数:每个函数在干什么、参数怎么影响它

图 5-2 已经把那个"一条龙"剪开了。下面把 图 5-1 的五个环节逐个对上函数与参数 —— 哪些能调、调坏了画面会怎样。

把 图 5-1 逐步对上函数与参数 —— 只看第 3 列就够了,前两列是查名字用的:

补一句实话:OpenCV 其实还给检测器留了三个旋钮 —— setEpsX / setEpsY(角点容差)、setUseAlignmentMarkers(要不要用校正图形),本项目一个都没动。真遇到"特别花、特别小的码"时,它们才是下一批该试的参数。

线环节 · 函数参数:动它会怎样对应原理
识
别
① 读图
cv2.imread
没有参数。读失败返回 None,程序打印 cannot read image 后退出 —— 路径错、图不存在都卡在这—
② 缩放
cv2.resize
detect.scale=0.5:缩一半,计算量降到 1/4。调小 → 远处小码直接糊掉;调大到 1.0 → 更稳但慢 4 倍决定"还能不能找到角标"(步骤 1 的前置)
③ 一条龙
detectAndDecode
没有参数可调 —— 内部四步全封死(见 图 5-2)。这是本项目最关键的一行,也是唯一"看不见里面"的地方步骤 1~4 全在这
④ 回算 + 多码
×factor / work[…]=255
MAX_DETECT_PASSES=8:一张图最多找 8 个码(防死循环)。角点是在缩小图上量的,必须乘回 1/scale,否则框画在错位置—
⑤ 画框标字
cv2.rectangle / putText
框线宽 2、字高 0.6、绿色 (0,255,0);文字超 40 字符截成前 37 + ...(所以长网址末尾会看到省略号)—
生
成
① 建对象
qrcode.QRCode(...)
四个参数一次定死:version(起点版本)、error_correction(L/M/Q/H)、box_size(每格像素)、border(静区格数)决定"能扛多少遮挡"(步骤 4 的预算)
② 塞数据
add_data
中文按 UTF-8 算字节:一个字 3 字节,同样字数会占更多格子步骤 3 的反向
③ 排矩阵
make(fit=True)
fit=True:装不下就自动升版,直到够用 —— 所以 yaml 里写 version: 1 只是起点步骤 1 的角标在这里被排进去
④ 染像素
make_image(...)
fill_color/back_color 定颜色;产出 (25 + 4×2) × 5 = 165 px。前景必须明显深于背景步骤 3 的物理落地

一句话记住两条线的差别:生成线是"把点摆到曲线上"—— 摆几个点、摆多密、留几格余量,你都能定(box_size、border、error_correction);识别线是"从曲线上把点读回来"—— 除了 scale,其余全在 OpenCV 里面。

配置参数一览:改哪个、动它会怎样

全部在 config/qrcode_config.yaml;改完重新跑命令就生效,不用改源码。想试不同参数又不想动原文件,可以复制一份用 --config 你的.yaml 指过去。

参数默认管什么动了会怎样
generate.version1起始版本(1~40)只是起点:装不下会自动升版。手动调大 → 码立刻变密,但能装更多字
generate.error_correctionM纠错等级 L/M/Q/H升到 H → 抗遮挡变强,但版本会被顶大一档、格子更密(见 04 节四张对比码)
generate.box_size5每格几像素5→8:165px 变 264px,打印更清楚;调到 3 以下 → 打印出来扫不出
generate.border4四周静区几格别改成 0:静区没了,机器容易找不到边界(标准最小 4)
generate.fill_color / back_color黑 / 白前景 / 背景 RGB前景必须明显深于背景;反色(浅字深底)很多扫码器读不出
detect.scale0.5检测前缩放倍数0.5 → 计算量 1/4;改 1.0 更稳但慢 4 倍;远处小码认不出就调大
source.ros.rgb_topic/ascamera_hp60c/…/rgb0/image实时模式取哪一路话题改错 → 打印 cannot open ros topic 后退出(注释里写了"勿 remap")
source.ros.timeout5.0实时模式首帧等待秒数太小 → 相机还没出图就超时退出;太大 → 出错时要等很久
display.window_nameqrcode本地窗口标题(--display 才用)板上没有显示器,用不到;远程看画面走 MJPEG

对着 02 节那四个子问题看这张表,参数就不是一堆名字了:detect.scale、box_size、border 决定的是子问题 1、2 成不成立(能不能找到、能不能按格读);error_correction 决定的是子问题 4(脏了、缺角能不能救回来);而 version 是被"要塞多少字"和前三个子问题一起挤出来的。06 节拖滑杆,拖的就是这四条边界。

你该看到的画面

画面 A · 单码
绿框 + 文字

二维码被绿色方框框住,框上方标着解读出的内容(超过 40 字符会截断成前 37 个字符加 ...)。终端同步打印 [QR] 解出的文字。

画面 B · 多码
一帧里几个框

画面里放多张码时,程序会逐个检测,每个都框出来。它用"掩膜"方式防止同一个码被重复读:找到一块就把它涂白,再找下一个,最多循环 8 次。

画面 C · 无码
没有框,但程序不崩

看不到码时终端一行都不打印(代码里是 if results: 才输出),画面继续刷新 —— 沉默就是"没找到",不是出错。

🔍 为什么先缩一半再检测?配置里 scale: 0.5 表示检测前把画面缩小一半。二维码本来格子就大,缩小后依然认得出来,但计算量只有原来的 1/4 —— 这是个很典型的"用分辨率换速度"的工程手段。如果远处的小码认不出,把 scale 改成 1.0 试试。

踩坑速查

现象原因怎么办
一启动就打印「mjpeg_streamer 未安装」然后直接退出(返回码 2)入口 app.py 开工第一件事就是 require() 检查它,三个模式都要pip install mjpeg_streamer(加清华镜像会快很多)
报错找不到 qrcode 模块(--detect 也会)板上没装这个库。检测端也要它 —— src/config.py 一进来就 import qrcode 读纠错等级常量pip install qrcode pillow
中文内容读出来是乱码 / 读不出多数是清晰度/遮挡问题,不是等级问题(旧环境曾出现 H 级 + 中文解不出,本板复测四档都能解)先把 error_correction 降回 M 试;仍不行就查编码、加 box_size
板上 --camera 0 报 can't open camera by indexHP60C 是深度相机,不走 V4L2 索引改用 --ros(先 source + 起 ascamera 驱动)
启动报 OSError: [Errno 98] address already in use上一次的 app.py 没退干净,8080 还占着pkill -f app.py 收掉,或临时换个端口 --port 8090
打印出来的码扫不出格子太小(box_size 太小)或白边被裁了box_size 提到 8~10;border 保持 4 以上
远处的码认不出检测前缩了 0.5 倍,远处小码信息不足detect.scale 改成 1.0(会慢一些)
同一个码每帧重复打印正常行为,每帧都重新检测(实测一段 150 帧的视频里打了 133 行 [QR])正常。想只报一次就在代码里加去重逻辑
画面有码但检测为 0(终端一声不吭)反光、太暗、或码被手指挡住角标调光照;确保三个角标都露出来
生成的图里有问号方块内容含中文但字体/编码不匹配先用纯英文数字测通,再加中文
💡 排查口诀先生成、后图片、最后相机。三步分开验:生成能出图 → 图片能读回 → 相机能读到。哪一步断了就修那一步,别一上来就怀疑相机。

关键代码:这套程序的核心只有两段

跑通之后回头看代码 —— 生成 16 行、检测 32 行,没有一行是"魔法"。下面是 src/qr.py 里的两个函数原文(src/app.py 只是个命令行外壳,负责读参数、调它们、存图)。

① 生成:把文字交给 qrcode 库,它按编码规则画格子。四个参数全来自 config/qrcode_config.yaml;fit=True 的意思是"从 version 这个版本起步,装不下就自动升版" —— 所以配置里写 version: 1 但你存 19 个字符时,它会自己升到能装下的版本。最后一行别忽略:库吐出来的是 RGB 顺序,OpenCV 全程用 BGR,这里转一次,不转的话颜色通道会错位。

def generate_qr(data: str, cfg: dict) -> np.ndarray:
    g = cfg["generate"]
    qr = qrcode.QRCode(
        version=g["version"],
        error_correction=error_correction_level(g["error_correction"]),
        box_size=g["box_size"],
        border=g["border"],
    )
    qr.add_data(data)
    qr.make(fit=True)
    img = qr.make_image(fill_color=tuple(g["fill_color"]),
                        back_color=tuple(g["back_color"])).convert("RGB")
    return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)

② 检测:一次 detectAndDecode 拿到内容和四个角点,然后把那块涂白再找下一个。三个细节对应本页讲的三件事:① 开头那段 resize 就是 detect.scale —— 小于 1 才缩,缩完记下放大倍数 factor,最后把坐标乘回去,所以框永远画在原图坐标上;② points 就是三个角标定位出来的四个角,透视摆正和切格子都在 detectAndDecode 内部完成,我们拿不到中间结果;③ 那个 while 式循环 + 涂白,就是"一帧能框出多个码"的实现,MAX_DETECT_PASSES = 8 是防死循环的上限。

def detect_qr(img: np.ndarray, scale: float = 0.5) -> list[QRResult]:
    h, w = img.shape[:2]
    work = img
    factor = 1.0
    if 0 < scale < 1.0:
        work = cv2.resize(img, (int(w * scale), int(h * scale)))
        factor = 1.0 / scale

    detector = cv2.QRCodeDetector()
    results: list[QRResult] = []
    for _ in range(MAX_DETECT_PASSES):
        data, points, _ = detector.detectAndDecode(work)
        if not data:
            break
        if points is None or points.size == 0:
            break
        pts = points[0].astype(np.float32)
        xs = pts[:, 0] * factor
        ys = pts[:, 1] * factor
        results.append(QRResult(
            text=data,
            x1=int(xs.min()), y1=int(ys.min()),
            x2=int(xs.max()), y2=int(ys.max()),
        ))
        # 掩掉已识别区域,继续找下一个
        x1 = int(max(0, xs.min() * scale - 2))
        y1 = int(max(0, ys.min() * scale - 2))
        x2 = int(min(work.shape[1], xs.max() * scale + 2))
        y2 = int(min(work.shape[0], ys.max() * scale + 2))
        work[y1:y2, x1:x2] = 255
    return results

想改它会发生什么:把 scale 改小到 0.3 → 远处小码直接糊掉、框不出现;把 MAX_DETECT_PASSES 改成 1 → 一帧只能框出第一个码,多码场景漏检;把最后那行 work[...] = 255 注释掉 → 同一个码被反复读,一张图会打印 8 遍 [QR](上限 8 次兜住了,不会真的卡死,但结果里同一句话重复 8 条)。什么时候失效:缩放后每个格子只剩一两个像素时就开始丢细节(本实验 box_size: 5 + scale: 0.5 恰好是每格 2.5 像素,还能读),再小就散架;另外码被遮到角标看不见时,detectAndDecode 直接返回空串 —— 它不会区分"没找到码"和"找到了但读不出",这两种情况在程序里长得一模一样。

进阶(可选)

进阶 1 · 生成一张带颜色的码约 15 分钟

配置里 fill_color 和 back_color 是可以改的。试试深蓝前景 + 浅黄背景。

注意一个硬限制:前景必须比背景明显深,因为检测器靠对比度找格子。前景比背景浅的话,很多扫码器会读不出来 —— 虽然它的算法理论上支持反色,但实际实现往往只做了常规情况。

进阶 2 · 把二维码当成机器人的位置标记约 40 分钟

工业 AGV 常用二维码做定位:地上贴一排码,车底的相机扫到哪个码就知道自己在哪一格。

思路:把内容编码成坐标(比如 A3 表示第 3 列第 2 行)。检测到码之后,不仅要读内容,还要读框的中心位置 —— 中心偏左说明车靠右了,得往左微调。这和 EXP-01 的"色块中心偏移"是同一个思路,只是目标从色块换成了二维码。

进阶 3 · 为什么有些码要加 logo选读 40 秒

因为纠错码的存在,二维码中间一小块区域被遮住也能读出来。所以很多品牌把 logo 放在正中间 —— 那一片区域的数据实际上被弃用了,但纠错机制能补回来。

能放多大?粗略地讲:选 H 级(30%)时,中间可以遮挡约 20%~25% 的面积;选 L 级就只有 7%,随便挡一下就废了。这就是为什么带 logo 的码大多选 H 级。

但 H 不是白给的:它把版本顶大一档(本页四张码里唯一变成 29×29 的那张),格子更密、每格更小,对打印精度和相机清晰度都更挑剔。这就是工程的真实状态:拿到抗造性,一定要从哪里还回去。

06互动:拖纠错等级,看抗遮挡换密度

📌 这一节是「掌握检查」:亲手改一个参数,看现象是否朝你预期的方向变,还要能说出为什么 —— 说得出,才算掌握。

上面几节你已经在板子上造过码、读过码。这一节把整条链路做成一个不用设备也能玩的模拟器:输入任意文字 → 真的生成一张码 → 拖参数 → 盖上遮挡块,每一步都对应 04 节讲过的环节和 05 节的配置项。

⚡ 参数实验台 · 文字 → 码 → 遮挡
M
0%
0.50
生成的码(可手机实扫)
检测器看到的(先按 scale 缩小再放大)
互动 6-1|左:按当前参数真实生成的二维码(矩阵由页面内联的编码器按 ISO/IEC 18004 算出,与板上 qrcode 库的结果逐位一致;默认内容 ims.rencaiya.vip,改成任意文字都行)。中:把左边按 detect.scale 缩小再放大,模拟检测器实际拿到的清晰度。右:版本、模块数、像素尺寸与容量换算。
五个控件里四个是 yaml 里的真参数:error_correction、box_size、border、detect.scale;「遮挡面积」不是程序参数 —— 它模拟的是外部干扰(贴纸、手指、污渍),所以别去 yaml 里找它。

动手做这个实验:① 把文字换成你自己的学号或一句中文,点「生成」—— 看模块数和版本怎么变(中文每个字 3 字节,同样字数会占更多格子);② 把「遮挡面积」拉过当前等级的上限,红块盖住码中心 —— 用手机直接扫屏幕,看还能不能读出原文(中心遮挡比随机污损更狠,理论上限不是保险箱);③ 把「检测缩放」拉到 0.2,看右边那张“检测器看到的”糊成什么样 —— 这就是远处小码读不出的原因;④ 把 box_size 调到 3,再想象一下把它打印出来贴到货架上;⑤ 把「静区格数」改成 0 —— 格子会直接顶到图片四个边,四周一点白都不剩;实测这一步会让检测器连码都找不到,这就是标准要求静区至少 4 格的原因。

对照真实产物:同一句话、四个纠错等级

实验台上拖的是模拟;下面这四张是板上真跑 --generate 出来的 PNG,同样一句话、同样 box_size: 5,只有纠错等级不同。

nano config/qrcode_config.yaml
generate:
  error_correction: M   ← 改成 L / Q / H 各试一次
  box_size: 5
  border: 4

改完重新生成同一个内容 —— 每换一级跑一次,集齐四张,对比四张图:

python src/app.py --generate "https://example.com" --save qr_L.png
QR 已保存: /home/iflytek/ros2_ws/src/xflz_eai_car1/vision/04_qrcode/qr_L.png

⚠️ 每换一级要改两处:配置里的 error_correction,和命令末尾的 --save 文件名(依次改成 qr_M.png / qr_Q.png / qr_H.png)。只改等级不改名字,四张会互相覆盖,最后只剩一张。

纠错等级 L 的真二维码,内容 https://example.com
L · 7% · 25×25
纠错等级 M 的真二维码,内容 https://example.com
M · 15% · 25×25 默认
纠错等级 Q 的真二维码,内容 https://example.com
Q · 25% · 25×25
纠错等级 H 的真二维码,内容 https://example.com
H · 30% · 29×29 最密
图 6-1|同一句话 https://example.com、同样 box_size: 5,四个纠错等级的实际产物。L/M/Q 都是 25×25 格(165×165 像素),H 被冗余顶到 29×29 格(185×185 像素)。

📱 拿手机实扫:四张码里装的都是同一句话 https://example.com —— 逐个扫一遍,四张都应该读出完全相同的内容。扫完再回看格子:L / M / Q 都是 25×25 格,只有 H 为了硬塞 30% 冗余自动升版到 29×29,格子明显更密 —— 这就是「校验占格子」长的样子。注意 L 和 Q 在这条内容上格数一样多:因为"https://example.com"这 19 个字符在 L 级下也装不进 21×21,库把三档都推到了同一个版本。等级的差别这时只体现在冗余多少上,等你要存更短或更长的内容,版本差异才会跳出来。

取舍的智慧:想要抗造就选高级别,但要付出"码更密、更难扫、存字更少"的代价。没有最优参数,只有最合适的参数。

07验证:三道小测

✅ 目标对账 · 验证掌握得有多好

回页首对照「实验目标」逐条自检:① 三大件各干什么活 → 试 Q1;② 生成→读回闭环是否亲手跑通 → 翻回第 05 节自查(两条命令、两行回执);③ 六个环节各由哪个函数完成、detect.scale 调太小为什么会读不出 → 试 Q3。答不上的条目回到对应小节补完再交卷 —— 三条全 ✓,这一课就完成了。

Q1二维码上的三个"回"字角标是干什么用的?
装饰,让码好看一点
让机器快速找到码的位置、算清倾斜和旋转角度
存数据的,内容都在里面
对。三个点定位置和朝向,第四个角按"它本来是正方形"推出来,所以右下角不必再放一个。真正的数据都存中间那些杂乱的格子里 —— 那些格子之所以看起来毫无规律,一半是数据、一半是掩码刻意打散,好让它们别"不小心拼出第四个回字"骗过扫描器。
Q2把纠错等级从 M 改成 H,会发生什么?
码变小了,能存更多字
码变得更密、抗遮挡更强,但可存字数变少、也更难被扫出来
没有任何变化
对。纠错码本身也占格子。等级越高,用在"校验"上的格子越多 —— 抗造能力上去了,但同样的内容需要更大的版本,而且每个格子要切得更准。
Q3你自己生成的码,用手机扫能读出来,但用板子的 --detect 读不出。最可能的原因?
板子的相机坏了
检测前缩了 0.5 倍,小码信息不足;或码在画面里太小
二维码生成错了
对。手机用的是全分辨率,而这里 detect.scale: 0.5 会先把画面缩一半。实测这只 165px 的码缩到 82px(scale 0.5)还能读,继续缩到 58px 就整片糊掉、彻底读不出 —— 所以远处小码要么靠近点,要么把 scale 调到 1.0,要么生成时把 box_size 调大。

08章节总结:先靠角标找到码,再靠纠错码把内容读回来

概念它解决了什么和别的关系
生成链三件套
add_data → make → make_image
把一段文字变成可打印的图:编码成字节 → 排成矩阵 → 染成像素全在 src/qr.py 的 generate_qr() 里;box_size 和 border 只在最后一步起作用,改它们不改变码的内容
定位角标 ×3
detector.detect()
在几百×几百个像素里先把码找出来 —— 三个点确定一个平面,顺手把倾斜和旋转角度也定了检测端第一步(图 4-5 ③);它给出的四个角点是下一步透视变换的唯一输入
透视变换(摆正)
getPerspectiveTransform + warpPerspective
斜着拍格子会变梯形,按四个角点把图拉回正方形源码里在 detectAndDecode() 内部完成,没有可调参数;斜到角标被压扁就直接检测失败、框都不出现
时序线 + 校正图形黑白相间告诉机器「一格有多大」,拍歪了也能把图摆正是切格子(切成 0/1 串)的依据,没有它格子大小不定
纠错等级
error_correction
内容写了好几遍,弄脏一部分还能从剩下的推回来生成端参数;等级越高码越密、可存字越少,检测端只管读
静区 border 与 box_size白边让机器找得到边界;每格像素数决定印出来扫不扫得出border 标准最小 4 别改成 0;box_size 太小打印就糊成一片
检测分辨率 detect.scale
cv2.resize
检测前把画面缩一半,计算量只剩 1/4,格子大所以照样认得出上游是相机画面,下游直接决定远处小码能不能读;实测 82px 能读、58px 就废,改 1.0 更稳但慢 4 倍
⚠ 反直觉的地方 ① 直觉是格子越多能存越多字,实际是同一条内容(https://example.com)从 M 升到 H,版本会从 25×25 硬顶到 29×29 —— 多出来的格子全用在冗余上,可存字反而变少、每格更难切准; ② 直觉是等级越高版本越大,实际是L / M / Q 三档在这条内容上是同一个 25×25 —— 因为 19 个字符在 L 级下也塞不进 21×21,库把三档一起推到了版本 2,等级的差别只落在冗余多少上。要更短的内容(比如 A3)才会看到 L 停在小版本、H 往上跳; ③ 直觉是检测当然要用原分辨率才准,实际是detect.scale: 0.5 先缩一半反而更好用 —— 二维码格子本来就大,这是典型的「用分辨率换速度」。
💡 三条要注意 ① L / M / Q / H 对应的 7% / 15% / 25% / 30% 是「能修复的面积比例」,不是「能挡住几块」;而且中心遮挡比随机污损更狠,理论上限不是保险箱,以扫码现象为准; ② border 的单位是格不是像素,标准最小 4、别改成 0;box_size: 5 指的是每格画 5×5 像素,打印出来扫不出就提到 8~10; ③ 三个库缺一不可,--detect 也要 qrcode。src/config.py 一进来就 import qrcode 去取纠错等级常量,所以缺它连图片读回都起不来;mjpeg_streamer 更靠前 —— 入口 app.py 第一句就 require() 检查,缺了直接退出(返回码 2),连生成模式都不进。

前置依赖 · 缺一样就跑不起来

类型具体项缺了会怎样 / 怎么补
环境实时扫才需要 ROS2:source /opt/ros/humble/setup.bash + source ~/ros2_ws/install/setup.bash(两句都要)生成与图片读回两步不需要 ROS,一条 python src/app.py 就跑完闭环;实时模式缺 source → ros2 命令或 ascamera 包找不到;驱动没起 → 打印 cannot open ros topic 后退出
Python 库三个必装:qrcode(画格子,两个模式都要)+ pillow(出 PNG)+ mjpeg_streamer(推 MJPEG 流,入口强制检查);检测另外要 OpenCV(板上自带的 cv2.QRCodeDetector)缺 mjpeg_streamer → 启动就打印安装指引并退出 2;缺 qrcode → ImportError: No module named 'qrcode';补齐:pip install qrcode pillow mjpeg_streamer(镜像用法见 05 节)
模型 / 资源本实验不需要模型 —— 生成靠 qrcode 按编码规则画格子,检测靠 OpenCV 自带检测器(零权重)—
配置config/qrcode_config.yaml:error_correction: M、box_size: 5、border: 4、detect.scale: 0.5;--config 可以指到别的 yaml,不改原文件也能试参数box_size 太小 → 打印出来扫不出;border 改 0 → 静区没了、难定位;scale 太小 → 远处小码信息不足
硬件 / 前置实验HP60C 相机 + ros2 launch ascamera hp60c.launch.py(仅实时模式);一张能打印或能在手机上显示的码没有相机也能完成「生成 → 图片读回」的闭环;板上 --camera 0 不可用(深度相机不走 V4L2),要相机只能走 --ros;无强制前置实验,进阶 2 只是借用 EXP-01 的中心偏移思路

这套代码用在哪儿

  • 库位 / 工序标识:码贴在货架或工位上 → --detect 吐出的是一行字节级精确的字符串(如 A3-07),直接拿去当库位号、工序号,喂给调度系统查「这是 3 号库位的零件 A」「现在该执行哪道工序」。
  • AGV 地标定位:地上贴一排码、内容编码成坐标(A3 表示第 3 列第 2 行),车底相机读到哪个码就知道自己在哪一格;再把框的中心位置拿出来 —— 中心偏左说明车靠右了,得往左微调,和 EXP-01 的色块中心偏移是同一个思路。
  • 带 logo 的品牌码:中间那块区域的数据实际被弃用,靠纠错码补回来,所以要选 H 级 —— 代价是版本被顶大一档(本页四张里唯一变成 29×29 的那张),格子更密、更依赖清晰度。这就是「拿到抗造性,付出密度」的真实账。
核心记忆点:三个角标负责「找到并摆正」,纠错码负责「脏了也能救」,剩下的全是取舍 —— 抗造、密度、可存字量三者不可兼得。