尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Gazebo仿真中UR5与D435手眼标定完整实操指南

Gazebo仿真中UR5与D435手眼标定完整实操指南 1. 为什么在仿真环境里做手眼标定很多人一听“Gazebo里做手眼标定”就会觉得多此一举仿真环境里每个坐标系的变换关系不都是精确已知的吗UR5的末端位姿可以从模型树里直接读相机挂在机械臂上的静态变换在URDF里就写好了还需要标定什么这个疑问我在刚开始做的时候也有。但实际把场景搭起来、把数据跑通以后我发现仿真里做的“手眼标定”一点不比实机省心它解决的问题也从来不是“我不知道真实变换”而是“我能否通过算法把相机观测和机械臂运动之间的关系估计出来”。这个流程一旦在仿真里跑通迁移到实机只是换数据和换驱动接口的问题很多算法层面的坑在仿真里反而更容易暴露和排查。先简单说下两种基本模式眼在手上eye-in-hand相机固定在机械臂末端跟随机械臂一起运动。标定结果是相机坐标系相对末端执行器坐标系的变换关系也就是T_camera_to_end_effector。眼在手外eye-to-hand相机固定在工作空间之外机械臂在相机视野内运动。标定结果是相机坐标系相对机器人基座坐标系的变换关系也就是T_camera_to_base。UR5加Realsense D435这种组合绝大多数应用场景是眼在手上。因为D435本身就是一个小尺寸的深度相机装在UR5的第六轴法兰盘上做抓取、视觉伺服、移动操作都方便。本文后面说的流程也以眼在手上为主。如果你要做眼在手外原理完全一样只是把公式里“末端位姿”换成“基座位姿”采集和验证方式略有不同。那为什么仿真里必须做一遍这个标定三个理由第一URDF里的安装位置是设计值不是实测值。你在URDF里写origin xyz0.05 0 0.1 rpy0 0 0/表示相机安装在末端法兰前方5厘米、上方10厘米这只是你期望的安装位置。实际导入Gazebo后模型对齐、link坐标系定义、传感器插件的坐标偏移任何一个环节出了偏差都会让你在仿真里拿到的图像和真实位姿对不上。仿真模型之间没有物理公差但有人为配置偏差。第二手眼标定的真正目标是建立“相机看到的”和“机械臂所在的”两个世界之间的桥梁。在Gazebo里你虽然知道所有模型的真实坐标但相机传感器输出的图像是经过渲染管线后生成的内参标定、畸变矫正、图像噪声、深度对齐这些环节和实机一样真实存在。相机内参在仿真里虽然可以直接从camera_info话题里读但外参手眼矩阵并不会自动写进TF树它需要你通过观测标定物来估计。第三仿真环境是调算法的绝佳测试场。实机上标定标定板位置放不好、光照有反光、机械臂运动幅度不够这些物理限制都会干扰结果。仿真里可以自由移动相机、随意改变标定物位姿、甚至直接把真实变换矩阵拿出来做对比验证标定误差到底是多少。这是实机给不了的调试自由度。所以这篇文章做的事情不是证明“仿真里不需要标定”而是回答“仿真里如何把标定流程做得跟实机一样规范并且利用仿真优势把每一步都验证清楚”。2. 仿真环境搭建UR5模型、D435相机与Gazebo的正确组合2.1 环境版本选择我用的环境是Ubuntu 22.04 ROS2 Humble Gazebo Classic 11。这里要说明一下虽然Ignition Gazebo现在叫Gazebo Harmonic功能更新但UR5和Realsense相关的现成插件、模型包绝大多数还是围绕Gazebo Classic做的用Classic能少踩很多版本兼容的坑。ROS2 Humble对应的机器人描述包建议直接用官方维护的版本ur_descriptionUR5的URDF/Xacro模型包含视觉和碰撞几何体。ur_gazeboUR5在Gazebo中的传动transmission、控制器插件配置。realsense2_descriptionIntel官方维护的Realsense相机URDF模型D435和D435i都有。这三个包在ROS2 Humble下都有对应的二进制版本直接apt安装就行不需要源码编译。我最初试过自己写URDF把D435模型拼到UR5上后来发现完全没必要官方模型已经处理好了大部分细节比如相机的link层级、惯性参数、视觉mesh路径。2.2 模型组合的关键步骤把D435挂到UR5上的方法推荐在UR5的xacro文件里额外添加一个相机link或者用xacro:include把D435模型包含进来后用fixed joint固定到tool0坐标系的子节点上。实际操作中我更推荐先启动UR5的完整模型再单独加载D435的URDF用一个静态变换发布器把它们关联起来。这样做的好处是便于调试如果相机位置不对或者相机的坐标系方向有问题只需要改动静态变换的参数不用重新启动整个Gazebo世界。具体做法是在launch文件中添加node pkgtf2_ros execstatic_transform_publisher args0.05 0 0.1 0 0 0 tool0 camera_link/注意这里的camera_link是D435模型中的根link。这个顺序不能反了tool0是父坐标系camera_link是子坐标系。但这个方法说实话只能应付快速验证。如果你想让相机真正跟随机械臂末端运动最好还是在URDF里定义好joint因为Gazebo里模型运动是靠joint连接的仅靠TF树不足以让视觉传感器跟着link移动。2.3 D435在Gazebo中的传感器插件配置D435在Gazebo里要正常出图需要给相机link加上sensor标签典型配置如下sensor typecamera namecamera_color camera horizontal_fov1.0472/horizontal_fov image width640/width height480/height formatR8G8B8/format /image clip near0.1/near far10.0/far /clip /camera plugin namecamera_driver filenamelibgazebo_ros_camera.so ros remapping~/color/image_raw:/camera/color/image_raw/remapping remapping~/camera_info:/camera/color/camera_info/remapping namespace//namespace /ros camera_namecamera/camera_name frame_namecamera_color_optical_frame/frame_name /plugin /sensor这里有个非常容易踩的坑frame_name一定要设置成camera_color_optical_frame而不是camera_link。因为Realsense的ROS驱动输出的图像其坐标系是光学坐标系——x轴朝右y轴朝下z轴沿着相机光轴朝前。如果这里不写对后面做手眼标定时你拿到的图像坐标和相机位姿全部错位而且错得很隐蔽因为看起来一切正常只是标定结果完全不对。类似的插件配置还要加一个深度相机的。D435是双目结构光深度相机在Gazebo里深度图的渲染方式和RGB图不同需要额外的深度传感器配置但如果你只是做2D标定RGB相机就够了。我后面说到的方法也是基于RGB图像检测ArUco码因此只需要确保彩色图像话题有数据就行。2.4 验证相机能正常出图启动完整的仿真环境后先做三件验证rviz2中添加Image显示订阅/camera/color/image_raw确认画面内容正确。用ros2 topic echo /camera/color/camera_info查看相机内参确认分辨率、焦距合理。用ros2 run tf2_ros tf2_echo tool0 camera_color_optical_frame确认TF树的坐标关系存在且无跳变。这三步做完环境就通了。很多人在仿真里标定出问题回溯到最后都是这几个基础环节没验证导致后面全是在错误的地基上盖楼。3. 标定物怎么搞仿真里的ArUco码与棋盘格3.1 三种方案对比实机标定常用棋盘格因为角点检测精度高、对光照鲁棒。但仿真里有个特殊情况棋盘格贴在Gazebo模型的表面时渲染出来的图像是经过纹理映射的Gazebo Classic的渲染引擎对高对比度的棋盘格纹理处理一般没问题但ArUco码这类二维码在远距离或者相机角度倾斜时检测效果会更差一些。三种方案对比方案检测方式仿真适用性推荐度ArUco码cv2.aruco.detectMarkers高一个码即可定位且码本身提供方向信息首选棋盘格cv2.findChessboardCorners中纹理分辨率低时角点容易漏检可用圆点网格cv2.findCirclesGrid中需要所有圆点完整可见备选我推荐ArUco码原因有三个第一一个码就能提供完整的6自由度位姿估计不需要像棋盘格那样必须让相机看到整块板子第二ArUco码自带ID和方向信息即使部分遮挡也能检测第三OpenCV的aruco模块提供了完整的位姿估计函数estimatePoseSingleMarkers直接返回旋转向量和平移向量省去自己解PnP的步骤。3.2 在Gazebo中创建ArUco标定物最简单的方法是直接创建一个带有ArUco纹理的立方体或平面板模型。我试过几种创建方式用Blender生成带纹理的平面导出为DAE或OBJ模型再用spawn_model命令放入Gazebo。这是热词里提到“blender导出gazebo模型”时大家常做的事。优点是纹理清晰可控缺点是流程长需要熟悉Blender的UV导出。直接写一个SDF文件用material标签指定纹理贴图。SDF支持给模型的面指定图片作为材质这个方法最轻量。在Gazebo的模型数据库里找现成的标定板模型不过质量参差不齐而且ArUco字典版本可能是老旧的。我自己用的是第二种写一个简单的SDF文件生成一块长宽为0.2米的板子把ArUco码的PNG图片作为纹理贴上去。ArUco码图片可以用OpenCV生成import cv2 import cv2.aruco as aruco dictionary aruco.Dictionary_get(aruco.DICT_6X6_250) img aruco.drawMarker(dictionary, 42, 200) cv2.imwrite(aruco_42.png, img)这里有个细节需要注意生成图片时使用的aruco.DICT_6X6_250必须和后面检测时使用的字典一致。不一致的话OpenCV会直接报找不到标记。我习惯把字典名、标记ID、板子尺寸都写在一个配置文件的顶部作为全局参数管理防止中途更换导致标定数据作废。SDF文件里给模型的表面指定纹理贴图用uri指向PNG文件路径。尺寸一定用真实物理单位米这样PnP求解出来的平移向量才是米制单位和UR5的坐标系保持一致。3.3 为什么仿真里的ArUco码有时候检测不到Gazebo Classic的渲染引擎对纹理的清晰度跟你图像分辨率、相机距离、码的像素大小直接相关。即使你把码贴到模型上如果码在图像里占的像素太少检测就会失败。一般规律ArUco码在图像中至少需要占据50x50像素以上检测才稳定。所以标定板的尺寸、相机的距离、分辨率三者之间要配合。我用的是640x480分辨率的图像标定板0.2米长宽相机距离标定板0.4到0.8米之间基本能稳定检测到标记。如果你发现检测不稳定优先排查贴图的文件格式是否为PNG是否带透明通道RGB纹理不要用带Alpha的PNG否则Gazebo渲染时可能出问题。相机曝光和图像亮度是否合适Gazebo Classic的相机插件默认不模拟自动曝光光照太暗会导致图像整体偏黑ArUco检测失败。这时需要在场景中加足够的光源。相机到标定板的距离是否太远把标定板往相机方向挪一下或者增大相机的near和far裁剪面的范围。是否需要调高相机的sensor分辨率如果只想用640x480那么标定板尽量靠近相机如果空间不够可以临时改成1280x720采集标定数据标定时用对应的内参。还有一个经常被忽略的点Gazebo Classic中材质贴图的方向可能和预期不同。你需要在仿真里实际看一下图像确认ArUco码的方向和尺寸是否符合预期而不要想当然地认为贴上去就是正的。我看到过好几次码倒过来了或者前后镜像了检测倒是能检测到但位姿解算整体多了个旋转偏差。4. 核心实现标定数据采集、求解与验证4.1 手眼标定的数学本质先建立概念。眼在手上时机械臂末端执行器坐标系为E相机坐标系为C标定板坐标系为B。我们希望求解的是T_C_E即相机坐标系到末端坐标系的齐次变换矩阵。机械臂运动到某个位姿时从TF树可以拿到末端到基座的变换T_E_BASE从ArUco检测可以拿到相机到标定板的变换T_C_B。标定板是固定的所以标定板到基座的变换T_B_BASE是常量。根据链式法则T_B_BASE T_E_BASE * T_C_E * inv(T_C_B)这个公式对所有运动位姿都成立联立多个观测就能求解出T_C_E。OpenCV的calibrateHandEye函数就是做这个解算的。它需要两组输入R_gripper2base和t_gripper2base末端到基座的旋转矩阵和平移向量。R_target2cam和t_target2cam标定板到相机的旋转矩阵和平移向量。注意calibrateHandEye的输入约定是gripper2base不是base2gripper这个方向搞反了标定结果会完全不对。我第一次用的时候在这个方向上栽过跟头后面详细讲。4.2 数据采集的完整流程数据采集是整个流程中最容易出错也最影响结果的一环。仿真里虽然不会因为机械臂抖动导致标定板移动但你依然要认真规划采集策略。我的采集流程是控制UR5末端执行器运动到一系列不同的位姿让相机从不同角度观察标定板。在每个位姿下同时记录以下数据末端执行器的位姿从TF树读取ros2 run tf2_ros tf2_echo tool0 base_link相机检测到的ArUco码位姿用estimatePoseSingleMarkers得到。每个位姿保存一份数据建议至少采集15到20组。将数据写入一份JSON或NPZ文件便于后续离线求解。关键点运动范围要尽量大不仅要平移还要旋转。如果相机和标定板的相对位姿变化太少手眼方程是病态的解出来的旋转矩阵会严重退化。实机上受机械臂工作空间限制仿真里没有这个限制所以我通常会设计一组涵盖不同高度、不同角度、不同距离的位姿列表poses [ (0.40, 0.00, 0.45, 0.0, 0.0, 0.0), (0.45, -0.10, 0.50, 0.0, 0.2, 0.0), (0.40, 0.10, 0.50, 0.0, -0.2, 0.0), (0.50, 0.00, 0.40, 0.15, 0.0, 0.1), (0.35, -0.05, 0.55, -0.15, 0.1, 0.2), # ... 继续添加 ]这里每个元组是(x, y, z, roll, pitch, yaw)的末端目标位姿。执行时用UR5的Gazebo控制器发送目标位置等待机械臂稳定后再采集一帧数据。等待的间隙可以用rospy.sleep(1.0)或者等一等状态话题确保机械臂已经完全停下否则采集到的位姿存在运动误差。采集完的数据建议直接保存为NumPy格式方便后续批量处理。我一般保存成三个数组gripper2base的旋转和平移target2cam的旋转和平移。保存格式如下np.savez(calib_data.npz, R_gripper2baseR_gripper2base_all, t_gripper2baset_gripper2base_all, R_target2camR_target2cam_all, t_target2camt_target2cam_all)4.3 求解代码的完整实现数据采集完成后用OpenCV求解import cv2 import numpy as np data np.load(calib_data.npz) R_gripper2base data[R_gripper2base] t_gripper2base data[t_gripper2base] R_target2cam data[R_target2cam] t_target2cam data[t_target2cam] R_cam2gripper, t_cam2gripper cv2.calibrateHandEye( R_gripper2base, t_gripper2base, R_target2cam, t_target2cam, methodcv2.CALIB_HAND_EYE_TSAI)OpenCV提供多种手眼标定求解算法Tsai-Lenz、Park、Horaud、Andreff、Daniilidis。我在仿真里对比过Tsai-Lenz在数据量足够、噪声水平较低时表现稳定Park和Horaud对旋转外参的求解精度更高Andreff是全局优化处理带噪声的数据时更鲁棒。实际使用中我推荐先用Tsai-Lenz求出初值再用Park或Andreff的结果作为交叉验证。如果不同算法得出的结果差异很大说明数据质量有问题需要回去重新采集而不是纠结于选哪个算法。求解得到的是相机到末端的变换T_C_E而ROS中我们通常需要的是T_E_C也就是相机坐标系在末端坐标系中的表示。这两个互为逆矩阵用的时候别搞混了。在TF树中发布静态变换时发布的是父坐标系到子坐标系的变换你要确定好父子关系。4.4 结果验证怎么知道标定对了标定结果对不对不能只看算法输出必须验证。验证方法有好几种我按可靠性排序与真值对比。这是仿真环境的独有优势。在URDF里你定义了相机的安装变换直接在Gazebo模型树里查询真值把标定结果和真值做差直接算出误差。用这个误差作为最严格的评价指标。重投影验证。利用标定结果将标定板上的已知特征点投影到图像上计算投影点与检测点之间的像素误差。这个误差一般在几个像素以内就算正常。独立测试集验证。训练时用一部分位姿的数据求解留出另外一部分位姿的数据做预测。利用标定结果和末端位姿预测相机观测到的标定板位姿再与该位姿的实际观测值对比。我在项目中三种方法都用了。真值对比在纯Gazebo环境里最方便直接算出旋转误差和平移误差def compute_pose_error(T_est, T_gt): R_diff T_est[:3, :3] T_gt[:3, :3].T angle np.arccos(np.clip((np.trace(R_diff)-1)/2, -1, 1)) t_diff np.linalg.norm(T_est[:3, 3] - T_gt[:3, 3]) return angle * 180 / np.pi, t_diff如果角度误差超过1度平移误差超过1厘米我会认为标定结果不可靠重新检查数据采集过程。5. 避坑指南我在Gazebo里做手眼标定踩过的6个坑5.1 坐标系方向搞反结果完全不可用这是最隐蔽也最致命的坑。ArUco位姿估计函数estimatePoseSingleMarkers返回的旋转向量和平移向量是从标定板坐标系变换到相机坐标系的变换也就是T_C_B。而calibrateHandEye需要的是T_B_C即标定板到相机的逆变换。很多人直接把estimatePoseSingleMarkers的输出丢给calibrateHandEye结果求出来的手眼矩阵和真值差了十万八千里。正确做法对旋转向量做cv2.Rodrigues转换成旋转矩阵后将平移和旋转取逆得到相机的T_C_B的逆矩阵T_B_C再填入R_target2cam和t_target2cam。用代码表示就是rvec, tvec, _ aruco.estimatePoseSingleMarkers(corners, marker_length, camera_matrix, dist_coeffs) R_CB, _ cv2.Rodrigues(rvec) T_CB np.eye(4) T_CB[:3, :3] R_CB T_CB[:3, 3] tvec[0][0] T_BC np.linalg.inv(T_CB)如果这一步漏了标定结果是不可能对的。5.2 TF树中读取末端位姿选错坐标系UR5的TF树中base_link和tool0是常见的坐标系但有些模型还会有wrist_3_link、flange等不同的末端坐标系。手眼标定中你使用的末端坐标系必须和实际安装相机的坐标系一致。如果你的相机是安装在tool0上的那么gripper2base读的应该是base_link到tool0的变换而不是base_link到wrist_3_link的变换。这两个坐标系之间通常有一个固定的偏移如果你混合使用标定结果会出现一个很大的常值平移误差。检查方法是在标定前先发布一个静态变换对比tool0和wrist_3_link之间的坐标关系确认你读取的是正确的坐标系。5.3 标定板在图像中太小位姿估计退化ArUco码的位姿估计依赖于码在图像中的清晰度和尺寸。当码在图像中只占几十个像素时角点检测的亚像素精度会大幅下降导致tvec和rvec存在噪声。这个噪声会直接传导到手眼标定结果中。在仿真里解决这个问题非常容易把标定板靠近相机或者把相机的视野调大一点。不要让码在图像中占的面积少于5%到10%。标定时我一般保证ArUco码的边长在图像中至少占100像素以上实测下来这个量级的数据标定结果非常稳定。5.4 数据采集的位姿分布不充分手眼标定本质上是通过机械臂的多种运动姿态来约束未知变换。如果机械臂只在一个平面上运动或者相机和标定板的相对姿态变化很小方程组的条件数会很大标定结果对噪声极其敏感。采集数据时要保证末端执行器的旋转角度覆盖足够大的范围。理想情况下末端执行器的Z轴方向应该尽量分散比如让相机从上方、侧面、斜上方等不同方向看标定板。仿真里没有机械臂奇异点的约束自由度很大建议采集数据时动动脑子设计一个空间分布均匀的位姿集合而不是让机械臂走一个固定的轨迹。5.5 Gazebo Classic中的图像噪声和纹理渲染问题Gazebo Classic默认不模拟相机噪声但纹理渲染在特定条件下会出现摩尔纹或锯齿特别是ArUco码的黑色边框边缘。这在检测角点时会产生像素级的偏差。缓解方法标定板贴图尽量放大让码的每个格子占据多个像素减少纹理混叠。在生成ArUco码图片时给图片边缘留一圈白色边框方便边缘提取。如果检测出的角点位置抖动可以对图像做一次高斯模糊再做亚像素角点提取。5.6 相机内参和图像话题的坐标系不匹配D435在ROS驱动中彩色图像的话题是/camera/color/image_raw对应的光学坐标系是camera_color_optical_frame。但Gazebo中默认的相机坐标系是camera_linkz轴朝向是相机的后方。如果你用默认配置启动相机会发现图像和坐标系完全对不上。务必在相机插件配置中显式指定frame_namecamera_color_optical_frame/frame_name同时在TF树中发布从camera_link到camera_color_optical_frame的静态变换两个坐标系之间的旋转关系是绕x轴旋转90度。这个变换如果不发布后面读取图像坐标和相机位姿时将无法对应。6. 几个让仿真标定更好用的实操细节6.1 用ros2 bag录制完整数据集标定数据采集过程中如果每个位姿手动保存一组数据不仅慢还容易漏。更高效的做法是控制机械臂连续运动同时用ros2 bag record录制/tf、/camera/color/image_raw、/camera/color/camera_info话题运动结束后离线处理bag文件从bag中提取每个时刻的末端位姿和图像检测结果。这样采集的数据更加密集也方便后续调整算法参数时重新处理。注意bag中/tf话题是全局坐标系的变换你需要从中提取base_link到tool0的变换以及camera_color_optical_frame到camera_link的变换。6.2 引入多组标定物增强鲁棒性仿真场景中可以同时放置多个ArUco码分布在不同的位置。这样机械臂运动到任意位姿时只要视野中有一个码可见就能解算位姿无需精确对准单个标定板。标定时选用检测置信度最高的那个码或者多个码取平均。多码方案还能让你在采集时少受“标定板必须始终出现在视野中心”的限制运动路径规划更自由。6.3 把时间同步当作一等公民处理仿真中虽然所有话题的时间戳都来自同一个模拟时钟但如果直接录制bag图像消息和TF消息之间存在微小的时序偏差。处理bag时不要把图像检测的位姿直接和最近一帧TF配对而是用消息的时间戳做插值或者对齐。我建议在数据采集时每个位姿让机械臂停稳后等0.5秒再记录图像和TF的快照。这样时间误差可以忽略不计比任何插值方案都可靠。6.4 标定前先做一个快速自检在正式标定前先采集3到4组数据快速跑一遍流程看看结果是否合理。如果连快速自检都过不了就不要浪费时间采集20组数据先解决基础问题。自检的判断标准在仿真里可以直接对比真值如果标定结果的角度误差大于3度基本说明数据采集或者代码逻辑有问题不需要继续采集。7. 从仿真结果迁移到实机时需要注意什么仿真标定流程跑通不代表实机可以直接照搬。实机上的物理约束、传感器噪声、标定板制造误差都会增加额外的变量。但从仿真实操中获得的最重要的是流程感——知道每一步为什么这么做遇到问题从哪里入手排查。仿真与实机最大的区别在于仿真里可以用真值验证误差实机里只能通过重投影误差和实际抓取效果来判断。所以在仿真中你应该刻意练习“如何判断标定结果好坏”的方法这些判断标准在实机上同样适用。另外仿真模型中的相机内参是理想值实机D435的内参最好用realsense-viewer单独标定不要直接从官方宣称的参数表读取。内参的微小偏差也会通过手眼标定的像素观测环节引入误差。我个人的体会是——仿真里做手眼标定的最大价值不是得到一个“完美的标定矩阵”而是把整个手眼标定的方法论和调试路径吃透。当你把这个流程迁移到实机上所有在仿真里踩过的坑都会变成你在实机调试时的直觉。这也是我在项目中最看重仿真环节的原因。
返回列表