QR码无处不在:想要创造更原创的解决方案吗?让我们自己建立一个基准标记,并学习如何检测和解码它。
在这篇文章中,让我们学习如何建立一个新的基准标记,并通过训练目标检测模型来检测它。然后,我们将学习如何使用图像处理技术解码我们的标记。
让我们分解为三个步骤:
- 创建基准标记
- 在图像中检测标记
- 解码标记
创建基准标记
计算机视觉已经存在很多基准标记,最著名的是二维码。还有其他二维码,使用程度更高或更低,鲁棒性不尽相同,也可以使用。以下是一些非全面的代码列表。

从上图可以看出,不同的基准标记可能有很大的差异,但它们都有相同的目的:包含易于解码的信息。
好的基准标记是什么?
理想情况下,一个好的基准标记具有以下特点:
- 易于检测:在能够解码标记之前,必须能够准确地在图像中检测到它
- 易于解码:解码标记必须简单明了,没有任何歧义(即,解码后的标记产生唯一值)
基于这些特点,现在让我们从已经存在的标记中建立自己的标记。
设计我们的基准标记
我个人喜欢RUNE标记(出于非常主观的原因):
- 圆形和点的形状让它比方形标记更柔和
- 它似乎非常可辨,使得对象检测模型更容易检测到它
- 它容易自定义:我们可以调整每个圆中的点数以及圆的数量,使其适应我们的需求和期望的美观
但它在原始形式下并不完美:两个旋转的标记可能导致相同的解码,也可能不会。

为了解决这个问题,我们将为标记添加一个条件:只有一个象限没有黑点,如下所示。

这样的标记很容易解码:我们考虑每个象限可以有三个可能的值:0、1或2,具体取决于三种可能的情况:
- 一个小黑点:0
- 一个大黑点:1
- 两个黑点:2

更一般地说,考虑一个具有C圆圈层的标记,一个象限最多可以有2ᶜ-1个值(因为没有黑点保留给象限0)。
最后,对于一个具有d+1个点的标记,可能的组合数等于(2ᶜ-1)ᵈ。对于一个具有2个圆圈层和每个圆圈标记使用20个点的标记,意味着有大约11.6亿个可能的值。
构建我们的特征标记
让我们在这里解释一下用于生成随机特征标记图像的代码片段。
生成随机标记的方法的要点。请参阅文章末尾的链接以获取完整工作代码。
正如您所看到的,第一步是生成一个随机值列表。考虑C为圆圈层数,d+1为每个圆圈的点数,我们使用numpy生成一个介于0和2ᶜ-1之间的d个值的列表。
基于这个随机值列表,我们计算点的值:0表示白点,1表示黑点。最后,我们根据像素大小绘制最终的标记,并将输出保存为图像。当然,完整工作代码存储库的链接在文章的末尾提供并进行了文档化。
我们选择了一个标记设计,并且知道了如何生成这样的标记。为了能够在实际条件下使用这样的标记,我们需要一个能够在图像中检测和解码这样的标记的解决方案。这非常简单,是一个顺序的2步流程:
- 使用物体检测来检测标记
- 解码检测到的标记
现在让我们进入流程的第一步。
检测标记
因此,第一步是检测给定图像中标记的存在和位置。为此,有许多物体检测模型可用。在这里,我们将使用一个非常容易在生产环境中训练和使用的YOLOv8模型。
但在能够实际训练物体检测模型之前,我们需要数据:来自不同背景和环境的图像,其中包含不同缩放级别和视角的标记。
我们将使用合成数据来训练模型,而不是收集和标记数据,因为后者可能非常耗时。
生成数据
为了生成用于训练物体检测模型的合成数据,我们只需要两个要素:
- 各种背景图像,可以从Unsplash等地方获取没有版权问题的图像
- 标记图像,我们将即时随机生成
有了这两个要素,我们只需要使用Albumentations应用一些增强操作,生成许多独特的合成图像以及它们关联的标签。
在这里我们提供一段代码,可以根据背景图像的路径和标记特征(如圆层的数量和每个圆的点数)生成图像。
生成合成数据所使用的代码要点。在文章末尾附带完整的可工作代码链接。
这是一段相当长的代码,请随意深入研究,但简言之,它的功能如下:
- 生成一个随机标签,图像的边界框标签作为图像边界
- 应用仿射、透视或缩放变换,感谢 Albumentations 库
- 随机将该标签插入随机选择的背景图像中
- 根据需要重复执行
通过这种方法,我们可以轻松生成足够大的数据集,其中包含数百甚至数千个图像。以下是一些以红色边界框标记的创建图像的示例:

可以看到,生成的图像相当各异,因为我们添加了各种背景和增强手段,如模糊和透视。
当然,我们不能为训练集和验证集使用相同的背景图像,以确保模型评估尽可能不偏袒。
在 github 存储库中提供了一个 Python 脚本,用于生成图像和相关标签到正确的文件夹中,以用于 YOLO 模型训练。
训练和评估模型
使用之前创建的数据集,我们现在可以在这些数据上训练目标检测模型。借助 YOLOv8 库,只需几行代码就可以训练一个新模型。
使用 YOLOv8 小模型在 100 个 epoch 上进行训练的代码要点。在文章末尾附带完整的可工作代码链接。
可以看到,我们只需要实例化一个模型并在数据上进行训练即可。经过 100 个 epoch 的训练后(或者如果训练时遇到提前停止条件,可在大约 80 个 epoch 后停止,就像我在这里做的那样),所生成的结果显示 mAP@50 约为 0.5。

虽然结果离完美还有距离,但对于仅使用合成数据训练的数据集来说,已经足够好了。现在,让我们使用网络摄像头来对模型进行实时测试。
为此,我们可以使用以下代码:
用于在网络摄像头中进行 YOLOv8 模型推断的代码要点。在文章末尾附带完整的可工作代码链接。
这段代码非常简单:
- 加载模型并获取网络摄像头的数据流
- 对于每个新图像,计算模型推断并显示任何检测到的边界框
- 按下 Esc 键停止视频流
我使用了手机上的一个标记图片运行了这段代码,如下图所示,结果还不错。

虽然它无法完美地检测所有配置中的标记,但对于仅使用合成数据训练的模型来说,结果已经足够好了。为了取得更好的效果,我相信数据增强可以稍作调整,当然真实的带标注数据也是非常有帮助的。
现在,我们已经完成了管道的第一部分,让我们进入第二步:标记解码。
解码标记
现在我们已经完全能够生成和检测我们的新参考标记的代码了。
一旦您能够在图像中检测到标记,下一步当然是解码它。让我们从之前训练模型得到的检测到的标记的裁剪图像开始。

我开发了一种解码算法,包含以下步骤:
- 用于检测点的Blob检测
- 外圆检测和椭圆拟合
- 用于计算单应性的点选择
- 计算单应性矩阵和图像去畸变
- 最后进行标记解码
主要思路是:只要我能够将检测到的标记与参考标记匹配(知道圆层数和每个圆上的点数),我就可以通过检查图像是白色还是黑色来相对容易地解码它。但是,为了做到这一点,我首先需要去除图像的畸变,使之与参考标记匹配。
让我们一起来了解这些步骤。
检测点
第一项任务是在YOLO模型检测到的图像中检测出点。
从输入的裁剪图像开始,我们将使用OpenCV进行以下图像处理操作:
- 将图像转换为灰度图
- 使用Otsu算法对图像进行二值化处理
- 使用Blob检测器找到点
下面的代码执行了这些操作:
用于从裁剪图像中检测点的代码。完整工作代码的链接请参见本文末尾。
如我们所见,为了最大化实际标记点的有效检测,对于Blob检测器设置了许多参数,例如最小和最大区域以及最小圆度。调整这些参数花费了相当多的时间,但请随意尝试进行调整。
使用此代码在我们的输入裁剪图像上进行点检测,得到以下Blob检测结果。

如我们所见,点被很好地检测出来。下一步是检测外圆。
检测外圆
现在,我们需要检测最外层的圆(不管标记中有多少个圆,这种方法都可以泛化)。这将允许我们找到外圆上的点,以便最终解除图像的畸变。
为了计算椭圆,我们只需保留较大的点(在OpenCV中称为关键点),并根据这些点拟合出椭圆方程。下面的代码实现了这一步骤:
用于从检测到的点计算椭圆方程的代码。请注意,该代码始终计算一个中心估计值,在后面的步骤中将很有用。
当我应用此代码并将检测到的点显示为散点图,并在图上显示拟合的椭圆时,我得到了以下结果:

正如我们所看到的,拟合的椭圆与点的位置相匹配且明确可见。请注意,由于我们正在拟合一个椭圆,无论通过透视有多少变形的检测标记,它都能起作用。
现在我们需要找到实际位于该椭圆上的点。这很容易:我们只需要找到满足我们刚刚计算的椭圆方程(带有给定阈值)的点的位置。以下是使用的代码片段:
使用返回椭圆上关键点的代码片段。可以在文章末尾的链接中查看完整可工作的代码。
现在,我们知道了点位于哪里,并且哪些点位于最外圆上,我们可以使用它们来计算单应矩阵并解除图像的透视变形。
选择用于单应矩阵计算的点
目标现在是在参考图像中找到几个匹配点,以便计算单应矩阵。

根据上面的参考图像,我们需要使用正确的单应矩阵将检测到的斑点进行透视变形。
为了计算单应矩阵,我们可以简单地使用OpenCV函数findHomography。该函数需要至少四对匹配点——参考图像和输入图像中的点——以便找到单应矩阵。然后,该单应矩阵将允许我们对检测到的图像进行透视变形,并与参考图像进行匹配。
从最外圆上检测到的斑点中,我们无法知道这些点在原始参考图像上的位置。因此,我们只会选择最外圆上最长的相邻点链,以便将它们与参考图像进行匹配。为此,需要以下两个步骤:
- 计算邻接矩阵,以了解每个点的相邻点是哪些(如果有的话)
- 根据邻接矩阵,计算相邻点的最长链
对于第一步,可以使用以下代码:
用于计算邻接矩阵的代码。可以在文章末尾的链接中查看完整可工作的代码。
这段代码将计算邻接矩阵,并将其作为Python字典返回:以外圆上的每个现有点索引为键,找到的相邻点索引列表为值。
通过这个邻接矩阵,现在很容易找到相邻邻居的最长链。为此,我使用了以下代码:
用于计算相邻点最长链的代码片段。可以在文章末尾的链接中查看完整可工作的代码。
这段代码将高效地找到相邻点的最长链,并返回它们的索引列表。
如果输出结果至少有4个点,我们理论上可以计算单应矩阵。但不幸的是,在大多数情况下,准确性不会很高,因为这些点几乎在同一条线上,无法准确计算单应矩阵。为了解决这个问题,我们将添加一个额外的点:一个相对于中心对称放置的点,这将提供更准确的单应矩阵计算。
我们可以使用以下代码找到相对于中心对称的点(在拟合椭圆时计算中心):
用于找到对称点的代码,给定最长链和椭圆上的所有关键点。可以在文章末尾的链接中查看完整可工作的代码。
请注意,由于我们在椭圆上,使用中心估计来找到给定点的对称点并不是一个100%可靠的方法:它可能输出错误的点。这是我们在计算解码时要记住的。
最后,我们得到了以下结果的图像,其中蓝色圆圈是最长链的点(除了最左侧的点)。红色圆圈是检测到的对称点。中心的红色圆点是估计的椭圆中心。图像由作者提供。

正如我们所看到的,我们确实选择了七个相邻点的链。并且我们选择了另一个点作为链中最左边的点的对称点。
图像去形变
现在我们已经在输入图像中选择了一些点,让我们找到参考图像中匹配的点并计算单应矩阵。为此,我们需要以下输入:
- 裁剪图像中所选点的位置:这是我们刚刚计算的
- 这些点在参考图像中的对应位置:这需要计算,需要知道参考标记
为了计算这些点的位置,我们将使用以下代码来计算点的位置。
用于计算单应矩阵计算的参考点位置的代码片段。在文章末尾链接中可以找到完整可工作代码。
请注意,我们添加了一个名为symmetry_index_offset的参数,这样可以给对称点计算中的可能错误增加一个偏移量。
通过正确获取裁剪图像和参考图像中的点位置,我们现在可以计算单应矩阵并对图像进行去形变。为了确保我们在对称点的处理上没有错误,我们将对偏移值范围[-2, 2]进行步幅为1的五次计算,如下面的代码片段所示:
用于计算单应矩阵和图像去形变的代码片段。在文章末尾的链接中可以找到完整可工作的代码。
我们所做的就是使用OpenCV的findHomography函数计算单应矩阵,然后使用warpPerspective对图像进行去形变。我们对5个偏移值进行计算,因此我们最终得到5个去形变图像。
结果图像如下:

正如我们所看到的,根据偏移值,去形变结果是不同的。即使通过视觉检查很容易理解偏移值为-1是正确的,但我们希望这个检查是自动化的。我们将在下一步中处理:实际的标记解码。
标记解码
从给定的去形变图像,最后一步就是解码标记。我们已经非常接近了,这一步可能是最简单的。
我们需要做的就是检查每个预期点位置周围3×3像素区域的去形变图像的颜色。由于图像经过Otsu二值化处理,这非常直观。我们只需检查预期点位置周围的区域是否有任何黑色像素:如果有,则表示有点;如果没有,则表示没有点。
用于从去形变图像计算代码列表的代码片段。在文章末尾的链接中可以找到完整可工作的代码。
以上就是上述代码的基本功能。然后根据位置赋值。因此,该函数的输出只是一个值列表。最后,我们寻找一个值为-1的位置(表示没有黑点的预期象限,请查看有关设计我们的标记的部分的重要提示),并重新排列数组以将其放置在最后一个索引位置。
例如,以下是每个去形变图像的计算代码:
- 偏移值为-2:[0, 2, 0, -1, 1, -1, 0, 0, 0, 2, 2, 1, 2, 2, 0, 2, 2, 2, 0, -1]
- 偏移值为-1:[2, 2, 2, 0, 2, 0, 1, 1, 1, 2, 2, 1, 2, 2, 0, 2, 2, 2, 0, -1]
- 偏移值为0:[0, -1, 2, 2, 0, 0, -1, 0, 0, -1, 0, 1, 2, 2, 0, 2, 2, 2, 0, -1]
- 偏移值为1:[-1, 2, 2, 2, 2, 0, -1, -1, 0, 0, 0, 0, 0, 2, 0, 2, 2, 2, 0, -1]
- 偏移值为2:[-1, 2, 1, 2, 1, 0, 1, -1, -1, -1, -1, 0, 0, 0, 2, 2, 0, 0, 2, -1]
正如我们所见,只有一个图像具有一个唯一的-1值,位于最后一个索引位置:使用偏移量-1进行反畸变的图像。这是我们良好的反畸变图像(通过视觉检查我们可以看到),可用于实际解码标记。
对于每个可能的标记,该代码列表是唯一的,您可以选择在此停止,或计算唯一的整数值。可以使用以下代码片段轻松计算唯一值:
代码片段的要点用于从代码列表计算解码的最终值。在文章末尾提供完全可工作代码的链接。
在我们的案例中,这将为所有错误的反畸变图像返回-1,并返回实际标记的值377667386。
就是这样,我们从输入图像一直到实际的唯一代码!现在让我们回顾并思考我们所做的限制。
创建完整的流程
现在我们有了所有的构建模块,我们只需要将它们组合起来,就能得到一个漂亮的、定制的基准标记解码器,可以替代QR码!
作为回顾,在完全工作的流程中有以下步骤:
- 从输入图像中,使用目标检测检测标记
- 对于每个检测到的物体,裁剪图像并进行下一步
- 使用大津二值化和斑点检测检测点
- 用椭圆拟合找到最外层的点
- 使用最长的最近邻点链和对称点计算单应矩阵
- 使用单应矩阵对图像进行反畸变
- 使用参考图像解码标记
就是这样!我不会让你亲自编写所有的代码,所有的代码都可以在github代码库中找到,还有一个预训练的目标检测模型。
在这个代码库中,你会找到运行子步骤的Python脚本(例如生成合成图像、训练目标检测模型等),以及一个使用网络摄像头作为输入运行完整流程的Python脚本,这样你就可以进行测试!
最终思考
我希望你喜欢这篇文章,并从中学到了东西!我个人非常喜欢这个项目,因为它结合了机器学习和传统的图像处理。
然而,我开发的算法有一些限制,我希望能够克服。事实上,并不是所有的标记都可以解码:
- 外圈没有超过2个相邻点的标记不能正确解码
- 同样,没有最长连线上对称点的标记将给出不可靠的结果,因为不准确的单应矩阵
另一个限制是有时单应矩阵在反畸变期间会对图像进行镜像处理,导致列表代码颠倒,因此最终解码的整数值会不同。
如果您对克服这些限制有任何想法,非常欢迎您给我发消息,甚至提出拉取请求!
另外一个话题是,在这里解码只给出一个整数值。您可以将这个整数值与应用程序中的任何相关内容(链接、物品、图像等)进行匹配,以使其真正有用。我相信可以直接将这样的标记解码为ASCII字符列表,但我自己没有尝试过:再次强调,任何贡献都是非常欢迎的。
参考资料
原始RUNE-Tag论文:
F. Bergamasco, A. Albarelli, E. Rodolà and A. Torsello, “RUNE-Tag: A high accuracy fiducial marker with strong occlusion resilience,” CVPR 2011, Colorado Springs, CO, USA, 2011, pp. 113–120, doi: 10.1109/CVPR.2011.5995544.
原始RUNE-Tag代码库:https://github.com/artursg/RUNEtag