多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

eye-in-hand视觉伺服仿真:从YOLOv5检测到Moveit闭环

eye-in-hand视觉伺服仿真:从YOLOv5检测到Moveit闭环 简介面向ROS机器人开发者与视觉伺服研究者这份资料围绕YOLOv5目标识别、MoveIt动作规划与Gazebo仿真环境提供eye-in-hand视觉伺服系统的完整工程实现。方案将相机安装于机械臂末端通过YOLOv5实时检测目标位置再交由MoveIt生成运动轨迹并在Gazebo中完成物理仿真验证适合学习计算机视觉与机器人控制融合的实践项目。资源包共105个文件压缩后8.02MB。其中20个launch文件用于启动系统与仿真节点19个xml和14个yaml定义模型、参数与配置13个stl和8个xacro构成机械臂与场景模型另有12个json、6个python脚本及rviz、srv等辅助文件结构清晰便于按功能模块查阅。目前已有89人学习。通过该资源可以获取项目源码、仿真场景配置及视觉伺服实现流程快速复现“识别-规划-控制”闭环包内还包含说明文档与演示幻灯片有助于理解各模块之间的数据传递与调试思路适合作为课程设计或科研入门的参考范例。1. 一套能跑通的 eye-in-hand 视觉伺服为什么非要用仿真先试把 yolov5 目标识别、moveit 动作规划、gazebo 仿真拼成一个 eye-in-hand 视觉伺服Image-Based系统很多人的第一反应不是报错而是机械臂追着目标跑目标一但偏离图像中心就飞出去最后整个系统在仿真里翻车。真正的问题往往不在某个单独模块而在「图像特征 → 末端速度 → 关节运动」这条链路的符号、延迟和限幅上。用 gazebo 仿真可以在不碰真实设备、不承担撞机风险的前提下把这条链路的每个环节拆开验证也能提前暴露相机曝光、手眼外参、规划状态同步这类实机才有的坑。这套方案适合有 ROS 2 基础、想把目标检测和机械臂运动控制真正闭环起来的开发者也适合做视觉伺服课题但不想反复折腾真机调试的人。仿真是成本最低的试错场前提是你要知道每一步在算什么。2. 视觉伺服的原理与系统架构先从误差定义说起2.1 Image-Based 视觉伺服在做什么误差定义与闭环逻辑视觉伺服不是让机械臂走到一个预设的笛卡尔位姿而是让「图像里的某个特征」到达期望位置。以 eye-in-hand 相机拍到的目标物体为例选图像特征为目标的像素中心 (u, v) 和包围框面积 s期望特征记为 (u_d, v_d, s_d)当前特征由 yolov5 检测得到。误差向量就是e [u - u_d, v - v_d, s - s_d]闭环逻辑是图像误差 e 通过伺服律计算末端速度指令机械臂运动后相机位姿改变图像特征随之变化直到误差收敛到零。常见伺服律是 v -λ · J⁺ · e其中 J 是图像雅可比矩阵J⁺ 是它的伪逆λ 是增益。这里的 v 是相机坐标系下的末端速度方向与误差方向相反所以公式里有个负号。仿真里最容易翻车的地方就在这个负号上——符号反了机械臂不是去追目标而是把目标推出视野。Image-Based 与 Position-Based 的差别在于后者要先从图像估计目标的 6D 位姿再规划位姿误差前者直接拿图像特征做控制省掉位姿估计这一步。在 gazebo 仿真里相机模型通常带噪声目标也可能被部分遮挡IBVS 对这类误差更宽容因为控制量直接来自图像不经过姿态求解的中间环节。代价是它需要一个较为准确的深度信息来归一化图像雅可比否则收敛性和稳态精度都会变差。2.2 eye-in-hand 与 eye-to-hand为什么仿真里选 eye-in-handeye-in-hand 相机装在机械臂末端跟着末端一起动eye-to-hand 相机固定在环境某处。标题定了 eye-in-hand选它的理由在仿真里也很明确目标在运动过程中始终处于相机视野内特征分辨率高且末端运动与图像变化的几何关系更直观。对 IBVS 来说相机系下的一阶运动模型比固定相机简单图像雅可比不需要额外乘以一个与机械臂结构相关的观察矩阵。eye-in-hand 的缺点是目标容易出视野所以伺服律里必须加约束和限幅这在仿真里恰好可以反复测试。仿真环境里建立 eye-in-hand 系统非常直接在机械臂末端 link 上挂一个 camera sensor 就行不需要考虑真实安装的机械干涉和线缆。想验证不同相机安装角度对伺服收敛的影响也只需要改一下相机 link 的坐标偏移。还有一点值得注意eye-in-hand 的手眼标定camera 到 end-effector 的外参 T_c_e在仿真里是可以拿到精确真值的这在实机上很难获得。先用仿真里的精确外参把伺服逻辑跑通再换成带误差的外参就能测试出系统对外参误差的敏感度。这个测试顺序是我最推荐的能帮你区分「控制律不对」和「标定不准」这两类问题。2.3 坐标系与数据流从像素误差到关节速度的变换链整套系统涉及四个坐标系图像像素系、相机系、末端系、基座系。像素坐标 p (u, v) 与相机系坐标的关系由内参矩阵 K 决定相机系到末端系由手眼外参 T_c_e 决定末端系到基座系由运动学正解 T_b_e 决定。视觉伺服有两种实现路径你需要先想清楚走哪条第一类是纯视觉伺服误差直接在相机系里计算速度指令也发到相机坐标系由底层控制器转换到关节速度。第二类是把图像误差先转换成末端位姿增量再用 moveit 做一次位姿规划。前者的控制频率一般在 20 Hz 以上适合快速收敛后者规划一次可能耗时几百毫秒适合作为粗定位阶段的引导。常见做法是两者结合先用 moveit 规划到初始观测位姿让目标进入视野然后再启动视觉伺服闭环。数据流是这样的gazebo 相机发布图像话题 → yolov5 检测节点推理并提取特征 → 伺服节点计算速度指令 → 速度指令传给 moveit servo 或底层速度控制器 → ros_control 把速度指令转成关节力矩 → gazebo 物理引擎更新机械臂位姿 → 相机图像变化。这条链路里每一步的话题名称、坐标系、更新率都可能成为问题源头。建议先画一张数据流图再写代码后面排查会快很多。3. gazebo 仿真环境搭建机械臂、末端相机与控制器3.1 以常见六轴机械臂模型为例准备 URDF 与 xacro搭建这套仿真环境我一般用常见的 panda 机械臂模型作为示例因为它在 gazebo 下的控制器支持比较完善URDF 也相对完整。如果你的模型是其他六轴臂同样的做法也适用只是关节名和连杆名需要替换。在 URDF 里加 eye-in-hand 相机最干净的做法是写一个 xacro 宏避免把相机参数散落到各个文件里。先给机器人加一个末端相机 link并指定坐标系朝向让相机光轴指向工作台方向。参考如下 xacro 片段!-- eye-in-hand 相机宏 -- xacro:macro namehand_camera link namehand_camera_link visual origin xyz0 0 0 rpy0 0 0/ geometry mesh filenamepackage://my_robot/meshes/camera.dae scale0.01 0.01 0.01/ /geometry /visual inertial mass value0.05/ inertia ixx1e-5 iyy1e-5 izz1e-5 ixy0 ixz0 iyz0/ /inertial /link joint namehand_camera_joint typefixed origin xyz0.05 0 0.03 rpy0 0 0/ parent linktool0/ child linkhand_camera_link/ /joint /xacro:macroorigin 里的 xyz 是相机相对末端法兰的安装偏移rpy 决定光轴朝向。这里把相机装在 tool0 的 Z 轴前方 5 cm、上方 3 cm 处光轴默认沿 X 轴正方向。如果你的目标物体在机械臂下方需要把 rpy 改成绕 Y 轴旋转约 -90 度让相机朝下看。这个安装偏移就是手眼外参 T_c_e 在模型层面的定义它和后面视觉伺服里用的外参必须保持一致很多奇奇怪怪的收敛失败都源于这里对不上。3.2 末端相机与目标物体的 gazebo 配置camera plugin 的关键参数光有 link 还不行gazebo 里要有 sensor 才会发布图像话题。给 hand_camera_link 挂一个 camera sensor参数里最重要的几个是分辨率、更新率和噪声。分辨率决定 yolov5 推理的像素输入质量更新率决定视觉伺服的控制频率上限噪声决定检测框的抖动程度。gazebo referencehand_camera_link sensor namehand_camera typecamera always_on1/always_on update_rate30/update_rate camera_namehand_camera/camera_name image width640/width height480/height formatR8G8B8/format /image distortion k10/k1 k20/k2 k30/k3 p10/p1 p20/p2 /distortion noise typegaussian/type mean0/mean stddev0.007/stddev /noise /sensor /gazeboupdate_rate 设为 30 Hz视觉伺服的控制闭环通常跑 20 Hz 左右留出余量避免图像积压。噪声 stddev 设为 0.007这是模仿真实相机传感器噪声的经验值不要设成 0否则仿真图像太干净测试出来的检测稳定性到了实机完全不可信。distortion 系数全设 0 可以因为 yolov5 对轻微镜头畸变不敏感但如果你想验证相机标定对伺服的影响再慢慢加 k1、k2。目标物体模型放置时要注意贴图和材质高反光材质在相机视角下会产生过曝区域检测框会抖纯色无纹理的物体又容易让检测器过拟合背景。常见做法是给目标物体贴上纹理贴图材质参数里把 specular 调低漫反射保持适中。这样 gazebo 渲染出来的图像接近真实场景yolov5 在仿真里学到的特征才有迁移价值。3.3 ros_control 控制器与话题检查动起来的最小命令gazebo 里机械臂要动起来靠的是 ros_control 接口。URDF 里需要加 gazebo_ros2_control 插件并给每个驱动关节配置 role 为 joint。控制器一般需要两类joint_state_broadcaster 用来发布关节状态joint_trajectory_controller 用来接收 moveit 的轨迹。伺服闭环阶段还需要一个 accepts velocity command 的控制器很多 ros_control 控制器组合里会用 velocity controller 或者 effort controller 配合前馈。启动完 launch 文件后先做三件事检查不要急着跑视觉伺服ros2 controller list ros2 controller list --configured ros2 topic hz /joint_states第一行看控制器有没有被 spawn第二行看是否 active第三行确认关节状态在实时发布。如果 /joint_states 的 hz 是 0 或者长时间没有更新说明控制器没有激活moveit 拿到的是静止的状态数据规划出来必然和你看到的不一致。这一步是最容易跳过的但也是后面 moveit 与 gazebo 同步问题的根源。4. yolov5 目标识别从模型训练到图像特征提取4.1 用自己的目标物体制作训练数据集仿真截图与标注流程标题里的 yolov5 要识别什么目标取决于你的伺服任务。如果你只想验证伺服算法那么要吸引注意识别一个简单的物体比如彩色方块、圆柱或球体。数据集从哪里来我见过不少人在网上找现成的数据集但仿真里的渲染图像和真实照片差异很大直接迁移会降低检测率。更好的做法是在 gazebo 里自己生成数据集把机械臂末端相机或者一个固定相机对准目标在多个角度、距离、光照条件下截图收集几百到一千张图就够了。标注格式用 YOLO 的 txt 格式每个图像文件对应一个同名 txt每行是 class_id x_center y_center width height坐标值归一化到 0~1。如果你做过交通标志识别这类 yolov5 项目流程几乎是同一套差别只在数据集和类别数。标注工具用常见图形标注工具就行标注时注意框要贴合目标主体不要把阴影包进框里。阴影边界在仿真里很锐利检测器会把阴影当特征学进去伺服时目标一动阴影变化检测框中心就会偏。4.2 训练与超参数调整影响伺服稳定性的三个超参数yolov5 的自定义训练命令并不复杂重点是超参数。默认的 hyp.scratch.yaml 里有一组基于 COCO 调出来的参数直接用于你的小数据集往往过拟合或者收敛不稳。伺服场景里我最关注三个超参数第一是 mosaic 数据增强。默认在训练前 70% 的 epoch 里开启它对提升泛化能力很有帮助但代价是目标的尺度分布被人为放大对伺服这种对中心坐标精度敏感的任务来说关闭或调 low 能让模型更适应目标在固定工作距离下的真实尺寸。第二是学习率 lr0数据集只有几百张时用默认 lr00.01 容易震荡我会降到 0.005 以下。第三是 imgsz训练和推理都用 320 而不是 640伺服任务里目标在图像中占比通常较大320 输入能明显降低推理延迟对控制闭环的帮助远大于那一点 mAP 的提升。训练自己的数据集命令参考python train.py --data my_data.yaml --weights yolov5s.pt --img 320 \ --epochs 150 --batch-size 16 --hyp hyp.scratch.yaml --project runs/trainmy_data.yaml 里写明 train/val 路径和 nc 类别数。训练结束后把 runs/train/exp/weights/best.pt 拷贝到你的推理节点目录后面加载模型时就用这个权重。150 轮对小数据集足够再多就会过拟合到仿真纹理上换个光照就失效。4.3 检测结果转图像特征中心坐标与面积的实时计算yolov5 输出的原始检测结果是坐标框但视觉伺服需要的不是框而是可用的标量特征。我通常取三个目标中心的 u、v 像素坐标以及包围框面积 s。s 可以用宽乘高也可以用面积用宽乘高对深度变化的响应更平滑。推理节点在 ROS 2 里的代码骨架如下# 视觉伺服特征提取节点订阅相机图像发布 [u, v, area] import rclpy import torch import numpy as np from rclpy.node import Node from sensor_msgs.msg import Image from std_msgs.msg import Float32MultiArray from cv_bridge import CvBridge class YoloFeatureNode(Node): def __init__(self): super().__init__(yolo_feature_node) self.bridge CvBridge() # 加载自定义训练权重 self.model torch.hub.load(ultralytics/yolov5, custom, pathbest.pt, force_reloadFalse) self.model.conf 0.35 # 置信度阈值 self.model.iou 0.45 # NMS 阈值 self.model.classes [0] # 只识别目标物体类别 self.sub self.create_subscription(Image, /hand_camera/image_raw, self.callback, 1) self.pub self.create_publisher(Float32MultiArray, /servo/feature, 1) def callback(self, msg): img self.bridge.imgmsg_to_cv2(msg, bgr8) results self.model(img, size320) # 用小尺寸推理降延迟 det results.pandas().xyxy[0] if len(det): row det.iloc[0] # 取置信度最高的目标 u (row[xmin] row[xmax]) / 2.0 v (row[ymin] row[ymax]) / 2.0 s (row[xmax] - row[xmin]) * (row[ymax] - row[ymin]) self.pub.publish(Float32MultiArray( data[float(u), float(v), float(s)])) def main(): rclpy.init() node YoloFeatureNode() rclpy.spin(node) if __name__ __main__: main()关键点在 det.iloc[0] 取的是 pandas DataFrame 里按置信度排序后的第一条也就是当前图像里最可靠的目标。如果场景里出现多个同类物体你需要加一个基于像素坐标的筛选逻辑比如选择离上次目标最近的。还有一个容易被忽略的问题模型在 ROS 2 节点里用 torch.hub.load 加载每次节点启动都要检查本地缓存如果目标机上没有网络缓存缺失会导致启动失败。提前把 weights 下载到本地目录然后用 path 直接加载就避免了这个问题。5. moveit 动作规划与视觉伺服闭环把误差变成运动5.1 moveit2 与 gazebo 同步状态同步与规划场景更新moveit 规划出一个轨迹的前提是它知道机械臂当前在哪。moveit2 的规划场景从 /joint_states 获取关节状态然后通过 robot_state_publisher 维护 TF。gazebo 里如果没有控制器发布 joint_statesmoveit 规划用的还是初始位形规划结果在 gazebo 里执行时机械臂就会从一个错误的状态开始跳变。这是「moveit2 的 rviz 与 gazebo 不同步」最常见的现象rviz 里规划很漂亮一执行机械臂就抽风。同步检查就那么几步但很多人漏掉。启动后先确认 joint_state_broadcaster active再确认 /joint_states 有数据最后在 rviz 里看机械臂模型是否与 gazebo 里的位姿一致。如果 rviz 里模型一直保持初始姿势不变说明 TF 树不完整常见原因是 robot_state_publisher 没有正确加载 xacro或者 fixed frame 设错了。5.2 伺服闭环节点从图像特征到速度指令的代码骨架闭环节点是整套系统的心脏。它订阅第 4 章发布的 /servo/feature算出与期望特征的误差再转换成速度指令发布到 moveit servo 对应的话题。这里的速度指令是相机坐标系下的 TwistStampedframe_id 设为 hand_camera_link。手动模式下用速度控制比每步调 moveit 规划一次要顺滑得多规划开销也小很多。# IBVS 视觉伺服节点订阅图像特征发布末端速度指令 import numpy as np import rclpy from rclpy.node import Node from std_msgs.msg import Float32MultiArray from geometry_msgs.msg import TwistStamped class IbvsNode(Node): def __init__(self): super().__init__(ibvs_node) self.sub self.create_subscription( Float32MultiArray, /servo/feature, self.feat_cb, 1) self.pub self.create_publisher( TwistStamped, /servo/cmd_vel, 1) # 期望特征目标中心在图像中心面积达到设定值 self.u_d, self.v_d 320.0, 240.0 self.s_d 30000.0 self.lambda_ 0.3 # 伺服增益 self.max_vel 0.15 # 末端最大线速度单位 m/s self.feat None def feat_cb(self, msg): self.feat np.array(msg.data) # [u, v, area] def control_loop(self): while rclpy.ok(): if self.feat is None: continue e np.array([self.feat[0] - self.u_d, self.feat[1] - self.v_d, self.feat[2] - self.s_d]) # 归一化像素误差方向是让目标回到期望位置 twist -self.lambda_ * e / (np.linalg.norm(e) 1e-6) twist np.clip(twist, -self.max_vel, self.max_vel) msg TwistStamped() msg.header.stamp self.get_clock().now().to_msg() msg.header.frame_id hand_camera_link msg.twist.linear.x float(twist[0]) msg.twist.linear.y float(twist[1]) msg.twist.linear.z float(twist[2]) self.pub.publish(msg) self.get_logger().info(ferr: {e[0]:.1f} {e[1]:.1f} {e[2]:.1f}) def main(): rclpy.init() node IbvsNode() node.control_loop() if __name__ __main__: main()这个节点做的是三自由度伺服两个平移自由度控制目标中心到图像中心一个平移自由度控制相机沿光轴靠近目标让面积达到期望值。速度方向算出来是在相机系下的如果机械臂要执行可以交给 moveit servo 按 body frame 解释或者在控制器里根据当前运动学转换到关节速度。视线方向速度直接映射到工具 Z 轴符号需要根据你的相机安装方向确认这是最容易出错的地方务必先用 0.05 这样的小增益开环测试一次。5.3 伺服参数表与调参顺序增益、频率、限幅伺服参数直接影响收敛曲线抄代码之前先把参数表过一遍。下表是我常用的初始值参数初始值说明lambda增益0.3太小收敛慢太大会振荡max_vel线速度限幅0.15 m/s防止机械臂猛冲控制频率20 Hz与 yolov5 推理帧率匹配期望面积 s_d30000根据实际目标尺寸调整目标中心死区5 px误差小于死区就停止伺服调参顺序也有讲究。第一步是验符号用一个固定误差手动发一条速度指令确认机械臂运动方向能缩小误差而不是扩大。第二步是纯比例控制lambda 从 0.1 开始观察误差是否单调下降出现振荡就减半。第三步才是加积分面积误差因为有深度耦合往往存在稳态误差加一个小的积分项能消除但积分增益不要超过比例的十分之一。最后才是调限幅限幅不是越大越好它决定的是机械臂在目标突然丢失时的行为限幅过大目标重新出现时机械臂已经在很远的位置了。6. 视觉伺服避坑与收敛验证仿真里翻车最频繁的几个环节6.1 图像发黑导致检测率骤降现象gazebo 里相机图像正常显示但 yolov5 的检测框时有时无目标稍远就完全检测不到。原因默认场景光照强度不够或者目标朝向相机的面正好背光图像整体偏暗检测器在低曝光下的特征响应变差。解决在 world 文件里加一个强度足够的方向光或者调整相机 sensor 的曝光参数。我一般会把场景光照从默认的 0.5 提到 0.9再观察检测置信度变化。还有一个习惯训练数据里刻意加入随机亮度和对比度增强这样检测器对光照变化的鲁棒性会好很多。6.2 目标飞出视野机械臂发散现象伺服启动后机械臂越动越快目标从图像边缘飞走再也没回来。原因增益 lambda 太大像素误差到速度的映射没有限幅机械臂在小误差时输出速度就已经很猛另一个常见原因是速度指令符号配反了本来该减小误差结果在放大误差。解决先开环验证符号再把 lambda 降到 0.1 量级同时加 max_vel 限幅。血泪经验是代码里看起来越简单的那一行负号越值得先测试再跑闭环。6.3 rviz 与 gazebo 机械臂状态不同步现象moveit 在 rviz 里规划的轨迹很标准但 gazebo 里机械臂执行时直接跳变或乱动。原因joint_states 没有从 gazebo 发布出来moveit 的规划场景一直停留在初始位形。解决先查 controller list 确认 joint_state_broadcaster 在 active 状态再查 /joint_states 话题频率。这类问题 80% 是控制器 spawn 顺序错误launch 文件里必须先启动 gazebo 和 ros_control再启动 moveit否则 moveit 拿到的状态是空数据。6.4 误差收敛但不为零停在某个像素区域现象目标中心停在距离期望位置几十像素的地方不动了误差不再减小。原因深度 Z 估计得不准导致图像雅可比归一化偏差或者检测框的中心与实际目标重心不重合比如目标有遮挡、阴影包含在框内。解决在仿真里用真值深度替代估计深度排除问题如果误差消除说明是深度估计问题如果还在就检查标注框是否严格贴合目标主体。还有一个容易忽略的点期望特征 s_d 设得比相机能达到的最大面积还大伺服会一直推着机械臂贴近目标直到碰撞这种不收敛本质上是期望设定不合理。6.5 验证伺服收敛的四类指标视觉伺服是否真的收敛不要只凭眼睛看机械臂停没停。我会记录四个指标误差下降曲线、目标在图像中的像素占比、收敛时间、稳态误差。误差曲线应当单调下降并有明显指数衰减的趋势目标像素占比反映整个过程中目标是否稳定留在视野内低于阈值就说明伺服轨迹规划有缺陷收敛时间设定一个硬上限仿真里超过 5 秒不收敛就意味着控制律参数不对稳态误差则对应伺服精度的最终水平。把这四个指标记录下来每次改参数都有对比依据。等仿真稳定之后如果想进一步优化增益调度也可以考虑把策略交给强化学习去学gazebo 的物理环境与 RL 训练接口本来就适配得不错。第一次调这套系统时我把 lambda 直接调到 0.8结果目标三秒内飞出视野追都追不回来。后来总结成一句话先验符号、再调增益、最后加限幅顺序不能乱。希望这几条避坑经验帮到你。本文还有配套的精品资源点击获取
返回列表