跳转至

第 3 章 碰撞检测与环境表示

规划器每秒钟要回答成千上万次“这个位形行不行”,回答的速度与正确性都由环境表示决定。规划场景(planning scene)是 MoveIt 侧唯一的环境真相来源:它装着机器人当前位形、世界里的物体、哪些 link 允许互碰,以及由传感器累积的占据地图。本章说明这三块内容的组织方式与更新成本,给出允许碰撞矩阵(allowed collision matrix, ACM)在运行时的实际形态,并用一组对照读数说明“逆解可用”与“位形合法”之间的距离。以下读数均取自运行环境 ROS 2 Humble + MoveIt 2 2.5.10 + FCL,样本臂为 moveit_resources 的 panda。

场景三块内容的汇总点,ACM 在检测入口过滤:

flowchart LR
    robot["机器人当前位形"] --> scene["planning scene"]
    world["世界物体"] --> scene
    octo["传感器占据地图"] --> scene
    scene --> fcl["FCL 碰撞检测"]
    acm["ACM:允许碰撞矩阵"] -.->|"放行合法接触对"| fcl

1. 场景的三块内容与两个服务

一份 moveit_msgs/PlanningScene 消息可以拆成三块:机器人状态(关节值 + 附着物体)、世界物体(碰撞物体表)、场景设置(ACM、link padding/scale、允许碰撞矩阵、octomap)。对外只有两个入口:

## 运行环境 ROS 2 Humble,move_group 已在跑
ros2 service type /get_planning_scene      # moveit_msgs/srv/GetPlanningScene
ros2 service type /apply_planning_scene    # moveit_msgs/srv/ApplyPlanningScene

/get_planning_scene 按请求里的 components 位掩码返回内容,/apply_planning_scene 接受 is_diff=true 的增量更新(新增、移动、删除物体都靠 CollisionObject.operation 区分:ADD / MOVE / REMOVE)。这两个服务就是所有场景编辑的统一入口——RViz 的交互对象、感知侧的物体注入、attach 操作,最后都走同一条通路。

物体最少要能回答三个问题:叫什么(id)、在哪(header 的坐标系 + 位姿)、什么形状(primitives 或 meshes 加各自的位姿)。写进场景后它就同时参与碰撞查询与最近距离计算,不需要任何额外的“注册”步骤。

2. 允许碰撞矩阵

默认假设是“任何两个 link 都可能相撞”,ACM 用来把物理上不可能相撞、或必须允许贴合的对儿排除掉。panda 样本的 panda.srdf 声明 34 对,运行时 ACM 的形态与之一致:

ACM 键=11,允许碰撞的矩阵单元=68(对称记两次,合 34 对);SRDF 声明 34 对
样例键: ['panda_hand', 'panda_leftfinger', 'panda_link0']
panda_hand 的禁用邻居数=6

矩阵是 entry_names × entry_values,每行是一个布尔数组——true 表示这一对允许碰撞。因为对称,同一对记两次,所以 68 个单元对应 34 对。SRDF 里每一对都带一个 reason,两类语义不同:

<disable_collisions link1="panda_link0" link2="panda_link1" reason="Adjacent"/>
<!-- 相邻 link 由关节连接,永远贴近:必须豁免 -->
<disable_collisions link1="panda_link0" link2="panda_link2" reason="Never"/>
<!-- 不相邻但受运动学约束不可能接触:豁免后省下查询代价 -->

Adjacent(及 Default)是机械性的,去掉就会让任何规划都失败;Never 是优化性的,去掉只增加代价。“规划总拒绝”的头号成因是这一表的反向问题:新加的末端工具或第三只手没有对应的豁免项,抓取姿态下工具与被抓物体必然重叠,于是所有目标位形都被判非法。检查方法是把位形交给合法性服务,让它直接报出冲突对:

## 返回体含 valid、contacts(每个带两个 body 名与深度),是排查 ACM 的第一手工具
ros2 service call /check_state_validity moveit_msgs/srv/GetStateValidity \
  "{robot_state: {joint_state: {name: [panda_joint1], position: [0.0]}}, group_name: panda_arm}"

3. 按需取回与更新成本

场景消息可能很大,取多少由 components 决定。用同一批物体测三档(每次取回都含 ACM、物体名与几何):

场景内容 取回耗时 序列化大小
空场景 1.3 ms 1.3 KiB
3 个 6 cm 方块 1.9 ms 1.3 KiB
203 个方块 18.8 ms(首次 28.7 ms) 47.2 KiB

203 个物体的场景取回约 19 ms、47 KiB,这还没算几何细节:只取名字(WORLD_OBJECT_NAMES)与同时取几何(WORLD_OBJECT_GEOMETRY)是两种量级的请求,前者只带 id 列表。规划回路里如果要高频同步场景,按需取组件是第一条原则;第二条是不把场景当查询数据库用——它是一次性快照,物体一多就退化成大对象搬运。

碰撞查询本身的代价则由位形与障碍的相对位置支配,不由物体总数线性支配:同一个 203 物体的场景,起点停在 ready(手在桌面上方远处)时重复 20 次合法性检查平均 0.33 ms/次;换成手伸进方块区的位形,同样亚毫秒返回,但会带出若干真实接触。差别不在耗时而在结果——这也说明“没报接触”不等于“查询很便宜所以没查”。

4. 图元、网格与近阶段检测

碰撞几何有两种载体。图元(shape_msgs/SolidPrimitive:BOX、SPHERE、CYLINDER、CONE、PLANE)有解析的距离查询;网格(shape_msgs/Mesh)需要三角级求交。近阶段碰撞检测(near-phase)指真正判断一对几何是否接触的那一步,它由插件承担,运行环境里同时装着两份实现:

libmoveit_collision_detection_fcl.so      ← 默认
libmoveit_collision_detection_bullet.so   ← 可选插件

判据级别的取舍(不做完整基准):

表达 优点 代价与坑
图元 查询便宜、无预处理、体积可控 形状偏差大,过包会吃掉工作空间,欠包会漏检
凸网格(convex decomposition) 贴合外形,近阶段仍可用凸-凸算法 需要离线分解工具,子块数量直接决定代价
三角网格(自碰撞模式) 建模侧省事 近阶段按三角对枚举,代价高且对连续运动不稳定

实践中最常犯的是“直接套用视觉侧的精细网格做碰撞”:那类模型是数十万三角片的渲染网格,用它做碰撞会把近阶段变成三角对扫描。规则是渲染网格与碰撞网格分开,碰撞侧优先用图元或分解后的凸块。此外 MoveIt 的查询带接触 padding(padding_offset,默认 0.01 m 量级)与 link scale,判定裕度时应按“是否超出 padding”来读,而不是把返回的深度当作真实穿模量。

5. 附着物体与接触清单

抓到东西之后,物体必须继续参与碰撞检查,但它不再属于“世界”——它跟着末端走。这就是 attached object(附着物体)的职责:把世界物体转成机器人状态的一部分,绑定到某个 link 上。请求形态只有一个 link_name 与一组 touch_links:

from moveit_msgs.msg import AttachedCollisionObject, CollisionObject

aco = AttachedCollisionObject()
aco.object.id = "carried"
aco.link_name = "panda_hand"                     # 物体挂在这个 link 上,随它运动
aco.touch_links = ["panda_hand"]                 # 只豁免列出的 link 与物体的接触

touch_links 的语义很直白:把一个 5 cm 方块挂到 panda_hand,只列 panda_hand 为接触清单时,合法性检查报出的是它与基座链路相交——

带附着体查初始状态(touch_links=[panda_hand]): valid=False
    panda_link0 × carried 深度=0.0031    panda_link1 × carried 深度=0.0040
把 touch_links 扩到 [panda_hand, panda_leftfinger, panda_rightfinger]:
    上两条例子不变,因为相交对象是 link0/link1,不在豁免名单里

接触清单不是“抓稳了”的声明,而是一张豁免表:只有列进去的 link 与被附着体之间的接触被忽略,其余 link 照常检查。放错了位置(本例中物体被放到基座附近)不会被这一机制掩盖。另有一条接口层面的事实:通过 /get_planning_scene 取回的 ACM 里不含被附着体的行(ACM 键数仍是 11)——附着体的豁免关系在 move_group 内部的 ACM 上维护,不随该组件回传,排查时不能靠读这条消息确认 attach 是否生效。

6. 占据地图的预算

没有传感器来源时,环境只由手工放置的物体组成。要把点云变成障碍,靠的是 octomap(八叉树占据地图)与它的更新插件。样本配置里的这一段就是全部旋钮:

## sensors_kinect_pointcloud.yaml:点云 → octomap 的更新器配置
sensors:
  - sensor_plugin: occupancy_map_monitor/PointCloudOctomapUpdater
    point_cloud_topic: /camera/depth_registered/points
    max_range: 5.0            # 超过 5 m 的点不入图:距离越远误差越大
    point_subsample: 1        # 抽稀步长;大点云靠它压算力
    padding_offset: 0.1       # 在物体外侧再扩 10 cm:把误差吃进安全侧
    padding_scale: 1.0
    max_update_rate: 1.0      # 每秒最多更新 1 次,规划侧不必追传感器帧率
    filtered_cloud_topic: filtered_cloud

四条预算要一起看:分辨率决定内存与查询代价(该配置未显式给出分辨率,运行时会取 0.1 m 作为默认值);max_range 决定哪些点可信;padding_offset 决定安全边界向外推多少;max_update_rate 决定地图刷新与规划的算力分配。octomap 分辨率并非越高越好:分辨率减半,内存与更新代价按立方增长,而噪声点还会被实体化成障碍——桌面操作里常见的是精细分辨率把工件的抖动放大成一片“不可进入区”,反而让规划失败。没有 3D 传感器插件被定义时,占据地图不会更新,场景只含手工物体,此时任何依赖地图的结论都不成立。

7. 校验先行:从逆解到规划的三道关

第 2 章留下的问题在这里收口:一个位姿的逆解返回成功,与“这个位姿可规划”之间隔着什么。同一个目标位姿(把一个 6 cm 方块的中心)分别走三条路:

路径 结果 读数
IK,avoid_collisions=False 出解 往返 1.7 ms
该解的合法性检查 非法 5 个接触:与 blkB 三处(最大 7.3 mm),自身两处(panda_link5×panda_hand 59.2 mm、panda_link7×panda_link5 15.7 mm)
IK,avoid_collisions=True 无解 返回无解码,2 s 预算烧满(往返 2001 ms)
规划请求(OMPL RRTConnect,5 s 预算,5 次尝试,plan_only) 拒绝 无轨迹,往返 5047 ms
同一朝向、抬高到方块上方 40 cm 成功 往返 55 ms,20 个航点

这张表把三道关摊开了。第一关是几何:IK 不看环境和自身,所以毫秒级返回的解可以既穿进物体又穿进自己的小臂。第二关是合法性:/check_state_validity 一次查询就能把接触对与深度列出来,这是唯一便宜的守门人。第三关是可达性:规划器要求的不只是目标位形合法,而是从起点到目标存在一条处处合法的连续路径——把目标抬高 20 cm(此时的新解虽与方块相交,但 /check_state_validity 判它合法)仍然被拒绝,抬高 40 cm 才在 55 ms 内出解。

另一个常见误解:碰撞感知 IK 不是绕障手段——打开 avoid_collisions 后,障碍附近的目标会返回无解并把超时烧满,因为该模式是在几何候选解上做一次过滤,而不是在障碍环境里搜索;搜索是规划器的职责(第 4 章)。高频的位姿跟踪需要绕障时,应使用 Servo 一类的实时通路(第 7 章),而不是把 IK 的超时调大。

8. 要点

  • 规划场景是 MoveIt 侧唯一的环境真相,对外只有两个入口:/get_planning_scene(按位掩码取组件)与 /apply_planning_scene(is_diff 增量,靠 operation 区分增删移动)。
  • ACM 是 entry_names × 布尔行 的对称矩阵;panda 样本运行时 11 键 / 68 单元,与 SRDF 的 34 对一一对应。Adjacent 是机械豁免,Never 是算力豁免。
  • 新加工具后“规划总拒绝”,先用 /check_state_validity 让服务把冲突对列出来,而不是先调规划器参数。
  • 场景成本按取的组件数与相对位置支配:203 物体全量取回约 19 ms / 47 KiB,而远离障碍的位形检查只要 0.3 ms;高频同步必须按需取组件。
  • 碰撞几何用图元或凸分解网格,渲染网格不入场;判定裕度按 padding 读,返回的深度不是真实穿模量。
  • touch_links 是豁免表而不是“抓稳”声明:未列出的 link 与被附着体照常判碰;attach 的豁免行不通过 /get_planning_scene 的 ACM 回传,不能靠读那条消息确认。
  • 占据地图的四条预算(分辨率、max_range、padding_offset、max_update_rate)必须一起看;未定义传感器插件时地图不更新,依赖地图的结论不成立。
  • 从逆解到规划隔着三道关:几何(IK 不看环境与自身)、合法性(一次校验服务)、可达性(需要处处合法的连续路径);同一目标抬高 20 cm 仍被拒、抬高 40 cm 才 55 ms 出解,就是这个差距的量。

参考文献与延伸阅读

  • Hornung, A., Wurm, K. M., Bennewitz, M., Stachniss, C., & Burgard, W. OctoMap: An Efficient Probabilistic 3D Mapping Framework Based on Octrees. Autonomous Robots, 2013. DOI
  • MoveIt. 项目门户与文档入口. 文档
  • Siciliano, B., & Khatib, O. (Eds.). Springer Handbook of Robotics (2nd ed.). Springer, 2016. DOI