ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

UR5机械臂+深度相机手眼标定:Gazebo仿真全流程详解

UR5机械臂+深度相机手眼标定:Gazebo仿真全流程详解 手眼标定这事我在真机上做过几回每次都是心惊胆战。机械臂要摆各种姿势、标定板得有人举着、一不留神相机视野就丢了、还得担心运动过程会不会撞到人。后来我换了思路先在 Gazebo 仿真里把整套流程跑通再上真机。这个决定帮我省掉了大量试错成本。UR5 机械臂加上 Realsense D435 深度相机的手眼标定流程其实完全可以先在仿真环境里完整走一遍从相机模型、标定板、位姿规划到解算验证每一步都能在虚拟环境里提前踩完坑。这篇文章就是把我实际操作中整理的完整流程和避坑经验分享出来适合刚开始接触机械臂视觉标定、希望先在仿真中验证算法与流程的朋友参考。1. 为什么非要在仿真里先做一遍UR5同款标定1.1 真机标定让人头疼的几件事先说真机标定的痛点。UR5 末端装上 D435 之后相机到末端的位姿变换是一个固定的刚体变换但它不来自机械臂设计图也不来自相机安装支架的 CAD 模型——因为安装公差和装配误差你没法从图纸上直接量出来。手眼标定就是通过一系列运动位姿和视觉观测来反推这个变换。真机上的麻烦事特别多。第一是安全问题机械臂要带着相机摆出各种姿态标定板放的位置、人的站位都得小心第二是数据不稳定光照环境变化、相机自动曝光、标定板反光都会影响特征检测精度出问题了你根本分不清是相机标定参数的问题还是手眼标定本身的问题第三是时间和硬件成本机械臂被占用了整整一个下午不说万一手上的相机或者标定板被碰歪了全部数据作废重新来。1.2 仿真环境的价值不只是安全在 Gazebo 仿真里做手眼标定的最大价值是能把系统问题和算法问题分开排查。所谓系统问题包括相机有没有图像、TF 树对不对、话题通不通、坐标系有没有对齐所谓算法问题才是标定方程怎么列、数据够不够、解算结果准不准。仿真环境里相机是虚拟的标定板纹理是贴上去的图像输出干净可控。这样你只要发现标定结果不对劲基本可以断定是算法或数据采集流程的问题而不是噪声干扰。另一个好处是仿真里一切变换关系都是已知的——你可以随时拿到相机坐标系到机械臂末端的真值直接和标定结果对比这在真机上是做不到的。我之前在真机上标定后要验证结果只能通过投影误差间接估计而在仿真里我可以直接算绝对误差这个反馈闭环非常舒服。1.3 这套流程适合谁这篇文章针对的是准备用 UR5或类似六轴工业机械臂搭配 Realsense D435 做视觉抓取、视觉定位等项目的朋友。如果你的目标是快速把视觉算法在机械臂上跑起来但又不想一开始就碰真机的安全风险或者你是学生、刚入行的工程师手边没有机械臂但想先把手眼标定这套东西搞明白那这条仿真流程会非常合适。软硬件上只需要一台能跑 Gazebo 的电脑环境我用的是 Ubuntu 22.04 加 ROS 2 HumbleGazebo 版本是 Gazebo 11这套组合在校园网和公司内网里都能很稳定地跑起来。2. 先把坐标系关系理顺眼在手上的标定方程到底是啥2.1 两种手眼关系先说清楚你的属于哪一种手眼标定按相机安装位置分成两类。眼在手上eye-in-hand指相机安装在机械臂末端随着机械臂一起动眼在手外eye-to-hand指相机固定在外部支架上只有机械臂自己在动。UR5 末端装 D435 这种方案绝大多数情况下属于眼在手上。这两种情况要标定的东西是不一样的。眼在手外时待求的是相机到机械臂基座或者说机器人基座到相机的固定变换而眼在手上时待求的是相机到机械臂末端法兰的固定变换。在 Gazebo 里做 UR5 加 D435 的标定我们关注的是后者也就是标定相机相对末端法兰的位置和朝向。这个区别特别容易搞混因为两个问题的数学表达式长得非常像很容易把坐标系代错。我的经验是画一张坐标系关系图把每一条变换边都标出来再动笔推。你在仿真里跑的时候如果发现最终结果差得离谱多半是这一步把某个变换方向写反了。2.2 AXXB 是怎么推出来的眼在手上情况下的核心方程是 AXXB。我把自己推导的过程写一下这个比直接背公式重要得多。记机械臂末端在基座坐标系下的位姿为 T_base_to_end这是可以直接从机械臂的正运动学或者 TF 话题里拿到的记相机外参即标定板坐标系到相机坐标系的变换为 T_cam_to_target这是相机拍到标定板之后通过外参估计得到的。那么标定板在基座坐标系下的位姿就是T_base_to_target T_base_to_end * X * T_cam_to_target其中 X 就是我们要标定的相机到末端的变换。因为标定板在整个采集过程中固定不动所以 T_base_to_target 是不变的常量。现在让机械臂运动两次得到两组 T_base_to_end1、T_base_to_end2 以及对应的 T_cam_to_target1、T_cam_to_target2于是有T_base_to_end1 * X * T_cam_to_target1 T_base_to_end2 * X * T_cam_to_target2两边同时左乘 T_base_to_end1 的逆再同时右乘 T_cam_to_target1 的逆整理得到(T_base_to_end1^(-1) * T_base_to_end2) * X X * (T_cam_to_target1^(-1) * T_cam_to_target2)把左边括号里的量记为 A右边括号里的量记为 B方程就变成了 AXXB。2.3 A 和 B 各自的物理含义A 表示机械臂末端在基座坐标系下的相对运动也就是两次运动中末端法兰的变化量B 表示标定板在相机坐标系下的相对运动也就是相机自己看到的标定板位置变化。这两个量都是从传感器或者运动学里直接得到的而 X 是连接两者的纽带。这个方程之所以成立关键前提是标定板固定不动、相机和末端刚性连接。所以在仿真里采集数据时标定板一旦在 Gazebo 中放置好就不要让它有任何位移哪怕是几毫米的漂移都会让方程解出来的结果有偏。了解了方程结构你就明白为什么要让机械臂走多种不同姿态了。如果机械臂只是沿着某个方向平移而几乎没有旋转那么 A 和 B 的旋转分量都很小方程在旋转部分的约束很弱标定出来的旋转部分就会不稳定。反过来如果机械臂位姿变化足够丰富旋转角度跨度足够大方程约束就越强结果越稳定。这是后面要设计一组合格位姿的理论依据。3. 仿真环境组装从Gazebo模型到D435相机插件3.1 UR5模型怎么在Gazebo里跑起来UR5 的仿真模型我建议直接用现成的 URDF 包不要白费劲从 CAD 模型往 Gazebo 导。universal_robot 仓库里的 ur_description 包提供了完整的 URDF 和网格文件配合 ur_simulation_gazebo 里的控制配置能让机械臂在 Gazebo 里正常受 ros_control 控制。如果你用的是 ROS 2可以找带调试好的 Gazebo 插件的版本自己从头写传动和控制器配置会比较痛苦。具体做的时候URDF 里每个关节要配置好 transmission 和 gazebo_ros_control 插件这样机械臂的 joint_state 才会发布出来MoveIt 规划得出的关节位置指令才能真正执行到仿真机械臂上。一个典型的 transmission 配置大概长这样transmission nameshoulder_pan_trans typetransmission_interface/SimpleTransmission/type joint nameshoulder_pan_joint hardwareInterfacehardware_interface/PositionJointInterface/hardwareInterface /joint actuator nameshoulder_pan_motor mechanicalReduction1/mechanicalReduction /actuator /transmission加载模型时需要注意 GAZEBO_MODEL_PATH 环境变量。常见的报错是模型里的网格文件找不到或者材质显示成一片灰白色。我的习惯是先把 .gazebo 文件夹下的 model 目录用 export GAZEBO_MODEL_PATH 指到 ur_description 的包路径再启动 world。3.2 D435相机的外壳和光学模型D435 在 Gazebo 里不算复杂完全可以只用一个 RGB 相机模拟。不过为了贴近真实建议把视觉模型和光学模型分开。视觉模型是相机外壳、镜头那几毫米的圆柱体你不一定真的需要在纯标定场景中可以忽略而光学模型才是关键——相机的图像平面、视场角、分辨率、内参。我通常的做法是在 URDF 里加一个 camera_link然后在 Gazebo 的 sensor 标签里挂一个 camera 类型的传感器。注意相机光学中心坐标系要建好一般叫 camera_color_optical_frame它和 camera_link 之间存在固定的坐标变换。在 Gazebo 里设置相机话题名ROS 2 下可以用 libgazebo_ros_camera.so 插件gazebo referencecamera_color_frame sensor typecamera namecamera_color_sensor update_rate30/update_rate camera horizontal_fov1.0821/horizontal_fov image width640/width height480/height formatR8G8B8/format /image clip near0.05/near far50/far /clip /camera plugin namecamera_controller filenamelibgazebo_ros_camera.so ros namespace/camera/namespace remappingimage_raw:color/image_raw/remapping remappingcamera_info:color/camera_info/remapping /ros camera_namecamera_color/camera_name /plugin /sensor /gazebo这里 horizontal_fov 的取值是有说法的。Realsense D435 的 RGB 相机标称视场角大概是 69.4 度水平但仿真中更稳妥的做法是先按相机的物理焦距换算。D435 在 640 乘 480 分辨率下fx 约为 615 像素左右那么水平视场角就满足 tan(fov/2) 320/615换算出来大概是 55 度左右。实际项目中我建议以官方标称的 69.4 度为基准配置然后在验证环节再校准内参——不过仿真里做手眼标定只要内参一致不会因为相差几度导致流程失败。3.3 如果非要复现真实D435还能加点什么如果你希望仿真更贴近真实的 D435除了 RGB 相机之外还可以添加左右红外相机和深度相机。但要注意在 Gazebo 里同时挂多个相机传感器会成倍增加渲染负担跑起来帧率会明显下降。我的建议是标定阶段只保留 RGB 相机等手眼标定跑通之后如果有必要再补上深度传感器做后续的视觉算法验证。在发布深度图像时用 libgazebo_ros_depth_camera.so 插件话题和 TF 树要和控制彩色相机一样仔细命名。D435 在仿真里还有一个小细节它的深度和彩色相机不在同一个光学坐标系。如果你后面要用深度图对齐彩色图就要在 URDF 里把 camera_depth_optical_frame 和 camera_color_optical_frame 之间的偏移设好真实的 D435 上二者的位置有一点点差异仿真里可以近似设成零但坐标系名字必须存在否则很多视觉工具链在找 TF 时会直接报错。4. 在Gazebo里摆好标定板让UR5摆出一组合格位姿4.1 标定板怎么生成ArUco板比棋盘格更省事手眼标定常用的标定板有棋盘格和 ArUco 板两种。棋盘格在标定相机内参时很好用因为角点检测稳定但在手眼标定场景中每张图像都要估计标定板到相机的位姿ArUco 码的单板位姿估计通常比整张棋盘格的角点外参估计更稳定而且 ArUco 不用关心部分遮挡的问题哪怕只看到部分码也能给出位姿。生成 ArUco 板非常容易用 OpenCV 的脚本就能搞定在 Python 里生成一张 7 行 5 列的 ArUco 网格图import cv2 import numpy as np aruco_dict cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_5X5_100) board cv2.aruco.GridBoard_create(7, 5, 0.04, 0.005, aruco_dict) img board.draw((700, 500)) cv2.imwrite(aruco_board.png, img)这里每个方格边长 4 厘米码间隔 5 毫米。实际尺寸单位无所谓因为在物理上只是等比缩放但解析端必须用同一个尺寸所以要记录好。在仿真里把这张图贴到 Gazebo 的平面模型上。我通常是先建一个简单的 box 或者 plane 模型在 Gazebo 模型配置文件里把图片设为贴图。需要注意贴图路径不要用中文也不要用带空格的路径否则模型加载时材质会崩。Gazebo 加载贴图之后在 RenderWindow 里如果看到一片白色多半是模型路径没找对或者图片格式问题换成 png 再试。4.2 标定板放哪里、相机怎么对标定板放置位置的核心原则是机械臂运动过程中相机能始终看到标定板并且标定板尽量充满视野三分之一以上别太小。通常我把标定板放在 UR5 工作空间的中部略偏外的位置高约 60 到 80 厘米。这样机械臂末端带着相机可以在一个半球范围内移动而不至于丢失视野。相机安装在用于模拟真实安装的方法可以在 URDF 里通过一个固定关节把 camera_link 固连到 ur5 的末端 link 上比如 wrist_3_link。安装方向我习惯让相机光轴朝前也就是大体沿着末端法兰的 z 轴或者 x 轴具体看你的支架设计但固定关节一旦定了后续标定求出来的就是它。4.3 让机械臂走出一组约束丰富的位姿生成标定数据最关键的部分就是位姿采样。我写了一个 Python 脚本用 MoveIt 的运动规划接口让 UR5 依次运动到 8 个预定义目标位姿。位姿的选择要遵守三个原则。第一位姿之间要有明显的旋转变化特别要包含绕不同轴的旋转。不要只在一个平面内平移否则旋转部分的方程约束不够第二标定板在图像里的位置和尺度要有变化覆盖图像的多个区域比如左上角、中心、右下角等避免标定板永远只出现在中心第三避免运动到奇异位姿。我实际用的 8 个位姿大致是末端分别指向标定板的左、中、右再分别抬高和放低形成几种不同的俯仰角最后一个位姿把相机旋转 45 度去拍标定板。这套位姿集合确保了方程两边都有足够多的旋转信息。在 MoveIt 里用 Python 做运动规划核心片段如下import moveit_commander move_group moveit_commander.MoveGroupCommander(manipulator) move_group.set_pose_reference_frame(base_link) move_group.set_end_effector_link(wrist_3_link) pose_goal { position: [0.5, -0.2, 0.7], orientation: [0.0, 0.0, 0.0, 1.0], } move_group.set_pose_target(pose_goal) plan move_group.plan() move_group.execute(plan, waitTrue)第一位姿可以直接用笛卡尔坐标设定后面几个位姿可以在 rviz 里拖拽碰撞检测没问题后保存下来。走完一个位姿后停 2 到 3 秒再采图像和机械臂位姿保证机械臂已经完全停稳避免运动模糊。5. 采集数据与解算easy_handeye的配置和运行5.1 easy_handeye 到底帮你做了什么手眼标定工具链里我推荐直接用 easy_handeye 包。它的原理是在标定过程中同时监听两个话题——机械臂末端到基座的变换通常从 TF 树获得以及相机检测到标定板后得到的标定板到相机的变换由 ArUco 检测节点发布。每在界面上点一次采集样本工具就把当前这两个变换锁存下来。采集完若干组之后调用 OpenCV 的 calibrateHandEye 解算 AXXB。这个包在 ROS 1 时代已经很成熟ROS 2 环境下有 community 维护的移植版本。我在 Humble 下用的一点问题不大但要注意编译前把依赖装齐aruco、opencv、geometry_msgs 这些。5.2 ArUco检测节点要让 easy_handeye 拿到标定板在相机坐标系下的位姿需要一个 ArUco 检测节点。ROS 2 里有现成的 aruco_ros 移植包也可以自己写一个极简节点发布 camera 坐标系到 marker 坐标系的 TF。一个重要细节是ArUco 板的坐标系方向会影响最终标定结果但只要你检测端和采集端用同一个定义标定出来的 X 仍然是自洽的。为了符合习惯我一般用 ArUco 板上从左到右、从上到下的方向让 marker 的 z 轴垂直板面朝外。一个最简的检测节点只需订阅 /camera/color/image_raw用 cv2.aruco.detectMarkers 找到角点再用 cv2.aruco.estimatePoseSingleMarkers 得到旋转向量和平移向量转成四元数和平移发布 TF。位姿估计算法对距离尺度的敏感度很高所以之前生成的 ArUco 板尺寸参数在检测端必须严格一致。5.3 让easy_handeye和MoveIt配合easy_handeye 的配置集中在标定参数文件里关键几项如下handeye/eye_on_hand: true handeye/robot_base_frame: base_link handeye/tool_frame: wrist_3_link handeye/camera_frame: camera_color_optical_frame handeye/marker_frame: aruco_board handeye/tracking_base_frame: camera_color_optical_frame handeye/tracking_marker_frame: aruco_board其中 eye_on_hand 设置为 true表明是眼在手上模式tool_frame 是 UR5 末端法兰所在坐标系camera_frame 是相机光学坐标系。这里最容易出错的点在于easy_handeye 要的手是末端法兰坐标系而不是相机的安装坐标系更不是随便一个工具坐标系如果 tool_frame 指错了整个标定的参考基准就错了结果当然对不上。启动之后GUI 页面会显示当前机器人末端到基座的变换和检测到的标定板到相机的变换。你点击 Next 可以触发机械臂运动到下一位姿点击 Take Sample 采集一组数据。如果你像我一样已经用脚本预先规划好了机械臂运动那么也可以在机械臂到位之后直接在 GUI 里点 Take Sample。当采集了 8 组样本后点击 Compute 按钮工具会调用算法解算并显示结果。5.4 解算算法的选择OpenCV 的 calibrateHandEye 支持多种算法easy_handeye 里也有对应的接口。项目初期用默认的 Tsai-Len 方法没什么问题但如果你发现标定结果在旋转部分很稳定、平移部分却来回跳动可以换 Daniilidis 的对偶四元数方法实验。它处理数据噪声的能力更强对平移估计也更稳。我写了一个简单的离线数据检查脚本把采集到的 A 序列和 B 序列打出来看它们的变化量级。如果 A 和 B 的旋转角度太小——比如连续几个位姿之间末端旋转不到 5 度——那标定结果肯定不行得回去调整位姿集。这是验证数据质量最快速的手段。6. 仿真标定踩坑录从没有图像到结果很奇怪6.1 坑一Gazebo里相机就是不出图这个坑我猜几乎所有新手都遇到过。现象是启动完 world机械臂和标定板都正常显示rostopic list 里却找不到 /camera/color/image_raw 话题或者话题存在但 echo 之后没有任何数据。排查思路要按顺序来先检查 camera 插件是否挂载到正确的 link 上因为 Gazebo 的 标签里的 reference 必须是 URDF 中存在的 link 名写错一个字符都不会帮你在日志里高亮接着检查插件参数中 remapping 的话题名和订阅端是否一致在 ROS 2 里namespace 逻辑和 ROS 1 不同非常容易多出一层/少一层的路径然后再看 launch 文件里是否设置了 use_sim_time没有设为 true 会导致仿真相机发布的消息时间戳不被正常同步订阅端可能因为时间跳变而丢掉消息。我最痛苦的一次排查最后发现只是插件库文件名发错了。ROS 2 Humble 用的相机插件是 libgazebo_ros_camera.so这个 .so 放在 gazebo_ros 包的 lib 目录下但如果你同时装了多个版本的 gazebo_ros会加载到不兼容的旧库导致 silently 没有图像。用 ldconfig -p | grep gazebo 检查一下插件路径是否干净。6.2 坑二TF树里找不到camera光学坐标系现象是 rviz 里能看到图像话题但 easy_handeye 启动后报错说 camera_color_optical_frame 不在 TF 树里。原因通常很简单URDF 里只定义了 camera_link没有定义 camera_color_optical_frame也没有把二者用 fixed joint 连接起来。很多相机 ROS 驱动会自己发布 optical frame 到原
返回列表