ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多机器人编队虚拟结构法:目标与当前位置偏差计算详解

多机器人编队虚拟结构法:目标与当前位置偏差计算详解

做过多机器人编队的朋友应该都有这种体会:队形能不能保持,很多时候并不取决于底层PID调得多好,而取决于你每个控制周期里给每个成员算出来的“目标点”准不准、稳不稳。在我接触过的编队控制方案里,虚拟结构法是一条特别顺手的路——把整个队形定义成一个虚拟的刚性结构,结构中心往前走,每个成员跟随自己在结构上的固定位置。这个过程最核心的一步,就是标题里这句话:计算虚拟结构目标与当前位置的偏差。

这篇文章就围绕这句话展开。它既是多机器人编队、协同导航里绕不开的基础环节,也是很多新手第一次接触“队形控制”时最容易被绕晕的地方。适合正在做编队控制、路径跟踪或者刚上手虚拟结构法的开发者,也适合想搞清楚“偏差计算到底在算什么、为什么这么算”的读者。下面内容偏工程实践,数学只用到二维平面旋转矩阵,我会尽量把每一步背后的理由都说透。

1. 虚拟结构法和偏差计算到底在解决什么

1.1 为什么编队控制要引入“虚拟结构”

多机器人协同任务里,最朴素的需求是“让一群机器人保持某个队形一起移动”。比如三台车组成三角形巡视,或者六台无人机排成人字形搜索。这个需求背后有一个隐含约束:队形不仅要在路径起点成立,还要在路径的任何中间位置都成立,队形不能随着运动逐渐歪掉。

领航跟随是很多人第一个想到的方案:让一台领航者走路径,其余成员实时计算自己相对领航者的期望位置。这个方案的好处是实现快,坏处是误差会沿通信链路往下游传,而且领航者一旦故障,整个编队就散了。更重要的是,领航跟随本质上只约束了“每个成员和领航者的相对关系”,没有显式约束“两个成员之间的相对关系”,所以队形几何是松散的。

虚拟结构法换了个思路:不把队形看成多个独立的跟随关系,而是看成一个刚性整体。你可以把它想象成一根看不见的刚性杆,杆上焊着若干支架,每台机器人就是焊在支架上的小球。杆子怎么动,支架就怎么动,小球的期望位置就被决定了。杆子就是虚拟结构,支架的位置就是每个成员相对虚拟结构中心的坐标。

1.2 “偏差”在控制闭环里处在什么位置

一个完整的虚拟结构编队控制系统,大体可以切成四段:

轨迹规划器输出虚拟结构中心的期望位置、期望速度和期望航向;根据中心期望位姿和成员相对坐标,计算出每个成员的期望目标位置;把目标位置与当前实际位置做差,得到偏差向量;偏差向量送入速度控制器,生成机器人底盘的线速度和角速度。

可以看到,“计算虚拟结构目标与当前位置的偏差”就是第二段和第三段之间的枢纽。这一步的输出直接决定了控制器能看到多大的误差,也直接决定了机器人跑多快、往哪个方向修正。如果偏差算错,后面的控制器再好也是白搭,甚至会把队形越控越乱。

1.3 三种常见编队方法的对比

我自己整理过一张对比表,方便理解虚拟结构法的位置:

方法队形保持强度计算量主要问题
领航跟随弱,误差沿链路累积低领航者故障或通信断开时队形崩溃
行为法中等,局部规则驱动中全局队形难以严格保证
虚拟结构法强,刚性几何约束稍高需要精确计算每个成员的目标点

虚拟结构法最大的价值,是把“队形保持”这件事从隐性的控制需求变成了显式的几何计算。代价就是偏差计算这一步必须做得特别扎实。

2. 偏差计算躲不开的数学底座:坐标变换

2.1 三个坐标系,一个都不能搞混

偏差计算涉及三个坐标系:世界坐标系、虚拟结构坐标系、机器人自身坐标系。

世界坐标系是整个场景的绝对参考,定位系统通常直接输出这个系下的位姿。虚拟结构坐标系固连在虚拟刚体上,编队形状的定义全部在这个系里完成。机器人自身坐标系则是以机器人中心为原点的局部参考,速度控制最终要落在它上面。

很多初学者一上来就在这三个系之间来回跳,最后连偏差的符号都搞反。我的建议是:先把“算偏差”这一步放在世界坐标系里做,位置偏差直接等于目标位置减当前位置。至于要不要转到机器人坐标系,那是控制器模块的事,不要混在偏差计算里一起处理。

2.2 旋转矩阵的顺序决定一切

虚拟结构中心在世界系里有一个位置,还有一个航向角。成员在虚拟结构坐标系里有一个相对坐标。要想得到成员在世界系里的期望位置,必须先把相对坐标乘上旋转矩阵,再加上中心位置。

二维旋转矩阵按逆时针为正,形式如下:

R = [[cos(θ), -sin(θ)], [sin(θ), cos(θ)]]

期望位置公式为:

p_d = p_c + R(θ_c) * r_i

其中 p_c 是虚拟中心位置,θ_c 是虚拟中心航向角,r_i 是成员在虚拟结构系里的相对坐标。注意顺序:先旋转,再平移。因为相对坐标是在虚拟结构系下描述的,要转到世界系,必须先乘旋转矩阵,再加上中心的平移量。

我举个例子方便对照。虚拟中心在 (5, 3),航向角30度,某个成员的相对坐标是 (-2, -1)。cos30约0.866,sin30约0.5,旋转后的分量是:

x' = -2 * 0.866 - (-1) * 0.5 = -1.232
y' = -2 * 0.5 + (-1) * 0.866 = -1.866

加上中心位置,得到期望目标位置约 (3.768, 1.134)。如果旋转矩阵转置一下或者加减号写反,目标点就会绕中心反转,编队直接变成镜像。

2.3 航向偏差为什么一定要归一化

位置偏差很直观,目标点减当前位置就完了。航向偏差就麻烦一点,因为它本质上是角度差,角度的取值天然有周期性。

实际系统里,航向角可能来自磁力计、陀螺积分或者姿态解算,常见的形式是 [-π, π] 或者 [0, 2π) 区间。如果目标航向是 +179 度,当前航向是 -179 度,直觉告诉我们只要顺时针转2度就到位。但如果直接相减,得到的是 -358 度,控制器会以为需要反向转一大圈,于是给出一个完全错误的角速度指令。

解决办法是把角度差归一化到 [-π, π]:

def wrap_to_pi(angle): while angle > np.pi: angle -= 2 * np.pi while angle < -np.pi: angle += 2 * np.pi return angle

这个函数看着简单,却是偏差计算里最容易因为“嫌麻烦”而漏掉的关键一步。漏掉之后的表现往往很诡异:队形大部分时间是好的,偶尔某个成员突然原地绕圈,很难查。

3. 完整实操:把偏差计算写进每个控制周期

3.1 第一步:定义编队形状参数

编队形状的基准,就是每个成员在虚拟结构坐标系里的相对坐标 r_i。我常用三机三角形编队做验证,形状参数如下:

成员编号相对坐标 x相对坐标 y
101.0
2-1.0-0.8
31.0-0.8

这组坐标是在虚拟结构系下定义的,跟虚拟中心在世界系里的位置无关。后面不管虚拟中心走到哪,这组相对坐标都不会变,除非你想做动态变形。

3.2 第二步:维护虚拟结构中心的目标轨迹

虚拟结构中心不会天生知道自己要去哪,它需要上层路径规划器不断给出期望位姿。通常包括期望位置、期望航向,以及可能用于前馈的期望速度和期望角速度。

这里有一个实操经验:给虚拟中心的目标轨迹一定要做平滑处理。如果路径规划器直接输出折线路径,在拐角处中心速度和航向会突变,成员目标点也会跟着剧烈跳变,编队轻则抖动,重则失控。比较省事的做法是限制中心目标航向的角速度上限,或者在路径点上做圆弧过渡。

3.3 第三步:写一个控制周期内的完整流程

虚拟结构控制是周期性任务,每个控制周期都要执行以下步骤:

  1. 读取所有成员的当前位置和当前航向。
  2. 读取虚拟结构中心的目标位置和目标航向。
  3. 用目标航向构建二维旋转矩阵。
  4. 对每个成员,根据相对坐标计算期望目标位姿。
  5. 计算位置偏差:期望位置减当前位置。
  6. 计算航向偏差:目标航向减当前航向,并做 wrap_to_pi 归一化。
  7. 把偏差向量交给速度控制器。

这个流程里,偏差计算看起来只占三四行,但它必须在每个周期都重新计算。因为虚拟中心在动,成员也在动,偏差是时变的,不存在“算一次就完事”的情况。

3.4 第四步:一份可以直接跑通的Python验证代码

为了验证坐标变换和偏差计算是否正确,我建议先用一段独立脚本跑通核心公式,再上真机或仿真。下面是我常用的验证模板:

import numpy as np def wrap_to_pi(angle): while angle > np.pi: angle -= 2 * np.pi while angle < -np.pi: angle += 2 * np.pi return angle def compute_deviation(p_c, theta_c, r_i, p_i, theta_i): # p_c: 虚拟中心位置 (x, y) # theta_c: 虚拟中心目标航向角,单位弧度 # r_i: 成员在虚拟结构坐标系中的相对坐标 (x, y) # p_i: 成员当前位置 (x, y) # theta_i: 成员当前航向角,单位弧度 R = np.array([ [np.cos(theta_c), -np.sin(theta_c)], [np.sin(theta_c), np.cos(theta_c)] ]) p_d = p_c + R @ r_i e_xy = p_d - p_i e_theta = wrap_to_pi(theta_c - theta_i) return p_d, np.array([e_xy[0], e_xy[1], e_theta]) # 测试:虚拟中心在原点,航向90度 # 成员相对坐标 (1, 0),期望位置应在 (0, 1) p_c = np.array([0.0, 0.0]) theta_c = np.pi / 2 r_i = np.array([1.0, 0.0]) p_i = np.array([0.0, 1.0]) # 刚好在目标点 theta_i = np.pi / 2 p_d, e = compute_deviation(p_c, theta_c, r_i, p_i, theta_i) print("目标位置:", p_d) print("偏差:", e)

这段代码会输出目标位置约 (0, 1),偏差接近 (0, 0, 0)。用它验证完旋转方向,再扩展到完整编队就心里有底了。

3.5 偏差向量如何进一步变成控制量

这一步严格说不属于偏差计算,但为了闭环理解,简单说明一下。位置偏差通常会先乘以比例增益,得到期望线速度;航向偏差乘以角速度增益,得到期望角速度。车式机器人还要额外考虑运动学约束,把目标速度映射到前后轮驱动或者麦克纳姆轮的分配上。

重点是:控制增益不需要一开始就调大,先用小增益跑通逻辑,确认偏差计算正确了,再逐步提高响应速度。否则偏差算错的时候,大增益会让机器人疯狂乱跑,很难排查。

4. 调试实录:四个让我头疼过的问题

4.1 队形整体镜像,怎么都立不起来

现象:三机编队跑起来之后,队形几何关系完全反了。中心往左,右边的成员反而往左跑,队形像照了镜子。

排查发现,旋转矩阵里的正负号写反了。我用的二维旋转矩阵代入的是标准逆时针旋转,但实际系统中某些库函数的 Y 轴方向是向下的,或者航向角定义方向不同,导致符号需要取反。这种问题最麻烦的点在于它不报错,只有把成员目标点打印出来画图对比才能发现。

我的排查方法固定为两步:先把虚拟中心固定在原点,航向设成90度,打印所有成员的目标点,画出来看相对位置是否为预期;再给中心一个纯旋转扰动,观察目标点是否绕中心正确旋转。两步都过了,才能继续跑路径。

4.2 角度跳变引起的“回头式”旋转

现象:某成员走着走着,突然在原地画一个大圈,然后又回到正轨,整个过程看起来像导航抽风。

问题出在航向偏差没有做归一化处理。当目标航向和当前航向刚好差一个接近正负180度的夹角时,名义上的小角度变化会被直接相减放大成接近360度的误差,控制器以为需要反向绕一大圈。

解决方式就是前面写的 wrap_to_pi。但要注意,归一化不是加一次就完事,在连续控制周期里,每一步都必须用当前最新角度重新归一化。

4.3 编队启动瞬间的严重抖车

现象:刚开始建队时,成员起点离目标队形位置很远,启动后速度指令立刻饱和,大量机器人横冲直撞。

原因是偏差太大,比例控制直接给到了最大速度。编队初期偏差几米是常事,全部换算成控制量会让底层饱和,一旦饱和就会出现超调、震荡甚至碰撞。

我的做法是增加一个编队生成阶段:启动时先让虚拟中心静止在原地,成员只做位置的渐进修正;等各成员与目标点的偏差缩小到安全范围,再开始移动虚拟中心。另外可以在偏差进入控制器前做一个限幅,把最大速度控制在安全阈值以内。

4.4 虚拟中心经过折线拐点时成员目标点乱跳

现象:路径上如果存在90度转弯,转到一半时成员目标点会出现台阶式跳变,队形瞬间松散。

根因是虚拟中心的航向角在折点处不连续。比如上一周期中心航向0度,下一周期直接变成90度,旋转矩阵瞬间变化,成员的期望位置也跟着跳。

常规处理是在路径规划层对航向做插值或限制角速度,让中心航向平滑过渡。另一个务实技巧是提高控制频率:在拐点附近,目标航向的微小变化都要及时反映到偏差计算里,控制周期太长会让跳变更加明显。

4.5 问题速查表

现象可能原因排查思路
镜像编队旋转矩阵符号反了中心静止时打印目标点画图
成员原地转圈航向偏差未归一化检查 wrap_to_pi
启动乱冲偏差过大导致控制饱和加编队生成阶段,限制最大速度
拐点抖动中心航向突变平滑轨迹,限制角速度
目标点频繁跳变控制周期太长或路径点过密缩短周期,检查规划器输出

5. 不止于编队:偏差计算的扩展视角

5.1 动态编队变换

虚拟结构法不只是维持一个固定队形,还能做动态变换。原理很简单:不改变虚拟中心轨迹,只改变每个成员的相对坐标 r_i。

比如从三角形编队变一字形编队,只需要在若干控制周期内让 r_i 从三角形坐标平滑插值到一字形坐标,偏差计算完全复用现有框架。这里有个细节:同样需要做平滑插值,直接一步切换到新目标点,会让成员速度瞬变,控制效果很差。

5.2 避障时的临时结构变形

遇到障碍物的时候,严格刚性虚拟结构反而吃亏,因为队形没有任何柔性,一个成员被挡住,整个编队都过不去。比较灵活的做法是临时允许障碍物附近的成员把相对坐标往安全方向偏移,等绕过障碍再恢复。

这里的偏差计算就多了一个维度:成员的目标点不仅由虚拟中心决定,还要叠加一个局部的避障偏移量。控制逻辑不变,核心偏差公式变成了目标点加避障修正量再减当前位置。注意修正量必须平滑进出,否则同样会引起抖动。

5.3 三维编队和其他协同领域

把虚拟结构从二维扩展到三维,公式会复杂一些,因为三维旋转矩阵不再是单个角度,常见的做法是用四元数表示期望姿态,用四元数旋转相对坐标,再叠加中心位置。直接用欧拉角做连续旋转会碰到万向锁和角度累积问题,不建议冒险。

类似思路也会出现在机械臂末端协同、空地协同等场景里。只要抽象出一个“虚拟参考体”,给每个执行单元分配一个固定相对位姿,每周期计算参考体目标位姿与当前实际位姿的偏差,就都可以套用今天讲的这套框架。差别主要在坐标系维度、旋转表示方式和控制器的运动学约束上。

如果让我提一条最实用的建议:调试时先把虚拟结构中心固定住,让队形在静止状态下生成,确认每个成员目标点和实际点的相对几何关系,再把中心放开跑。多做这一步,真的能省很多事。坐标变换正确之后,再做纯平移,再做旋转,最后才上完整轨迹。这套顺序帮我避开了大部分坐标系和符号相关的低级错误。要是你手头也有编队相关的项目,尤其是队形怎么都“立不起来”的情况,先检查偏差计算这一步,多半是它的锅。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进