【多机器人路径规划】基于优先级规划与Petri网融合的多机器人路径规划框架研究(Matlab代码实现)
💥💥💞💞欢迎来到本博客❤️❤️💥💥
🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。
🎁完整资源、论文复现、期刊合作、论文辅导及科研仿真定制事宜点击:
👉👉👉本文完整资源下载
⛳️座右铭:行百里者,半于九十。
⛳️赠与读者
👨💻做科研,涉及到一个深在的思想系统,需要科研者逻辑缜密,踏实认真,但是不能只是努力,很多时候借力比努力更重要,然后还要有仰望星空的创新点和启发点。建议读者按目录次序逐一浏览,免得骤然跌入幽暗的迷宫找不到来时的路,它不足为你揭示全部问题的答案,但若能解答你胸中升起的一朵朵疑云,也未尝不会酿成晚霞斑斓的别一番景致,万一它给你带来了一场精神世界的苦雨,那就借机洗刷一下原来存放在那儿的“躺平”上的尘埃吧。
或许,雨过云收,神驰的天地更清朗.......🔎🔎🔎
💥第一部分——内容介绍
摘要
多机器人路径规划(Multi-Agent Path Finding, MAPF)要求在共享环境中为若干智能体寻找从各自起点到终点的无冲突路径,是仓储物流、无人机编队与自动驾驶等应用的基础问题。精确求解方法在智能体数量增大时计算代价急剧上升,而优先级规划以「按序逐个规划、后续智能体规避先前路径」的策略,在多项式时间内给出可行解,是大规模场景下的实用选择。本文对一套完整的 MAPF MATLAB 代码库进行系统性解读。该框架将求解过程组织为几何、拓扑、调度与呈现四个相互解耦的层次,依次完成地图解析、四邻域拓扑与 Petri 网构建、基于优先级启发式的路径规划、时间同步与冲突消解,以及动画与视频可视化。框架的核心设计思想在于将空间规划与时间消解分离:先在空间维度上允许路径重叠并以动态边权膨胀引导避让,再在时间维度上以等待动作消解残余冲突。本文逐模块阐述其建模思路、算法流程与工程取舍,分析各模块之间的接口契约与不变量,并对框架的完备性、复杂度与可扩展性作出评价。全文旨在为 MAPF 的研究与教学提供一份可复现、可批判的参考文本。
关键词:多机器人路径规划;优先级规划;Petri 网;冲突消解;动态边权膨胀;时间同步;可视化
1 引言
1.1 问题背景
多机器人路径规划是移动机器人学与组合优化交叉领域的经典问题。其现实原型广泛存在于自动化仓库的搬运机器人调度、无人机集群的编队飞行、以及自动驾驶车辆的路口通行等场景之中。这些场景的共同特征是:多个智能体共享一片有限的、可能含有静态障碍的空间,每个智能体需要在有限时间内从各自的起点抵达各自的目标,而任意时刻任意两个智能体不得占据同一位置,任意两个智能体也不得在相邻时刻互换位置。若把空间离散为栅格、把时间离散为步数,则该问题可被表述为一个带容量约束与交换约束的最短路径组合问题。
从计算复杂度的角度看,MAPF 的精确求解是 NP-hard 的。基于冲突的搜索(Conflict-Based Search, CBS)通过「先各自规划、再检测冲突、再对冲突分支加约束」的两层结构,在多数实例上表现优异,但其最坏情形下的搜索树规模随冲突数指数增长。基于整数规划(Integer Programming, IP)的全局方法可以给出最优解,但变量数与约束数随智能体数、时间步数、地图规模三者之积增长,在大规模实例上很快触及求解器的时间与内存上限。相比之下,优先级规划(Prioritized Planning, PP)放弃最优性、换取多项式时间的可行性:它先为智能体确定一个处理顺序,再按序为每个智能体规划路径,规划时把先前智能体的路径视为动态障碍。PP 的代价是不完备——某些可解实例可能因优先级顺序不当而失败;但其收益是可扩展——在数百个智能体的规模上仍能在秒级给出可行解。因此,PP 成为大规模 MAPF 的实用基线,也是本文所解读代码库的算法骨架。
1.2 代码库的定位与特色
本文解读的代码库并非一个孤立的算法实现,而是一套覆盖「数据入口—建模—规划—消解—呈现」全链路的工程框架。其特色可归纳为五点。
第一,动态边权膨胀。在优先级规划的逐体求解过程中,代码库并未对先前智能体的路径采取「硬封锁」,而是对其邻域施加与轨迹长度成正比的惩罚。这一设计保留了后续智能体「必要时穿越」的可能性,从而避免了硬封锁在狭窄地图上造成的虚假不可解。
第二,节点预留历史。在时间同步阶段,代码库以每个节点为键、以「时刻—智能体」二元组为值,构造哈希式的预留历史。这使得冲突查询从「扫描整条路径」降为「查表」,把同步阶段的时间复杂度从平方级压到接近线性。
第三,几何惩罚驱动的局部同步。代码库在规划阶段就以「路径代价是否超过步数」为判据,标记出可能冲突的节点,并把这一信息传递给同步阶段。同步阶段于是只需检查这些候选节点,而不必逐时刻扫描全路径。这一「信息前移」的设计,是框架在同步效率上的关键创新。
第四,Petri 网双阶段优化。除图搜索路径外,代码库还提供了一条基于 Petri 网的整数规划求解路径:第一阶段最小化单元容量以判定是否存在无碰撞解,第二阶段在容量约束下最小化触发序列长度。两阶段之间以容量值衔接,把「是否存在解」与「解是否最优」解耦。
第五,双坐标系统与完整可视化链路。从 MovingAI 字符地图到笛卡尔绘图、从 MATLAB 原生动画到 Python 渲染后端,代码库提供了端到端的呈现能力,使得算法行为可被直观观察与验证。
1.3 全文组织
全文按数据流顺序组织。第 2 节给出总体架构与坐标约定,阐明四层划分的依据与各层之间的接口。第 3 节阐述地图解析,说明 MovingAI 格式的两种变体及其鲁棒处理。第 4 节阐述拓扑构建与 Petri 网建模,讨论邻接矩阵的构造技巧与稀疏存储的必要性。第 5 节阐述初始与目标标记的构造。第 6 节阐述优先级规划,包括启发式设计、动态边权膨胀、几何惩罚与可行性预检查。第 7 节阐述时间同步与冲突消解,比较三种同步策略的取舍。第 8 节阐述可视化链路。第 9 节阐述 Petri 网整数规划求解。第 10 节阐述栅格分解等辅助模块。第 11 节汇总主程序流程与关键设计决策。第 12 节对框架的完备性、复杂度与可扩展性作出评价,并指出改进方向。
2 总体架构与坐标系统
2.1 四层划分的依据
把 MAPF 求解组织为四层,并非随意的工程分层,而是对问题内在结构的回应。MAPF 的输入是一张几何地图与一组起止坐标,输出是一组带时间戳的轨迹。从输入到输出,需要跨越三道鸿沟:其一是「几何到拓扑」的鸿沟——连续或半连续的地图必须被离散为图,节点与边的定义决定了后续一切算法的语义;其二是「拓扑到调度」的鸿沟——图上的最短路径是空间概念,而多智能体的无冲突要求是时间概念,二者必须通过某种同步机制缝合;其三是「调度到呈现」的鸿沟——节点编号必须被翻译回坐标,才能被人眼理解。
代码库的四层划分正是对这三道鸿沟的逐一跨越。几何层负责把字符地图翻译为占用矩阵并建立坐标约定,跨越第一道鸿沟的前半;拓扑层把占用矩阵约化为只含自由单元的图,并进一步导出 Petri 网关联矩阵,跨越第一道鸿沟的后半;调度层先用优先级启发式确定顺序、再以动态膨胀的图搜索求路径、再以插入等待缝合时间,跨越第二道鸿沟;呈现层把节点序列反投影为坐标序列并驱动动画,跨越第三道鸿沟。
这一划分的关键收益是可替换性。几何层的地图来源可替换为 MovingAI 文件、随机生成器或手工构造;拓扑层的邻接定义可替换为四邻域、八邻域或栅格分解;调度层的规划器可替换为 Dijkstra、A* 或整数规划;呈现层的后端可替换为 MATLAB 原生或 Python。各层之间仅通过四类数据结构耦合——占用矩阵、邻接矩阵、节点序列、坐标序列——接口清晰、契约稳定。读者若需把本框架迁移到自己的地图或机器人模型,只需替换对应层的实现,而不必理解其余三层的内部细节。
2.2 坐标系统与不变量
代码库采用两套坐标系统,二者的差异是阅读代码时最容易出错的地方。图像坐标系源于 MovingAI 数据集的原始约定:原点位于左上角,纵轴向下增长,这与图像处理领域的惯例一致。笛卡尔坐标系源于 MATLAB 绘图与机器人学的惯例:原点位于左下角,纵轴向上增长。二者之间的转换是一次性的、全局的:地图加载函数在读取地图后立即执行纵轴翻转与矩阵翻转,此后全部模块均假定地图已是笛卡尔朝向。
这一「一次转换、处处假定」的设计,其价值在于消除重复转换的风险。若允许多个模块各自转换,则任何一处的遗漏或重复都会导致「地图上下颠倒」或「轨迹飞到障碍里」的隐性错误,而这类错误往往在可视化阶段才暴露,调试代价极高。代码库把转换收敛到单一入口,并在绘图层保留了一个兼容参数,用于处理未经过翻转的旧地图。这一兼容参数的存在本身就是一个提醒:它是一个兜底路径,而非推荐用法;新代码应当始终假定地图已是笛卡尔朝向。
与之配套的是线性索引与坐标的互转约定。地图矩阵按行优先存储,线性索引由行列经标准转换得到;笛卡尔坐标则以「列减一、行减一」的方式从行列导出。这一约定看似琐碎,却是连接「地图矩阵」「图节点编号」「绘图坐标」三者的唯一纽带。代码库中的双向映射表——从原始地图索引到约简图节点编号的正向表、以及其逆向表——正是建立在这一约定之上。正向表用于把规划结果翻译回地图坐标,逆向表用于把地图坐标翻译为规划输入。二者在自由单元上互为逆映射,任何一处的编号错位都会在可视化阶段以轨迹异常的形式暴露。因此,绘图层中保留了一段被注释掉的一致性检查代码,用于调试时验证每个轨迹节点对应的地图单元确实为自由。这段被注释的代码,是框架对「不变量可能被破坏」这一风险的自觉回应。
3 地图解析
3.1 MovingAI 格式的两种变体
MovingAI 是路径规划领域最常用的公开数据集之一,其地图文件以纯文本存储,头部声明高度与宽度,随后以字符矩阵表示占用情况:障碍以特定符号标记,自由单元以点号标记。场景文件则逐行给出若干组起止坐标及对应的最优路径长度。
代码库对这两种文件都做了鲁棒处理。地图解析以关键字为数据起始标志,逐行读取字符矩阵并二值化。场景解析则先检测是否存在版本头部行,若存在则跳过,否则回退文件指针从头读取。这一「先探测、后解析」的策略,使得同一份解析代码可以兼容数据集的多种发布版本,而不必为每个版本维护单独的分支。
值得说明的是,代码库中存在两个功能重叠的地图读取函数。其中一个承担完整的加载职责——读取地图、读取场景、执行坐标翻转、返回起止点;另一个则只读取地图本身、不做坐标翻转、返回图像系网格。二者的并存并非冗余,而是服务于不同的下游需求:前者服务于「规划与绘图」链路,后者服务于「栅格分解」链路。栅格分解需要在原始朝向上进行几何计算,若入口处已翻转,则分解后的单元顶点会与地图像素错位。因此,「按需保留原始朝向」是一种有意的设计选择,而非疏漏。读者在扩展代码库时,应当根据下游需求选择正确的读取函数,而非盲目统一。
3.2 解析的性能考量
从数值实现的角度看,地图解析的性能瓶颈在于逐行读取与逐字符比较。对于数百像素见方的地图,这一开销可以忽略;但若扩展到数万像素级地图,更高效的做法是一次性读入全部文本并向量化比较。代码库目前保持了「可读性优先」的实现风格,这在教学与调试场景下是合理的取舍;若需处理超大规模地图,则解析层是最先应当优化的环节。
4 拓扑构建与 Petri 网建模
4.1 四邻域邻接的构造技巧
拓扑构建的核心是把占用矩阵翻译为邻接矩阵。代码库采用四邻域定义:两个自由单元相邻,当且仅当它们的曼哈顿距离为一。这一定义与栅格地图的物理语义一致——机器人只能上下左右移动,不能斜穿障碍角落。
邻接矩阵的构造有一个容易忽视的工程细节:若不加处理地枚举全部四个方向,则每条无向边会被写入两次,而稀疏矩阵构造函数在遇到重复三元组时会累加而非覆盖。于是同一条边的权重会从一变为二,破坏二值性,进而影响后续图算法的语义。代码库的应对策略是「只枚举东与南两个方向,再对称化」:每条无向边恰好被写入一次,对称化则保证矩阵的对称性。这一技巧在稀疏图构建中是标准做法,但其必要性常被初学者忽略;代码库还额外在子图提取后执行了一次二值化确认,构成双重保险。
另一个值得讨论的设计是邻接矩阵的维度。代码库首先构造完整的、与地图同尺寸的方阵,其中障碍单元对应的行列全为零;随后再按自由单元索引提取子图。这一「先完整、后压缩」的两步走,初看似乎浪费内存,实则有明确的收益:完整矩阵的线性索引与地图索引直接对齐,使得「地图坐标—图节点」的翻译只需一次查表;压缩后的子图则使图算法的节点编号与 Petri 网的库所编号一一对应,为后续整数规划建模铺平道路。两步之间的桥梁正是那对双向映射表。
4.2 Petri 网建模的语义
Petri 网建模把图搜索问题翻译为标记演化问题。其语义可概括为三句话:每个自由单元是一个库所;每条无向边(两个方向各一条)是一个变迁;一个标记代表一个机器人。触发一个变迁,意味着某个机器人沿对应边移动一步;标记在库所之间的流动,则刻画了机器人在空间中的移动。
这一建模的价值在于,它把「多机器人无冲突」这一时间性质,编码为「每库所至多一标记」这一容量约束。容量约束是静态的、逐时刻可检验的,因而可以直接写入整数规划的约束矩阵。相比之下,若直接用图搜索建模,则无冲突要求只能以「路径之间不得共享节点」的形式表达,而这在多智能体场景下是一个组合约束,难以线性化。Petri 网建模的这一「以容量换冲突」的翻译,是代码库能够提供整数规划求解路径的根本原因。
关联矩阵的构造同样值得注意。前关联矩阵标记每条边的起点库所,后关联矩阵标记每条边的终点库所,二者之差即为状态演化的转移矩阵。稀疏存储在这里不是可选项而是必需品:对于万级节点的地图,稠密矩阵将占用数 GB 内存,而稀疏矩阵仅随边数线性增长。代码库在构造时直接使用稀疏三元组形式,避免了「先稠密后稀疏」的中间峰值。
5 初始与目标标记
标记的构造是 Petri 网建模的入口。给定机器人的起止坐标,代码库先将其翻译为地图线性索引,再经逆向映射表翻译为约简图节点编号,最后构造两个单位指示向量,分别标记初始库所与目标库所。这一流程看似平凡,却包含两处必须显式检查的边界:起点或终点若落入障碍,则逆向映射表返回零,此时必须抛出错误而非静默继续;起点或终点若超出地图边界,则索引转换会产生越界,同样必须拦截。
单位指示向量的语义是「一个机器人对应一个标记」。单机器人场景下,初始与目标标记都恰有一个非零元;多机器人场景下,则通过多个标记的叠加实现。这一语义在整数规划建模中被显式使用:容量约束中的「每库所至多一标记」,正是对「一个机器人对应一个标记」的直接翻译。读者若需扩展到「一个机器人携带多个标记」的模型(例如考虑机器人携带的货物),则需修改这一语义并相应调整容量约束。
代码库还提供了一个单点版本的翻译函数,用于在规划过程中动态地把新坐标翻译为节点编号,而不必构造完整的标记向量。这一函数的存在,使得「坐标到节点」的翻译能力可以被任意调用方复用,而不必依赖标记构造的上下文。
6 优先级规划
6.1 优先级启发式的设计空间
优先级规划的第一问是:以何种顺序处理智能体?这一问题的答案直接决定了后续智能体的绕行代价。代码库提供了六种启发式,覆盖了「随机」「按路径长度」「按冲突程度」「按原始顺序」四个维度。
按路径长度排序的两种启发式,其直觉恰好相反。短路径优先的理据是:短路径智能体的可选路径少,若让其后处理,则可能因长路径智能体已占据关键通道而无路可走;先处理短路径,等于先锁定「刚性需求」,再让长路径智能体在剩余空间中迂回。长路径优先的理据则是:长路径智能体的绕行代价高,若让其后处理,则一次绕行可能带来数倍的路径增长;先处理长路径,等于先锁定「高价值需求」,再让短路径智能体承担较小的绕行。两种理据各有道理,实际效果取决于地图拓扑与起止点分布,代码库把选择权交给使用者。
按冲突程度排序的两种启发式,其依据是静态冲突计数——即某智能体的无约束路径与其他智能体无约束路径的共享节点数。冲突计数高的智能体,若后处理,则其最优路径很可能已被先前智能体占据,被迫大幅绕行;先处理它,等于让它获得无惩罚的最优路径,而让冲突较少的智能体承担后续绕行。这一启发式在多智能体密集场景下往往优于纯长度排序,因为长度并不反映「路径之间的交叠程度」,而交叠程度才是冲突的根源。
静态冲突计数的计算复杂度与智能体数的平方成正比,与平均路径长度成正比。对于百级智能体、百级路径长度的规模,这一开销在秒级以内,可以接受。代码库对空路径做了无穷大保护,使得极端地图(部分智能体不可达)下不会因集合交集为空而崩溃——这一细节虽小,却是鲁棒性的体现。
6.2 动态边权膨胀的机理
优先级规划的第二问是:如何处理先前智能体的路径?最直接的做法是「硬封锁」——把先前路径经过的节点从图中删除。但硬封锁有一个致命缺陷:在某些地图上,先前路径可能恰好穿过唯一的通道,硬封锁会使后续智能体虚假不可解,尽管实际上只要让先前智能体稍作等待就能共存。
代码库采用「软惩罚」替代硬封锁。其做法是:对与先前轨迹相邻的每条边,加上一个与轨迹长度成正比的惩罚;随后在该加权图上求最短路径。惩罚的语义是「鼓励绕行,但不禁止穿越」。若绕行代价小于惩罚,则后续智能体会选择绕行;若绕行不可行(例如唯一的通道被先前轨迹占据),则后续智能体仍可穿越,只是其路径代价会高于步数——这一「代价超出步数」的信号,正是后续同步阶段识别冲突节点的依据。
惩罚强度的选择是一个经验问题。惩罚过小,则后续智能体倾向于穿越而非绕行,导致同步阶段大量等待;惩罚过大,则后续智能体可能被逼入不可达区域,触发回退。代码库取「轨迹长度」作为惩罚量,使得「绕行一步」与「与整条历史轨迹冲突」在量级上可比,这是一种折中。读者若需针对特定地图调优,可以把惩罚强度参数化,并以「同步后总等待数」为指标做网格搜索。
硬封锁的另一变体是「目标封锁」——即已到达的机器人占据其终点,后续机器人不得再进入或离开。这一封锁是合理的,因为已到达的机器人确实会永久占据终点,若允许后续机器人把该终点当作中转站,则同步阶段必然产生无法消解的顶点冲突。代码库在规划完每个智能体后,立即封锁其目标节点的全部入边与出边,从而在空间规划阶段就排除了这一隐患。这一设计体现了「空间规划阶段尽量消除可预见的冲突」的工程哲学。
6.3 几何惩罚与冲突节点的前移
代码库在规划阶段就计算「几何惩罚」——即路径代价与路径步数之差。若惩罚为零,说明路径完全避开了所有惩罚边,因而不可能与先前轨迹冲突;若惩罚为正,说明路径穿越了某些惩罚边,这些边对应的节点就是潜在冲突点。代码库把这些节点收集起来,存入冲突节点表,供同步阶段使用。
这一「信息前移」的设计,其价值在于把同步阶段的检查范围从「全路径」压缩到「候选节点」。在冲突稀疏的场景下,候选节点数远小于路径长度,同步阶段因而获得数量级的加速。其理论依据是一个简单的观察:若某节点在规划阶段未被惩罚,说明没有历史轨迹经过其邻域,因而在同步阶段也不可能发生顶点冲突;交换冲突的概率更低,可以忽略或交由最终验证阶段兜底。
需要指出的是,代码库中对冲突节点表的记录存在一段重复代码——同一段收集逻辑在条件分支内外各出现一次。这段重复在逻辑上等价,不影响正确性,但在大规模实例上会造成重复的路径扫描,属于可优化的性能细节。读者在重构时可以将其合并到单一位置。
6.4 可行性预检查的分层
代码库在优先级规划之前,先对每个智能体在原始无约束拓扑上求解最短路径;若某智能体的最短路径为无穷大,则判定其几何不可解,整批任务终止。这一「先验证、后规划」的分层,其理据是:若某智能体在地图上根本不可达(其起点或终点被障碍完全包围),则任何优先级顺序都无法使其可解;提前拦截是唯一合理的选择,而在规划循环内部处理这一分支只会增加复杂度。
预检查的另一收益是,它把「几何不可解」与「调度不可解」两类失败区分开来。前者是地图与起止点的问题,后者是优先级顺序与同步策略的问题。二者需要不同的应对:前者应调整地图或场景,后者应调整启发式或增加回溯。代码库通过预检查明确区分了这两类失败,使得调试方向清晰。
7 时间同步与冲突消解
7.1 三种同步策略的取舍
代码库提供了三种时间同步策略,它们的复杂度与适用场景各不相同。
第一种是节点预留历史控制器。它以每个节点为键,维护一张「时刻—智能体」的预留表;对每个智能体的每一步移动,检查目标节点在下一时刻是否已被更高优先级智能体占用、以及当前节点与目标节点是否构成交换。冲突时在当前节点插入等待,时间推进后重检。这一策略的优点是逻辑直观、实现简洁;其性能关键在于「先查历史再进循环」的快速路径——仅当相关节点的历史非空时才进入详细检查,从而在冲突稀疏时把大多数移动的处理降为常数时间。
第二种是基于碰撞数据的鲁棒同步。它先把所有路径填充至统一最大长度,使逐时刻比较可用简单索引完成;再利用时间缓存实现常数时间的冲突查询;冲突时在冲突时刻前插入等待,并把其余路径尾部填充以保持长度一致。这一策略的优点是「填充—比较—消解」三段式结构清晰,且对交换冲突采取「等待两步」的过度消解,鲁棒性优于精确的一步消解。其代价是填充带来的内存开销与重填充带来的额外计算。
第三种是局部同步。它只在规划阶段已标记的候选节点处检查冲突,把检查范围从全路径压缩到候选集。其优点是效率最高,适合冲突稀疏的大规模实例;其代价是放弃了全路径扫描的完备性,若候选节点表因某种原因遗漏了真正的冲突点,则同步会失败。代码库通过「规划阶段收集候选、同步阶段只查候选、最终验证阶段兜底」的三段式设计,部分弥补了这一代价。
三者的取舍可概括为:若追求实现简洁与调试方便,选第一种;若追求鲁棒性与可预测性,选第二种;若追求大规模实例的效率,选第三种。代码库在主线流程中使用第一种,同时保留第二、三种作为可选路径,体现了「按场景选策略」的工程智慧。
7.2 交换冲突的特殊性
交换冲突——两个机器人在相邻时刻互换位置——是 MAPF 中最难消解的冲突类型。顶点冲突只需一方等待即可消解;交换冲突则不然:若只让一方等待一步,则等待方在下一时刻恰好到达对方刚离开的节点,虽不构成顶点冲突,却可能与对方的后续移动再次形成交换。
代码库对交换冲突的处理有两种:第一种在预留历史控制器中被注释掉,注释说明「MATLAB 中为性能常跳过详细交换检查,依赖稳健的顶点检查」。这一取舍的理据是:交换冲突的概率远低于顶点冲突;且在某些拓扑(宽度为一的走廊)下,交换冲突根本无法通过等待消解,必须依赖更上层的规划调整(例如让一方绕行或重新排序)。因此,把交换消解交给验证阶段或更复杂的同步策略,是一种合理分层。
第二种在鲁棒同步策略中采取「等待两步」的过度消解。其几何直觉是:等两步让高优先级机器人完全通过该边,从而彻底消除交换的可能。这一策略以少量额外等待换取同步的正确性,在鲁棒性上优于精确的一步消解。其代价是可能引入不必要的等待,从而增大总同步代价。读者若需在「正确性」与「最优性」之间调优,可以把等待步数参数化,并以「总等待数」为指标评估。
7.3 同步代价的度量
代码库在同步完成后计算三项指标:总同步代价、基础几何代价、总延迟。总同步代价是全部同步路径长度之和;基础几何代价是全部规划路径长度之和;总延迟是二者之差。这三项指标分别刻画了「考虑等待后的总耗时」「纯移动的总耗时」「等待造成的额外耗时」。
总延迟的意义在于,它直接反映了冲突消解的代价。若总延迟为零,说明规划阶段的动态膨胀已完全消除了冲突,同步阶段无需插入任何等待;若总延迟较大,说明规划阶段的避让不足,同步阶段承担了大量消解工作。这一指标可以反过来指导惩罚强度的调优:若总延迟过大,则应增大惩罚强度,让规划阶段承担更多避让;若总延迟为零但路径长度过长,则应减小惩罚强度,让规划阶段更倾向于最短路径。
代码库还验证了「内部计数器」与「路径长度增量」是否一致——即同步控制器报告的消解冲突数,是否等于全部路径长度增量之和。这一验证是对同步逻辑正确性的直接检验:若二者不符,说明某处的等待插入未被正确计数,或某处的路径长度被意外修改。这一「生成—验证」的双轨设计,是框架可维护性的重要保障。
8 可视化链路
8.1 地图绘制的取反技巧
地图绘制的一个细节值得单独讨论:二值地图的「障碍」通常以高值表示,「自由」以低值表示;而绘图函数默认把低值映射为暗色、高值映射为亮色。若不取反,则自由单元会显示为黑色、障碍显示为白色,与直觉相反。代码库通过一次取反,使自由单元映射为亮色、障碍映射为暗色,从而与「白底黑障」的直觉一致。
绘图的图层顺序同样重要。地图作为背景,应当位于所有轨迹与标记之下;若地图后绘制,则会遮挡轨迹。代码库显式地把地图图层压到底层,确保后续绘制的轨迹、起点、终点、机器人标记都可见。这一细节在多图层可视化中常被忽略,却是「轨迹可见」的必要条件。
8.2 坐标翻译的中心与角点之争
节点序列到坐标序列的翻译,有一个看似琐碎却影响视觉的选择:机器人标记应落在单元格中心,还是单元格角点?中心模式让标记落在格子正中,视觉上更自然,适合演示;角点模式与像素边界对齐,适合与栅格叠加显示。代码库把这一选择参数化,默认使用中心模式,同时保留角点模式以兼容栅格叠加场景。
若地图未经翻转(即仍是图像系),则纵坐标翻译需额外翻转,且中心与角点的翻转公式不同。代码库在翻译函数中同时处理了这两种朝向与两种模式的四种组合,其分支虽多,但逻辑清晰:朝向决定是否翻转,模式决定偏移量。读者在扩展时应注意,这一函数的正确性依赖于「地图朝向」与「翻译模式」两个参数的协同,任一参数错误都会导致轨迹整体偏移半格或上下颠倒。
8.3 动画的视觉编码
动画的核心是「逐时刻更新机器人位置与轨迹尾迹」。代码库在更新位置的同时,还根据「当前节点是否等于上一时刻节点」判断机器人是否在等待;等待时把标记染红,移动时恢复原色。这一「红色等待」的视觉编码,使观众能直观看出哪些机器人在为他人让路,是演示冲突消解机制的有效手段。
动画的性能瓶颈在于刷新与暂停的调用频率。刷新过频则动画卡顿,暂停过短则人眼无法捕捉。代码库把暂停时长参数化,默认值对应约二十帧每秒,适合演示;若需录制视频,则应把暂停设为零并改用视频写入器。视频写入器的编码格式选择也有讲究:某些格式在特定操作系统上需要额外编解码器,而运动 JPEG 格式兼容性最好,无需额外依赖。
8.4 可区分颜色的生成
当机器人数量超过内置色板的容量时,颜色开始重复,观众难以区分不同机器人。代码库提供了一个可区分颜色生成器:在色相—饱和度—明度空间中采样候选色,排除过灰(低饱和)与过暗(低明度)的区域,再以贪心最大最小距离策略选取一组颜色——每次选取「与已选颜色最小距离最大」的候选,从而使任意两色在视觉上尽可能不同。
这一贪心策略的复杂度与颜色数、候选数、已选数三者之积成正比。对于百级颜色、数百候选的规模,计算量在百万级,可接受。若需更优的全局区分度,可改用聚类或力导向布局,但贪心在工程上已足够。饱和度与明度的下限选择,是基于「白底地图上清晰可见」这一实际需求的经验取值;若地图背景为深色,则应相应调整下限。
8.5 跨语言渲染桥接
代码库还提供了一条跨语言渲染路径:把 MATLAB 中的地图、起止点、轨迹转换为 Python 可读的数组与列表,调用 Python 端的动画类进行渲染。这一桥接的关键在于坐标系的再次转换——MATLAB 端的笛卡尔坐标需转回图像坐标,地图需再次翻转,才能与 Python 端假定图像系的渲染逻辑对齐。
这一「MATLAB 翻转一次、Python 再翻转一次」的往返,初看冗余,实则是跨语言协作的必然代价:每个语言生态都有自己的坐标惯例,强行统一会破坏各生态内部的一致性。代码库选择在边界处做显式转换,而非在内部做隐式假定,这是一种可维护的设计。读者在扩展跨语言桥接时,应特别注意「谁负责翻转」这一契约,避免双重翻转或遗漏翻转。
8.6 环境与区域的绘制
除地图与轨迹外,代码库还提供环境与区域的绘制函数,用于展示栅格分解结果、起点终点标记、区域分组着色等。这些函数主要服务于 Petri 网求解结果的迭代可视化:把每个中间标记对应的机器人位置叠加在地图上,便于观察标记如何流动、机器人如何逐步接近目标。
区域分组着色的设计,使得「哪些区域属于同一任务」「哪些区域是障碍」等信息可以在一张图上同时呈现。起点以三角或箭头标记、终点以菱形或方块标记,形状的差异使得即便颜色因打印或投影而失真,观众仍能区分起止。这些视觉细节虽不涉及算法正确性,却是「让结果可被理解」的关键。
9 Petri 网整数规划求解
9.1 两阶段结构的动机
Petri 网求解路径的核心是「两阶段整数规划」。第一阶段最小化单元容量——即「同时占用同一单元的最大机器人数」。若最小容量为一,说明存在无碰撞解,无需进入第二阶段;若最小容量大于一,则说明无碰撞解需要更多中间步骤,进入第二阶段。
这一「先容量、后序列」的两阶段结构,其动机是把「是否存在解」与「解是否最优」解耦。若把二者耦合在一个模型中,则变量数需同时覆盖容量与序列,模型规模急剧膨胀;分离后,第一阶段只需判定容量是否为一,模型规模小、求解快;第二阶段才在已知可行容量的前提下优化序列,避免了对「容量未知」这一维度的搜索。两阶段之间以容量值衔接:第一阶段的容量作为第二阶段的中间步数初值,若第二阶段不可行则递增,直至成功或达到上限。
9.2 容量约束的编码
容量约束的编码是 Petri 网建模的精髓。其形式可概括为:在每一步,每个库所的标记数加上该步流入的标记数,不得超过一。这一约束的语义是「每库所至多一标记」,即「每单元至多一机器人」。
这一编码的巧妙之处在于,它把「多机器人无冲突」这一时间性质,翻译为「逐时刻的静态容量约束」。静态约束可以直接写入整数规划的约束矩阵,而时间性质若直接编码则会引入「任意两机器人任意时刻不得同址」的组合约束,难以线性化。Petri 网建模以「标记流动」为中介,把组合约束转化为容量约束,这是它能够被整数规划求解的根本原因。
9.3 布尔公式约束的扩展
代码库还支持在整数规划中加入布尔公式约束,用于表达「必须访问某些区域」「不能同时访问某些区域」等任务规约。其编码方式是大 M 法:引入辅助二值变量表示「是否访问某区域」,再以「区域标记数」与「辅助变量」之间的双向不等式约束,把「访问」这一逻辑概念翻译为「标记数非零」这一数值条件。
大 M 常数的选择是一个经典问题。取值过小则约束失效,取值过大则 LP 松弛变松、分支定界变慢。代码库取「机器人数乘以一个经验倍数」作为大 M,保证其大于任何可能的区域标记数,同时不至于过大。读者若需调优,应以「求解时间」与「LP 松弛间隙」为指标做实验。
9.4 伪解的检验
整数规划的解不一定对应可行的触发序列——某些变迁的计数虽非零,但其输入库所在任何可达标记下都无标记,这类解称为伪解。伪解的产生源于整数松弛:松弛后的模型允许「计数上可行但顺序上不可行」的解。
代码库通过逐触发模拟检验伪解:从初始标记出发,循环触发可行变迁,记录每个机器人的库所序列;若中途无可触发变迁但计数非零,则判定为伪解。这一模拟既是对整数规划结果的最终检验,也是把「计数向量」翻译为「机器人轨迹」的必要步骤。其复杂度与总触发数成正比,对于百级触发、百级库所的规模,可忽略。
伪解的存在提醒我们:整数规划给出的是「计数层面的可行」,而非「执行层面的可行」。任何基于整数规划的 MAPF 求解器,都必须包含这一「计数到执行」的翻译与检验环节,否则可能在报告「求解成功」的同时给出无法执行的方案。
10 栅格分解等辅助模块
10.1 规则栅格分解
栅格分解把连续或半连续的地图划分为规则单元,每个单元以顶点坐标与中心坐标描述,单元之间的邻接以中心距离判定。这一分解服务于两类需求:其一是把「像素级地图」粗化为「区域级地图」,以降低后续规划的规模;其二是把「区域访问」这一任务规约翻译为「单元访问」,以适配 Petri 网的区域约束。
代码库提供了多个栅格分解变体:按地图文件分解、按环境边界分解、按随机障碍分解。三者的共同结构是「生成单元顶点—构建邻接—分类障碍与自由」,差异仅在单元的来源与障碍的生成方式。这一「同构多态」的设计,使得新增一种分解方式只需替换单元生成部分,而不必重写邻接构建与分类逻辑。
10.2 感兴趣区域与障碍分区
在栅格分解之上,代码库还支持「感兴趣区域」的定义:每个区域是一组单元,单元的中心被归类到某区域或自由空间;区域的组合以观测集编码,用于表达「哪些区域可被同时观测」。这一机制服务于需要「区域级任务规约」的场景,例如「必须依次访问三个区域」「不能同时进入两个区域」。
障碍分区的处理则涉及一个易错细节:当障碍单元被从单元序列中移除后,区域对应的单元索引需相应偏移,否则会出现「区域指向错误单元」的隐性错误。代码库在分区函数中显式处理了这一偏移,并在自由空间的计算中以集合差集表达「不属于任何区域的单元」。这一细节虽小,却是栅格分解正确性的关键。
📚第二部分——运行结果



部分代码:
function run_python_animation(B, robotPts, sched_points, pyFile)
%RUN_PYTHON_ANIMATION 从MATLAB调用Python的Animation动画类,实现多机器人路径可视化
% 输入参数说明:
% B : H×W 二值地图矩阵 (1代表障碍物,0代表自由空间),笛卡尔坐标系,已经完成flipud上下翻转处理
% robotPts : R×1元胞数组,R为机器人数量;每个元胞存储 [起点x 起点y; 目标x 目标y],笛卡尔坐标系
% sched_points : R×1元胞数组,每个元胞为T×2矩阵,存储单个机器人时序路径点(x,y),笛卡尔坐标系,同步时间步
% pyFile : Python动画类脚本的文件路径,例如 'animation_icbs.py'
% 前置依赖:执行 python -m pip install numpy matplotlib 安装python依赖包
arguments
B {mustBeNumeric, mustBeNonempty} % 二值地图,非空数值矩阵
robotPts cell % 机器人起止点元胞数组
sched_points cell % 机器人规划路径时序点元胞数组
pyFile {mustBeTextScalar} % Python脚本路径,字符串标量
end
% ====================== 1) 初始化MATLAB与Python环境,加载Python模块 ======================
% 若需要强制指定特定Python版本,取消下面一行注释并修改路径:
% pyenv('Version','C:\Users\TuUsuario\AppData\Local\Programs\Python\Python311\python.exe');
pe = pyenv;
fprintf('[pyenv] Python解释器路径: %s (版本=%s)\n', pe.Executable, pe.Version);
% 将Python脚本所在文件夹添加到Python搜索路径,防止import找不到模块
[pyFolder, pyName, ~] = fileparts(pyFile);
if ~isempty(pyFolder)
% 如果该目录不在python sys.path列表中,则插入到路径列表最前面
if count(py.sys.path, string(pyFolder)) == 0
insert(py.sys.path, int64(0), string(pyFolder));
end
end
% 导入Python模块,reload用于在不重启MATLAB的情况下,加载修改后的py代码
mod = py.importlib.import_module(pyName);
py.importlib.reload(mod);
% ====================== 2) 坐标系转换:MATLAB笛卡尔坐标 → Python图像像素坐标 ======================
% 重要说明:
% MATLAB侧地图B使用笛卡尔坐标系,已经flipud;
% Python动画类内部逻辑:地图np.transpose + x轴翻转,绘图使用图像坐标系
% 图像坐标系约定:(0,0)在图片左上角,行row向下增加,列col向右增加
% 坐标转换规则:
% B_img = flipud(B) 还原回图像格式地图
% 笛卡尔坐标点(x,y) → 图像坐标 (row = H-1-y, col = x)
[H, W] = size(B);
B_img = flipud(B); % 将笛卡尔地图还原为图像格式(图像原点左上)
my_map_np = py.numpy.array(uint8(B_img)); % 转换为numpy uint8数组,1代表障碍物
% 转换所有机器人起点、终点:笛卡尔坐标 → 图像(行,列) 0起始索引
R = numel(robotPts);
starts_py = py.list; % Python列表,存放所有机器人起点
goals_py = py.list; % Python列表,存放所有机器人终点
for r = 1:R
P = robotPts{r}; % 取出第r个机器人 [sx sy; gx gy] 笛卡尔坐标
sx = P(1,1); sy = P(1,2);% 起点笛卡尔x,y
gx = P(2,1); gy = P(2,2);% 终点笛卡尔x,y
row_s = int64(H - 1 - sy);% 起点:笛卡尔y → 图像行号
col_s = int64(sx); % 起点:笛卡尔x → 图像列号
row_g = int64(H - 1 - gy);% 终点:笛卡尔y → 图像行号
col_g = int64(gx); % 终点:笛卡尔x → 图像列号
starts_py.append(py.tuple({row_s, col_s})); % 添加起点元组到python列表
goals_py.append(py.tuple({row_g, col_g})); % 添加终点元组到python列表
end
% 转换所有机器人时序路径点:笛卡尔路径序列 → 图像(行,列)序列
paths_py = py.list;
for r = 1:R
XY = sched_points{r}; % 第r个机器人时序路径 T×2 [x,y] 笛卡尔坐标
if isempty(XY) % 路径为空时,存入空列表
paths_py.append(py.list);
continue;
end
rc_list = py.list; % 存储当前机器人转换后的图像坐标序列
for k = 1:size(XY,1)
x = XY(k,1); y = XY(k,2);
% 坐标映射 + 四舍五入到最近栅格节点(支持中心点坐标如x.5,y.5)
row = int64(round(H - 1 - y));
col = int64(round(x));
rc_list.append(py.tuple({row, col}));
end
paths_py.append(rc_list);
end
% ====================== 3) 实例化Python动画类,执行可视化 ======================
% 创建动画对象,传入numpy地图、起点列表、终点列表、各机器人路径列表
anim = mod.Animation(my_map_np, starts_py, goals_py, paths_py);
% 弹出交互式窗口,展示动画
anim.show();
% 可选:导出mp4视频文件,取消注释启用;需要提前安装FFmpeg
% anim.save('anim_icbs.mp4', int64(1)); % 第二个参数为播放速度系数 speed=1
end
🎉第三部分——参考文献
文章中一些内容引自网络,会注明出处或引用为参考文献,难免有未尽之处,如有不妥,请随时联系删除。(文章内容仅供参考,具体效果以运行结果为准)
🌈第四部分——本文完整资源下载
资料获取,更多粉丝福利,MATLAB|Simulink|Python|数据|文档等完整资源获取
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)