MoveIt 机械臂运动规划

MoveIt 是机械臂规划的事实标准,真正决定成败的是规划场景、碰撞检测配置与规划器组合。本文讲清规划组与监控场景的管理、碰撞世界与 ACM 的配置、FCL 与自碰撞检测的加速、OMPL 规划器与规划时间的调优、规划请求适配器、笛卡尔路径、MoveIt Task Constructor 抓取流水线,以及与真实控制器的轨迹交接。

引言

MoveIt 是机械臂运动规划的事实标准,它把逆解、碰撞检测、采样规划、时间参数化、轨迹执行串成一条可配置的管线。很多团队用 MoveGroupInterface 几行代码就跑通了 demo,却在上真机后发现「规划失败率高、规划耗时抖动大、偶发撞到物体」。这些问题的根因几乎都不在规划算法本身,而在规划场景的维护与碰撞检测的配置。

工程上的第一个难点是「规划场景与现实的一致性」。规划场景是 MoveIt 内部对世界的认知,它由机器人模型、静态环境、动态障碍、附着物体四部分构成。如果场景里没有及时加入当前抓着的物体,规划器就会认为机械臂是空的,从而规划出一条撞到物体的轨迹。场景维护是应用层的责任,不是 MoveIt 的。

第二个难点是碰撞检测的成本。规划器 90% 的时间花在碰撞检测上,而碰撞检测的配置(ACM、安全裕度、FCL 参数)直接决定规划速度与成功率。默认配置在很多场景下既慢又容易误判「自碰撞」。

第三个难点是任务级编排。抓取一件物体不是一次规划,而是「接近 → 抓 → 抬起 → 搬运 → 放置」多个阶段的状态机,涉及物体附着/脱离、多规划组协同、失败回退。MoveIt Task Constructor(MTC)正是为此设计。本文聚焦规划侧,机械臂的控制与执行管线(ros2_control、轨迹跟踪、装配力控)见机械臂控制与抓取规划 ,通用规划算法见运动规划与轨迹优化 。

目录

  1. MoveIt 规划体系总览
  2. 规划组、规划场景与监控场景
  3. 碰撞世界:添加、附着与更新
  4. ACM 与自碰撞检测配置
  5. FCL 与碰撞检测加速
  6. OMPL 规划器配置与规划时间
  7. 规划请求适配器与响应适配器
  8. 笛卡尔路径规划与步长
  9. MoveIt Task Constructor 抓取流水线
  10. 与真实控制器的轨迹交接
  11. 可视化、调试与性能剖析
  12. 仿真验证与回归测试

1. MoveIt 规划体系总览

MoveIt 的核心进程是 move_group,它把多个能力节点聚合在一个入口后面,外部只需与 move_group 交互。

move_group 聚合的能力:
  规划管线   :规划请求适配器 → 规划器插件 → 响应适配器
  逆解       :kinematics 插件(KDL / TRAC-IK / IKFast)
  碰撞检测   :CollisionEnv(FCL/Bullet)+ ACM
  规划场景   :PlanningSceneMonitor 维护世界状态
  执行       :把轨迹交给 controller(通常经 ros2_control)
  服务/动作  :/plan、/execute、/compute_ik、/apply_planning_scene ...

三类外部接口:
  C++ MoveGroupInterface   :生产代码首选,功能最全
  Python moveit_py         :脚本与实验
  RViz MotionPlanning 插件 :可视化调试

理解 MoveIt 的关键是分清「配置」与「运行时」:SRDF、kinematics.yaml、ompl_planning.yaml 是静态配置,决定能力边界;规划场景与规划请求是运行时的输入,决定每次规划的具体问题。本文的重点是这两者如何配合。

2. 规划组、规划场景与监控场景

规划组(planning group)是 MoveIt 的基本操作单元,规划场景是它对世界的完整认知。

PlanningScene 的四个组成部分:
  1. 机器人状态(RobotState):当前关节角、末端位姿
  2. 机器人模型(RobotModel):URDF + SRDF 描述的结构
  3. 世界几何(World Geometry):静态环境 + 动态障碍
  4. 附着物体(Attached Bodies):机器人抓着的物体

PlanningSceneMonitor 的三种同步方式:
  1. 订阅 /monitored_planning_scene 话题(MonitoringScene)
     —— 来自外部(如感知节点)的场景更新
  2. 订阅 /planning_scene 话题(完整场景替换)
  3. 订阅 /collision_object 与 /attached_collision_object
     —— 增量的碰撞体添加/移除,最常用
// 用 PlanningSceneInterface 增删碰撞体
#include <moveit/planning_scene_interface/planning_scene_interface.h>

moveit::planning_interface::PlanningSceneInterface psi;

moveit_msgs::msg::CollisionObject table;
table.header.frame_id = "world";
table.id = "table";
shape_msgs::msg::SolidPrimitive box;
box.type = shape_msgs::msg::SolidPrimitive::BOX;
box.dimensions = {1.2, 0.8, 0.05};
table.primitives.push_back(box);
geometry_msgs::msg::Pose pose;
pose.position.z = 0.4;
table.primitive_poses.push_back(pose);
table.operation = table.ADD;

psi.applyCollisionObjects({table});          // 加入世界
// psi.removeCollisionObjects({"table"});     // 移除

一个高频错误:规划场景的坐标系与世界坐标系不一致。碰撞体的 header.frame_id 必须是 TF 树里存在的坐标系,通常是 world 或 base_link。若写成 odom 而该坐标系不在 TF 树里,MoveIt 会静默丢弃该物体,规划器仍认为世界是空的。

3. 碰撞世界:添加、附着与更新

「附着物体」是抓取场景的关键:抓起来之后,物体必须从世界几何移到机器人上,跟随机械臂运动。

// 抓取成功后把物体附着到末端连杆
moveit_msgs::msg::AttachedCollisionObject aco;
aco.link_name = "panda_hand";                 // 附着到的连杆
aco.object = box_object;                      // 被附着的物体
aco.touch_links = {"panda_hand", "panda_leftfinger",
                   "panda_rightfinger"};      // 允许接触的连杆
aco.object.operation = aco.object.ADD;

psi.applyAttachedCollisionObject(aco);
附着/脱离的状态机(必须成对且顺序正确):
  抓取成功 → applyAttachedCollisionObject(ADD)
  放置成功 → applyAttachedCollisionObject(REMOVE) 然后 applyCollisionObject(ADD)
           (物体回到世界上,位置更新为放置点)

错误做法:
  抓取后忘了附着 → 规划器认为臂是空的 → 抬起时撞到刚抓的物体
  放置后忘了从世界移除再重新加 → 物体停留在被抓走时的位置

touch_links 是最容易被忽略的参数。附着的物体与夹爪之间必然接触,若不把夹爪连杆列入 touch_links,MoveIt 会认为「机器人碰到物体」而永远无法规划。同理,touch_links 不宜设得过宽(比如把整条臂都列进去),否则会漏检真实的碰撞。

4. ACM 与自碰撞检测配置

Allowed Collision Matrix(ACM)是碰撞检测的加速核心:它记录「哪些连杆对不需要检查碰撞」。

<!-- SRDF 中的 disable_collisions:ACM 的来源 -->
<disable_collisions link1="panda_link0" link2="panda_link1" reason="Adjacent"/>
<disable_collisions link1="panda_link0" link2="panda_link2" reason="Never"/>
<disable_collisions link1="panda_link1" link2="panda_link3" reason="Never"/>
ACM 的三类来源:
  1. SRDF 的 disable_collisions(静态,来自 URDF 结构)
  2. 运行时通过 PlanningScene 的 allowed_collision_matrix 动态设置
  3. 附着物体的 touch_links(临时的允许接触对)

收益:
  七轴臂若不配置,需检查 C(7,2)=21 对自碰撞
  配置后通常只剩 3~5 对需要检查,规划提速 2~4 倍
  漏配的代价:规划极慢(每步都做全量自碰撞检查)
// 运行时动态放宽/收紧 ACM(例如允许某两个连杆接触)
collision_detection::AllowedCollisionMatrix acm =
    planning_scene->getAllowedCollisionMatrix();
acm.setEntry("panda_link7", "panda_hand", true);   // true = 允许碰撞
planning_scene->setAllowedCollisionMatrix(acm);

reason="Adjacent" 与 reason="Never" 的区别很重要。Adjacent 表示两连杆通过关节直接相连,永远不该检查碰撞;Never 表示几何上永远不可能相交。MoveIt Setup Assistant 能自动生成,但生成后必须人工核对 Never 类条目——一旦错标,真实的碰撞会被漏检,导致撞机。ACM 的正确性是安全底线,不是性能优化。

5. FCL 与碰撞检测加速

碰撞检测的底层库与参数决定单次查询的成本。

碰撞检测的三层加速:
  层次 1:包围体层次(BVH)
    FCL 默认用 OBB,对凸多面体效果好
    典型:无 BVH 时 100 µs/次 → 有 BVH 时 2 µs/次
  层次 2:连续碰撞检测(CCD)
    离散采样(快,可能漏检)vs 扫掠体(准,慢)
    工程折中:采样间隔 < 最薄障碍厚度的一半
  层次 3:碰撞缓存
    缓存「已知无碰撞的构型」,重复查询直接命中
    MoveIt 的 CollisionEnv 支持缓存,固定工位命中率高
# move_group 的碰撞检测参数
move_group:
  ros__parameters:
    # 连续碰撞检测的最大步长(米),越小越安全越慢
    # 机械臂建议 0.01~0.05
    # 由 planning_pipeline 的 collision_checking 配置
    default_planning_pipeline: ompl
    planning_pipelines:
      pipeline_names: ["ompl", "pilz_industrial_motion_planner"]
ompl:
  ros__parameters:
    planning_plugin: ompl_interface/OMPLPlanner
    request_adapters: >-
      default_planner_request_adapters/AddTimeOptimalParameterization
      default_planner_request_adapters/FixWorkspaceBounds
      default_planner_request_adapters/FixStartStateBounds
      default_planner_request_adapters/FixStartStateCollision

安全裕度(padding) 是另一个关键旋钮。给机器人或障碍加几毫米的 padding 能让轨迹与障碍保持安全距离,但过大(如 5 cm)会让狭窄空间无解。做法是按场景给不同物体设不同 padding:环境障碍设 1~2 cm,需要贴近操作的物体设 0。

6. OMPL 规划器配置与规划时间

OMPL 是 MoveIt 默认的采样规划器集合,配置的核心是「选哪个规划器 + 给多少时间」。

ompl:
  ros__parameters:
    planner_configs:
      RRTConnect:
        type: geometric::RRTConnect
        range: 0.0                    # 0 表示用默认步长
      RRTstar:
        type: geometric::RRTstar
        range: 0.15
        goal_bias: 0.05
      BITstar:
        type: geometric::BITstar
      PRM:
        type: geometric::PRM
    panda_arm:
      default_planner_config: RRTConnect
      planner_configs: ["RRTConnect", "RRTstar", "BITstar", "PRM"]
      projection_evaluator: joints(panda_joint1,panda_joint2)
      longest_valid_segment_fraction: 0.005   # 碰撞检测的离散化步长
规划器选型与规划时间:
  RRTConnect :双向扩展,首次解最快,工业首选(默认)
  RRTstar    :渐近最优,路径更短但要更长时间
  BITstar    :综合最优性与速度,近年推荐
  PRM        :固定环境多次查询,需预构建路标图
  EST/KPIECE :特定场景,一般不用

规划时间的设置(关键工程经验):
  规划时间不是「越长越好」,而是「覆盖 p99 即可」
  机械臂典型:0.5~2.0 s,配合 5 次重试
  p50 通常 20~100 ms,p99 可能 1~2 s,设置要看 p99
  盲目设 10 s 只会让失败场景等更久,不提高成功率

longest_valid_segment_fraction 是常被忽视但影响巨大的参数:它决定路径上每隔多远做一次碰撞检测。值太小(0.001)会让碰撞检测次数暴增、规划变慢;值太大(0.05)可能漏检细障碍。机械臂关节空间的推荐值是 0.005 左右,即「单个关节最多变化 0.5% 的行程就检查一次」。

7. 规划请求适配器与响应适配器

适配器(adapter)是插在规划前后的可插拔处理步骤,是 MoveIt 灵活性的体现。

请求适配器(规划前,修正问题):
  FixStartStateBounds       :把起始状态夹到关节限位内
  FixStartStateCollision    :起始状态在碰撞中时,微调到最近的自由构型
  FixStartStatePathConstraints:修正起始状态违反的路径约束
  FixWorkspaceBounds        :给未定义的工作空间边界补默认值
  ValidateWorkspaceBounds   :校验工作空间边界合理性
  CheckStartStateBounds     :只检查不修改(会报错而非静默修正)

响应适配器(规划后,优化轨迹):
  AddTimeParameterization   :给几何路径加时间(旧)
  AddTimeOptimalParameterization:TOTG,推荐
  ResolveConstraintFrames   :解析约束中的坐标系
适配器顺序很重要(默认顺序即推荐顺序):
  1. 先 Fix(修正)再 Validate(校验)
  2. 时间参数化必须放在最后(前面都是几何层面)
  3. 关闭 FixStartStateCollision 会让「起始状态碰撞」直接失败,
     保留它能让系统更鲁棒,但会掩盖真实的场景错误

FixStartStateCollision 是双刃剑。它让系统在起始状态轻微碰撞时仍能规划,提高了鲁棒性;但如果它频繁生效,说明规划场景与真实状态已经偏离(比如附着物体没更新),是隐藏 bug 的信号。建议在生产环境中监控这个适配器的触发次数,把它当作场景一致性的健康指标。

8. 笛卡尔路径规划与步长

笛卡尔路径要求末端走直线或指定形状,用于接近、插拔、涂胶等场景。

// 笛卡尔直线规划:逐点逆解,fraction 是关键指标
std::vector<geometry_msgs::msg::Pose> waypoints = {start, mid, end};
moveit_msgs::msg::RobotTrajectory trajectory;
const double eef_step = 0.01;         // 末端步长 1 cm
const double jump_threshold = 1.5;    // 关节跳变阈值(0 表示禁用,不推荐)
double fraction = arm.computeCartesianPath(
    waypoints, eef_step, jump_threshold, trajectory);
if (fraction < 0.99) {
  RCLCPP_WARN(node->get_logger(),
              "cartesian path only %.1f%% complete", fraction * 100);
  return false;                        // 必须检查,不能直接执行
}
eef_step 的取舍:
  0.005 m :最平滑,逆解次数最多,慢
  0.01 m  :推荐起点,平衡
  0.05 m  :快,但中间可能跳过奇异位形导致跟踪偏差

jump_threshold 的作用:
  检测相邻路径点之间关节角是否发生「非物理的跳变」
  逆解在多解间跳变时,关节角会突然变化,执行时是灾难
  设为 0 会禁用检测,奇异附近可能产生瞬间大幅跳变

笛卡尔路径失败的常见原因是「经过奇异位形」。当末端接近工作空间边界或腕部对齐时,逆解退化,fraction 骤降。对策是「把长直线拆成多段,绕开奇异区」,或在接近奇异时切换到关节空间运动。

9. MoveIt Task Constructor 抓取流水线

MTC 把抓取任务表达成一棵阶段(Stage)树,是任务级编排的标准工具。

MTC 的阶段类型:
  CurrentState        :捕获当前状态作为起点
  GenerateGrasps      :生成候选抓取位姿
  ComputeIK           :对每个候选求逆解
  Connect             :连接两个阶段(可带碰撞检测)
  MoveTo              :移动到命名目标
  ModifyPlanningScene :临时增删场景物体(如打开容器盖)
  AllowCollision      :临时允许某些碰撞(如插入时贴边)

流水线骨架:
  CurrentState → MoveTo(ready) → Connect(→ 预抓取) → GenerateGrasps
               → ComputeIK → AllowCollision(接近时) → MoveTo(抓取)
               → ModifyPlanningScene(附着物体) → MoveTo(抬起)
               → Connect(→ 放置上方) → MoveTo(放置) → ModifyPlanningScene(脱离)
// 用 MTC 构建一个最小抓取任务
auto node = std::make_shared<rclcpp::Node>("mtc");
auto task = std::make_unique<moveit::task_constructor::Task>();
task->stages()->setName("pick");
task->loadRobotModel(node);
task->setProperty("group", "panda_arm");

auto current = std::make_unique<stages::CurrentState>("current");
task->add(std::move(current));

auto move_to_pick = std::make_unique<stages::MoveTo>("approach", solver);
move_to_pick->setGoal("ready");
task->add(std::move(move_to_pick));

// 计划:MTC 会展开所有阶段的解空间,选择整体代价最小的组合
if (task->plan(5) == moveit::core::MoveItErrorCode::SUCCESS) {
  task->execute(*task->solutions().front());
}

MTC 的价值在于「全局搜索」:它不是逐阶段贪心,而是展开各阶段的候选解(多个抓取位姿 × 多条连接路径),选整体代价最小的组合。这解决了「贪心选了一个抓取位姿,结果后面接不上」的问题。代价是求解时间更长(数秒),因此适合离线规划或允许等待的场景。

10. 与真实控制器的轨迹交接

规划出的轨迹要交给真实控制器执行,这一交接是「规划成功但执行失败」的高发区。

交接的三个检查点:
  1. 起始点匹配
     轨迹第一个点的关节角必须与当前 /joint_states 在容差内
     否则控制器报 "start point not within tolerance"
     对策:规划前先更新 RobotState,或设置合理的起始容差

  2. 时间戳
     轨迹点的时间戳必须单调递增且从当前时刻开始
     规划耗时(如 1 s)会让「从 0 开始」的时间戳过期
     对策:执行前重新做时间参数化,或让控制器接受相对时间

  3. 速度/加速度缩放
     max_velocity_scaling_factor 只缩放速度,不缩放加速度
     时间被拉长后,加速度需求反而可能增加
     对策:同时设置 max_acceleration_scaling_factor
// 规划与执行的缩放因子策略
arm.setMaxVelocityScalingFactor(0.3);      // 调试 0.1~0.2,生产 0.5~0.8
arm.setMaxAccelerationScalingFactor(0.2);  // 通常 ≤ 速度缩放
arm.setPlanningTime(1.0);
arm.setNumPlanningAttempts(5);

// 执行前确认状态已同步(异步执行时尤其重要)
arm.asyncExecute(plan);                    // 或 execute(plan) 阻塞等待

规划与执行必须用同一份 RobotState。如果规划时用的是 100 ms 前的关节状态,而执行时机器人已经动了一点,起始点不匹配会导致执行失败或产生微小跳变。异步场景下,规划完成后应再次拉取最新状态并校验,必要时重新规划。

11. 可视化、调试与性能剖析

MoveIt 的问题大多能在 RViz 里看出来,善用可视化能省下大量真机调试时间。

RViz MotionPlanning 插件的关键显示项:
  Planning Scene      :世界几何、附着物体(先确认场景对不对)
  Planned Path        :规划出的路径(看是否绕远、是否贴障)
  Start State         :规划时的起始状态(看是否与真机一致)
  Goal State          :目标状态
  Collision Contacts  :碰撞点(诊断「为什么判定碰撞」)
  Trajectory Trail    :执行轨迹(对比规划与实际)
# 常用诊断命令
ros2 topic echo /monitored_planning_scene --once     # 场景内容
ros2 service call /compute_ik moveit_msgs/srv/GetPositionIK "{...}"
ros2 topic hz /joint_states                          # 状态频率
ros2 run moveit_ros_benchmarks moveit_benchmark      # 规划器基准测试
ros2 param get /move_group robot_description_kinematics.panda_arm.kinematics_solver

关键监控指标有四个:规划耗时(p50/p99)、规划失败率、逆解失败率、场景一致性告警(FixStartStateCollision 触发次数)。规划失败率超过 5% 就应排查配置,配置良好的系统应在 1% 以下。规划耗时是重尾分布,必须看 p99 而非均值。

12. 仿真验证与回归测试

MoveIt 的配置改动必须在仿真里回归,因为很多问题只在特定位形下出现。

回归测试的最小用例集:
  1. 从多个起始位形规划到多个目标位形(覆盖工作空间)
  2. 带障碍物的场景(验证碰撞规避)
  3. 抓取与放置全流程(验证附着物体的增删)
  4. 逆解边界位形(验证 IK 插件成功率)
  5. 长时间连续规划(验证场景不累积垃圾物体)

指标:成功率、p50/p99 规划耗时、路径长度、逆解调用次数
# moveit_ros_benchmarks 可对多个规划器做定量对比
ros2 launch moveit_ros_benchmarks panda_benchmark.launch.py \
  planner_configs:=RRTConnect BITstar
# 输出成功率、耗时分布、路径长度,用于选型决策

仿真验证的载体见机器人仿真一篇,抓取位姿如何从感知得到并进入 MTC 流水线见机器人视觉与抓取 。逆解插件(KDL/TRAC-IK/IKFast)的取舍与执行侧的控制器配置见机械臂控制与抓取规划一篇,本文不再重复。

权衡取舍

决策选 A选 B
规划器RRTConnect:首次解最快RRTstar/BITstar:路径更优、更慢
规划时间短(0.5s)+ 多次重试:快长(2s):成功率略高但失败更慢
碰撞安全裕度大:安全、窄空间无解小:能过窄处、靠传感器兜底
场景同步增量话题:省带宽完整场景:一致性强
笛卡尔步长小(0.005):平滑、慢大(0.05):快、可能跳过奇异
任务编排手工状态机:可控MTC:全局最优、求解慢
逆解插件KDL:零配置TRAC-IK:成功率高

核心原则:先保证规划场景正确,再谈规划器调优。九成的「规划失败」根因是场景不一致(附着物体没更新、碰撞体坐标系错误、ACM 漏配),而不是规划器参数不好。

常见坑清单

  1. 抓取后忘记附着物体,规划器认为臂是空的,抬起时撞到刚抓的物体——抓取成功必须 applyAttachedCollisionObject(ADD)。
  2. touch_links 未包含夹爪连杆,附着物体被判为碰撞,永远规划失败——把必然接触的连杆列入 touch_links。
  3. 碰撞体 header.frame_id 用了 TF 树里不存在的坐标系,物体被静默丢弃——坐标系必须存在于 TF 树。
  4. ACM 的 Never 条目错标,真实碰撞被漏检导致撞机——Setup Assistant 生成后必须人工核对。
  5. 规划时间设成 10 s,失败场景等待过久——按 p99 设定,配合有限次重试。
  6. longest_valid_segment_fraction 太大,路径离散化漏检细障碍——机械臂取 0.005 量级。
  7. 笛卡尔路径 fraction 小于 1.0 仍直接执行,轨迹不完整——必须检查 fraction 接近 100%。
  8. jump_threshold 设为 0,奇异附近关节瞬间大幅跳变——设为合理值而非禁用。
  9. 规划与执行用不同时刻的 RobotState,起始点不匹配导致执行失败——执行前校验并同步状态。
  10. 只缩放速度不缩放加速度,时间拉长后加速度需求反增——同时设置加速度缩放因子。

小结

MoveIt 的成败在配置而不在算法:规划场景是否与现实一致、ACM 与碰撞检测是否正确、规划器与规划时间是否匹配场景、任务编排是否覆盖多阶段。把这几件事做对,默认的 RRTConnect 就能满足绝大多数需求。

下一步建议阅读机械臂控制与抓取规划一篇,看规划出的轨迹如何经 ros2_control 与 joint_trajectory_controller 落地执行,以及逆解插件与装配力控的细节;机器人视觉与抓取一篇讲清抓取位姿如何从感知进入规划;运动学正逆解 是理解逆解插件与奇异问题的基础。

最后一句经验:每次「规划失败」先打印规划场景,确认世界是对的。场景正确之后,剩下的才是规划器参数问题。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「机器人」更多文章

  1. 机器人实时控制与嵌入式
  2. ROS 2 通信与 QoS
  3. 足式与人形机器人运动控制