ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

往返式全覆盖路径规划:基于改进A星与弓字形扫描的Matlab实现

往返式全覆盖路径规划:基于改进A星与弓字形扫描的Matlab实现

简介:基于A星算法的改进往返式全覆盖路径规划Matlab源码与数据包,主要面向机器人、无人机及自动驾驶等需全图遍历与避障的应用场景,也适合路径规划初学者与研究者作为练习和改进基线。代码在二维栅格地图上建模,通过定义优先级运动规则从起始点开始遍历,利用A星算法逃离死角、寻找最近未覆盖节点,持续完成全局覆盖;其中算法一执行常规往返式全覆盖,算法二重点应对死角碰撞与障碍规避。压缩包共8个m文件,体积仅6KB,包含主程序main1.m、main2.m以及gn.m、hn.m、minInOpen.m等辅助函数,分别负责实际代价计算、启发成本估计和开放列表择优,模块划分清晰,便于按需修改。已有594人学习下载。配合运行可观察完整覆盖路径生成过程,并能灵活调整栅格地图的障碍物与起始点,快速验证不同场景下的改进效果,为后续拓展到三维空间或动态环境提供参考。

1. 往返式全覆盖路径规划为什么卡在A星:一个容易被忽略的起点

做路径规划的人都知道,A星(A*)是求解两点最短路径的经典方法,但一提到“全覆盖路径规划”,很多人第一反应是“把A星跑一遍就行”。真实情况是:A星默认是“点到点”寻路,它不关心机器人是否扫过整个工作区域。你要做的是清扫、消杀、植保这类任务时,缺的恰恰是“遍历所有可达格子”的算法。这个项目标题里的关键词“往返式全覆盖路径规划”指的就是让机器人像耕地一样来回扫过整个地图,而改进算法的核心是把A星从“找路”变成“找未清扫区域”,再结合往返式弓字形扫描,把重复路径压到最低。

这套Matlab源码加数据包适合正在做移动机器人全覆盖遍历、扫地机器人路径、无人机二维巡检或者喷漆路径规划的从业者和学生。你手里如果已经有一张栅格地图,想从“两点寻路”升级到“全图覆盖”,这篇文章会帮你把思路、参数和坑一次讲透。下面从原理开始,逐步落到代码怎么改、参数怎么调、数据怎么验证。

2. 往返式全覆盖路径规划的核心:从A星到弓字形扫描的改进逻辑

2.1 为什么直接套A星不行:点到点与遍历的“目标差异”

A星算法的标准输入是起点、终点和一个启发函数,它的目标是找到一条从起点到终点的代价最小路径。但全覆盖路径规划的目标是“用一条连续的路径遍历所有可通行栅格”,并且尽量减少转弯、减少重复覆盖。两者在目标函数上根本不同。A星只保证局部最优,它甚至可能为了绕开一个障碍物走出回头路,这在覆盖任务里会造成大量重复扫描。

在这套Matlab源码里,作者把A星的作用从“主路径生成器”改成了“连接器”:先用往返式弓字形扫描生成一条覆盖主路径,当遇到障碍物或者地图边界把路径打断时,再用改进后的A星算法把断点之间的“空白区域”连接起来。这个思路很朴素,但很管用。我最初拿到代码时也疑惑:既然都用弓字形了,还要A星干嘛?后来跑了一遍才发现,单纯弓字形在地图有凹形障碍物时会产生“死区”,需要A星做区域间的跳转,否则覆盖率会掉到90%以下。

2.2 往返式(弓字形)扫描的数学模型:方向交替与行距设定

往返式全覆盖路径规划的经典做法是:把栅格地图按行(或列)划分,机器人从地图一侧出发,沿当前行直行覆盖,到达边界或障碍物后转向下一行,方向交替,像写“弓”字。这种路径的优点是转弯次数少、路径规律,缺点是遇到障碍物时容易产生漏扫。改进算法要解决的第一个问题就是“行距”怎么定。

行距通常取机器人的作业宽度,但要加上一个重叠系数。在Matlab实现中,相关参数如下:

参数名推荐值说明
width(车辆宽度/作业宽度)根据地图分辨率换算,通常1~3个栅格决定单行覆盖的横向跨度
overlap(重叠率)0.1~0.2防止定位误差导致漏扫,代价是重复路径增加
step(行距)width * (1 - overlap)实际每次换行时偏移的栅格数

我一般会先把地图分辨率确认好:如果每个栅格是0.5米,机器人作业宽度是1米,那width=2(栅格),overlap取0.15,step = 2 * (1 - 0.15) = 1.7,在栅格地图里可以取整为2,表示每行之间隔一个栅格重叠一列。这个细节会影响最终的重复覆盖率和路径长度,代码里通常直接写成整数,但你需要知道这个换算关系。

2.3 改进的A星算法在哪里介入:分段连接与局部重规划

改进算法的关键代码段是connectRegions函数。它做的事情是:先把弓字形扫描生成的路径拆成多个“段”,每一段是一段连续的直线覆盖轨迹。当一段路径结束后,如果下一段路径的起点与当前终点不相邻,就需要调用改进的A星来寻找连接路径。改进点在于启发函数不再只考虑欧氏距离,而是加入了“转向代价”和“未覆盖区域惩罚”。

function [connPath, cost] = connectRegions(startPt, goalPt, map, visitedMap, wTurn, wUnvisited) % startPt: 当前段终点 [x, y] % goalPt: 下一段起点 [x, y] % map: 栅格地图, 1-障碍物, 0-可通行 % visitedMap: 已经覆盖过的栅格标记, 1-已覆盖 % wTurn: 转向权重, 默认2.0 % wUnvisited: 未覆盖区域吸引权重, 默认0.5 % 启发函数中使用转向代价和未覆盖区域信息 heuristicFcn = @(node) abs(node.x - goalPt(1)) + abs(node.y - goalPt(2)) ... + wTurn * node.dirChangeCount ... + wUnvisited * sum(visitedMap(max(1,node.y-1):min(size(visitedMap,1),node.y+1), ... max(1,node.x-1):min(size(visitedMap,2),node.x+1)), 'all'); % 调用带自定义启发函数的A星 connPath = astar_custom(startPt, goalPt, map, heuristicFcn); cost = length(connPath); end

这段代码的逻辑说明:heuristicFcn在传统A星曼哈顿距离的基础上,额外累加了两个惩罚项。第一个是wTurn * node.dirChangeCount,记录当前路径走到该节点时已经转了几次弯,转弯越多代价越高;第二个是wUnvisited * sum(visitedMap局部窗口),如果节点附近仍有大量未覆盖栅格,会适度增加代价,让连接路径尽量贴着已覆盖区域的边缘走,避免把未覆盖区域切成碎片。这样改进后的A星生成的连接路径既能最短到达目标,又不会制造太多新的“未覆盖孤岛”。

参数上,wTurn如果设得太大(比如大于5),A星会过度绕路去减少转弯,导致连接路径长度指数级增长;wUnvisited如果设成0,则退化回普通A星,只找最短连接路径。源码中的默认值2.0和0.5是作者在几种典型地图上试出来的,拿到你的地图上,建议先用默认值跑一遍,再看覆盖率曲线决定是否调整。

2.4 与传统全覆盖算法(如Boustrophedon分解)的对比

业内还有另一种拱形覆盖街算法叫Boustrophedon分解,它把地图按照障碍物的“关键点”竖向切分成多个小区域,每个区域内做弓字形覆盖,再通过连接路径穿起来。这个思路和本项目的往返式+改进A星很相似,但实现复杂度更高。本项目的做法是直接在原地图上整体做行扫描,遇到障碍物边界就把路径打断,然后用改进A星去连,省去了区域分解的步骤。

从效果上看,Boustrophedon分解在复杂障碍物地图上覆盖率更高,但需要精确计算关键点,参数多、调参难。这套源码的“往返式+改进A星”在简单仓库、大房间场景下非常够用,而且代码结构清晰,适合二次开发。如果你要做的是多楼层或高度不规则环境,建议再考虑其他方案,这个包的优势是“快速跑通、结果直观”,不是“算法顶配”。

3. Matlab实现:源码结构与关键函数拆解

3.1 文件清单与数据格式

拿到源码包后,先确认目录结构。典型文件如下:

文件/目录作用
main.m主程序入口,设置地图、起点、参数,调用覆盖路径规划
astar_custom.m改进后的A星算法实现
regionScan.m往返式弓字形扫描生成器
connectRegions.m段间连接逻辑,调用改进A星
plotPath.m路径可视化与覆盖率统计
maps/map1.mat测试地图数据,例如20x20栅格地图
results/输出路径图像与覆盖率报告

地图数据是Matlab的.mat文件,里面通常有一个二维矩阵,元素为0(可通行)、1(障碍物)。有的版本还包含起点坐标和作业宽度参数。打开后可以用imagesc(map)直接显示。我建议先用load('maps/map1.mat')看一下变量名,因为不同作者定义的变量名可能不一样,有的是map,有的是myMap。

3.2 主程序入口的参数设置与调用流程

main.m是整个流程的起点,它做四件事:加载地图、初始化参数、调用路径生成、输出结果。下面是一个精简后的框架:

clear; clc; close all; % 加载地图 load('maps/map1.mat', 'map'); startPt = [2, 2]; % 起点坐标(x,y) width = 2; % 作业宽度(栅格数) overlap = 0.15; % 重叠率 wTurn = 2.0; % 转向权重 wUnvisited = 0.5; % 未覆盖区域吸引权重 % 生成覆盖路径 [fullPath, coverageMap] = generateCoveragePath(map, startPt, width, overlap, wTurn, wUnvisited); % 统计覆盖率 totalFree = sum(map(:) == 0); covered = sum(coverageMap(:) == 1); coverageRate = covered / totalFree * 100; fprintf('覆盖率:%.2f%%\n', coverageRate); % 可视化 plotPath(map, fullPath, coverageMap);

参数说明:startPt是机器人开始覆盖的位置,一般设在地图左上角或左下角,不要选在障碍物内部。width和overlap决定行距,前面已经说过。wTurn和wUnvisited是改进A星的权重,直接影响连接路径形状。运行完会打印覆盖率,并弹出路径图像。如果你看到的覆盖率低于95%,优先检查width是否比实际地图的“窄通道”宽度还大,那会导致无法通过某些1格宽走廊,机器人会被困在局部。

3.3 弓字形扫描生成器:regionScan的实现要点

regionScan负责生成主覆盖路径。它的核心逻辑是:从起点开始,沿当前方向(比如向右)逐行扫描,遇到障碍物或地图边界就回退一格,转向下一行继续。这里的关键是“转向点的记录”。

function [segmentList, visitedMap] = regionScan(map, startPt, step) % 初始化 visitedMap = zeros(size(map)); x = startPt(1); y = startPt(2); dir = 1; % 1:向右, -1:向左 segmentList = {}; while y <= size(map, 1) % 记录当前段起点 segStart = [x, y]; % 沿当前方向扫描到障碍物前 while true nx = x + dir; if nx < 1 || nx > size(map, 2) || map(y, nx) == 1 break; % 到达边界或障碍物 end visitedMap(y, x) = 1; x = nx; end visitedMap(y, x) = 1; % 保存当前段 segmentList{end+1} = [segStart; x, y]; %#ok<AGROW> % 转向下一行 dir = -dir; y = y + step; end end

这段代码主要说明“段”的构成逻辑,实际源码中还会处理地图不足一行时的残段和上下边界检查。逻辑说明:每完成一段直线扫描,就把起点和终点存到segmentList里。step是行距,决定换行时y方向偏移多少。注意,这里没有处理“当前行扫描一半被障碍物打断”的情况,真实代码会额外判断是否需要提前转向,否则容易漏扫障碍物下方的区域。

我测试时发现一个常见问题:step如果设成奇数,而地图宽度是偶数,最后一行可能只剩几个栅格,导致覆盖路径断断续续。解决办法是把step取为偶数,或者在地图底部增加一行虚拟行,代码里一般会有if y > size(map,1), y = size(map,1); end的钳位处理,但你要确认用的版本有这个分支。

3.4 改进A星的具体实现差异

astar_custom区别于普通A星的地方在于它接受外部传入的heuristicFcn函数句柄,这样可以在不修改A星搜索主结构的情况下,换掉启发函数。节省空间起见,这里不贴完整A星代码,只说几个差异点。

传统A星的open list按照f = g + h排序,g是从起点到当前节点的实际代价,h是预估代价。改进版把h替换成heuristicFcn返回的值,同时g里加入了转弯次数累计代价。也就是说,每次节点扩展时除了计算移动距离,还要判断当前方向与父节点方向的差异,若改变方向则增加wTurn * 1的代价。这个改动让A星在寻找连接路径时更倾向于“走直线”,避免连接路径在地图上画出折线影响后续覆盖。

另外,open list的优先级队列在Matlab中通常用min-heap或者普通数组加sort实现。源码为了易读性可能用了简单数组,导致节点数量大时速度偏慢。如果你要跑100x100以上的地图,建议把open list改成containers.Map或Java优先级队列。

3.5 路径可视化与结果输出

plotPath不仅画路径,还会把覆盖区域用不同颜色标记。常见输出图有三层:灰色表示障碍物、浅色表示已覆盖区域、蓝色线条表示真实路径。运行结束后保存到results/目录,文件名带时间戳。你可以利用这个可视化的覆盖图来判断漏扫区域集中在哪。我一般会额外输出一张“覆盖次数图”,即每个栅格被经过了几次。这样能直观看到重复覆盖热点,判断overlap是不是设得过大。

4. 避坑指南:覆盖率、重复路径和边界处理的常见问题

4.1 现象:覆盖率始终低于90%,地图角落有整片未覆盖

原因分析:弓字形扫描的方向只在水平方向交替,没有垂直方向上的“换行”策略。如果地图是矩形但起点不在左上角,或者障碍物把地图切割成多个独立区域,扫描路径会被障碍物截断,切断了角落区域的进入通道。而connectRegions只连接“可通行”区域之间的断点,当两个区域之间只有狭窄的1格通道时,width设太大导致A星认为通道不可通过(因为A星沿栅格中心走格子,宽度大于1格时不会选择穿窄缝)。

解决方法:先把width降到1,验证地图最窄通道到底有几个栅格宽。如果最窄通道只有1格,那么请把width保持为1,或者修改A星的邻居扩展规则,让路径可以贴着障碍物边缘走。另外,检查起点是否位于最大连通区域内,如果起点被孤立在小区域,覆盖率自然上不去。

4.2 现象:路径来回重复次数太多,耗时比预计长2倍

原因分析:overlap设置过大,比如0.5,导致每行之间有一半区域被重复扫描,整个路径长度接近理论最短值的1.5倍以上。还有一种可能是改进A星的wUnvisited设得太高,使得连接路径故意绕开未覆盖区域,绕出很远的距离。

解决方法:把overlap降到0.1~0.2,观察覆盖率是否仍然达到95%以上。同时把wUnvisited降到0.3以下,跑一次对比路径总长度。你会发现覆盖率略有下降(比如从99%降到98%),但路径长度能缩短20%以上。在工程上,如果机器人的定位精度足够高,优先保证覆盖率,把重叠率留小。

4.3 现象:A星连接路径会“撞墙”,经过障碍物内部

原因分析:改进A星的邻居扩展没有检查逗留点是否在障碍物内部。常见错误是地图矩阵是以(y,x)形式索引,而坐标是按(x,y)传入,导致行列颠倒。比如用map(y, nx)时,y是行索引,x是列索引,如果你写成map(nx, y),就会把障碍物判断完全反过来。

解决方法:在AStar扩展节点时,强制加一条断言:

if map(node.y, node.x) == 1 error('A星节点进入障碍物!检查坐标索引顺序'); end

也可以在生成地图时画出坐标轴,把矩阵索引和物理坐标的关系搞清楚。另一个常见原因是地图边缘处理:A星允许的坐标范围是1到size(map,1),如果目标点正好在边界上,而代码里用了x <= size(map,2)-1,会导致目标点不可达,路径绕回起点。

4.4 现象:Matlab运行时间长,大地图卡顿

原因分析:源码用了多层for循环和数组拼接,比如segmentList{end+1}不断增长,在Matlab中非常慢。另外,A星的open list如果用sort排序,每次扩展都全量排序,时间复杂度O(n^2 log n)。

解决方法:把预分配数组改成cell(1, 1000)然后索引赋值,避免动态扩容。A星部分改用二叉堆Java对象,或者用minimumSpanningTree思路降低节点扩展数。这个项目是教学演示向,性能优化不是重点,但如果你要跑200x200的地图,建议把regionScan中的while循环改成矩阵操作,比如用diff找障碍物边界,一次性生成整行路径。

4.5 现象:两次运行结果不完全一致,随机起点导致不同路径

原因分析:源码中可能用了randperm随机选择起点或随机初始化地图,或者A星中搜索邻居的顺序不固定,导致在同地图上出现多种结果。

解决方法:在main.m开头固定随机种子:rng(2024);。另外,检查是否在代码的某个位置调用了rand,如果只是路径生成,不应当有随机性。如果地图数据是从map.mat加载,那结果应该确定,但为了可复现性,建议在输出文件名中加入地图名和参数列表。

5. 进阶:如何验证改进效果并调出一份可用的路径

5.1 建立你自己的性能基准:覆盖率和重复率的双指标

我拿到任何全覆盖规划代码,第一件事不是看路径长与短,而是先算两个数:覆盖率(coveredFree / totalFree)和重复率(总路径长度超出理论最短覆盖路径长度的百分比)。理论最短覆盖路径长度约等于“可通行栅格数 / width”,这是理想状态。用总路径长度除以这个理想值,得到“重复系数”。改进版算法在简单地图上重复系数应该低于1.3,复杂地图低于1.6。如果重复系数大于2,说明参数或算法存在严重问题。

建议写一个自动验证脚本,把地图列表和参数网格化,跑出三列数据:地图名、覆盖率、路径长度。然后用表格对比。我在复现这个项目时,用map1.mat得到覆盖率98.5%,重复系数1.28,效果符合预期。你可以把结果导出来,作为后续调参的基准。

5.2 参数敏感性分析:哪些参数值得精调

在这个算法中,wTurn、wUnvisited、overlap是三个影响最大的参数。wTurn影响连接路径的转弯次数,值大会导致连接距离拉长,但好处是后续覆盖时不用频繁转向;wUnvisited影响路径对未覆盖区域的“引导”程度,值太大容易绕路,值太小则可能把未覆盖区域切成碎片,导致后续又得回头补扫。

我习惯用网格搜索:wTurn取[0.5, 1, 2, 4],wUnvisited取[0, 0.3, 0.5, 0.8],overlap取[0.05, 0.1, 0.15, 0.2]。跑完64组组合,用帕累托前沿选一组覆盖率>97%、路径长度最短的参数组合。在常见20x20地图上,最优组合往往是wTurn=2, wUnvisited=0.3, overlap=0.1。但换了地图就变,所以不要迷信默认参数。

5.3 引入“边界收缩策略”进一步降低漏扫

如果你的地图边界不规则,比如有锯齿状边缘,往返式扫描的最后一列容易在边界处留出楔形漏扫区域。一个简单有效的技巧是:扫描完成后,把覆盖地图与原始地图按“横向、纵向”分别做一次膨胀补充。具体做法是:找到所有未覆盖且与已覆盖区域相邻的栅格,将距离最近的已覆盖栅格作为下一段路径起点,再用改进A星补扫该小区域。

function [fullPath, finalMap] = patchGaps(map, visitedMap, pathList, wTurn, wUnvisited) % 找出未覆盖但紧邻已覆盖的栅格 [uncoveredY, uncoveredX] = find(map == 0 & visitedMap == 0); % 对每个未覆盖栅格,计算与最近已覆盖栅格的距离 % 按距离排序,逐个生成补扫路径 for i = 1:length(uncoveredX) % 找到最近已覆盖点 coveredIdx = find(visitedMap(:) == 1); [cy, cx] = ind2sub(size(visitedMap), coveredIdx); dist = sqrt((cx - uncoveredX(i)).^2 + (cy - uncoveredY(i)).^2); [~, minIdx] = min(dist); targetPt = [uncoveredX(i), uncoveredY(i)]; startPt = [cx(minIdx), cy(minIdx)]; % 用改进A星生成补偿路径 patchPath = connectRegions(startPt, targetPt, map, visitedMap, wTurn, wUnvisited); pathList = [pathList; patchPath]; %#ok<AGROW> % 标记补偿路径覆盖过的栅格 for j = 1:length(patchPath) visitedMap(patchPath(j,2), patchPath(j,1)) = 1; end end fullPath = pathList; finalMap = visitedMap; end

这段代码的逻辑说明:它先把所有未覆盖栅格找出来,逐个找最近的已覆盖栅格作为起点,再用改进A星把机器人引过去。这里有一个性能隐患:如果未覆盖栅格数量多,两重循环耗时明显。我实际测试时,20x20地图有十几个漏扫格,耗时还能接受,但100x100地图就要跑几十秒。更高效的做法是对整个图像做距离变换(bwdist),然后按距离从大到小连接。不过这里的朴实现版本胜在好理解,方便你验证算法行为。

5.4 验证改进效果时的几个习惯

第一,每次跑完保存路径数据,不要把path变量直接覆盖,因为你需要对比不同参数下的结果。第二,把覆盖率、路径长度、转弯次数三个指标同时打印出来,不要只看覆盖率。第三,如果发现覆盖率提高了但路径长度暴增,多半是wUnvisited过大的副作用,这时候优先降低权重而不是调整别的。第四,用同一张地图对比普通A星全覆盖(把A星逐点寻路串起来)和改进版算法,你会看到重复系数差异明显。

从那以后,我每次拿这类全覆盖源码,都会强制自己先跑一遍普通A星作为对照组,再用参数网格测改进算法,最后补扫边角。整个流程跑下来,源码值不值得用、参数该往哪个方向调,心里基本有数。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表