
简介在机器人视觉引导系统中坐标系的统一是实现精准抓取与装配的基础。手眼标定是连接相机与机械臂坐标系的关键步骤其核心原理基于AXXB方程通过多姿态采样求解相机与机械臂之间的固定变换。眼在手外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 米。”机械臂说“我的末端当前在基座坐标系下 x0.35、y0.2、z0.4。”这两句话之间没有任何换算关系。手眼标定做的事就是在这两套语言之间架一座桥。这个桥在 ROS 里以 TF 树的形式存在。整个项目跑起来之后你会在 TF 树里看到base_link → tool0 → camera_link或base_link → board_link → camera_link这样的链路。后面我会专门讲 TF 树怎么搭因为这里有个非常隐蔽的坑相机坐标系分为camera_link和camera_color_optical_frame两者之间不仅差一个平移还有一次 180° 的旋转用错一个标定结果全部白搭。2. 手眼标定公式里的AXXB到底在算什么2.1 一个姿态不够得让机械臂“转起来”看很多初学者以为手眼标定是拍一张照片算一次就完事实际上它需要机械臂带着标定板或带着相机运动到多个不同姿态每个姿态记录一组数据最后把所有数据丢进公式里拟合。为什么要多个姿态因为单次观测的方程是欠约束的。我们只知道“标定板在相机坐标系下的位姿”和“机械臂末端在基座坐标系下的位姿”但要求的“相机和机械臂基座或末端之间的固定变换”存在一个自由度的模糊性必须通过多次运动的相对关系把它约束出来。数学上每次机械臂从姿态 1 运动到姿态 2都会产生两组相对变化A标定板在相机坐标系下的位姿变化B机械臂末端在基座坐标系下的位姿变化。理想情况下两组变化之间满足A × X X × B这里 X 就是我们要标定的手眼矩阵。如果你代入多组姿态这个方程会变成一个超定方程组然后通过最小二乘或者更鲁棒的算法求出最优解。这就是 AXXB 的来历。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 namenamespace valueeasy_handeye_eye_on_base / arg nameeye_on_hand valuefalse / arg namerobot_base_frame valuebase_link / arg namerobot_effector_frame valuetool0 / arg nametracking_base_frame valuekinect2_rgb_optical_frame / arg nametracking_marker_frame valuekinect2_board / node pkgeasy_handeye typehandeye_calibration_camera namehandeye_calibration_camera outputscreen remap fromimage to/kinect2/hd/image_color_rect / remap fromcamera_info to/kinect2/hd/camera_info / param nametracking_base_frame value$(arg tracking_base_frame) / param nametracking_marker_frame value$(arg tracking_marker_frame) / param namemarker_size value0.025 / param namemarker_type valuecheckerboard / /node /launch有几个参数必须跟你实际环境对齐robot_effector_frame机械臂末端坐标系Aubo 里通常叫tool0tracking_base_frame必须是相机光学坐标系而不是相机外壳坐标系两者差一个旋转marker_size棋盘格格子边长单位米写错这个参数标定结果会整体缩放。4.3 采样过程与求解启动好 launch 之后用rosrun easy_handeye handeye_calibration_gui打开标定界面。界面上能看到相机画面和标定板检测框。操作流程是用 MoveIt 脚本把机械臂移动到第一个位姿 → 确认画面里标定板检测正常 → 点 “Take Sample” → 移动到下一个位姿 → 继续采样。这里有个经验采样时不要按固定顺序走最好随机打乱位置让机械臂的关节组合尽量多样。每次都从同一个方向接近标定板会造成姿态分布单一求解结果会偏。采够 15 到 20 组后点 “Compute”工具会给出求解后的平移和旋转。项目文档里展示了一组实际结果平移x0.312, y-0.045, z0.482单位米旋转四元数w0.921, x0.137, y-0.284, z0.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 pkgtf2_ros typestatic_transform_publisher这样每次启动机器人系统变换关系自动生效。5. 眼在手上全流程为什么比眼在手外更容易栽在采样上5.1 相机装到机械臂末端之后的变化眼在手上模式里Astra 相机被固定在 Aubo 末端法兰上。启动 Astra 驱动后相机坐标系会随着机械臂运动而移动TF 树的链路是base_link → tool0 → camera_link。这一模式在 easy_handeye 里的配置和眼在手外很相似但关键参数不同launch arg namenamespace valueeasy_handeye_eye_in_hand / arg nameeye_on_hand valuetrue / arg namerobot_base_frame valuebase_link / arg namerobot_effector_frame valuetool0 / arg nametracking_base_frame valueastra_color_optical_frame / arg nametracking_marker_frame valueastra_board / node pkgeasy_handeye typehandeye_calibration_camera namehandeye_calibration_camera outputscreen remap fromimage to/camera/color/image_raw / remap fromcamera_info to/camera/color/camera_info / param nametracking_base_frame value$(arg tracking_base_frame) / param nametracking_marker_frame value$(arg tracking_marker_frame) / param namemarker_size value0.025 / param namemarker_type valuecheckerboard / /node /launch标定板这次固定在工作台上不能移动。整个采样过程中是机械臂带着相机围绕标定板运动而不是标定板跟着机械臂动。5.2 采样姿态设计的关键技巧眼在手上标定最容易翻车的地方在于姿态设计。因为相机装在末端机械臂本身又有工作空间限制很多新手会让机械臂在一个小范围内小幅转动导致采样的姿态变化不够。实际标定时我用的策略是让机械臂末端在标定板正上方、正前方、左前、右前等几个位置分别停驻在每个位置分别让末端绕 X、Y、Z 三个轴旋转 ±20° 到 ±40°每次旋转后采样一组保证标定板在画面中始终完整且占比在 40% 以上避免标定板在画面里过于倾斜倾斜角超过 60° 后角点检测精度会急剧下降。这里有一个我反复验证过的现象如果采样姿态全部集中在一个很小的球面范围内AXXB 求解出的旋转分量看起来没问题但平移分量会非常不稳定甚至出现几百毫米的离谱值。把姿态分散开后平移结果的重复性会变得非常好。所以当你觉得标定结果“这次和上次差很多”的时候先看看采样姿态是不是太集中了。5.3 标定板检测失败的处理套路Astra 彩色相机的画质不如 Kinect2尤其是光线不足时棋盘格角点检测经常失败。项目源码里其实预留了两套检测方式棋盘格和 Aruco 码。如果棋盘格检测不稳定可以直接改用 Aruco 板。Aruco 的优势是每个码都有独立 ID即便画面中有部分遮挡也能通过剩余角点估算位姿。我在实际测试中遇到过一种情况棋盘格在画面中大部分时候能检测到但偶尔会少检测几列角点导致输出的标定板位姿跳变。这种样本如果不剔除会让求解结果带上很大的噪声。处理办法是在 GUI 中采样时每采一组就检查一下画面里的覆盖框是否完整有一点残缺就重新调整机械臂姿态再采。宁可少采几组也不能采脏数据。6. 标定结果怎么验、怎么用TF树与精度校验6.1 先看标定矩阵的物理合理性拿到标定结果后别急着写进系统先看一眼数值是否符合物理直觉。以眼在手上的结果为例Astra 相机装在末端法兰下方平移量应该在 z 方向有几十厘米因为相机和法兰之间有一个安装支架。如果你标出来的平移量是 x1.2、y-0.8、z1.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 解算出的标定板位姿不准后面 AXXB 求解出来的手眼矩阵也不可能准。所以我把“先标内参、再标手眼”列为这个项目的铁律。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% 的精力要花在数据采集和坐标系统一上算法只占很小一部分。如果你发现标定结果不稳定先别急着换求解算法回到数据层面看采样姿态是否足够分散、标定板检测是否稳定、坐标系是否挂对。把这三件事做好标定结果基本不会差到哪里去。这套项目里的源码和文档设计得比较好的地方也正是它把注意力引向了这些更容易出问题的环节而不是让人一上来就死磕数学公式。本文还有配套的精品资源点击获取