调机械臂的时候,最让人抓狂的场景之一就是起点终点都清清楚楚,OMPL 也给出了解,但那条轨迹扭得像拧麻花,关节一会儿猛冲一会儿急停,放到真机上电机直接报警。这个时候很多人会去 MoveIt 的规划器列表里翻,翻到一个叫 CHOMP 的 Planner,改几个权重发现轨迹顺滑了不少,但要问它到底在优化什么、那些参数之间怎么互相拉扯,多半就说不太清了。
这篇东西就是我围绕neotic-moveit(ROS Noetic 版本的 MoveIt)里的 CHOMP Planner 做的一轮完整梳理,从它背后的数学直觉、Noetic 下的配置方式、参数逐个拆解,到实机上踩过的坑,都写进去。适合已经在用 MoveIt 做过项目、想让机械臂轨迹更好看更平滑的人,也适合刚接手一套机械臂代码、被chomp_planning.yaml里十几个参数绕晕的人。看完之后你应该能做到三件事:知道 CHOMP 什么时候该用什么时候别碰、能自己改参数改出想要的效果、遇到规划不动的时候知道往哪个方向排查。
1. CHOMP 到底在做什么
1.1 采样与优化:两条完全不同的技术路线
想理解 CHOMP,得先把它和 OMPL 放到一起看,因为它俩的思路根本不是一回事。
OMPL 走的是采样路线。它在关节空间里随机撒点,判断这个点有没有碰撞、能不能和已有的点连起来,一点点长出一棵搜索树,最后从树里挑一条从起点到终点的路径。RRT、RRT-Connect、PRM、BiTRRT 都是这个家族。它的强项是全局搜索:哪怕起点在桌子底下、终点在柜子后面,中间要绕一个很刁钻的弯,只要给的时间够,采样法大概率能找到解。它的弱项是解的质量不稳定——找到的往往是一条"能走通"的路,但不一定是条好路,路径可能贴着障碍物蹭过去,也可能在某些关节上突然来一个大幅度摆动。这也是为什么 OMPL 后面通常要挂一串后处理,比如简化、插值、时间参数化。
CHOMP 走的是优化路线,全称是 Covariant Hamiltonian Optimization for Motion Planning。它压根不做随机搜索,而是先造一条初始轨迹,然后像揉面团一样反复揉它,让这条轨迹在"平滑"和"避障"之间慢慢找到平衡点。
我习惯用这个类比:OMPL 像是你在一张陌生的地图上摸索着找路,走走停停、试错前进;CHOMP 像是你手里有一根橡皮筋,先把两头钉在起点和终点上,中间随手拉一条弧线,然后让这根橡皮筋自己在障碍物周围弹开、自己绷直。橡皮筋最终停下来的形状,就是 CHOMP 给出的轨迹。
这个区别直接决定了两者的使用场景。如果你需要全局搜索能力,用 OMPL;如果你已经有一条大差不差的初始路径,想把它磨得更光滑更安全,用 CHOMP。反过来用会很痛苦:让 CHOMP 从一堆死胡同里杀出来找解,它经常会卡在局部最优里出不来;让 OMPL 去追求轨迹的平滑度,调半天也只能得到"勉强能看"的结果。
这里要提前说一个 CHOMP 的硬限制:它本质上是一个局部优化器,需要一条"还算靠谱"的初始轨迹。MoveIt 的实现里给了几种初始轨迹生成方式(后面 3.2 节会细讲),默认是五次样条插值,也就是在起点和目标之间拉一条平滑曲线。如果这条曲线正好穿过了障碍物内部,CHOMP 得靠障碍代价函数把它"推"出来,推得出来就成功,推不出来就失败。所以当你发现 CHOMP 反复失败的时候,第一反应不应该是加参数,而是先想想初始轨迹是不是太离谱了。
1.2 目标函数:CHOMP 眼里的"好轨迹"长什么样
CHOMP 的整个逻辑可以浓缩成一个目标函数,它要做的就是把这个函数的值压到最小:
U(ξ) = U_smooth(ξ) + U_obs(ξ)其中 ξ 表示整条轨迹,也就是一长串关节角序列,一般写成 ξ = [q₁, q₂, ..., qₙ],每个 q 是一个 N 维向量(N 是机械臂自由度)。
平滑项 U_smooth衡量的是这条轨迹"抖不抖"。实现上通常用有限差分近似出轨迹上每个点的速度、加速度,然后把它们的平方加起来。你可以理解成:每个轨迹点都在被左右两边的邻居拉扯,谁偏离了邻居的平均值,谁就要付出代价。这一项的作用是让轨迹尽量均匀、连续、不突兀,同时它还有一个很重要的作用——它让整个优化问题是"良态"的,不至于因为障碍项的存在而变得没法求解。MoveIt 里对应的权重是smoothness_cost_weight,默认 0.1,这个值后面会重点讲。
障碍项 U_obs衡量的是这条轨迹"贴不贴障碍物"。具体做法是对轨迹上的每一个离散点,依次计算机器人上每个近似几何体(球体)到环境中最近障碍的距离,如果距离小于某个阈值,就产生一个代价,距离越近代价越大,是一个单调递减的势场函数。
这里的势场函数有个细节值得说:它不是离得近就无穷大。CHMP 用的是有限截断的势场,距离超过collision_threshold之后代价归零,距离接近 0 时代价趋于一个有限大值。这个设计看起来有点反直觉——碰到障碍了居然不是无穷代价?其实这是刻意的,因为如果用无穷大的势场,梯度会变成无穷,数值上没法算,优化直接炸掉。用有限势场,靠的是让代价随着距离减小而急剧上升,把轨迹"顶"出去,而不是一味禁止。
理解了这两项,你就能理解 CHOMP 所有调参的本质:它就是在平滑和避障之间找一个平衡点,而你手里所有的权重参数,都是在调整这个平衡点偏向哪一边。后面 3.1 节会具体展开怎么调。
1.3 协变梯度:为什么不能直接做梯度下降
这一小节可能是全文最"数学"的地方,但我觉得必须讲,因为它解释了一个实际现象——为什么有些轨迹明明可以更平滑,CHOMP 却会在某个位置莫名其妙地抽动一下。
最朴素的想法是:既然有了目标函数 U(ξ),那我直接沿负梯度方向更新就好了:
ξ ← ξ - η · ∇U(ξ)问题在于,这样做出来的更新在关节空间里是不均匀的。梯度 ∇U 的大小跟每个关节的坐标尺度相关,如果某个关节的取值范围是 ±3.14(比如大臂回转),另一个关节只有 ±0.5(比如腕部),那么同样的梯度分量作用在这两个关节上,造成的实际物理位移完全不一样。结果是某些关节动得猛、某些关节几乎不动,轨迹看起来就会很别扭。
CHOMP 的解法是引入一个度量矩阵 A,这个矩阵由有限差分算子构造,本质上刻画了轨迹的"形状刚度",也就是把相邻轨迹点连起来的那根"弹簧"的弹性。更新公式变成:
ξ ← ξ - η · A⁻¹ · ∇U(ξ)这个 A⁻¹ 起到了"归一化"的作用,让梯度的更新在整个关节空间中是协变的(covariant,名字的由来)。直观理解:它让优化过程不是在原始坐标里下降,而是在一个由轨迹本身形状定义的度量空间里下降。结果就是轨迹的每个点在更新时都被邻居"照顾"到,整体形状是一起变化的,不会出现某个点单独跳出去的情况。
A 矩阵有个潜在问题:它可能是奇异的,或者接近奇异,求逆会数值爆炸。MoveIt 提供了两个应对手段:
ridge_factor:给 A 的对角线加一个小的正数,也就是常说的岭回归阻尼,牺牲一点点精度换取数值稳定。use_pseudo_inverse配合pseudo_inverse_ridge_factor:用伪逆代替普通求逆,进一步兜底。
注意:
ridge_factor调大能让优化过程更稳,但代价是收敛变慢,轨迹会显得"黏"。默认值在很多配置里是 0.0,如果你遇到优化过程中轨迹突然乱飞,可以试试给到 1e-3 到 1e-2 这个量级。
另外还有一个容易忽略的点:CHOMP 每一次迭代都要对整条轨迹做一次矩阵运算,而这个矩阵的大小跟轨迹离散点的数量正相关。轨迹离散点越多(比如为了精度把discretization调小),单次迭代就越慢。这是 CHOMP 在高维机械臂上跑不快的主要原因之一。
2. ROS Noetic 下把 CHOMP 真正跑起来
2.1 装包、验证与依赖检查
MoveIt 1 在 Noetic 上的打包是拆开的,CHOMP 规划器单独在一个包里。很多人装完ros-noetic-moveit之后在 RViz 的规划器列表里找不到 CHOMP,就是因为漏装了规划器包。
# 主体功能包 sudo apt install ros-noetic-moveit # 三个主流规划器包,按需装,建议一起装上方便对比 sudo apt install ros-noetic-moveit-planners-ompl sudo apt install ros-noetic-moveit-planners-chomp sudo apt install ros-noetic-moveit-planners-stomp装完之后先确认包在不在:
rospack find chomp_interface正常会输出类似/opt/ros/noetic/share/chomp_interface的路径。如果报[rospack] Error: package 'chomp_interface' not found,那就是包没装或者环境变量没 source。
接着验证插件有没有被 MoveIt 正确识别。MoveIt 的规划器是通过 pluginlib 加载的,可以用这条命令列出所有注册的规划插件:
rospack plugins --attrib=plugin moveit_core或者更直接一点,去看 move_group 启动时的日志,搜chomp关键字,能看到类似Loading planning plugin chomp_interface/CHOMPPlanner的输出就说明加载成功了。
实操提示:Noetic 之后 MoveIt 的很多包名和路径跟 ROS1 早期版本不一样,网上大量教程还是 Kinetic 或者 Melodic 时代的写法,比如
moveit_planners_chomp这个包名在 Noetic 里仍然是moveit_planners_chomp,但接口层已经挪到了chomp_interface命名空间下。抄配置的时候一定对着自己本地的文件路径核对一遍,别直接复制。
还有一件事要做:确认你的 URDF 里每个 link 都有collision几何体,而且最好不是特别离谱的复杂 mesh。原因在 3.3 节会详细讲,这里先埋个伏笔——CHOMP 对机器人几何的处理方式跟 OMPL 不一样,它对几何质量更敏感。
2.2 chomp_planning.yaml 逐项拆解
MoveIt Setup Assistant 生成的配置包里,规划器相关的 YAML 一般长这样:
my_robot_moveit_config/config/ ├── ompl_planning.yaml ├── chomp_planning.yaml └── stomp_planning.yaml如果你在 Setup Assistant 里没勾选 CHOMP,这个文件可能压根不存在,需要自己建一个。一个完整的 CHOMP 配置大致是这样:
planning_time_limit: 10.0 max_iterations: 200 max_iterations_after_collision_free: 5 smoothness_cost_weight: 0.1 obstacle_cost_weight: 1.0 learning_rate: 0.01 smoothness_cost_velocity: 0.0 smoothness_cost_acceleration: 1.0 smoothness_cost_jerk: 0.0 ridge_factor: 0.0 use_pseudo_inverse: false pseudo_inverse_ridge_factor: 1e-4 joint_update_limit: 0.1 collision_clearance: 0.2 collision_threshold: 0.07 use_stochastic_descent: true enable_failure_recovery: true max_recovery_attempts: 5 trajectory_initialization_method: "quintic-spline" animate_path: false random_jump_amount: 1.0 use_hamiltonian_monte_carlo: false hmc_discretization: 0.01 hmc_stochasticity: 0.01 hmc_annealing_factor: 0.99我按功能把这些参数分成四组来理解:
| 分组 | 参数 | 默认值 | 作用 |
|---|---|---|---|
| 代价权重 | smoothness_cost_weight | 0.1 | 平滑项权重,越大轨迹越顺但越贴障碍 |
| 代价权重 | obstacle_cost_weight | 1.0 | 障碍项权重,越大越保守但轨迹越僵 |
| 代价权重 | collision_clearance | 0.2 | 期望保持的安全间隙 |
| 代价权重 | collision_threshold | 0.07 | 障碍势场的触发阈值 |
| 优化过程 | learning_rate | 0.01 | 每次迭代的步长 |
| 优化过程 | joint_update_limit | 0.1 | 单次关节角更新上限,防跳变 |
| 优化过程 | ridge_factor | 0.0 | 度量矩阵的阻尼项 |
| 优化过程 | use_stochastic_descent | true | 随机梯度下降,帮助跳出局部极小 |
| 终止条件 | planning_time_limit | 10.0 | 单次规划的时间上限(秒) |
| 终止条件 | max_iterations | 200 | 最大迭代次数 |
| 终止条件 | max_iterations_after_collision_free | 5 | 无碰撞后额外迭代次数 |
| 兜底机制 | enable_failure_recovery | true | 失败后是否自动重试 |
| 兜底机制 | max_recovery_attempts | 5 | 重试次数上限 |
| 兜底机制 | trajectory_initialization_method | quintic-spline | 初始轨迹生成方式 |
这张表建议存下来,调参的时候对着看,比翻代码快得多。
关于这些参数的默认值,我要坦白一句:不同补丁版本的 MoveIt 里默认值存在差异,尤其是planning_time_limit和max_iterations,我见过 10.0/200 的,也见过 6.0/100 的。所以不要迷信任何一个博客上的数值,最靠谱的做法是去你本地的源码里翻一眼:
# 找到 chomp 的参数定义文件 rospack find moveit_planners_chomp # 一般在 src/chomp_interface/chomp_planning.yaml 附近对着实际文件确认一遍,省得调了半天发现改的是个不生效的参数。
launch 层面,你需要让 move_group 知道有 chomp 这条管线。如果是 Setup Assistant 生成的配置包,通常会有chomp_planning_pipeline.launch.xml这样一个文件,内容大致是:
<launch> <arg name="pipeline" default="chomp" /> <include file="$(find my_robot_moveit_config)/launch/planning_context.launch"> <arg name="load_robot_description" value="true"/> </include> <node name="$(arg pipeline)_planning_pipeline" pkg="moveit_ros_planning" type="moveit_planning_pipeline" output="screen" respawn="false"> <param name="planning_plugin" value="chomp_interface/CHOMPPlanner" /> <param name="request_adapters" value="default_planner_request_adapters/AddTimeParameterization default_planner_request_adapters/ResolveConstraintFrames default_planner_request_adapters/ValidateWorkspaceBounds default_planner_request_adapters/CheckStartStateBounds default_planner_request_adapters/CheckStartStateCollision" /> <rosparam command="load" file="$(find my_robot_moveit_config)/config/chomp_planning.yaml"/> </node> </launch>然后在planning_pipeline.launch.xml里按 pipeline 参数分发:
<launch> <arg name="pipeline" default="ompl" /> <include file="$(find my_robot_moveit_config)/launch/ompl_planning_pipeline.launch.xml" if="$(eval arg('pipeline') == 'ompl')"/> <include file="$(find my_robot_moveit_config)/launch/chomp_planning_pipeline.launch.xml" if="$(eval arg('pipeline') == 'chomp')"/> </launch>最后在demo.launch里把默认 pipeline 设成 chomp,或者保留成 ompl、在 RViz 里临时切换。我个人的习惯是默认用 ompl,需要精修的时候在 RViz 里手动切 chomp,因为 CHOMP 的启动和参数加载都比 OMPL 慢一些,长时间挂着没必要。
RViz 里切换的位置在 MotionPlanning 面板的Context标签页,把Planning Pipeline改成chomp,下面的Planner会自动变成CHOMP,然后回到 Planning 标签页点 Plan and Execute 就行。
2.3 用代码触发一次 CHOMP 规划
RViz 适合调试,但真正部署的时候还是得写代码。用moveit_commander调用 CHOMP 的大致流程是这样:
import rospy import moveit_commander moveit_commander.roscpp_initialize([]) rospy.init_node("chomp_demo", anonymous=True) arm = moveit_commander.MoveGroupCommander("panda_arm") # 关键一步:切到 chomp 管线 # 如果你的 moveit_commander 版本没有这个方法,就改在 launch 里把默认 pipeline 设成 chomp arm.set_planning_pipeline_id("chomp") arm.set_planner_id("CHOMP") # CHOMP 属于优化型规划器,给足时间比给足次数更重要 arm.set_planning_time(10.0) arm.set_num_planning_attempts(3) # 速度、加速度缩放,注意这是给时间参数化用的,不影响 CHOMP 本身的优化 arm.set_max_velocity_scaling_factor(0.3) arm.set_max_acceleration_scaling_factor(0.3) arm.set_start_state_to_current_state() arm.set_named_target("ready") plan = arm.plan() if plan and len(plan.joint_trajectory.points) > 0: rospy.loginfo("CHOMP 规划成功,轨迹点数: %d", len(plan.joint_trajectory.points)) arm.execute(plan) else: rospy.logwarn("CHOMP 规划失败")有三个地方值得单独拎出来说:
第一,set_planning_time和set_num_planning_attempts在 CHOMP 上的意义跟 OMPL 不一样。OMPL 是随机采样,重试真的会得到不同的解,所以增加 attempts 有意义;CHOMP 是确定性优化(除非开了随机梯度下降),同样的输入跑两遍结果基本一样,重试主要靠enable_failure_recovery里面的随机扰动。所以给 CHOMP 调优时,优先加时间,而不是加次数。
第二,set_planner_id("CHOMP")这个名字不能瞎写。它对应的是chomp_planning.yaml里注册的规划器标识,MoveIt 的 CHOMP 接口默认就叫CHOMP,全大写。写成chomp或者Chomp都可能加载失败。
第三,速度缩放因子不影响 CHOMP 优化出来的轨迹形状。它作用在时间参数化那一步,只改变轨迹的时间分布,不改变几何路径。有些人调不好 CHOMP 的平滑度就跑去改速度缩放,那是南辕北辙——轨迹抖动是几何问题,不是速度问题。
3. 参数调优:从"能规划"到"规划得漂亮"
3.1 平滑与避障的拔河
smoothness_cost_weight和obstacle_cost_weight这两个参数是 CHOMP 调参的主战场,它俩的关系非常像拔河——你把哪边拉过去,轨迹就往哪边偏。
默认配置里smoothness_cost_weight = 0.1,obstacle_cost_weight = 1.0,也就是障碍项是平滑项的十倍。这个比例说明 MoveIt 的默认取向是安全优先:宁可轨迹稍微不那么顺,也不能碰。
实际调的时候,我一般按这个思路来:
- 轨迹有轻微抖动、关节加速度尖峰明显:把
smoothness_cost_weight从 0.1 加到 0.3 到 0.5,同时观察轨迹还贴不贴障碍。加太多的话,轨迹会开始"抄近路",从障碍物边缘擦过去。 - 轨迹明显贴着障碍物走、安全余量不够:把
obstacle_cost_weight从 1.0 加到 2.0 甚至 5.0。但要注意,这个值加太大会让轨迹变得非常"僵硬",转弯半径变大,某些狭窄场景下甚至会规划失败——因为轨迹被推得太开,反而找不到可行路径了。 - 两者都要调:那就先把障碍项固定住,单独调平滑项,找到抖动能接受的最大平滑权重,然后再回去微调障碍项。
collision_clearance和collision_threshold这两个跟避障相关的参数,经常被人当成一回事。我的理解是这样:
collision_threshold决定了障碍势场的作用范围,也就是距离小于它才产生代价。它相当于势场的"零点边界",默认 0.07 米,算是一个比较紧凑的安全距离。collision_clearance描述的是期望保持的安全间隙,默认 0.2 米,比 threshold 大不少。它更多是给优化过程一个"目标距离",让轨迹倾向于停在离障碍 0.2 米左右的地方,而不是紧贴着 0.07 米的边界走。
实际调的时候,如果你的机械臂在工作台上做抓取,周围环境比较密集,我建议把collision_clearance降到 0.1 左右、collision_threshold保持在 0.05 附近,给优化器留出更多可行空间。反过来,如果是大空间里的搬运任务,把 clearance 提到 0.3 会让轨迹明显更"大方",看起来更舒服。
踩坑记录:我曾经在一个 7 自由度机械臂上把
collision_clearance设成 0.35,结果在靠近墙面的抓取任务里 CHOMP 十次有八次失败。后来降到 0.15 就稳定了。原因很简单——clearance 太大,工作空间被"吃掉"了一大块,机器人在靠近墙面时无论如何都满足不了那么大的间隙要求,优化器只能一直顶着失败状态迭代到超时。
3.2 迭代、学习率与收敛
learning_rate是优化的步长,默认 0.01。这个值的敏感度比很多人想象的高。
步长太小,优化慢,可能跑满planning_time_limit还没收敛,最后 MoveIt 返回的是中间结果,轨迹看起来"优化了一半"。步长太大,轨迹会震荡——这一轮迭代把轨迹往左推,下一轮又推回来,甚至越推越远,最后发散。判断是不是发散有个简单办法:打开animate_path,在 RViz 里看优化过程,如果轨迹在障碍物周围来回甩,那就是步长太大了。
我的经验区间是0.005 到 0.05。默认 0.01 在大多数场景下够用,如果收敛慢,先试 0.02;如果震荡,降到 0.005。
joint_update_limit默认 0.1(弧度),这个参数容易被忽略但很重要。它限制的是单次迭代中每个关节允许的最大变化量,相当于给优化过程加了一道保险,防止某一轮迭代突然把某个关节甩出去很远。如果你发现规划出来的轨迹里有某个关节突然出现一个非常大的跳变(在 RViz 里表现为机械臂瞬间闪一下),八成是这个值太大了,降到 0.05 试试。
use_stochastic_descent是个挺有意思的开关,默认 true。它让优化过程中的梯度下降带一点随机性,作用是帮助跳出局部极小值。所谓局部极小值,就是轨迹卡在一个位置,无论怎么微调代价都降不下去了,但明显还不是最优解。加上随机扰动之后,优化器有可能"抖"出这个坑,找到更好的解。代价是,每次规划的轨迹可能会略有不同——这在调试的时候会比较烦,因为结果不可复现。如果你在做参数对比实验,可以临时把它关掉;实际部署建议打开。
trajectory_initialization_method决定了初始轨迹怎么生成,常见取值有:
| 取值 | 含义 | 适用场景 |
|---|---|---|
| quintic-spline | 五次样条插值 | 默认值,适合大多数场景 |
| linear | 线性插值 | 最快,但初始轨迹有折角,优化负担重 |
| cubic | 三次样条插值 | 介于两者之间 |
| riccati | 用最优化方法生成初始解 | 初始解质量最高,但计算成本大 |
如果你遇到 CHOMP 长时间规划失败的情况,可以试着把初始化方法从quintic-spline换成linear看看。听起来反直觉——linear 明显更粗糙啊?但它有个好处:线性插值的轨迹点分布更均匀、计算更快,优化器有更多时间去迭代。我遇到过几次样条初始化因为曲率太大会在起点附近产生"回弯"的情况,换成 linear 反而好了。
3.3 机器人几何近似带来的隐藏成本
这一节要讲的是一个官方文档里提得不多、但实际影响巨大的点:CHOMP 对机器人几何的处理方式跟 OMPL 完全不同。
OMPL 判断碰撞用的是精确的网格碰撞检测,只要碰撞几何体没相交就算通过。CHOMP 不一样,它需要计算"到最近障碍的距离"以及这个距离的梯度,精确网格之间算距离梯度是很麻烦的事。MoveIt 的 CHOMP 实现采用的方案是:把每个 link 的碰撞几何体用一组球体来近似,然后计算球心到障碍的距离。
这个方案带来三个连锁反应:
第一,URDF 里 collision 几何体的复杂度直接影响球体数量。如果你的 collision 用的是原始的高精度 STL 模型,里面几万个三角面片,CHOMP 会生成大量的近似球体,计算量直接爆掉,规划时间从几秒变成几十秒。反过来,如果 collision 用一个简化过的低面数 mesh,或者干脆用圆柱体、长方体这类基本几何体,效率会好很多。
第二,球体近似会带来"假碰撞"。一个细长的连杆用一串球体来近似,球体半径如果取得保守,实际机器人没碰但球体已经碰上了,CHOMP 就会认为碰撞,白白绕路。这在狭小空间里表现特别明显。
第三,通孔类几何体基本没法正确近似。如果你的末端执行器是那种中间有孔的夹爪,用球体近似会把这个孔填上,CHOMP 就会认为障碍物没法插进夹爪,抓取规划会失败。
那怎么办?我的做法是给 CHOMP 单独准备一套几何体:
<!-- 视觉用高精度模型,collision 用简化模型或基本几何体 --> <link name="upper_arm"> <visual> <geometry> <mesh filename="package://my_robot/meshes/upper_arm_visual.stl"/> </geometry> </visual> <collision> <!-- 用圆柱体代替精密 mesh,能大幅降低 CHOMP 的计算量 --> <geometry> <cylinder radius="0.05" length="0.3"/> </geometry> <origin xyz="0 0 0.15" rpy="0 0 0"/> </collision> </link>实操心得:不用给所有 link 都做简化,把运动链上那几个"体积最大、运动幅度最大"的连杆(大臂、小臂)换成基本几何体,效果就已经很明显了。我手上有个项目,仅把小臂的 collision 从 6 万面片的 mesh 换成圆柱,CHOMP 的单次规划时间从 8 秒降到了 1.2 秒。末端执行器因为形状复杂,还是得保留 mesh,但可以考虑手工做一版面数控制在几千的低模。
另外别忘了检查 moveit 配置包里的collision matrix。Setup Assistant 会自动采样生成 self-collision 的禁用列表,如果你的机器人有些 link 天然就会贴在一起(比如同一个关节两侧的连杆),漏配会导致 CHOMP 认为它们一直互撞,规划永远失败。在 RViz 里可以用 MotionPlanning 面板的 Collision 标签页目视检查一遍。
4. 实战排查:CHOMP 不听话的时候怎么办
4.1 现象速查表
下面是这几年我在 CHOMP 上遇到过的典型症状和对应的排查方向,整理成表,方便直接查。
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 规划一直失败,日志无报错 | 初始轨迹严重穿障,或目标本身不可达 | 关掉随机梯度看是否稳定失败;先用 OMPL 验证目标可达性 |
| 规划成功但轨迹贴障碍 | obstacle_cost_weight 偏低,或 clearance 太小 | 提高 obstacle_cost_weight,调大 collision_clearance |
| 轨迹有周期性抖动 | learning_rate 偏大,或 smoothness 权重不足 | 降低 learning_rate,提高 smoothness_cost_weight |
| 某个关节突然大跳变 | joint_update_limit 过大 | 降到 0.05 甚至 0.02 |
| 规划时间特别长 | 碰撞几何太复杂,球体数量爆炸 | 简化 collision mesh,换基本几何体 |
| 同一输入结果每次都不一样 | use_stochastic_descent 开启 | 调试期关掉,部署期打开 |
| 优化到一半轨迹发散 | ridge_factor 为 0 导致矩阵病态 | 设 ridge_factor = 1e-3 试试 |
| 起点附近就碰撞报错 | 起始状态本身在碰撞中 | 检查当前机器人状态,先做碰撞检测 |
这张表不是我拍脑袋编的,每一条都对应一次实际排查。比如"轨迹周期性抖动"这条,我一开始以为是平滑权重的问题,加了一倍发现没用,后来才发现是learning_rate设成了 0.05,优化步长太大导致轨迹在最优解附近来回振荡,降到 0.01 立刻就稳了。这也是为什么我说调参要有顺序——先排除几何和碰撞矩阵的问题,再调权重,最后调优化参数,乱调一通只会把自己绕进去。
4.2 几个少有人提的细节
细节一:CHOMP 的失败恢复机制会消耗额外时间。enable_failure_recovery打开时,如果第一轮优化失败,它会做随机扰动重试,默认最多 5 次。这意味着一次规划请求的最坏耗时可能是planning_time_limit × (max_recovery_attempts + 1)。在一个 10 秒时间限制的配置下,最坏情况可能等 60 秒才返回失败。如果你在做实时性要求高的应用,要么把max_recovery_attempts调小到 2,要么接受"快速失败 + 上层重试"的策略。
细节二:max_iterations_after_collision_free这个参数的含义经常被搞错。它不是"无碰撞后再迭代多少次",而更准确地说,是当优化器发现当前轨迹已经没有碰撞之后,还需要继续迭代多少轮来把平滑项压下去。设得太小,轨迹虽然安全但形状一般;设得太大,会把时间浪费在边际收益递减的迭代上。默认 5 是个偏保守的值,如果你的场景对平滑度要求高,可以提到 20 到 50 试试。
细节三:CHOMP 对起点状态非常敏感。因为它是局部优化,起点和终点的关节构型会直接影响优化结果。同一个目标位姿,如果你在调用前先把机械臂手动拖到一个相对"顺"的位置作为起点,规划出来的轨迹通常会更好。这在有冗余自由度的 7 轴机械臂上尤其明显——同样的末端位姿,肘部朝上还是朝下,CHOMP 给出的解完全不同。
细节四:RViz 里的 animate_path 是个被低估的调试神器。把它设成 true,CHOMP 会在优化过程中把每一轮的轨迹都发布出来,你在 RViz 里能看到轨迹像被揉面团一样慢慢变形。很多参数问题,看一眼动画就明白了——比如轨迹是在障碍物附近反复被推来推去(说明障碍项和平滑项在打架),还是整体形状一直没变(说明学习率太小或者陷入局部极小)。
5. 选型与概念辨析:什么时候该用 CHOMP
5.1 CHOMP、OMPL、STOMP 怎么选
MoveIt 里主流的三个规划器我做了个对比,供选型参考:
| 维度 | OMPL | CHOMP | STOMP |
|---|---|---|---|
| 核心方法 | 随机采样 | 梯度优化 | 随机优化 |
| 是否需要初始解 | 不需要 | 需要 | 需要 |
| 全局搜索能力 | 强 | 弱 | 中等 |
| 轨迹平滑度 | 一般,需后处理 | 好 | 好 |
| 约束支持 | 支持关节/位置/朝向约束 | 主要面向关节空间目标 | 类似 CHOMP |
| 高自由度表现 | 良好 | 明显下降 | 一般 |
| 结果可复现 | 否 | 是(关随机时) | 否 |
| 调参难度 | 中 | 中高 | 中 |
选型的核心判断逻辑是:先问有没有初始路径,再问对轨迹质量的要求有多高。
如果你的任务是"从当前状态去一个抓取位姿",中间障碍不多,那用 OMPL 就足够了,加上 OMPL 自带的 RRT-Connect 和路径简化,效果已经能看。如果你的任务是"沿着一条工艺流程走一串路点,每段都要平滑过渡",或者你的机器人要在人机协作场景里运行、对加速度连续性有硬性要求,那 CHOMP 值得花时间调。
还有一个折中方案很多人不知道:用 OMPL 出解,再用 CHOMP 精修。具体做法是把 OMPL 规划出来的轨迹作为 CHOMP 的初始轨迹,虽然 MoveIt 的默认接口里没有直接暴露这个功能,但可以通过自己写一个规划请求适配器(planning request adapter)来实现,或者在应用层拿到 OMPL 的轨迹后手动构造 CHOMP 的输入。这条路我试过,在 7 轴臂上能把轨迹的加速度尖峰压下去 40% 左右,代价是多一次规划耗时。值不值,看你项目的实时性预算。
5.2 名字很像但完全不是一回事的那些 planner
搜 CHOMP 的时候,很容易被一堆名字里带 planner 的东西带偏,这里顺手澄清几个,省得你浪费时间。
EGO-Planner是无人机领域的轨迹规划器,走的也是梯度优化路线,用 ESDF 或者类似的场来做避障,思路跟 CHOMP 确实有渊源——都是"先给一条粗糙轨迹,再用梯度把它磨平"这套逻辑。但它是为四旋翼那种微分平坦系统设计的,输入输出都是位置和速度的多项式,跟 MoveIt 的关节空间规划不是一回事。你在 CHOMP 的语境里看到这个词,只能当思路参考,代码没法直接搬。
Mission Planner是另一个完全不同的东西,它是固定翼和多旋翼的地面站软件,主要用来做航点编辑、参数配置、飞行日志分析这一套。有人在搜"mission planner 地面站怎么控制船"这类问题,说明它在水面载具上也有人用。但它是任务级的地面站工具,跟运动规划算法没有交集。这两者唯一的共同点就是名字里都有 planner。
Scan Planner这类词,大多出现在扫地机器人或者激光建图场景里,指的是覆盖路径规划或者扫描路径生成,属于任务规划层面。跟 CHOMP 这种底层轨迹优化相比,它们解决的问题层次完全不同——一个是"往哪走",一个是"怎么走得好"。
理清这些对做技术选型很有帮助。很多时候项目里的问题被归结为"规划器不行",其实真正缺的是一个上层任务规划器,或者在两个层次之间缺一个衔接。搞清楚你的问题在哪一层,比纠结用哪个算法重要得多。
我个人在 CHOMP 上折腾得最久的一次,是给一台双臂机器人做协同搬运的轨迹优化。一开始想当然地给两条臂各配一个 CHOMP 规划器分别优化,结果两条臂各自优化的轨迹虽然都平滑,但合到一起就会出现互相让路导致的周期停顿。后来改成把两条臂当成一个统一的规划组来做联合优化,自由度从 2×7 变成 14,CHOMP 的迭代时间立刻涨了三倍多,但轨迹质量确实上去了。这件事让我意识到,CHOMP 在低自由度上的表现和在高自由度上完全是两个东西,自由度一上去,度量矩阵的规模、迭代的收敛速度都会明显劣化,这时候要么接受更长的规划时间,要么老实退回采样法加后处理的组合。
最后再分享一个我一直在用的小技巧:在正式调参之前,先把animate_path打开,用同一个起点终点跑三遍,把轨迹在 RViz 里的形态看清楚了再动手。绝大多数时候,你需要调什么参数,光看动画就能猜个八九不离十,比盲目试数值高效太多。