简介:这是一个基于ROS的手眼标定完整实战项目,适合机器人视觉、机械臂控制领域的开发者和研究人员。资源围绕手眼标定核心问题,提供项目源码与流程教程,涵盖Tsai-Lenz等标定算法、图像处理、坐标转换等关键环节,帮助用户理解并实现相机与机械臂之间的位姿关系求解。压缩包共78个文件,包含19个Python脚本、11个.launch启动文件、3个C++源文件和头文件、配置文件、日志与CSV数据等,整体约5.25MB,目录结构清晰,便于按模块学习。已有459人学习下载。项目不仅附有可直接运行的源码,还提供详细流程教程与实战案例分析,可指导用户从环境准备、图像采集到参数求解、结果验证完整走通标定流程。特别针对Aubo与Jaka机械臂通信场景做了适配,配合README文档,能显著降低上手门槛,非常适合作为机器人手眼标定入门到进阶的优质参考项目。
1. 手眼标定到底在解什么:别急着调参,先把坐标变换理清
机械臂装上相机后抓取不准,多数情况不是视觉算法的问题,而是相机坐标系和机械臂基座坐标系之间那个 4×4 变换矩阵没对齐。手眼标定要解的就是这个矩阵,数学上最终收敛成 AX=XB 的矩阵方程。反直觉的一点是:整个过程不需要知道相机相对于机器人装在哪、装成什么角度,只要让机械臂带着标定板或者相机走十几组不同姿态,记录机械臂末端位姿和标定板在相机下的位姿,就能把这个固定变换解出来。这套流程对所有要走视觉抓取、视觉定位、视觉引导装配的 ROS 项目都适用,无论是眼在手上的协作臂,还是眼在手外的固定相机工位。下面按数据构造、OpenCV 求解、ROS 数据流、结果验证的顺序拆开讲,照着做两个小时内能拿到第一版可用的外参。
2. 手眼标定原理:两种安装构型下的 AX=XB 数据构造
先明确“手”和“眼”分别指什么。“手”是机械臂末端,也就是法兰盘或 tool0 坐标系;“眼”是相机。手眼标定的目标,是求相机坐标系和某个机器人坐标系之间的固定齐次变换矩阵 H。眼在手上(eye-in-hand)时相机装在末端,标定板固定在外部;眼在手外(eye-to-hand)时相机固定在工作区,标定板装在末端法兰上。两种构型在 ROS 项目里都很常见,实验数据的格式和 OpenCV 接口的输入约定有细微差别,稍不注意就会解出一组“看起来合理、实际全错”的结果。
2.1 AX=XB 方程为什么成立
用 T_gripper2base 表示机械臂末端在基座坐标系下的位姿,由机器人正运动学给出;用 T_target2cam 表示标定板在相机坐标系下的位姿,由 solvePnP 对图像中角点求解得到;用 X 表示待求的相机到末端的变换矩阵。同一时刻,标定板在基座坐标系下的位姿可以写成:
T_target2base = T_gripper2base × X × T_target2cam
其中 T_target2base 是一个常量,因为标定板固定在工作台上。取两个不同时刻 i、j 的观测值,把上式两边分别求逆后联立,可以消掉 T_target2base:
inv(T_gripper2base_i) × T_gripper2base_j × X = X × T_target2cam_j × inv(T_target2cam_i)
这正是 AX=XB 的标准形态。OpenCV 的 calibrateHandEye 接受四个输入数组:R_gripper2base、t_gripper2base、R_target2cam、t_target2cam,输出 R_cam2gripper、t_cam2gripper,也就是 X。接口内部已经实现了 Tsai-Lens、Park、Horaud、Andreff、Daniilidis 等算法,不需要自己解非线性方程。
2.2 眼在手外时怎么套 OpenCV 的接口
eye-to-hand 构型下待求量从 cam→tool 变成了 cam→base,直接套用接口会得出错误的坐标关系。常见做法是先把采集到的 T_gripper2base 取逆得到 T_base2gripper,把 T_target2cam 取逆得到 T_cam2target,再喂给同一个接口,最后把输出结果取逆,得到的就是固定相机在基座坐标系下的位姿。这个换算特别容易写反,我一般会在代码里加一步打印验证:用任意一组数据重组 T_target2base,验证残差小于 1e-6 才继续往下走。
| 构型 | 相机位置 | 待求变换 | 输入 A 来源 | 输入 B 来源 | 输出 X 含义 |
|---|---|---|---|---|---|
| 眼在手上 | 装在末端 | cam→tool | 末端→基座运动 | 标定板→相机运动 | cam→tool |
| 眼在手外 | 固定在外 | cam→base | 基座→末端(取逆) | 相机→标定板(取逆) | 取逆后 cam→base |
2.3 数据规范:单位、四元数与旋转顺序
数据规范是“标定结果看起来能用、一抓就偏”的最大来源。三点必须统一:平移单位一律用米,很多机械臂 SDK 会返回毫米,混合单位会让平移残差差出三个数量级;四元数在喂给 OpenCV 前要归一化,ROS 消息里的四元数通常已经归一化,但从欧拉角手动转过来时容易漏;旋转矩阵和四元数互转时,OpenCV 的 cv2.Rodrigues 与 tf 的 quaternion_matrix 都遵循右乘、ZYX 顺序约定,不要混用不同库的约定。
提示:采集数据时不要只平移机械臂,要确保每两组位姿之间末端旋转角度超过 30°。纯平移会让 AX=XB 方程病态,标出的平移分量会严重偏离真实值。
3. 用 OpenCV calibrateHandEye 在 ROS 环境里跑通最小闭环
环境准备以 Ubuntu 20.04 + ROS Noetic + Python 3 + OpenCV 4 为例。如果机器上还没有 ROS,小鱼ROS一键安装脚本可以快速搭好基础环境,装完再补 opencv-python 和相机驱动相关依赖。下面这套代码可以脱离机械臂用仿真数据跑通,也可以直接封装成 ROS 节点接入真实数据。
3.1 最小可复现的 calibrateHandEye 调用
import cv2 import numpy as np def solve_handeye(mech_poses, cam_poses, method=cv2.CALIB_HAND_EYE_PARK): # mech_poses: 末端在基座坐标系的 4x4 齐次矩阵列表(眼在手上场景) # cam_poses: 标定板在相机坐标系的 4x4 齐次矩阵列表 R_g2b = np.array([p[:3, :3] for p in mech_poses]) t_g2b = np.array([p[:3, 3] for p in mech_poses]).reshape(-1, 3, 1) R_t2c = np.array([p[:3, :3] for p in cam_poses]) t_t2c = np.array([p[:3, 3] for p in cam_poses]).reshape(-1, 3, 1) R_c2g, t_c2g = cv2.calibrateHandEye( R_g2b, t_g2b, R_t2c, t_t2c, method=method) X = np.eye(4) X[:3, :3] = R_c2g X[:3, 3] = t_c2g.flatten() return X这段代码里 calibrateHandEye 的输入单位是米和弧度。method 参数有五种可选:CALIB_HAND_EYE_TSAI 计算快但噪声敏感,CALIB_HAND_EYE_PARK 对旋转噪声更稳,CALIB_HAND_EYE_HORAUD 和 ANDREFF 介于两者之间,CALIB_HAND_EYE_DANIILIDIS 基于对偶四元数,数据质量差时鲁棒性最好。我一般先用 Park 出一版结果,残差偏大再换 Daniilidis 对比。t_g2b 和 t_t2c 必须 reshape 成 N×3×1,OpenCV 对输入形状有严格检查,少了这一步会直接抛异常。
3.2 求解后的残差回代验证
拿到 X 之后不能只看矩阵数值是否“像样”,必须用原始数据回代 AX=XB 方程。用任意一组 A、B 计算 A 与 X@B@inv(X) 的差,就能量化标定质量:
def handeye_residual(A_list, X, B_list): err = 0.0 for A, B in zip(A_list, B_list): # 理论关系: A = X @ B @ inv(X) pred = X @ B @ np.linalg.inv(X) err = max(err, float(np.linalg.norm(A - pred))) return err残差由旋转和平移混合叠加。经验阈值是:平移残差小于 5mm、旋转残差对应的等效轴角小于 0.01rad,标定结果可以进入后续验证。如果残差达到厘米级,先回头检查数据构造,而不是盲目更换求解算法。
3.3 把标定结果发布成 static_transform_publisher
拿到 X 后,最直接的落地方式是在 launch 文件里把它固化成静态 TF:
<node pkg="tf2_ros" type="static_transform_publisher" name="camera_to_tool" args="0.06 -0.01 0.12 -0.003 0.02 -0.015 tool0 camera_link" />xyz 和 rpy 从 X 分解出来:平移取 X[:3, 3],旋转用 tf.transformations.euler_from_matrix 转成 rpy。需要特别注意,static_transform_publisher 后面六个参数是 x y z yaw pitch roll,顺序是偏航、俯仰、横滚,不是机械臂 URDF 里常见的 XYZRPY 顺序。发布完成后,在 rviz 里显示 camera_link 坐标系,用一个固定标识物放在相机视野内,观察坐标轴是否与实物方位一致,这一步能直观暴露符号错误。
4. ROS 实战链路:从话题数据到机械臂标定的完整流程
最小闭环跑通后,剩下的活就是把 ROS 里的真实数据接进来替换测试数据,同时保证标定板检测和机械臂位姿获取在时间上同步。整套链路涉及三个数据源:机械臂末端位姿、标定板在相机下的位姿、相机内参。
4.1 机械臂末端位姿的三条获取路径
最常见的获取方式有三种:直接读 MoveIt 的 planning scene,planning_frame 通常就是 base_link;订阅 /joint_states 自己调正运动学库;读 /tf 树里 base_link 到 tool0 的变换。我一般用第三种,命令最直接:
rosrun tf2_ros tf2_echo base_link tool0输出里包含 position 和 quaternion,用 tf.transformations.quaternion_matrix 转成 4×4 矩阵再填充平移向量。这里有个隐蔽的坑:工具坐标系的定义每家机械臂不一样,有的 tool0 在法兰盘中心,有的带工具之后重新定义了 tool_tcp,手眼标定用到的“末端”必须是实际安装相机那个法兰面,而不是加了工具之后的 TCP。UR 机械臂、国产协作臂都暴露了标准 TCP 接口,但一定要确认你订阅的坐标系名称对应的是安装面。
4.2 标定板位姿:aruco_ros、charuco_detect 与手动 solvePnP
ROS 生态里最省事的是 aruco_ros 或 charuco_detect 包,直接订阅 /aruco_single/pose 就能拿到 marker 在相机坐标系下的位姿。charuco 板比单 marker 检测更稳,角点数量多,solvePnP 的数值稳定性更好。如果相机内参没标过,先做一次内参标定,否则标定板位姿里的平移会偏差几个毫米,直接污染手眼标定结果。camera_calibration 工具可以快速完成:
rosrun camera_calibration cameracalibrator.py --size 9x7 --square 0.024 image:=/camera/color/image_raw执行完会得到 camera_info 话题上的内参矩阵,后续的 solvePnP 和 aruco 检测都用这套内参。棋盘格边长 0.024 对应 24mm,实际使用以你打印的棋盘格为准。
4.3 采集脚本:按空格键记录一组位姿
| 数据项 | ROS 消息类型 | 转换目标 |
|---|---|---|
| 末端位姿 | tf2_msgs/TFMessage | 4×4 齐次矩阵 |
| 标定板位姿 | geometry_msgs/PoseStamped | 4×4 齐次矩阵 |
| 相机内参 | sensor_msgs/CameraInfo | 3×3 矩阵 |
采集脚本负责把这两类数据同步保存。下面是一个轻量实现,订阅 TF 和标定板位姿,收到按键指令就记录一组:
import rospy import tf2_ros import numpy as np class HandEyeCollector: def __init__(self): self.tfbuf = tf2_ros.Buffer() self.listener = tf2_ros.TransformListener(self.tfbuf) self.samples = [] def capture(self): try: trans = self.tfbuf.lookup_transform( 'base_link', 'tool0', rospy.Time(0)) # 此处 pose_sub 保留最近一帧 /aruco_single/pose H_g2b = tf2_transform_to_matrix(trans) H_t2c = pose_msg_to_matrix(self.pose_sub.latest) self.samples.append((H_g2b, H_t2c)) rospy.loginfo("captured %d", len(self.samples)) except Exception as e: rospy.logwarn(str(e))这段代码的关键在于查询时间戳。lookup_transform 用 rospy.Time(0) 取最近可用变换,标定板位姿也取最近一帧缓存,两者在时间上的偏差控制在几十毫秒内就行,毕竟机械臂在采集时保持静止,不需要做时间插值。采集完成后把 samples 存成 npz,标定脚本直接加载。
5. 姿态采样策略、交叉验证与三个避坑点
手眼标定的数据质量比算法选择更重要,用对采样策略就能显著提升结果稳定性。最后一章落在这组具体方法上。
5.1 姿态采样策略
| 参数 | 推荐值 | 原因 |
|---|---|---|
| 采样组数 | 15~25 组 | 太少欠定,太多浪费时间 |
| 相邻旋转变换 | 大于 30° | 保证 AX=XB 方程良态 |
| 标定板占画面比例 | 1/3 到 1/2 | solvePnP 特征点分布均匀 |
| 标定板到相机距离 | 0.3m 到 0.8m | 覆盖实际工作范围 |
手动示教时,让末端每采一组就绕自身 z 轴转一个角度,同时改变俯仰,尽量覆盖相机可见空间的不同位置。不推荐使用纯平移轨迹或者小幅抖动式采集,那只会让数据近乎线性相关,解算出的平移分量严重失真。
5.2 留出验证集:交叉验证重投影残差
把 20 组数据分成 16 组建模、4 组验证,用建模组的 X 反推验证组里标定板在基座坐标系下的位姿,再投影回像素坐标,和图像里实际检测到的角点位置对比。像素残差稳定在 2px 以内,说明标定结果可以用于视觉引导。这比只看矩阵元数值是否“正常”可靠得多,因为旋转矩阵的四元数表示即使有几十毫米平移误差,数值看起来依然人模人样。
5.3 三个最常踩的坑
第一,eye-to-hand 场景直接拿 calibrateHandEye 的输出用,坐标系取反导致标定结果在 rviz 里看起来正确、抓取时完全偏掉。第二,标定板用了混合尺寸的 marker 或不同词典,检测结果不稳定,换 charuco 加固定词典能解决大部分问题。第三,rpy 分解顺序写错,static_transform_publisher 的旋转参数顺序是 yaw pitch roll,很多人在这一步把俯仰和偏航写反,导致相机坐标系绕自身轴旋转了 90 度以上。
将标定结果写进 URDF 的 fixed joint 之后,在 rviz 里观察 TF 树是否连续跳动。三个指标全过——平移残差小于 5mm、旋转残差小于 0.01rad、重投影像素残差小于 2px,这套手眼标定就可以放心交给后续的抓取和装配流程了。
本文还有配套的精品资源,点击获取