做机器人抓取或者视觉定位,最绕不开的一步就是手眼标定。UR机械臂配Realsense深度相机,这套组合在实验室和工业现场都很常见,但很多人在标定阶段就被卡住了——要么标出来的结果误差大得离谱,要么压根不知道该采哪些数据、数据怎么用。我前前后后给几套UR5、UR10和D435i、D415都做过手眼标定,踩过的坑不少,这次把完整流程和关键细节一次性理清楚。
这套方案解决的是"相机看到的物体位置,怎么换算成机械臂能抓的坐标"这个核心问题。适合正在做视觉抓取、视觉定位、移动抓取项目的开发者参考,也适合刚接触ROS和机械臂、想走通一遍标定流程的新手。内容会覆盖原理、环境准备、实操步骤、数据保存和结果验证,全部基于ROS Noetic + Ubuntu 20.04,UR机械臂仿真和真机都适用。
1. 手眼标定核心概念与方案选型
1.1 为什么机械臂和相机必须建立变换关系
机械臂自己只知道"我的末端在基座坐标系下的位姿",相机只知道"目标物体在相机坐标系下的位置"。这两个坐标系之间没有直接联系,机器人就没办法根据相机看到的目标位置去规划抓取路径。手眼标定干的事情,就是求解相机坐标系和机械臂基座坐标系之间的变换矩阵,让相机看到的像素坐标能够换算成机械臂基座坐标系下的三维坐标。
这个过程里有两个坐标系变换关系是已知的:一是机械臂基座到工具末端的变换,可以直接从机械臂控制器的正运动学读取;二是相机到标定板的变换,可以通过检测标定板的特征点并解PnP问题得到。手眼标定要算的,就是机械臂基座到相机(或者相机到机械臂末端)的那个固定变换。
这里要理解一个核心逻辑:标定板放在机械臂的工作空间内,位置固定不动,机械臂末端带着(或者对着)相机从不同角度观察标定板。换句话说,机械臂动、标定板不动、相机跟着动或者固定不动,三种情况组合出两种标定模式。
1.2 eye-in-hand与eye-to-hand两种模式的本质区别
根据相机安装位置,手眼标定分为两大类。第一种是眼在手上(eye-in-hand),相机固定在机械臂末端法兰盘上,随着机械臂一起运动;第二种是眼在手外(eye-to-hand),相机固定在外部支架上,工作过程中不移动。
这两种模式下,待求的变换关系完全不同。眼在手上要求解的是相机坐标系到机械臂末端坐标系的变换(cam到end),因为相机跟随末端移动,这个变换是固定不变的;眼在手外要求解的是相机坐标系到机械臂基座坐标系的变换(cam到base),相机固定时这也是一个固定值。
实操中怎么判断自己该用哪种?看你的应用场景:如果是机械臂末端安装吸盘或夹爪,相机装在末端朝下拍工作台上的工件,那就是典型的眼在手上;如果是相机架在工作区域正上方或者斜上方,机械臂在相机视野内活动,那就是眼在手外。
两种模式标定的流程也有差异。眼在手上时,标定板要放在机械臂前方固定位置,机械臂运动到不同姿态拍摄标定板;眼在手外时,标定板通常是固定在机械臂末端,机械臂带着标定板运动,让外部相机从不同角度拍摄。注意,这个区别很多人一开始会搞混,导致采集数据全部作废。
1.3 AX=XB方程与最小二乘求解思路
不管哪种模式,手眼标定在数学上都归结为求解AX=XB形式的矩阵方程。这个方程的意义是:机械臂两次运动之间的齐次变换矩阵A,与相机两次观察标定板之间的齐次变换矩阵B,通过未知的手眼变换X构成一个闭环约束。
具体来说,对眼在手上的情况,假设机械臂末端在两个位置之间的相对变换是A(可以由正运动学算出来),相机在两个位置观察同一块固定标定板的相对变换是B(可以由检测标定板并解PnP得到),那么满足AX=XB。每两次不同姿态的运动就能提供一个这样的方程,多次运动后构成超定方程组,再用李群李代数和最小二乘方法求解最优的X。
为什么至少要采集两次不同姿态?因为一次位置只能建立一个约束方程,两个未知数(旋转和平移)解不出来。实际工程中要求采集8到12组不同姿态的数据,姿态差异还要足够大,否则矩阵病态、求解结果不稳定。我习惯每次采集之间让机械臂的末端姿态至少改变30度以上,确保旋转部分的约束充分。
1.4 标定工具选型与对比
ROS生态里做手眼标定,主流的工具是easy_handeye,另外还有OpenCV自带的calibrateHandEye函数、Visp手眼标定模块。我从易用性和维护活跃度两个维度对比一下。
| 工具 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| easy_handeye | ROS环境,UR+Realsense组合 | 集成度高,RViz可视化,直接发布TF | 依赖aruco/棋盘格检测,需要配置launch文件 |
| OpenCV calibrateHandEye | 纯算法验证,非ROS项目 | 接口简单,支持多种求解方法 | 需要自己管理输入输出数据格式 |
| Visp hand-eye | 视觉伺服项目 | 精度高,鲁棒性强 | 配置繁琐,学习曲线陡 |
我推荐直接用easy_handeye,因为它专门为ROS机器人设计,自带标定板的检测、位姿采集、求解、结果发布全套流程,而且和UR机械臂的MoveIt驱动接口能无缝对接。数据采集过程中还能在RViz里实时看到相机检测标定板的结果,排查问题直观得多。
2. 环境准备与驱动配置细节
2.1 ROS环境搭建与鱼香ROS一键安装
标定工作依赖ROS环境,目前最稳的组合是Ubuntu 20.04 + ROS Noetic。UR机械臂官方驱动ur_robot_driver和Realsense的realsense-ros都完整支持这个组合。如果你是从零开始搭建环境,ROS安装这一步很容易劝退新手——那么多依赖包、换源、初始化、环境变量,每一步都可能出错。
我实测下来比较推荐鱼香ROS的一键安装脚本,它在国内网络环境下非常省心。安装命令就一行:
wget http://fishros.com/install -O fishros && . fishros执行后它会弹出功能选项菜单,选择ROS安装对应的选项,再选择Noetic(Ubuntu 20.04)版本即可。脚本会自动处理软件源、依赖、环境变量和命令行补全,整个过程大概十几分钟,比手动逐个apt install靠谱很多。需要注意安装过程中它会询问是否添加rosdep,建议选是,后面编译工作空间经常需要它来安装依赖。
提示:如果你的Ubuntu版本是22.04,对应的ROS发行版是Humble,UR官方驱动也有支持,但教程里很多launch文件和依赖包名称会有差异。稳定起见,标定这种精细活建议先别追新,用20.04 + Noetic的组合最省事。
2.2 UR机械臂驱动与仿真环境搭建
UR机械臂(以UR5为例)在ROS中的驱动方案是ur_robot_driver配合MoveIt。真机连接时,这个驱动通过Ethernet与机械臂控制柜的Dashboard接口通信,可以读取关节状态、发送运动指令、控制机械臂使能和释放。
如果没有真机,也可以在Gazebo仿真环境里先跑通整个标定流程。ur_robot_driver本身不直接支持Gazebo,需要额外安装ur_gazebo和ur_description这两套描述包。我建议先仿真后真机,仿真环境里即使标定错误也不会损坏设备,适合验证流程。
ur_robot_driver的安装:
sudo apt install ros-noetic-ur-robot-driver要启动仿真环境,通常会配合使用ur5e_moveit_config这类配置包。运行仿真机械臂的launch命令大致是:
roslaunch ur_gazebo ur5e_bringup.launch roslaunch ur5e_moveit_config moveit_planning_execution.launch sim:=true启动之后MoveIt的RViz界面里会有一个可以拖拽规划、执行运动的UR5E模型,仿真状态下手眼标定流程跟真机完全一致。这里有个实操心法:先用仿真把easy_handeye的采集流程完完整整走一遍,搞清楚每一步的输入输出,再上真机,能帮你省下大量现场调试时间。
2.3 Realsense相机驱动与ROS接口配置
Realsense D435i(或者D415)在ROS下使用,需要装两个层面的东西:底层硬件驱动librealsense和ROS封装层realsense-ros。
sudo apt install ros-noetic-realsense2-camera这个包装了之后,可以通过launch文件启动相机节点,发布彩色、深度和IMU话题。推荐先用官方的rs-enumerate-devices命令确认固件版本和序列号识别正常,再启动ROS节点。
roslaunch realsense2_camera rs_camera.launch默认launch会发布/camera/color/image_raw、/camera/depth/image_rect_raw等话题。手眼标定模式下,我们需要的是彩色图像话题/camera/color/image_raw和对应的相机内参/camera/color/camera_info。这两个话题在easy_handeye中直接通过ROS话题名引用。
2.4 easy_handeye安装与依赖检查
easy_handeye在ROS Noetic下可以直接用apt安装,也可以从源码编译。源码编译的好处是可以拿到最新的bug修复,缺点是编译时间稍长。我这里说apt方式:
sudo apt install ros-noetic-easy-handeye它会一起装上aruco检测相关的依赖库。需要确认一下aruco_ros、visp_hand2eye_calibration这些依赖包是否被正确解析。
注意:easy_handeye在不同发行版中的launch文件组织方式略有不同。Noetic版本中,一般是
eye_on_hand和eye_on_base两个launch文件,分别对应眼在手上和眼在手外模式,后面实操章节我会具体展开。
3. 手眼标定完整实操流程
3.1 标定板的选择与生成
easy_handeye默认支持aruco码标定板,推荐使用aruco板而不是棋盘格,因为aruco码在图像中更容易被检测、每个码自带ID、部分遮挡时鲁棒性更好。生成aruco标定板的方式很多,我用的是easy_handeye仓库自带的生成脚本。
rosrun aruco_ros create_marker 0 # 生成单个ID=0的aruco码不过单个aruco码在标定中容易因为视角过大导致检测失败,我更推荐打印一张4x4或5x5的aruco标定板。可以用以下Python脚本生成并保存为PDF打印:
import cv2 import numpy as np aruco_dict = cv2.aruco.Dictionary_get(cv2.aruco.DICT_4X4_50) board = cv2.aruco.GridBoard_create(5, 7, 0.04, 0.02, aruco_dict) img = board.draw((840, 594), marginSize=20) cv2.imwrite("aruco_board.png", img)关键参数说明:GridBoard_create前两个参数是标定板的格子数量(7列5行),0.04是每个格子边长(米),0.02是码与码之间的间距(米)。你打印之后需要测量实际物理尺寸,把这两个值改成你打印纸上的真实值。
注意:标定板一定不要用普通的A4纸直接放桌上,最好贴在硬纸板或者亚克力板上,保证表面平整。标定板弯曲是标定精度杀手,误差甚至能达到厘米级。
3.2 相机内参确认
手眼标定的输入数据包含相机的内参矩阵,如果内参不准确,后面PnP求解的位姿全是歪的,手眼标定结果也会带上同样偏差。Realsense出厂时自带内参校准,正常使用可以直接读取/camera/color/camera_info话题里的内参。
启动相机后,查看内参的方式:
rostopic echo /camera/color/camera_info输出的K矩阵就是内参。如果发现内参异常(比如焦距、主点偏移过大),则需要用ROS的camera_calibration包重新标定一次。
rosrun camera_calibration cameracalibrator.py --size 8x6 --square 0.108 image:=/camera/color/image_raw camera:=/camera/color注意这里的棋盘格内角点数和格子实际边长要填准确。如果用的是Realsense,出厂内参通常精度够用,一般不推荐自己重新标定,因为需要打印很标准的高精度棋盘格,普通打印机的精度反而可能引入误差。
3.3 眼在手外(eye-to-hand)标定全流程
眼在手外模式适合相机固定、机械臂运动的工作场景。整个标定的目标是通过easy_handeye求解相机坐标系到机械臂基座坐标系的变换关系。这里我把完整操作拆解成步骤。
第一步,确认机械臂和相机节点正常启动。真机场景:
roslaunch ur_robot_driver ur5e_bringup.launch robot_ip:=192.168.1.100 roslaunch realsense2_camera rs_camera.launch如果使用仿真环境,则分别启动ur_gazebo和moveit相关launch文件。
第二步,启动easy_handeye的眼在手外配置:
roslaunch easy_handeye publish.launchpublish.launch实际上是发布标定板TF用的,有些版本放在easy_handeye的示例launch里。这一步要确保相机能够检测到并发布标定板的TF,TF名称通常类似/aruco_marker或/target。
第三步,启动手眼标定主程序:
rosrun easy_handeye eye_on_base_calibrate.py运行后会弹出easy_handeye的GUI窗口,同时显示相机图像和标定检测结果。如果检测到标定板,画面中会出现aruco码的边框和坐标轴。
第四步,规划机械臂运动到初始化位姿。在MoveIt的RViz界面中,拖动机械臂模型到能看到标定板的初始位置,执行规划并让机械臂运动过去。这个初始位置建议让标定板在图像中央附近,标定板的姿态不要太倾斜(法线与相机光轴夹角不要超过45度)。
第五步,在easy_handeye GUI中点击"Plan"按钮。它会自动计算下一步机械臂的运动目标,让机械臂以一个新的姿态观察标定板。检查规划结果没有碰撞后,点击"Execute"执行运动。
第六步,运动完成后,确认相机画面中的标定板处于有效检测状态,然后点击"Take sample"采集一组数据。每组数据包含机械臂的位姿和相机检测到的标定板位姿。重复这个过程8到12次。
这里有个关键技巧:机械臂每次运动的姿态差异要大一点,不要只在同一姿态附近微调。姿态变化不够会导致求解出的旋转矩阵分量条件数差,标定结果看起来每个姿态都合理,但不同初始值解出的结果差异很大。
第七步,采集完所有样本后,点击"Compute"按钮计算标定结果。easy_handeye会输出一个齐次变换矩阵,并保存到.yaml文件里。得到的文件路径会在输出信息中显示,通常是~/.ros/easy_handeye/eye_on_base_xxx.yaml。
这个yaml文件里包含旋转四元数和平移向量,是最终要用的标定结果。它的意义是"相机坐标系在机械臂基座坐标系中的位姿",也就是从相机坐标变换到机械臂基座坐标的变换矩阵。
3.4 眼在手上(eye-in-hand)标定流程差异
眼在手上模式和眼在手外模式流程上几乎相同,区别在于标定板放在固定位置,机械臂末端带着相机从多个角度观察标定板。
启动方式改变为:
roslaunch easy_handeye publish.launch rosrun easy_handeye eye_on_hand_calibrate.py以及对应地,相机固定在机械臂末端时,标定过程中的数据采集逻辑是机械臂运动、相机跟着运动、标定板不动。计算出的结果是相机坐标系在机械臂末端坐标系下的变换。
3.5 标定结果验证方法
标定完不能直接拿去用,必须先验证。验证方法很简单:让机械臂运动到工作空间内的另一个位置,用标定得到的结果将相机检测到的标定板中心点坐标变换到机械臂基座坐标系,再和机械臂正运动学中已知的标定板真实位置对比。
如果想在RViz里直观验证,可以在终端用rostopic查看当前相机检测的标定板位置,然后用TF工具将坐标系转换到base_link,看是否与标定板的实际位置重合。误差在几毫米以内算是正常范围;如果误差超过1厘米,那这个手眼标定结果基本不可用,需要重新再来。
此外可以检查一个数据点:将相机检测到的标定板中心变换到机械臂基座坐标系后,与机械臂示教器上显示的工具中心点(TCP)位置做比较。标定板如果正好在机械臂末端下方,两者的距离应该约等于标定板的厚度和安装偏移量,偏差过大说明标定结果有系统误差。
4. 标定常见问题与精度专项排查
4.1 标定结果跳变与不稳定的原因
如果连续两次标定算出来的结果差异很大,或者每次加入新的数据样本后结果明显跳变,大概率是机械臂运动规划时姿态变化不够,导致AX=XB方程的病态约束。我在一次UR10标定中遇到过:前6组数据姿态变化只有5度左右,算出来的平移量前后相差5厘米,后来加大姿态变化到30度以上重新采集,结果就稳定在2毫米以内。
数据采集时机械臂还可能出现温飘问题,尤其是真机连续运行较长时间后,关节间隙和减速器温度变化会导致正运动学计算出的末端位姿与实际不一致。解决办法是采集过程中不要拖太久,一次标定采集尽量在5到10分钟内完成;同时让机械臂预热一段时间再开始标定,让关节温度进入稳定状态。
4.2 aruco码检测失败的处理
相机画面中看不到标定板的识别边框,这是新手最常见的卡点。排查路径分三步:
先检查图像话题是否正常,/camera/color/image_raw里有没有画面;再检查标定板曝光和清晰度,Realsense在强光下容易过曝,aruco码边缘泛白会导致识别失败,这时候适当降低相机的曝光时间或者用遮挡物避免直射光——我见过不少用D435i的小伙伴直接对着窗户光标定,怎么都识别不出来,拉上窗帘就好了;最后检查标定板尺寸参数是否和打印出来的实际尺寸匹配,如果格子边长填错了,PnP求解出来的位姿会非常不稳定。
如果镜头离标定板太近导致aruco码超出视野,或者太远导致码区域过小(小于30像素),也会检测失败。
4.3 坐标变换与TF树连接问题
标定完成后如果发现坐标变换发布不正常,要检查TF树是否完整连接。正常的TF树应该有如下链路:
base_link -> (眼在手外标定结果) -> camera_color_optical_frame如果是眼在手上,则是:
base_link -> tool0 -> (眼在手内标定结果) -> camera_color_optical_frame可以运行rosrun tf view_frames生成TF树图来检查连接情况。常见的问题是相机的TF名称不对,Realsense发布的相机光学坐标系名称通常是camera_color_optical_frame,而不是camera_link,使用easy_handeye的launch时需要在配置中指定正确的相机TF名称,否则标定结果发布时TF树会断开。
4.4 精度评估的量化指标
手眼标定的精度不能光靠感觉,建议做一次量化评估。选定工作空间内3到5个不同的验证点,让机械臂末端(或固定标定板)移动到每个点,记录相机检测到的位置和机械臂示教器显示的实际位置,计算两者误差:
| 验证点 | X方向误差(mm) | Y方向误差(mm) | Z方向误差(mm) | 综合误差(mm) |
|---|---|---|---|---|
| 点1 | 1.2 | 0.8 | 1.5 | 2.1 |
| 点2 | 0.9 | 1.1 | 1.3 | 1.9 |
| 点3 | 2.1 | 1.8 | 2.5 | 3.8 |
综合误差超过5毫米时,需要重新标定或者检查机械臂末端负载是否导致正运动学误差增大。有个经验数据:UR系列机械臂本身重复定位精度是±0.1毫米级别,手眼标定的误差通常主要来自相机标定板检测误差和标定方程求解误差,正常标定结果综合误差在2到4毫米是比较常见的水平。
注意:标定结果与机械臂的工作区域有关。UR机械臂在工作空间边缘区域的正运动学误差比中心区域大,所以标定时的采样位置尽量覆盖实际抓取任务中会用到的工作区域,不要只在机械臂正前方的一个小区域采集数据。
4.5 真机标定前的安全检查
如果使用真机标定,安全第一位。我的习惯是先在空载状态下用慢速(MoveIt中设置最大速度比例0.2)做一次完整的走点流程,确认机械臂的运动路径上没有障碍物和人员,再正式开始采集数据。
采集过程中,操作人员应该站在机械臂工作空间之外,手边随时能摸到急停开关。UR控制柜的示教器上也要提前设置好安全停止的I/O配置。切记不要在机械臂运动过程中伸手调整标定板,等机械臂停稳后再调整位置。
还有一个细节:真机标定时,机械臂运动到某些极限姿态时可能出现奇异点,导致MoveIt规划失败。遇到这种情况,可以手动调整一下初始位姿再重新规划,或者适当降低目标位置离奇异点的距离。
5. 标定结果在视觉抓取项目中的应用
标定完成后,手眼变换关系要接入到视觉抓取或者视觉定位的完整流程中。这里以眼在手外的模式为例,说明标定数据怎么用。
相机检测到目标物体的像素坐标,通过深度图获得物体在相机坐标系下的三维坐标,然后通过标定得到的齐次变换矩阵变换到机械臂基座坐标系,最终通过MoveIt规划机械臂移动到目标位置。
核心代码思路如下,用C++和ROS的tf库实现:
tf::TransformListener listener; tf::StampedTransform cam_to_base; // 等待并获取标定得到的变换 listener.waitForTransform("base_link", "camera_color_optical_frame", ros::Time(0), ros::Duration(3.0)); listener.lookupTransform("base_link", "camera_color_optical_frame", ros::Time(0), cam_to_base); // 目标在相机坐标系下的位置 tf::Vector3 pos_in_cam(x, y, z); tf::Vector3 pos_in_base = cam_to_base * pos_in_cam;如果用Python写也是一样的逻辑,用tf2_ros.Buffer接口查询变换,然后把相机坐标点通过变换矩阵映射到机械臂基座坐标系。
这段代码的本质就是把标定结果变成运行时的一个坐标变换关系。需要注意的是,标定得到的yaml文件要加载到参数服务器,并且在程序初始化时读取。
实际项目中,手眼标定的结果往往不是一成不变的——相机如果被撞了一下、机械臂末端负载变化、镜头拧动过,都可能导致标定结果失效。所以一个可以单独启动的标定验证工具非常重要。我的实践是在项目里留一个"验证模式",每次开始任务前用标定板放在已知位置快速验证一下,误差超标就触发重新标定提醒。
提示:如果使用eye-in-hand模式,变换链是base_link -> tool0 -> camera_color_optical_frame。执行抓取时除了手眼变换,还需要考虑工具末端坐标系到末端执行器(比如吸盘)的偏移,建议单独定义tool0到抓取点的tf变换,与手眼变换分开管理,层次清晰便于维护。
6. 标定工作流优化与个人经验小结
从零走通UR机械臂和Realsense相机的ROS手眼标定,整个流程的各个关键点都覆盖了。再强调几个对结果影响最大的环节:标定板必须平整、机械臂采样姿态差异要大、数据采集要快避免温飘、结果必须做量化验证。
我在实际项目中形成的习惯流程是:先用虚拟仿真环境跑通整套标定流程,确认launch文件和代码逻辑无误,再切换到真机操作;真机上先低速走一遍运动轨迹确认安全,再正式采集数据;标定完成后在工作空间多个位置做验证测试,误差达标后才把标定结果接入视觉抓取流程。
最后分享一个调试小技巧:如果标定结果用起来总觉得差一点,不要急着重复整个标定流程,先在RViz里标出相机检测到的标定板中心和机械臂示教器得到的基准位置,看看误差分布在哪个方向上。有一次我遇到Z方向固定偏差,查了半天发现是标定板厚度导致的坐标系偏移——标定板贴在亚克力板上,厚度1厘米,检测到的坐标系在标定板表面,而机械臂参考的是标定板底部,差了整整一个厚度。这种问题不把数据可视化出来,靠猜很难发现。