简介:在机器人视觉引导系统中,坐标系的统一是实现精准抓取与装配的基础。手眼标定是连接相机与机械臂坐标系的关键步骤,其核心原理基于AX=XB方程,通过多姿态采样求解相机与机械臂之间的固定变换。眼在手外(Eye-to-Hand)与眼在手上(Eye-in-Hand)两种模式分别适用于全局定位和近距离精细作业,在ROS环境下结合easy_handeye工具可高效完成标定流程。本文从实战角度出发,以Kinect2与Astra相机配合Aubo机械臂为例,详细梳理了从驱动安装、标定板准备、采样姿态设计到结果验证的完整链路,并针对数据质量、坐标系混淆和TF树配置等高频问题给出了工程化解决方案,适合从事机器人视觉引导、手眼标定相关研发的工程师和学生参考。 如果你做过机械臂视觉抓取,手眼标定一定是绕不过去的一关。我最近完整过了一套把 Kinect2 眼在手外标定、Astra(奥比中光)眼在手上标定和 Aubo 机械臂放在一起的项目源码,从驱动安装、数据采集到求解验证,整个链路都跑通了。这篇文章不聊理论上的花活,直接把我实际操作的步骤、采样时的细节和最后踩过的坑写出来,适合正在做机器人视觉引导、毕设或者刚接触 ROS 手眼标定的人参考。
先说一个直观结论:手眼标定这事的本质不是算法难,而是数据工程难。标定板的打印质量、相机的曝光设置、机械臂的采样姿态分布,每一项都比“用什么算法求解”更影响最终精度。很多时候你觉得标定结果差,不是代码写错了,而是标定板在画面里太小、姿态太单一或者光照在闪烁。这套项目之所以能拿高分,并不是因为它用了多冷门的技术,而是它把两种标定模式完整走通了,还配了能直接改参数复用的源码和文档——这恰恰是绝大多数博客里看不到的部分。
1. 一套项目两种标定:Kinect2与Astra分别解决什么
1.1 两个相机在物理空间里的位置关系
拿到这个项目时,我第一反应是看两个相机分别装在什么位置,因为这决定了后续所有坐标系变换的方向。
Kinect2 在项目里负责的是眼在手外(Eye-to-Hand):相机固定在工作空间外侧,不跟着机械臂动,标定板固定在机械臂末端法兰上。这种布置适合“全局视角”的应用场景,比如让机械臂去抓传送带上的工件,相机从上往下看整个工作台,机械臂只需要按照相机给的坐标去抓。
Astra 相机负责的是眼在手上(Eye-in-Hand):相机直接装在 Aubo 机械臂的末端,机械臂转到哪儿,相机就跟到哪儿。这种布置适合“局部精细视角”,比如机械臂靠近目标之后,需要相机贴上去确认目标位姿,再调整末端姿态去执行插入、装配这类动作。
两套方案本质上解决的是不同层次的问题,一个负责“看全局、给目标”,一个负责“贴近了、看清细节”。很多实际项目里两种模式会同时存在,这也正是这个项目把两种标定都做进去的原因。
1.2 标定出来的“数字”到底是什么
标定最终得到的是一组 4×4 的齐次变换矩阵,描述的是一个坐标系相对于另一个坐标系的旋转和平移。用 ROS 的话说,这叫 TF 变换。比如眼在手外标定完后,得到的是camera_link到base_link的变换矩阵,相机识别到的任何一个点,乘以这个矩阵,就能换算成机械臂基座坐标系下的坐标。
在没有这个矩阵之前,相机看到的坐标和机械臂理解的坐标是两套语言。相机说:“工件在画面中央偏右 100 像素、深度 0.6 米。”机械臂说:“我的末端当前在基座坐标系下 x=0.35、y=0.2、z=0.4。”这两句话之间没有任何换算关系。手眼标定做的事,就是在这两套语言之间架一座桥。
这个桥在 ROS 里以 TF 树的形式存在。整个项目跑起来之后,你会在 TF 树里看到base_link → tool0 → camera_link或base_link → board_link → camera_link这样的链路。后面我会专门讲 TF 树怎么搭,因为这里有个非常隐蔽的坑:相机坐标系分为camera_link和camera_color_optical_frame,两者之间不仅差一个平移,还有一次 180° 的旋转,用错一个,标定结果全部白搭。
2. 手眼标定公式里的AX=XB到底在算什么
2.1 一个姿态不够,得让机械臂“转起来”看
很多初学者以为手眼标定是拍一张照片算一次就完事,实际上它需要机械臂带着标定板(或带着相机)运动到多个不同姿态,每个姿态记录一组数据,最后把所有数据丢进公式里拟合。
为什么要多个姿态?因为单次观测的方程是欠约束的。我们只知道“标定板在相机坐标系下的位姿”和“机械臂末端在基座坐标系下的位姿”,但要求的“相机和机械臂基座(或末端)之间的固定变换”存在一个自由度的模糊性,必须通过多次运动的相对关系把它约束出来。
数学上,每次机械臂从姿态 1 运动到姿态 2,都会产生两组相对变化:
- A:标定板在相机坐标系下的位姿变化;
- B:机械臂末端在基座坐标系下的位姿变化。
理想情况下,两组变化之间满足:
A × X = X × B
这里 X 就是我们要标定的手眼矩阵。如果你代入多组姿态,这个方程会变成一个超定方程组,然后通过最小二乘或者更鲁棒的算法求出最优解。这就是 AX=XB 的来历。
2.2 眼在手外和眼在手上,X的含义完全不同
我在看这套项目源码的时候,发现作者在注释里特别强调了一点:眼在手外和眼在手上虽然都叫手眼标定,但求解出的 X 物理含义不一样。
- 眼在手上:相机装在末端,X 是相机坐标系到末端法兰坐标系的变换;
- 眼在手外:相机固定在外面,X 是相机坐标系到机械臂基座坐标系的变换。
这两个变换在 TF 树里的位置完全相反。如果用错了,后面做视觉引导时,机械臂会朝一个诡异的镜像方向运动,轻则抓不到,重则撞机。
我拆解一下两个场景的变换链:
眼在手上时,机械臂基座 → 末端 → 相机 → 标定板。所以标定板在基座坐标系下的位姿可以写成:T_base_board = T_base_tool × T_tool_camera × T_camera_board
眼在手外时,机械臂基座 → 末端 → 标定板 → 相机。所以标定板在基座坐标系下的位姿可以写成:T_base_board = T_base_tool × T_tool_board × T_board_camera
项目源码里把这两个链路分别用eye_in_hand.yaml和eye_on_base.yaml两个配置文件管理,参数写死之前务必确认你在哪个模式下。
2.3 数据质量比算法选择更影响结果
用 easy_handeye 标定时,默认提供了 Tsai-Lenz、Park 等几种求解算法。我在实际测试中换过算法,结果差异其实不大,真正导致标定结果差的,是采样数据的质量。
采样时有几个硬性要求:
- 至少采集 15 到 20 组有效姿态,少于这个数量求解结果不稳定;
- 姿态要“发散”,要覆盖机械臂工作空间的不同位置和角度,不能只在起点附近小幅挪动;
- 标定板在画面里的占比不能太小,一般要占到画面高度的一半左右;
- 画面里的棋盘格或者 Aruco 码必须清晰、完整,有反光或者遮挡的样本直接丢。
我在跑这个项目时,第一次只采了 12 组,且姿态几乎都在同一平面内旋转,结果求解出来的平移误差能到好几厘米。后来把姿态分散到三个维度,平移误差立刻降到毫米级。所以如果你标定结果不理想,先别怀疑算法,去检查数据够不够“散”。
3. 环境搭建里最容易翻车的三个驱动细节
3.1 Kinect2 的USB带宽和分辨率设置
Kinect2 在 ROS 下的驱动主要有两个层次:底层是 libfreenect2,上层是 iai_kinect2 包。安装 libfreenect2 时需要接上设备,运行探测脚本确认 USB 控制器识别到的是 USB 3.0 通道。如果插在 USB Hub 上,或者主板后置 US B3.0 口没插对,设备经常出现一开流就掉线的情况。
Kinect2 全分辨率输出时数据量非常大,如果 USB 带宽不够,画面会出现严重撕裂,甚至直接卡死。我在项目中实际用的参数是:
roslaunch kinect2_bridge kinect2_bridge.launch depth_method:=opengl然后在kinect2_bridge的参数文件里把深度分辨率调到 640×480、RGB 调到 1280×720,帧率保持 30 帧。这样既保证标定板检测的清晰度,又不会把 USB 带宽打满。如果你的工作空间比较大,需要更大视野,可以保持 1920×1080 的 RGB,但那时就必须把深度降到 320×240,否则带宽不够。
另外,Kinect2 的三个摄像头(RGB、深度、红外)在标定时其实只用到了 RGB 图。因为手眼标定不需要深度值,标定板的位姿是通过 RGB 图像里的角点检测加 PnP 解算出来的。这点很多人会误解,以为挂了深度相机就必须用深度数据。
3.2 Astra 相机的新旧驱动选择
Astra(奥比中光)相机在 ROS 下的驱动有两代:比较老的是ros_astra_camera,新的是奥比中光官方维护的orbbec_camera。如果你的系统是 ROS Melodic 或者 Noetic,建议直接用新驱动,老驱动在 Ubuntu 20.04 上编译会遇到一堆依赖问题。
Astra 默认能输出 color、depth 和 ir 三个话题。手眼标定时我只用 color 话题,但这里有一个容易忽略的点:Astra 的彩色图默认可能是自动曝光模式,自动曝光会导致画面亮度不断变化,棋盘格角点检测在亮暗变化时会偶尔丢帧。最好把曝光固定:
rosrun rqt_reconfigure rqt_reconfigure在camera/color参数组里关掉auto_exposure,手动设置一个合适的曝光值。如果环境光照稳定,这一步基本一劳永逸。
还有一个细节:Astra 的驱动默认会把深度图对齐到彩色图,这个对齐会占用不少 CPU,如果在标定过程中出现帧率抖动,可以考虑在对齐参数里关掉深度对齐,只订阅彩色话题。
3.3 Aubo 机械臂的 ROS 接口和运动控制
Aubo 机械臂在 ROS 里主要依赖aubo_robot包,里面包含 MoveIt 配置和底层驱动。启动后你会看到aubo_i5或aubo_i10的 move_group 节点。手眼标定采样时,机械臂需要按照预设路径运动到多个位姿,我建议直接用 MoveIt 的 Python API 写采样脚本,把每个位姿定义为目标点,依次执行。
实际项目中控制机械臂运动的代码核心是:
import rospy import moveit_commander moveit_commander.roscpp_initialize(sys.argv) robot = moveit_commander.RobotCommander() scene = moveit_commander.PlanningSceneInterface() arm = moveit_commander.MoveGroupCommander("manipulator") arm.set_pose_reference_frame("base_link") arm.set_planning_time(5) arm.set_max_velocity_scaling_factor(0.2)关键点是set_max_velocity_scaling_factor一定要设低一些,机械臂快速运动时会有惯性抖动,抖动期间相机画面的标定板会模糊,采到的样本基本是废的。我实际用的速度因子是 0.15 到 0.2,慢是慢一点,但样本质量高很多。
4. 眼在手外全流程:从数据采集到结果落地的完整链路
4.1 标定板选择和相机内参准备
眼在手外模式下,标定板固定在机械臂末端。标定板建议用 7×6 的棋盘格(内角点 6×5),格子边长 25mm 到 30mm 比较合适。太小了相机离远看不清,太大了又会超出机械臂末端的安装范围。项目源码里默认给的标定板尺寸是 25mm,棋盘格行列数 9×7,打印出来贴在硬质铝板上效果最好,普通 A4 纸会翘边,直接影响角点坐标精度。
做手眼标定之前,务必先确认相机的内参是准的。Kinect2 和 Astra 出厂都有标定参数,但如果你发现图像畸变明显,可以用 ROS 的camera_calibration包重新标一遍内参。内参不准,手眼标定出来的一定不准,别想着后面能靠手眼矩阵把误差补偿回来。
4.2 写 easy_handeye 的 launch 文件
项目里用的是 ROS 生态里最常用的 easy_handeye 工具包。安装方式:
sudo apt install ros-$ROS_DISTRO-easy-handeye如果你需要改源码,也可以从 GitHub 拉下来编译。启动眼在手外标定的 launch 文件核心参数如下,这是我从项目里抽出来的实际配置:
<launch> <arg name="namespace" value="easy_handeye_eye_on_base" /> <arg name="eye_on_hand" value="false" /> <arg name="robot_base_frame" value="base_link" /> <arg name="robot_effector_frame" value="tool0" /> <arg name="tracking_base_frame" value="kinect2_rgb_optical_frame" /> <arg name="tracking_marker_frame" value="kinect2_board" /> <node pkg="easy_handeye" type="handeye_calibration_camera" name="handeye_calibration_camera" output="screen"> <remap from="image" to="/kinect2/hd/image_color_rect" /> <remap from="camera_info" to="/kinect2/hd/camera_info" /> <param name="tracking_base_frame" value="$(arg tracking_base_frame)" /> <param name="tracking_marker_frame" value="$(arg tracking_marker_frame)" /> <param name="marker_size" value="0.025" /> <param name="marker_type" value="checkerboard" /> </node> </launch>有几个参数必须跟你实际环境对齐:
robot_effector_frame:机械臂末端坐标系,Aubo 里通常叫tool0;tracking_base_frame:必须是相机光学坐标系,而不是相机外壳坐标系,两者差一个旋转;marker_size:棋盘格格子边长,单位米,写错这个参数标定结果会整体缩放。
4.3 采样过程与求解
启动好 launch 之后,用rosrun easy_handeye handeye_calibration_gui打开标定界面。界面上能看到相机画面和标定板检测框。操作流程是:用 MoveIt 脚本把机械臂移动到第一个位姿 → 确认画面里标定板检测正常 → 点 “Take Sample” → 移动到下一个位姿 → 继续采样。
这里有个经验:采样时不要按固定顺序走,最好随机打乱位置,让机械臂的关节组合尽量多样。每次都从同一个方向接近标定板,会造成姿态分布单一,求解结果会偏。
采够 15 到 20 组后点 “Compute”,工具会给出求解后的平移和旋转。项目文档里展示了一组实际结果:
- 平移:x=0.312, y=-0.045, z=0.482(单位米)
- 旋转四元数:w=0.921, x=0.137, y=-0.284, z=0.224
这组数据的意思是,Kinect2 相机光学坐标系相对于 Aubo 基座坐标系,在 x 方向偏移约 31 厘米,z 方向偏移约 48 厘米。如果相机架得比较高,这个 z 值是正的且比较大,符合物理直觉。
4.4 把结果写进 TF
标定完成后,需要在 ROS 系统里把结果固化下来。最直接的方式是发布一个静态坐标变换:
rosrun tf2_ros static_transform_publisher 0.312 -0.045 0.482 0.137 -0.284 0.224 1.0 kinect2_rgb_optical_frame base_link注意static_transform_publisher的参数顺序,六自由度依次是 x y z roll pitch yaw,也可以用四元数形式发布。更规范的做法是写进 URDF 或 launch 文件里的node pkg="tf2_ros" type="static_transform_publisher",这样每次启动机器人系统,变换关系自动生效。
5. 眼在手上全流程:为什么比眼在手外更容易栽在采样上
5.1 相机装到机械臂末端之后的变化
眼在手上模式里,Astra 相机被固定在 Aubo 末端法兰上。启动 Astra 驱动后,相机坐标系会随着机械臂运动而移动,TF 树的链路是base_link → tool0 → camera_link。
这一模式在 easy_handeye 里的配置和眼在手外很相似,但关键参数不同:
<launch> <arg name="namespace" value="easy_handeye_eye_in_hand" /> <arg name="eye_on_hand" value="true" /> <arg name="robot_base_frame" value="base_link" /> <arg name="robot_effector_frame" value="tool0" /> <arg name="tracking_base_frame" value="astra_color_optical_frame" /> <arg name="tracking_marker_frame" value="astra_board" /> <node pkg="easy_handeye" type="handeye_calibration_camera" name="handeye_calibration_camera" output="screen"> <remap from="image" to="/camera/color/image_raw" /> <remap from="camera_info" to="/camera/color/camera_info" /> <param name="tracking_base_frame" value="$(arg tracking_base_frame)" /> <param name="tracking_marker_frame" value="$(arg tracking_marker_frame)" /> <param name="marker_size" value="0.025" /> <param name="marker_type" value="checkerboard" /> </node> </launch>标定板这次固定在工作台上,不能移动。整个采样过程中,是机械臂带着相机围绕标定板运动,而不是标定板跟着机械臂动。
5.2 采样姿态设计的关键技巧
眼在手上标定最容易翻车的地方在于姿态设计。因为相机装在末端,机械臂本身又有工作空间限制,很多新手会让机械臂在一个小范围内小幅转动,导致采样的姿态变化不够。实际标定时,我用的策略是:
- 让机械臂末端在标定板正上方、正前方、左前、右前等几个位置分别停驻;
- 在每个位置,分别让末端绕 X、Y、Z 三个轴旋转 ±20° 到 ±40°,每次旋转后采样一组;
- 保证标定板在画面中始终完整,且占比在 40% 以上;
- 避免标定板在画面里过于倾斜,倾斜角超过 60° 后,角点检测精度会急剧下降。
这里有一个我反复验证过的现象:如果采样姿态全部集中在一个很小的球面范围内,AX=XB 求解出的旋转分量看起来没问题,但平移分量会非常不稳定,甚至出现几百毫米的离谱值。把姿态分散开后,平移结果的重复性会变得非常好。所以当你觉得标定结果“这次和上次差很多”的时候,先看看采样姿态是不是太集中了。
5.3 标定板检测失败的处理套路
Astra 彩色相机的画质不如 Kinect2,尤其是光线不足时,棋盘格角点检测经常失败。项目源码里其实预留了两套检测方式:棋盘格和 Aruco 码。如果棋盘格检测不稳定,可以直接改用 Aruco 板。Aruco 的优势是每个码都有独立 ID,即便画面中有部分遮挡,也能通过剩余角点估算位姿。
我在实际测试中遇到过一种情况:棋盘格在画面中大部分时候能检测到,但偶尔会少检测几列角点,导致输出的标定板位姿跳变。这种样本如果不剔除,会让求解结果带上很大的噪声。处理办法是:在 GUI 中采样时,每采一组就检查一下画面里的覆盖框是否完整,有一点残缺就重新调整机械臂姿态再采。宁可少采几组,也不能采脏数据。
6. 标定结果怎么验、怎么用:TF树与精度校验
6.1 先看标定矩阵的物理合理性
拿到标定结果后,别急着写进系统,先看一眼数值是否符合物理直觉。
以眼在手上的结果为例,Astra 相机装在末端法兰下方,平移量应该在 z 方向有几十厘米(因为相机和法兰之间有一个安装支架)。如果你标出来的平移量是 x=1.2、y=-0.8、z=1.5 这种量级,那大概率是坐标系搞错了——比如tracking_base_frame填成了camera_link而不是camera_color_optical_frame,或者机器人末端坐标系写错成了base_link。
旋转分量也同样可以验证:把相机坐标系原点在末端坐标系下的位置打印出来,再用尺子量一下实际安装距离,两者应该比较接近。如果不接近,检查数据采集和参数配置,不要急着继续。
6.2 TF树的正确结构
标定完成后,ROS 的 TF 树需要按以下方向搭建:
眼在手外模式:
base_link → tool0 → board_link → kinect2_rgb_optical_frame这里board_link是标定板坐标系,它在末端法兰的位姿是已知的(标定板装夹时测量得到),而kinect2_rgb_optical_frame在board_link下的位姿由手眼标定结果给出。用这套 TF 树,相机看到的任何点都能转换到base_link下。
眼在手上模式:
base_link → tool0 → astra_color_optical_frame标定板固定在工作台上,它在base_link下的位姿可以通过T_base_tool × T_tool_camera × T_camera_board推算出来。
这里最容易犯的错误是把camera_link和camera_color_optical_frame混用。相机驱动发布的话题里,camera_info对应的坐标系通常是camera_color_optical_frame,它相对camera_link有一个固定旋转。标定结果是在光学坐标系下算出来的,发布 TF 时也必须挂到光学坐标系下,如果挂错,机械臂会以错误的方向运动,且这个错误非常难察觉。
6.3 用重投影和端到端实验验证精度
验证标定结果最直接的方法是重投影实验。具体做法是:让机械臂带着标定板(眼在手外)或带着相机(眼在手上)运动到某个位姿,通过 TF 树把标定板的某个角点在基座坐标系下的坐标算出来,再和机械臂实际位姿推算出的坐标对比。
更实用的端到端验证是:在相机视野里放一个已知位置的标记物,相机识别后把坐标转换到机械臂基座坐标系,然后让机械臂末端移动到该点。观察末端和标记物之间的偏差,这个偏差就是最终系统精度。
我在这套项目里实测的数据是:眼在手外模式下,机械臂末端去碰标定板角点的误差在 3mm 到 8mm 之间;眼在手上模式下,误差在 5mm 到 10mm 之间。这个精度对于抓取、分拣、装配类应用基本够用。如果你发现误差超过 20mm,优先检查标定板是否翘边、机械臂运动学参数是否准确、相机内参是否标定到位。
7. 我在跑这套项目时踩过的坑和最终验证结论
7.1 一个最容易被忽略的坑:内参没标就上手眼标定
我在最初复现时,拿着相机出厂内参直接开始手眼标定,结果误差一直下不来。后来用camera_calibration重新标了 Astra 的内参,发现出厂参数在画面边缘有接近 5 个像素的畸变误差。手眼标定里用的角点坐标就是像素坐标,内参不准意味着 PnP 解算出的标定板位姿不准,后面 AX=XB 求解出来的手眼矩阵也不可能准。
所以我把“先标内参、再标手眼”列为这个项目的铁律。Kinect2 的出厂内参一般比较准,但 Astra 这种入门级相机,尤其是被拆装过的,一定要重新标。
7.2 采样过程中 TF 树闪断导致样本无效
easy_handeye 在采样时会读取当前 TF 树里的机械臂末端位姿。如果机械臂驱动发布 TF 的节点在运动过程中偶尔断流,GUI 里可能显示采样成功,但实际数据是错的。我在跑眼在手外标定时遇到过两次:机械臂已经在运动,我不小心提前点了 “Take Sample”,导致一组样本的末端位姿和相机画面不对应。
解决办法是:每次采样前,先确认机械臂已经完全停止,画面中的标定板稳定显示,再点采样。即使这样,也建议在最终求解前删掉一些明显异常的样本。easy_handeye 会显示每个样本的误差分布,误差突然变大的那一个点,直接移除重采。
7.3 拿到项目源码后的正确打开方式
这套高分项目的源码包里,除了 launch 文件和 Python 脚本外,最重要的其实是 README 和文档。我建议拿到类似项目后,先按这个顺序过一遍:
- 看 README 里声明的 ROS 版本和依赖包,不要直接
catkin_make,先逐个比对依赖; - 看 launch 文件里的 frame 名称是否和你的机器人、相机一致;
- 看标定板的尺寸参数,按你的实际打印尺寸改;
- 跑通一次完整标定流程之前,不要改任何算法参数,先确保数据链路是通的。
我见过不少人拿到源码就急着改代码、换算法,最后跑来问为什么标定结果不对。大多数时候问题根本不在算法层,而是某个 frame 名字拼错了,或者标定板尺寸和实际不符。
最后再说一个我自己的体会:手眼标定这类工作,90% 的精力要花在数据采集和坐标系统一上,算法只占很小一部分。如果你发现标定结果不稳定,先别急着换求解算法,回到数据层面看:采样姿态是否足够分散、标定板检测是否稳定、坐标系是否挂对。把这三件事做好,标定结果基本不会差到哪里去。这套项目里的源码和文档设计得比较好的地方,也正是它把注意力引向了这些更容易出问题的环节,而不是让人一上来就死磕数学公式。
本文还有配套的精品资源,点击获取