十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Apriltag坐标系方向可视化:原理、Python实现与避坑指南

Apriltag坐标系方向可视化:原理、Python实现与避坑指南 做机器人和视觉定位的朋友应该都绕不开Apriltag。我最早碰Apriltag是2017年做巡检机器人的回充对接当时只需要知道码在画面里的位置后来慢慢发现真正麻烦的是判断“码头朝着哪个方向”。机器人接近充电桩、机械臂去抓取一个贴着Apriltag的工件光知道ID和中心点远远不够你必须知道码的X轴往哪偏、Z轴是不是朝向相机。这时候Apriltag坐标系方向可视化就成了调试里最顺手也最救命的工具。这套可视化说白了就是把检测到的Apriltag自身坐标系画出来X轴、Y轴、Z轴三条方向线直接叠加在图像或3D场景里一眼看出码当前的姿态。这篇文章我会从“为什么要可视化”讲起把坐标系定义和旋转矩阵这些原理铺开再给出一份可以直接抄的Python实现最后把我实际踩过的坑整理成排查表。正在做相机标定、机器人定位、机械臂抓取、AR姿态估计的同学或者被“旋转矩阵到底转没转对”折磨的人这篇应该能帮上忙。1. 需求背景与整体设计思路1.1 为什么非要可视化坐标系方向很多人刚开始用Apriltag觉得能检测出ID、能拿到中心点像素坐标就够了。但真到了位姿估计这一步问题就来了你拿到的是一串旋转矩阵或者四元数这玩意在代码里看就是9个浮点数鬼知道它代表的姿态对不对。我印象特别深的一次是做机械臂手眼标定。标定完之后算法告诉我“末端坐标系到相机坐标系的变换已经算出来了”但机械臂实际去抓的时候目标点偏了将近5厘米。后来我把Apriltag码贴在工件上把相机坐标系和码坐标系的轴都画出来一眼就发现问题旋转矩阵里有个符号反了导致Y轴指向了完全相反的方向。如果当时只盯着数字看可能还要排查一整天。所以坐标系方向可视化本质上是一件“把空间想象变成肉眼可见事实”的事情。它适合这几个场景验证相机标定、手眼标定结果是否正确调试机械臂抓取时确认码的Z轴法向量是否朝向目标方向开发AR应用时检查虚拟物体在码上的叠加方向做C形臂X光机这类医学影像设备标定时确认多个坐标系之间的旋转关系。人眼对图像非常敏感但对数字和矩阵非常迟钝。把方向画出来是一个低成本的调试手段。1.2 三种可视化方案怎么选实现坐标系方向可视化的方案不少我实际试过三种各有适用场景。第一种是直接在图像上用OpenCV画三个轴。原理是把码坐标系下的三根轴端点的三维坐标投影到像素坐标系然后用cv2.line()画出来。优点是轻量、直观你看到的就是相机看到的日常调试最推荐。第二种是用Matplotlib画三维场景。把相机坐标系、多个Apriltag码坐标系都放进同一个3D坐标空间里适合展示“机器人和目标物的相对位姿关系”。缺点是画起来比较重而且和实际图像对不上调试效率低一些。第三种是接RViz或者Open3D适合已经在用ROS、点云的场景。但很多2D视觉项目根本用不上这套重型工具杀鸡用牛刀。我这里重点讲的是第一种OpenCV 2D投影叠加。它足够简单却覆盖了绝大多数需求。后面的核心代码也围绕这个方案展开。1.3 可视化内容拆解一套完整的方向可视化画的东西可以分成四层第一层也是最核心的是三根轴。按行业惯例X轴红色、Y轴绿色、Z轴蓝色。这样看颜色就能脑补出坐标方向。第二层是码的中心点。通常在原点画一个小圆点方便定位码的几何中心。第三层是ID文本。直接把tag的ID画在码旁边多码场景下才知道自己在看哪个码。第四层是可选信息比如把欧拉角roll、pitch、yaw打印在ID旁边或者把法向量单独画出来。这个对判断“码是否正对相机”很有用。四层内容叠在一起基本就是一套完整的Apriltag可视化工具了。2. 核心细节坐标系定义与姿态提取原理2.1 Apriltag检测后拿到的到底是什么先理清一个基本问题Apriltag检测器到底能返回什么东西。大多数Python绑定库比如apriltag、pupil_apriltags在检测成功后会返回一个对象里面至少包含id码的IDcorners四个角点在图像上的像素坐标顺序一般按顺时针或者逆时针排好center码中心的像素坐标如果开启了位姿估计还会有pose_R和pose_t分别是旋转矩阵和平移向量。这里我要多说一句很多教程一上来就讲pose_R、pose_t但不同库对这两个量的约定其实有细微差异。尤其是pose_R到底是从相机坐标系转到码坐标系还是反过来不同版本可能不一样。所以我个人更推荐一个笨但稳的办法只用库返回的角点然后自己用PnP求位姿。这样每根轴的定义你心里清清楚楚出问题也好排查。标准的Apriltag坐标系定义是这样的原点在码的中心X轴沿码面水平向右Y轴沿码面垂直向下Z轴垂直码面朝外。注意这里“Y轴向下”和我们平时数学课上的直角坐标系不一样因为图像坐标系的原点在左上角、Y轴本来就在下边。这个问题很多人第一次接触都会懵后面我会单独说。2.2 旋转矩阵与欧拉角怎么转换拿到旋转矩阵R之后很多人第一反应是想提取欧拉角。这个可以但一定要清楚欧拉角有很多种定义方式同一个R在不同定义下算出来的roll、pitch、yaw完全不一样。我常用的提取方式是ZYX内旋代码长这样import numpy as np def rotation_matrix_to_euler(R): # R 是 3x3 旋转矩阵 sy np.sqrt(R[0, 0]**2 R[1, 0]**2) if sy 1e-6: roll np.arctan2(R[2, 1], R[2, 2]) pitch np.arctan2(-R[2, 0], sy) yaw np.arctan2(R[1, 0], R[0, 0]) else: # 万向锁情况pitch接近正负90度 roll np.arctan2(-R[1, 2], R[1, 1]) pitch np.arctan2(-R[2, 0], sy) yaw 0.0 return np.array([roll, pitch, yaw])这组公式在绝大多数场景下够用但必须提醒两点。第一万向锁问题。当pitch接近正负90度时roll和yaw会退化算出来的值跳跃很厉害。可视化的时候你会看到欧拉角度数在剧烈跳动这不是你的代码错了是欧拉角的数学缺陷。第二欧拉角适合给人看不适合给机器算。做判断、做插值、做位姿纠偏建议直接用旋转矩阵或四元数。欧拉角只用来打印到画面上帮你直觉判断当前码的姿态。2.3 三种坐标系别搞混做坐标系可视化至少涉及三个坐标系很多人画出来的轴不对往往就是这三者搞混了。图像坐标系原点在图像左上角x轴向右y轴向下单位是像素。相机坐标系原点在相机光心x轴向右y轴向下z轴沿光轴朝前单位是米。这是三维坐标也是相机“看到”世界的坐标系。码坐标系原点在Apriltag码中心x轴沿码面水平向右y轴沿码面垂直向下z轴垂直码面朝外单位是米。从码坐标系到相机坐标系靠的是[R|t]变换。从相机坐标系到图像坐标系靠的是相机内参矩阵K的投影。投影公式长这样s * [u, v, 1]^T K * [R|t] * [X, Y, Z, 1]^T其中[X, Y, Z]是码坐标系下的三维点K是相机内参[u, v]是图像像素坐标。新手最容易忽略的问题是图像坐标系Y轴向下。所以一个码正正地出现在画面里它的Y轴在图像中也是朝下的。有人会下意识觉得“方向不对”实际上这是完全正确的。只要把三根轴画出来对比一下你心里预期的方向就不会被这个反直觉的设定带偏。3. 实操过程与核心代码实现3.1 环境与依赖准备先准备环境。我用的是Python 3需要装的库如下pip install numpy opencv-python apriltag matplotlibapriltag这个库是AprilRobotics官方算法的一个Python绑定支持tag36h11、tag25h9这些常见家族。装好之后用import apriltag导入。另外需要准备一个Apriltag码图自己打印一张贴在纸板上就行。推荐tag36h11家族检测稳定性和抗遮挡能力都更好。还有一个容易被忽略的东西相机内参。你至少要知道相机的焦距fx、fy和光心cx、cy。这个可以通过棋盘格标定拿到。如果只是做初步验证可以用一个近似值顶一下但不要拿它做精密测量。3.2 第一步检测角点用PnP求位姿我建议不要直接依赖库的pose_R而是自己走一遍PnP。这样你对坐标系朝向的把控会更扎实。代码的核心逻辑是先检测出码的四个角点再把这四个角点和它们在码坐标系下的三维坐标对应起来交给cv2.solvePnP()解算。import numpy as np import cv2 import apriltag # 相机内参请替换成你自己的标定结果 K np.array([ [600.0, 0.0, 320.0], [0.0, 600.0, 240.0], [0.0, 0.0, 1.0] ], dtypenp.float64) # Apriltag码的实际物理边长单位米 tag_size 0.05 # 5cm # 码坐标系下四个角点的三维坐标 # 原点在码中心X向右Y向下Z垂直码面朝外 half tag_size / 2.0 obj_points np.array([ [-half, -half, 0.0], [ half, -half, 0.0], [ half, half, 0.0], [-half, half, 0.0] ], dtypenp.float64) detector apriltag.Detector() def detect_and_pose(image): gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) detections detector.detect(gray) for det in detections: corners det.corners.astype(np.float64) # 如果库没排序这里记得统一角点顺序 ret, rvec, tvec cv2.solvePnP(obj_points, corners, K, None) if not ret: continue # 画坐标系 draw_coordinate_axes(image, rvec, tvec, K, tag_size) # 画ID cv2.putText(image, ID: {}.format(det.id), (int(det.center[0]) - 20, int(det.center[1]) - 30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2) return imagervec和tvec就是码坐标系到相机坐标系的旋转向量和平移向量。tvec的物理含义是码中心在相机坐标系下的三维位置。3.3 三轴投影到图像并绘制拿到rvec和tvec之后下一步就是把三根轴画出来。具体思路是在码坐标系下定义原点(0,0,0)和X、Y、Z轴方向上的三个端点比如(L,0,0)、(0,L,0)、(0,0,L)。然后用cv2.projectPoints()把这四个三维点投影到图像上再用cv2.line()从原点画到各个端点。def draw_coordinate_axes(image, rvec, tvec, K, tag_size): axis_len tag_size * 0.8 # 轴长取码边长的80% # 码坐标系下的原点与三个轴端点 origin np.array([[0.0, 0.0, 0.0]], dtypenp.float64) axis_points np.array([ [axis_len, 0.0, 0.0], # X轴端点 [0.0, axis_len, 0.0], # Y轴端点 [0.0, 0.0, axis_len] # Z轴端点 ], dtypenp.float64) all_points np.vstack([origin, axis_points]) proj, _ cv2.projectPoints(all_points, rvec, tvec, K, None) origin_px tuple(proj[0].ravel().astype(int)) x_px tuple(proj[1].ravel().astype(int)) y_px tuple(proj[2].ravel().astype(int)) z_px tuple(proj[3].ravel().astype(int)) # 注意OpenCV 是 BGR所以 (0,0,255) 是红色 cv2.line(image, origin_px, x_px, (0, 0, 255), 2) # X 红 cv2.line(image, origin_px, y_px, (0, 255, 0), 2) # Y 绿 cv2.line(image, origin_px, z_px, (255, 0, 0), 2) # Z 蓝 # 在原点画一个小圆点方便定位 cv2.circle(image, origin_px, 4, (255, 255, 255), -1)这段代码跑起来之后你应该能在图像上看到从码中心伸出的三根彩色线。当码正对相机时Z轴蓝色会指向相机方向在图像里看上去就是一根很短的线段甚至是一个点。当码发生旋转时三根轴的方向会跟着实时变化这就是我们想要的效果。3.4 把欧拉角也打印到画面上三根轴只能看方向如果要记录或复现姿态最好把欧拉角也叠加上去。在detect_and_pose()里加几行就行# 把旋转向量转成旋转矩阵 R, _ cv2.Rodrigues(rvec) euler rotation_matrix_to_euler(R) # 弧度转角度 deg np.degrees(euler) info roll:{:.1f} pitch:{:.1f} yaw:{:.1f}.format(deg[0], deg[1], deg[2]) cv2.putText(image, info, (int(det.center[0]) - 20, int(det.center[1]) 40), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 255), 2)这样每个码下面会显示一组欧拉角。调试的时候你手转码体角度数字跟着变那种“代码掌控感”是很爽的。3.5 用Matplotlib画全局位姿关系如果你想看的不只是单个码在图像里的姿态还想看清相机和多个码在三维空间里的相对关系可以用Matplotlib画一个简单的三维场景。思路是把相机放在原点根据每个码的rvec、tvec画出它的三轴。from mpl_toolkits.mplot3d import Axes3D import matplotlib.pyplot as plt fig plt.figure() ax fig.add_subplot(111, projection3d) # 相机在原点画一个小坐标 ax.quiver(0, 0, 0, 0.1, 0, 0, colorr) ax.quiver(0, 0, 0, 0, 0.1, 0, colorg) ax.quiver(0, 0, 0, 0, 0, 0.1, colorb) # 对每个tag画轴 for det, rvec, tvec in tag_list: R, _ cv2.Rodrigues(rvec) origin_cam tvec.ravel() x_axis R[:, 0] * 0.05 y_axis R[:, 1] * 0.05 z_axis R[:, 2] * 0.05 ax.quiver(*origin_cam, *x_axis, colorr) ax.quiver(*origin_cam, *y_axis, colorg) ax.quiver(*origin_cam, *z_axis, colorb) ax.set_xlabel(X) ax.set_ylabel(Y) ax.set_zlabel(Z) plt.show()这个三维图适合写在周报里或者用来向别人解释“我的标定结果长什么样”。日常调试我还是更推荐直接用图像叠加效率最高。4. 常见问题与避坑技巧4.1 问题速查表我在不同项目里反复调试Apriltag可视化遇到过的怪问题基本可以汇总成一张表现象可能原因解决方案Z轴指向码背后不朝向相机库返回的pose_R约定不同或你自己构造的角点顺序反了统一角点顺序检查R是否右手系必要时对R第三列取反图像里的Y轴方向和我预期相反图像坐标系Y轴向下不是数学系里的Y轴向上记住这是正常现象看三根轴的整体右手关系来判断轴在画面上抖动或忽长忽短相机内参不准确或码距离相机太远重新标定相机让码在画面中占足够面积欧拉角数值剧烈跳变万向锁问题欧拉角只用于显示不要作为控制量用多个码时ID和轴对不上没有给每个检测结果做单独绘制在同一个循环里用一个变量保存当前码的信息库的pose_R和自己PnP算出来的R不一致不同库的坐标系原点定义不同不要混用两套结果统一用一种方式这张表是我在调试中最常翻的。很多问题不是算法错了而是约定不一致。4.2 相机内参没标定的后果坐标系方向可视化里面相机内参的准确性直接影响可视化结果。如果内参不准最典型的症状是码明明正对相机Z轴画出来却歪向一边或者你转动码体的时候三根轴的变化幅度和实际旋转幅度对不上。我在早期图省事直接拿手机标称焦距当fx、fy结果画出来的轴在画面边缘偏移很严重越靠近边缘歪得越厉害。后来老老实实用棋盘格跑了一次完整标定问题立刻消失。用OpenCV做单目标定其实不麻烦大致流程是拍20张不同角度的棋盘格照片用cv2.findChessboardCorners()找角点再用cv2.calibrateCamera()算内参和畸变系数。标定完之后记得在读取图像时用cv2.undistort()去除畸变否则画面边缘的轴依然会偏。注意如果你只是做个demo用近似内参也能看到三根轴的大致方向但千万不要把这种结果作为机械臂抓取或者标定的依据。精密场景下内参必须实测标定。4.3 怎么验证可视化结果是不是对的画出来的轴到底对不对我有一个土办法屡试不爽。打印一张Apriltag码贴在纸板上。然后把相机固定用手拿着纸板做几个已知动作观察画面里的三根轴第一码正对相机时Z轴应该指向相机。在图像中Z轴投影最短且指向画面外方向几乎看不出来。如果Z轴很明显地指向画面深处说明定义有问题。第二把码绕X轴旋转90度Y轴应该从竖直方向变成水平方向Z轴从指向相机变成指向左侧或右侧。跟着感觉走三根轴的变化应该符合右手定则。第三打印两张不同的码把码A绕Z轴旋转180度观察它X轴和Y轴应该整体掉头。这能确认旋转矩阵的正负号是否正确。还有个数学检查方法旋转矩阵的行列式必须为1。如果np.linalg.det(R)为-1说明坐标轴里有一根被镜像了也就是左右手系搞反了。R, _ cv2.Rodrigues(rvec) det np.linalg.det(R) assert abs(det - 1.0) 1e-6, 旋转矩阵不是右手系只要两步检查都通过你的可视化结果基本就是可信的。4.4 一些小工具习惯最后说几个我用顺手的小习惯。每次拿到新相机先标定然后把内参和一个测试Apriltag图放同一个文件夹跑一遍这个可视化demo确认画面里三根轴正常再继续下一步。写可视化代码时把颜色固定下来X红、Y绿、Z蓝。不管是在图像里还是三维图里保持一致。这样换项目时不用重新适应。如果你发现某个库返回的pose_R方向总是和预期相反不要慌直接在我上面的代码里手动把obj_points的角点顺序换一下或者对R做一次左右乘修正通常能解决。重点是你自己要知道最终输出的坐标系定义是什么。还有一点调试用的Apriltag尽量贴在有纹理的平面上别贴在反光材料上。反光会造成角点检测偏移三根轴画出来就会歪。光滑塑料、玻璃表面都不合适哑光纸打印最稳。这几条习惯看着琐碎但都来自真实踩坑。很多时候项目推进不下去不是算法不行而是这些边界细节没处理好。我个人在实际操作中的体会是坐标系方向可视化这个工具一旦用顺了就会成为你做视觉相关项目的标配。它不只是画三根线更像是在空间感知和代码逻辑之间架了一座桥。每次拿起相机先让三根轴出现在画面里再开始调参数思路都会清楚很多。
返回列表