1. 什么是 AprilTag

AprilTag 是一种广泛应用于机器人、无人机、增强现实和相机标定中的视觉基准标记系统。它由密歇根大学的 April Robotics Lab 提出,通过识别黑白方块组成的编码图案,可以在单张图像中快速、鲁棒地检测标签的 ID,并估计标签相对于相机的三维位置与姿态。

与常见的 QR 码、ArUco 码相比,AprilTag 具有以下优势:

  • 检测速度快,适合实时视觉系统;
  • 抗模糊、抗光照变化能力较强;
  • 支持更小的标签尺寸,适用于高精度定位;
  • 提供丰富的标签家族,如 tag36h11tag25h9tag16h5 等。

2. AprilTag 定位的基本原理

AprilTag 的定位过程通常分为两个层次:检测与位姿估计。

2.1 标签检测

标签检测的目的是在图像中找到 AprilTag 的四边形轮廓,并识别出标签 ID。典型流程如下:

  1. 将图像转换为灰度图;
  2. 计算图像梯度,寻找边缘;
  3. 对边缘进行聚类,提取候选四边形;
  4. 对四边形区域进行透视变换,得到标准化的标签图像;
  5. 将标准化图像与已知标签编码库进行匹配,得到标签 ID。

2.2 位姿估计

在检测到标签的四个角点后,利用透视投影模型求解相机与标签之间的刚体变换。

设相机的内参矩阵为 K,标签上某点的世界坐标为 P_w,其对应的像素坐标为 p,则满足:

s * p = K * [R | t] * P_w

其中 R 是旋转矩阵,t 是平移向量,s 是尺度因子。

求解该问题的常用方法是 PnP,包括:

  • DLT 直接线性变换;
  • P3P;
  • EPnP;
  • 非线性优化。

AprilTag 定位中通常先使用 DLT 或 EPnP 给出初值,再通过 Levenberg-Marquardt 等方法进行非线性优化,以提高位姿精度。

3. 环境准备

本文以 Python 为例,使用 OpenCV 与 apriltag 库完成检测与定位。

安装依赖:

pip install opencv-python
pip install apriltag

如果你的 OpenCV 版本自带了 cv2.aruco,也可以用它实现类似功能。本文重点介绍 apriltag 库,因为它对 AprilTag 家族支持更完整。

验证安装:

import cv2
import apriltag

print("OpenCV 版本:", cv2.__version__)
print("AprilTag 库加载成功")

4. 相机内参标定

要获得真实世界的距离信息,必须先标定相机内参。相机内参描述了像素坐标系与相机坐标系之间的映射关系,主要包括焦距、主点和畸变系数。

使用 OpenCV 棋盘格标定的基本代码如下:

import cv2
import numpy as np
import glob

chessboard_size = (9, 6)
square_size = 0.025  # 棋盘格方块尺寸,单位:米

objp = np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2)
objp *= square_size

objpoints = []
imgpoints = []

images = glob.glob('calibration_images/*.jpg')

for fname in images:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    ret, corners = cv2.findChessboardCorners(gray, chessboard_size, None)

    if ret:
        objpoints.append(objp)
        corners2 = cv2.cornerSubPix(
            gray,
            corners,
            (11, 11),
            (-1, -1),
            (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001),
        )
        imgpoints.append(corners2)

ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
    objpoints, imgpoints, gray.shape[::-1], None, None
)

print("相机内参矩阵:")
print(mtx)
print("畸变系数:")
print(dist)

np.savez('camera_calibration.npz', mtx=mtx, dist=dist)

标定完成后,保存内参矩阵与畸变系数,后续定位时直接加载即可。

5. 生成与检测 AprilTag

5.1 生成 AprilTag 图片

可以使用 apriltag 库生成标签图片,方便打印后粘贴到目标物体上:

from apriltag import apriltag_generator

generator = apriltag_generator('tag36h11')
tag = generator.generate(tag_id=0)
tag.save('tag36h11_id0.png')

也可以使用在线工具生成 PNG 或 SVG 文件。实际使用时应选择合适的大小,通常标签边长在 5 到 20 厘米之间更容易获得稳定的检测与定位结果。

5.2 检测 AprilTag

读取图像并检测所有标签:

import cv2
import apriltag

image = cv2.imread('scene.jpg', cv2.IMREAD_GRAYSCALE)

options = apriltag.DetectorOptions(
    families='tag36h11',
    nthreads=4,
    quad_decimate=1.0,
    quad_sigma=0.0,
    refine_edges=True,
    decode_sharpening=0.25,
    debug=False,
)

detector = apriltag.Detector(options)
results = detector.detect(image)

print(f"检测到 {len(results)} 个 AprilTag")

for r in results:
    print(f"标签 ID: {r.tag_id}")
    print(f"中心坐标: {r.center}")
    print(f"四个角点: {r.corners}")

检测结果中的 corners 顺序通常为左下、右下、右上、左上,但在不同版本或库中可能有所不同。涉及位姿计算时,务必确认角点顺序与使用的 PnP 接口要求一致。

6. 计算标签位姿

检测到标签角点后,结合相机内参即可求解标签相对于相机的旋转矩阵和平移向量。

6.1 使用 OpenCV solvePnP

import cv2
import numpy as np
import apriltag

# 加载相机内参与畸变系数
with np.load('camera_calibration.npz') as data:
    mtx = data['mtx']
    dist = data['dist']

# 标签边长,单位:米
tag_size = 0.108

# 标签坐标系下的四个角点:左上、右上、右下、左下
# 原点位于标签中心
half = tag_size / 2.0
object_points = np.array([
    [-half,  half, 0],
    [ half,  half, 0],
    [ half, -half, 0],
    [-half, -half, 0],
], dtype=np.float64)

image = cv2.imread('scene.jpg', cv2.IMREAD_GRAYSCALE)

options = apriltag.DetectorOptions(families='tag36h11')
detector = apriltag.Detector(options)
results = detector.detect(image)

for r in results:
    image_points = r.corners.reshape(-1, 2).astype(np.float64)

    ret, rvec, tvec = cv2.solvePnP(
        object_points,
        image_points,
        mtx,
        dist,
        flags=cv2.SOLVEPNP_IPPE_SQUARE,
    )

    # 将旋转向量转换为旋转矩阵
    R, _ = cv2.Rodrigues(rvec)

    print(f"标签 {r.tag_id} 的平移向量 tvec: {tvec.ravel()}")
    print(f"标签 {r.tag_id} 的旋转矩阵 R:")
    print(R)

    x, y, z = tvec.ravel()
    distance = np.linalg.norm(tvec)
    print(f"标签到相机的距离约为 {distance:.3f} 米")

6.2 使用 detector.detection_pose

apriltag 库也提供了底层的位姿估计接口:

import apriltag

for r in results:
    pose, init_error, final_error = detector.detection_pose(
        r,
        (fx, fy, cx, cy),
        tag_size=tag_size,
    )

    print(f"标签 {r.tag_id} 的位姿矩阵:")
    print(pose)
    print(f"初始误差: {init_error:.5f}, 优化后误差: {final_error:.5f}")

其中 fxfycxcy 来自相机内参矩阵:

fx = mtx[0, 0]
fy = mtx[1, 1]
cx = mtx[0, 2]
cy = mtx[1, 2]

pose 通常是一个 4x4 的齐次变换矩阵,表示从相机坐标系到标签坐标系的变换。使用前建议核对所用库版本的矩阵方向约定,避免出现旋转方向相反的问题。

7. 在图像上绘制定位结果

为了直观查看检测与定位效果,可以将标签边框、ID 和坐标信息绘制到原始图像上:

import cv2

color_image = cv2.imread('scene.jpg')

for r in results:
    corners = r.corners.astype(int)

    # 绘制标签边框
    cv2.polylines(color_image, [corners], True, (0, 255, 0), 2)

    # 计算中心点
    cx, cy = map(int, r.center)
    cv2.circle(color_image, (cx, cy), 5, (0, 0, 255), -1)

    # 显示标签 ID
    cv2.putText(
        color_image,
        f"ID: {r.tag_id}",
        (cx - 30, cy - 15),
        cv2.FONT_HERSHEY_SIMPLEX,
        0.7,
        (255, 0, 0),
        2,
    )

cv2.imwrite('detected_result.jpg', color_image)

如果已经计算出平移向量,还可以将相机到标签的距离显示在画面上:

distance_text = f"Distance: {distance:.2f} m"
cv2.putText(
    color_image,
    distance_text,
    (30, 30),
    cv2.FONT_HERSHEY_SIMPLEX,
    0.8,
    (0, 0, 255),
    2,
)

8. 实时视频定位

将上述流程应用到摄像头视频流中,即可实现实时定位:

import cv2
import numpy as np
import apriltag

with np.load('camera_calibration.npz') as data:
    mtx = data['mtx']
    dist = data['dist']

tag_size = 0.108
half = tag_size / 2.0
object_points = np.array([
    [-half,  half, 0],
    [ half,  half, 0],
    [ half, -half, 0],
    [-half, -half, 0],
], dtype=np.float64)

cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)

options = apriltag.DetectorOptions(families='tag36h11', refine_edges=True)
detector = apriltag.Detector(options)

while True:
    ret, frame = cap.read()
    if not ret:
        break

    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    results = detector.detect(gray)

    for r in results:
        image_points = r.corners.reshape(-1, 2).astype(np.float64)
        ret, rvec, tvec = cv2.solvePnP(
            object_points,
            image_points,
            mtx,
            dist,
            flags=cv2.SOLVEPNP_IPPE_SQUARE,
        )

        distance = np.linalg.norm(tvec)

        corners = r.corners.astype(int)
        cv2.polylines(frame, [corners], True, (0, 255, 0), 2)
        cv2.putText(
            frame,
            f"ID: {r.tag_id}  D: {distance:.2f} m",
            (int(r.center[0] - 50), int(r.center[1] - 20)),
            cv2.FONT_HERSHEY_SIMPLEX,
            0.7,
            (0, 0, 255),
            2,
        )

    cv2.imshow('AprilTag Tracking', frame)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

运行后,将打印出来的 AprilTag 放在摄像头前方,即可看到标签被实时框选,并显示标签 ID 与距离信息。

9. 提高定位精度的建议

9.1 保证标签平整

AprilTag 位姿估计算法默认标签位于平面上。如果标签弯曲、褶皱或贴在曲面物体上,会导致角点检测误差增大,进而影响定位精度。

9.2 选择合适的标签尺寸

标签过小会导致图像中的角点像素数量不足,检测不稳定;标签过大又会限制相机与标签之间的有效距离范围。建议根据工作距离选择标签尺寸,使标签在图像中的边长保持在 20 到 100 像素以上。

9.3 控制光照条件

强烈反光、过暗或阴影过重都可能使标签边缘提取失败。可使用散射光源,并尽量保证标签表面光照均匀。

9.4 使用较高的图像分辨率

分辨率越高,角点定位越精细。但高分辨率会增加计算时间,实际应用中需要在精度与速度之间权衡。

9.5 开启亚像素角点细化

在检测选项中设置 refine_edges=True,并对角点使用 cv2.cornerSubPix 进一步细化,可以显著提高位姿估计精度。

9.6 优化相机标定质量

标定图像的覆盖范围、角度多样性和数量会直接影响内参精度。建议拍摄至少 15 到 20 张不同姿态的棋盘格图像,并在标定时排除重投影误差过大的图像。

10. 常见问题与排查

10.1 检测不到标签

可能原因:

  • 标签不在图像中或距离过远;
  • 图像模糊、运动模糊严重;
  • 光照过强或过暗;
  • 标签被遮挡;
  • 标签家族设置错误,例如使用了 tag36h11 标签却配置为 tag25h9
  • 图像分辨率过低。

排查方法:

  • 适当靠近标签;
  • 降低相机运动速度;
  • 调整光照;
  • 确认 families 参数与实际标签一致;
  • 尝试将 quad_decimate 调小,如设为 0.5,以提高对远距离标签的检测能力。

10.2 距离误差较大

可能原因:

  • 相机内参不准确;
  • 标签物理尺寸设置错误,例如实际标签为 10.8 厘米,代码中却写成 0.050 米;
  • 标签平面与相机光轴夹角过大;
  • 标签发生了透视变形或弯曲。

10.3 检测结果抖动

可以加入滤波算法进行平滑,常用的有一阶低通滤波、卡尔曼滤波等。以下是一个简单的一阶低通滤波示例:

alpha = 0.3
smoothed_distance = None

for r in results:
    # ... 计算 distance
    if smoothed_distance is None:
        smoothed_distance = distance
    else:
        smoothed_distance = alpha * distance + (1 - alpha) * smoothed_distance

11. 典型应用场景

  • 移动机器人物料搬运:将 AprilTag 粘贴在货架或工位上,帮助机器人进行视觉引导定位;
  • 无人机平台精准降落:地面铺设 AprilTag,无人机通过识别标签实现自主着陆;
  • 增强现实:将虚拟物体固定在 AprilTag 上,实现虚实融合;
  • 相机标定与多相机组网:利用 AprilTag 作为标定板,标定多相机系统外参;
  • 自动驾驶与仓储 AGV:作为低成本、高精度的视觉定位标记。

12. 总结

AprilTag 提供了一套完整的视觉基准标记检测与定位方案。通过“边缘检测 → 四边形提取 → ID 识别 → 角点匹配 → PnP 位姿求解”的流程,可以在已知相机内参和标签物理尺寸的前提下,实时估计标签相对于相机的三维位置与姿态。

使用 AprilTag 进行定位时,关键要点包括:

  • 正确选择标签家族并保证家族参数一致;
  • 认真完成相机标定;
  • 准确设置标签物理尺寸;
  • 保持良好的成像条件;
  • 必要时对检测结果进行平滑滤波。

在移动机器人、无人机、增强现实等领域,AprilTag 已经成为一种成熟且可靠的视觉定位方案,值得深入学习和实践。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐