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
本文作者原创,未经许可不得转载,谢谢配合