5.28. QR 码与 AprilTag

目前为止介绍的检测器——色块、直线、圆、矩形——找的都是几何特征:位置和轮廓,需要由后续处理阶段去解读。剩下的检测器找的则是符号特征:其视觉结构专门为编码某种载荷而设计的印刷图案。摄像头定位它们,解码器读取其中的比特,返回的不是位置,而是符号印制者刻意选定的字符串(或ID)。

在小型摄像头应用中,主要有两大类这样的符号。QR 码可携带任意文本、URL、联系人名片或二进制载荷——这是面向消费者的二维码,出现在海报、包装和登机牌上。AprilTag 则携带来自一个小型固定集合的单个数字 ID,即使在远距离也能快速解码,并且(在提供镜头内参时)可报告标签在摄像头坐标系中的 6 自由度位姿——这是面向机器人领域的二维码,用于标记无人机、标定靶以及基准标志。两种检测器返回的结果对象都使用与色块和矩形检测器相同的边界框词汇,但其载荷使它们与目前介绍过的任何东西都有本质区别。

5.28.1. QR 码

find_qrcodes() 在帧中扫描 QR 码,并返回一个 QRCode 结果对象列表:

codes = img.find_qrcodes()

for c in codes:
    img.draw_rectangle(c.rect, color=(0, 255, 0))
    for corner in c.corners:
        img.draw_circle((corner[0], corner[1], 4),
                        color=(0, 255, 0))
    print(c.payload)

该检测器接受一个可选的 roi 参数以限定搜索范围。它需要灰度输入——彩色帧在解码前会在内部进行转换。

每个检测结果都携带边界框(xywhrect)、检测到的四个角点(corners,即 QR 码的定位图案勾勒出的投影四边形),以及解码后的字符串载荷。在标注检测结果时,应当绘制角点——斜视角下观察的 QR 码并非轴对齐,边界框只能给出一个粗略的外形。

解码器元数据涵盖了 QR 解码器在解码过程中获取的所有信息。version 是 QR 码版本(1 到 40),它决定了模块网格大小(版本 1 的码宽为 21 个模块,版本 40 为 177 个)。ecc_level 是纠错级别(0 到 3,对应 L / M / Q / H);级别越高,预留给纠错的码字越多,能承受更多损坏,但代价是载荷空间更小。mask 是编码器为尽量减少解码器混淆而选取的掩码图案(0 到 7)。data_type 是解码器报告的编码方式——数字、字母数字、二进制或日文汉字——而 is_numeric / is_alphanumeric / is_binary / is_kanji 标志则以更友好的布尔值形式暴露相同的值。

eci 是扩展通道解释(Extended Channel Interpretation)值,它标识这些字节所采用的文本编码(UTF-8、ISO-8859-1 等等)。来自任意印刷材料的 QR 码不一定保证是 UTF-8;需要正确解码字节的应用应检查 eci 并据此进行解码。特别是日文汉字的情况:MicroPython 不解析日文汉字编码,因此 is_kanji 的载荷必须被当作字节数组处理,由应用自行解码。

一个典型用法:摄像头从传送带上读取 QR 码,并将解码后的载荷上报给主机。摄像头每帧调用一次 find_qrcodes(),遍历返回的列表,挑选 data_type 符合应用预期的码,然后通过 UART 或 USB 转发 c.payload。边界框和角点数据对 IDE 预览很有用,但并不是主机所关心的内容。

5.28.2. AprilTag

find_apriltags() 在帧中扫描 AprilTag,并返回一个 AprilTag 结果对象列表:

tags = img.find_apriltags(families=image.TAG36H11)

for t in tags:
    img.draw_rectangle(t.rect, color=(0, 255, 0))
    img.draw_cross(t.cx, t.cy, color=(0, 255, 0))
    print(t.id, t.decision_margin)

AprilTag 与 QR 码在设计目标上有所不同。QR 码旨在用单个稠密符号编码任意数据,用户在近距离读取一次。AprilTag 旨在用稀疏符号编码一个小型 ID,让摄像头从远处连续读取,并具备其家族汉明码所允许的尽可能高的容错能力。这种取舍体现在两个方向上:QR 码可携带数百字节,但需要近距离读取;AprilTag 只能携带几百个唯一 ID,但能从数米之外可靠读取。

families 关键字接受一个表示要解码的标签家族的位掩码。可用的家族有 image.TAG16H5image.TAG25H9image.TAG36H10image.TAG36H11image.TAGCIRCLE21H7image.TAGCIRCLE49H12image.TAGCUSTOM48H12image.TAGSTANDARD41H12image.TAGSTANDARD52H13。每个家族都在 ID 数量与鲁棒性之间做出权衡。名称中的 H 数字是该家族中任意两个码之间的最小汉明距离——即一个有效码变成另一个有效码需要翻转多少个比特——TAG16H5 在距离 5 时有 30 个 ID,TAG25H9 在距离 9 时有 35 个 ID,而 TAG36H11(默认且最常用)在距离 11 时有 587 个 ID。无论哪个家族,检测器最多纠正两个比特错误,因此距离决定了这种纠正的风险有多大:噪声帧中的某个随机图案只要落在某个有效码的两个比特范围内,就会被解码为误检;而距离越大的家族将其码分布得越稀疏,这类碰撞就越罕见——这正是推荐选用 TAG36H11 的原因。检测时间随启用的家族数量增加,因此应用应只启用它实际印制的家族。当一次调用中需要多个家族时,位掩码即为这些家族常量按位或的结果。

每个检测结果都携带边界框词汇——xywhrectarea,整数和亚像素质心(cxcycxfcyf)——以及检测到的四个角点(corners)。随后是标识字段:id 是家族内的数字 ID(TAG36H11 为 0 到 586),family 是数字形式的家族常量,name 是字符串形式的家族名称。

匹配质量字段是应用用来过滤检测结果的依据。decision_margin 是 0.0 到 1.0 的置信度评分;越高越好,过滤掉 decision_margin > 0.1 以下的检测结果可几乎无代价地清除大部分虚假命中。hamming 统计解码器为此标签接受的比特错误数——越低越好,0 表示完美解码。goodness 是一个历史遗留的图像质量指标,当前解码器已不再计算它;它始终为 0.0,可以忽略。

5.28.3. 由内参求位姿

find_apriltags() 的变革性特性——也是 AprilTag 成为机器人首选基准标志的理由——在于该方法能够直接从检测到的角点和一小组标定内参中恢复出标签在摄像头坐标系中的 6 自由度位姿。这些内参是摄像头以像素为单位的 X 和 Y 焦距(fxfy)以及以像素为单位的光心(cxcy),这四个值都由应用通过一次标定流程测得,之后便硬编码下来。

提供内参时,返回的 AprilTag 会用标签相对于摄像头的位置填充其 x_translationy_translationz_translation 字段,并用标签的朝向填充 x_rotationy_rotationz_rotation(以及为对称起见重复的 rotation)。不提供内参时,这六个字段均为 0.0,应用需自行负责所需的任何位姿估计。

平移字段以标签宽度为单位报告:解码器将标签视为 1 个单位宽,因此应用需将每个平移值乘以印制标签的物理宽度,以得到公制距离。一个印制宽度为 100 mm、报告 z_translation = 8.3 的标签距摄像头 830 mm;同一标签若以 50 mm 宽度印制并处于相同距离,则会报告 z_translation = 16.6。旋转字段以弧度为单位,无需缩放。

位姿估计是众多机器人应用的基础:将机器人对接到带标签的充电站、沿着印制的路点轨迹行进、从环境中多个已知标签恢复摄像头自身的位姿。一台知道内参、看到一个标签并且拥有该标签真实世界位置的摄像头,通过同样的运算,便拥有了自身的真实世界位置。

5.28.4. 如何选择

QR 码与 AprilTag 解决的是不同的问题。两者之间的选择归结于印制符号所携带的内容

当应用需要通过印制符号携带任意数据时——一个 URL、一个序列号字符串、一条联系人记录——QR 码是正确的选择。中等大小的码即可容纳数百字节,其编码是公开的且每部智能手机都支持,解码器也能应对旋转、中等程度的损坏和斜视角度。

当应用需要一个从远处连续读取、可选附带位姿的小型 ID 时——移动机器人上的基准标志、房间里的标定靶、充电站上的对接标记——AprilTag 是正确的选择。数百个 ID 对该用例而言绰绰有余,汉明码能从那些会让 QR 码失效的比特错误中恢复,并且一旦标定好内参,位姿估计便是免费附赠的。

有些应用两者并用:用一个 AprilTag 标记一个已知位置,而旁边印制的关联 QR 码携带该位置意味着什么的元数据。两个检测器在同一帧上独立运行,应用通过关联它们的边界框,将每个标签与其配套的码匹配起来。