ArUco与AprilTag简介与基本使用

May 30,2022   7556 words   27 min

Tags: SLAM

1.什么是ArUco?

ArUco是科尔多瓦大学“人工视觉应用”研究小组(A.V.A)设计开发的一个微型现实增强库,官网地址是这里。ArUco主要的作用就是用于检测平面ArUco标记,并基于此估计相机位姿。它本身是一个开源的代码库,项目的源代码在这里可以下载。但目前ArUco的一些相关功能都被集成到了OpenCV中,包括ArUco标记的生成、检测等。所以如果没有特殊需求,直接使用OpenCV里带的相关功能即可。如果想从源码编译ArUco库,可以参考这个网页,里面说的比较详细。一个普通的ArUco板大约是长这个样子。 板子由许多个像二维码的小块组成。之后我们会进一步介绍如何使用。

2.什么是AprilTag?

AprilTag是由University of Michigan的APRIL Robotics Laboratory提出的,官网是这里。官方自己对AprilTag的描述是视觉基准系统(Visual Fiducial System),其应用领域包括AR、机器人、相机校正等。通过对AprilTag Marker的识别,可以确定相机的位姿(相对于Marker)。AprilTag除了常规的方形,还可以包含其它“奇形怪状”的样子,如下。 一个普通的AprilTag大约长下面这个样子。 和ArUco板子类似的,它也是由许多像二维码的小块组成。之后我们会进一步介绍如何使用。

3.ArUco和AprilTag有什么异同?

总体而言,ArUco和AprilTag都是基于二维码的Marker,通过识别特定信息,实现对相机相对位姿的解算。不管是ArUco还是AprilTag都是比较流行的视觉基准系统。这里简单列举ArUco和AprilTag的优缺点,主要参考这个网页

ArUco

优点:

  • (1) 配置简单,基于OpenCV实现
  • (2) 更低的错误检测(默认参数)

缺点:

  • (1) 新版本ArUco换成了GPL许可,因此OpenCV中的ArUco停留在了老的BSD版本
  • (2) 在中等到远距离的场景,更容易受到旋转不确定性的影响
  • (3) 需要更多的参数
  • (4) 需要更多的密集计算

AprilTag

优点:

  • (1) BSD许可
  • (2) 更少的可调节参数
  • (3) 在长距离场景中表现依然较好
  • (4) 被NASA采用
  • (5) 更加灵活的marker设计(maker不一定是方形的)
  • (6) 计算量更少
  • (7) 支持Tag bundle,减少旋转歧义性
  • (8) 更多的误检测(默认参数)

4.ArUco的使用

本部分的所有代码上传到到了Github上,点击查看

4.1 ArUco的生成
4.1.1 单个ArUco Marker的生成

利用下面的代码可以生成单个ArUco Marker。

import cv2
import numpy as np

if __name__ == '__main__':
    marker_size = 200  # marker影像大小
    marker_id = 50  # marker的ID
    marker_boarder = 1  # marker边界

    # step1 加载预定义的字典
    dict = cv2.aruco.Dictionary_get(cv2.aruco.DICT_6X6_250)

    # step2 新建一张空白影像用于存放marker
    markerImg = np.zeros([marker_size, marker_size], np.uint8)

    # step3 获得marker内容并赋值
    # 第一个参数:使用的字典
    # 第二个参数:marker的id
    # 第三个参数:marker影像的像素大小
    # 第四个参数:边界宽度参数,表示将多少位(块)作为边界添加到marker中
    markerImg = cv2.aruco.drawMarker(dict, marker_id, marker_size, marker_boarder)

    # step4 保存marker
    cv2.imwrite("aruco_" + str(marker_id).zfill(3) + ".png", markerImg)

上述代码中,核心的就是cv2.aruco.drawMarker()函数,其余就是一些零碎操作了。运行上述代码,就会生成一个aruco_050.png的Marker影像,如下所示。 当然,你可以重复执行上述代码,进而生成多个Marker。

4.1.2 ArUco Board生成

为了使用方便,基于上面的代码,我们还可以生成具有多个ArUco Marker的板子,代码如下。

import cv2
import numpy as np

if __name__ == '__main__':
    grid_rows = 3  # 行数
    grid_cols = 5  # 列数
    grid_interval = 10  # marker之间的间隔像素
    grid_size = 200  # 单个marker像素大小
    marker_boarder = 1  # 单个marker边界
    grid_background = 1  # 0-black, 1-white

    # step1 加载预定义的字典
    dict = cv2.aruco.Dictionary_get(cv2.aruco.DICT_6X6_250)

    # step2 新建一张空白影像用于存放marker
    img_height = grid_rows * grid_size + (grid_rows + 1) * grid_interval
    img_width = grid_cols * grid_size + (grid_cols + 1) * grid_interval
    markerImg = np.zeros([img_height, img_width], np.uint8)
    if grid_background == 1:
        markerImg += 255

    for i in range(grid_cols):
        for j in range(grid_rows):
            tmp_index = i * grid_rows + j
            start_x = j * grid_size + (j + 1) * grid_interval
            start_y = i * grid_size + (i + 1) * grid_interval
            # step3 获得marker内容并赋值
            tmp_marker = cv2.aruco.drawMarker(dict, tmp_index, grid_size, marker_boarder)
            markerImg[start_x:start_x + grid_size, start_y:start_y + grid_size] = tmp_marker

    # step4 保存marker
    cv2.imwrite("arucoBoard.png", markerImg)

运行上述代码,可以得到如下所示的3×5的ArUco板子。

4.2 ArUco的检测

运行下面的代码,可以实现对ArUco的检测。

import cv2

if __name__ == '__main__':
    # step1 读取待检测影像
    img_path = "aruco_test.jpg"  # 影像路径
    img = cv2.imread(img_path)

    # step2 指定待检测Marker的字典并开始检测
    used_dict = cv2.aruco.Dictionary_get(cv2.aruco.DICT_6X6_250)
    markerCorners, markerIds, rejectedCandidates = cv2.aruco.detectMarkers(img, used_dict)

    # step3 解析结果并可视化
    # 需要注意的是ArUco检测结果角点的顺序是顺时针的:左上→右上→右下→左下
    # 另外,检测的坐标是整型数
    for i in range(len(markerCorners)):
        tmp_marker = markerCorners[i][0]
        print("Marker ID:", markerIds[i])
        print(tmp_marker)
        tmp_marker_tl = (tmp_marker[0][0], tmp_marker[0][1])
        tmp_marker_tr = (tmp_marker[1][0], tmp_marker[1][1])
        tmp_marker_br = (tmp_marker[2][0], tmp_marker[2][1])
        tmp_marker_bl = (tmp_marker[3][0], tmp_marker[3][1])
        cv2.circle(img, tmp_marker_tl, 10, (0, 0, 255), -1)
        cv2.circle(img, tmp_marker_tr, 10, (0, 255, 0), -1)
        cv2.circle(img, tmp_marker_br, 10, (255, 0, 0), -1)
        cv2.circle(img, tmp_marker_bl, 10, (0, 170, 255), -1)
        cv2.putText(img, "ID: " + str(markerIds[i]), (int(tmp_marker_tl[0] + 10), int(tmp_marker_tl[1] + 10)),
                    cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 1,
                    cv2.LINE_AA)

    # step4 保存图片
    cv2.imwrite("aruco_detection.jpg", img)

如下图所示,我们拍摄了一张ArUco影像。 我们运行上面的代码,即可实现对于角点的检测,如下。

5.AprilTag的使用

5.1 AprilTag的生成

AprilTag官方提供了可以自动生成Marker的代码,见Github。如果只是用一下,也可以直接下载生成好的图像,官方也给出了一些常用的Marker,见]Github。但是所有的Marker都很小,是10×10像素以下的,所以如果需要使用,还要再缩放一下。比如说可以在PS里重新设置图像大小,插值方式选择最近邻,如下图所示。 比如下图是从tag36h11文件夹中的mosaic.png图像中裁取的4行8列的Marker。 由于AprilTag的生成不能通过OpenCV进行,得要用它自己的库,所以这里就不再多做介绍了,感兴趣可以进一步了解。

5.2 AprilTag的检测

OpenCV虽然不能生成AprilTag,但是我们可以用其它库实现对AprilTag的检测。在Windows平台中,输入pip install pupil-apriltags即可安装,在Ubuntu平台中,输入pip install apriltag即可。然后编写代码,如下。

import cv2
import pupil_apriltags as apriltag

if __name__ == '__main__':
    # step1 读取影像并转成灰度(OpenCV中的AprilTag只支持灰度影像)
    img_path = "apriltag_test.jpg"  # 待检测影像路径
    img = cv2.imread(img_path)
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # step2 构造检测器开始检测
    detector = apriltag.Detector()
    results = detector.detect(img_gray)
    print("Found ", len(results), "apriltag markers")

    # step3 解析结果
    markerCorners = []
    markerIds = []
    for i in range(len(results)):
        tmp_obj = results[i]
        markerCorners.append([tmp_obj.corners])
        markerIds.append(tmp_obj.tag_id)

    # step4 可视化结果
    # 需要注意的是AprilTag检测结果角点的顺序是逆时针的:左下→右下→右上→左上
    # 另外,检测的坐标是浮点型小数,可视化的时候需要转成int类型
    for i in range(len(markerCorners)):
        tmp_marker = markerCorners[i][0]
        print("Marker ID:", markerIds[i])
        print(tmp_marker)
        tmp_marker_tl = (int(tmp_marker[3][0]), int(tmp_marker[3][1]))
        tmp_marker_tr = (int(tmp_marker[2][0]), int(tmp_marker[2][1]))
        tmp_marker_br = (int(tmp_marker[1][0]), int(tmp_marker[1][1]))
        tmp_marker_bl = (int(tmp_marker[0][0]), int(tmp_marker[0][1]))
        cv2.circle(img, tmp_marker_tl, 10, (0, 0, 255), -1)
        cv2.circle(img, tmp_marker_tr, 10, (0, 255, 0), -1)
        cv2.circle(img, tmp_marker_br, 10, (255, 0, 0), -1)
        cv2.circle(img, tmp_marker_bl, 10, (0, 170, 255), -1)
        cv2.putText(img, "ID: " + str(markerIds[i]), (int(tmp_marker_tl[0] + 10), int(tmp_marker_tl[1] + 10)),
                    cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 1,
                    cv2.LINE_AA)

    # step5 保存图片
    cv2.imwrite("apriltag_detection.jpg", img)

将上面裁剪的AprilTag板打印出来拍照,如下。 利用上述代码进行检测,如下(图片小的话可以放大看)。

6.进阶操作

6.1 基于ArUco标定板计算相机位姿

从本质上来说,计算位姿就是求解两个点集之间的变换关系。对于标定板和成像平面而言,因为都是平面,所以用单应变换是最合适的。进一步,为了求解这个单应变换,需要寻找两个点集之间的对应关系,找到之后,按常规求解即可。所以无论是ArUco Board、AprilTag Board还是Chess Board,都是为了方便找到这种对应关系而提出的。无论各种板子的检测算法是什么,最终检测的结果都是角点坐标。再配合我们板子各个角点的理论坐标,就可以计算了。一个简单的基于ArUco Board计算位姿的脚本如下。

import cv2
import numpy as np

if __name__ == '__main__':
    src_pts = []  # 某个Marker角点理论坐标值
    tar_pts = []  # 某个Marker角点实际坐标值

    # 构造第一个Marker的角点理论坐标值
    src_tl_pt = (0, 0)
    src_tr_pt = (200, 0)
    src_br_pt = (200, 200)
    src_bl_pt = (0, 200)
    src_pts.append(src_tl_pt)
    src_pts.append(src_tr_pt)
    src_pts.append(src_br_pt)
    src_pts.append(src_bl_pt)
    print("Src pts:\n", src_pts)

    # 读取影像
    img_path = "aruco_test.jpg"
    img = cv2.imread(img_path)

    # 检测Marker
    used_dict = cv2.aruco.Dictionary_get(cv2.aruco.DICT_6X6_250)
    markerCorners, markerIds, rejectedCandidates = cv2.aruco.detectMarkers(img, used_dict)

    # 获取第一个Marker的角点坐标
    # 需要注意的是,返回的列表并不是按照marker ID排序的,所以要获取ID为0的索引
    marker0_index = list(markerIds).index([0])
    tmp_block = markerCorners[marker0_index][0]
    tar_tl_pt = (tmp_block[0][0], tmp_block[0][1])
    tar_tr_pt = (tmp_block[1][0], tmp_block[1][1])
    tar_br_pt = (tmp_block[2][0], tmp_block[2][1])
    tar_bl_pt = (tmp_block[3][0], tmp_block[3][1])
    tar_pts.append(tar_tl_pt)
    tar_pts.append(tar_tr_pt)
    tar_pts.append(tar_br_pt)
    tar_pts.append(tar_bl_pt)
    print("Target pts:\n", tar_pts)

    # 利用得到的对应关系计算单应变换
    homo_mat, mask = cv2.findHomography(np.array(src_pts), np.array(tar_pts))
    print("Homography matrix:\n", homo_mat)

运行以后,可以得到单应矩阵,如下。

7.参考资料

  • [1] https://guyuehome.com/35459
  • [2] https://www.guyuehome.com/36463
  • [3] https://blog.csdn.net/u013019296/article/details/118426493
  • [4] https://zhuanlan.zhihu.com/p/159395546
  • [5] https://blog.csdn.net/u013019296/article/details/120030783
  • [6] https://blog.csdn.net/qq_44989881/article/details/118657230
  • [7] https://blog.csdn.net/weixin_42840360/article/details/120978589
  • [8] https://github.com/AprilRobotics/apriltag-imgs
  • [9] https://github.com/AprilRobotics/apriltag-generation

本文作者原创,未经许可不得转载,谢谢配合

返回顶部