☰
MATLAB栅格地图Dijkstra路径规划:原理、实现与避坑指南
2026/10/14 1:27:48 网站建设 项目流程

简介:基于栅格地图的迪杰斯特拉(Dijkstra)算法路径规划MATLAB实现,面向机器人导航、游戏AI及地理信息系统等领域的开发者与学习者,解决在离散化地图上寻找最短路径的典型问题。压缩包共6个文件,以5个MATLAB脚本为主,另有1张示意图,总大小56KB。脚本涵盖地图矩阵构建、障碍物设置、优先队列排序、邻域遍历及路径回溯等核心模块,并附可直接运行的示例工程。已有5028人浏览学习,适用于课程设计或算法入门。通过该实现可掌握栅格地图建模、优先队列更新机制、最短路径回溯等关键思路,清晰理解迪杰斯特拉算法从初始化到目标收敛的完整流程,便于在此基础上扩展双向搜索或A*算法,是一份轻量而实用的路径规划参考代码,适合初学者对照学习与实际调试。

1. 栅格地图Dijkstra路径规划:比A*慢但最稳的“保底”选择

如果你要做移动机器人或AGV调度,第一版路径规划的“保底”算法大概率就是栅格地图上的Dijkstra。它没有启发式函数的加速,没有A*那种“大概率更快”的玄学,但只要你把代价矩阵构建正确,它给你的就是严格最短路径——这是全局路径规划里最硬的结论。用MATLAB实现这套东西,最大的好处是调试成本低:你能直接看到栅格、障碍、代价和路径四个层次叠加在同一张图上。这篇文章就围绕MATLAB栅格Dijkstra展开,从地图建模、邻域代价、参数设置到常见的翻车点,按我实际调试过的顺序完整走一遍,适合正在做课程设计、机器人比赛调度模块或想快速验证全局路径算法的开发者。

2. 栅格与代价模型:先搞清楚路径是在“什么图”上算的

2.1 栅格地图的表达:0/1逻辑矩阵与障碍膨胀

在MATLAB里,栅格地图最常见的表示是一个二维逻辑矩阵map,其中0代表可通行区域,1代表障碍物。矩阵的行列索引天然对应平面坐标,但需要注意:map(row, col)的第row个维度对应y方向,第col个维度对应x方向,这在后面画图时特别容易踩坑。

我一般会在生成地图后先做一步“障碍膨胀”。原因很实际:机器人不是质点,它的外轮廓半径在栅格地图上至少占1~2个格子。如果直接用原始0/1矩阵跑Dijkstra,规划出来的路径往往会贴着障碍物边缘走,实际执行时机器人稍微偏一点就会碰撞。膨胀的做法不复杂,标准工具是图像处理里的imdilate,结构元素用一个半径为inflateR的方形或圆形核:

% map: 0为可行, 1为障碍 inflateR = 2; % 膨胀半径,单位:栅格 se = strel('disk', inflateR, 0); % 圆形结构元,0表示不近似 mapInflated = imdilate(map, se); mapInflated = logical(mapInflated);

膨胀之后,mapInflated里为1的区域就是Dijkstra算法需要避开的“真实不可行区域”。我这里用strel('disk', ...)而不是strel('square', ...),原因是方形膨胀会把直角角落填成直角,生成的路径在拐角处容易出现不必要的阶梯状;圆形膨胀更接近机器人转弯半径的实际约束。inflateR这个参数直接决定路径离障碍的远近,调得太大路径会绕远甚至把窄通道堵死,调太小又起不到安全作用。经验值是两个栅格起步,具体看你的栅格分辨率。

2.2 Dijkstra核心机制:松弛扩展与先验代价

Dijkstra在栅格地图上做的事情,本质上是在一个带权图上做“从单源到所有节点的最短路径求解”。每个栅格是一个节点,相邻栅格之间有边,边的权值就是相邻移动的代价。算法每轮从当前未访问节点中取出距离起点最小的节点,然后尝试用“当前节点的距离 + 相邻边代价”去更新邻居节点的距离记录,这个操作叫松弛。

栅格场景下,“相邻”的定义决定了图的形状。最常用的是4邻域和8邻域两种。4邻域只允许上下左右移动,代价都是1;8邻域允许斜向移动,直线代价是1,斜线代价是√2。很多人在初始化代价矩阵时偷懒,8邻域里斜边也写1,这样算出来的路径会过度倾向走斜线,视觉上路径比实际最短路径“更短”,但放到真实机器人运动模型里是不成立的,因为斜向移动距离确实是1.414倍。所以代价矩阵的构造必须跟邻域类型严格对应:

% 8邻域代价: [dx, dy, cost] neighbors8 = [1, 0, 1; -1, 0, 1; 0, 1, 1; 0, -1, 1; ... 1, 1, sqrt(2); 1, -1, sqrt(2); -1, 1, sqrt(2); -1, -1, sqrt(2)];

这个neighbors8矩阵就是后面主循环的“移动规则表”。之所以强调它,是因为Dijkstra路径规划的几乎所有“看起来诡异”的结果,最终都能追溯到邻域定义或代价定义上。一个常见的反面教材是:地图里明明有一堵斜墙,算法却“穿墙而过”,查到最后发现是斜向移动的[1,1]方向没有被障碍判定拦住——运动规则和障碍碰撞检测没有同时更新。

2.3 为什么栅格场景优先选Dijkstra而不是A*

A确实更快,但我自己给AGV调度做基建时,第一版几乎都用Dijkstra。原因有三点:第一,A受到启发式函数质量的影响,启发式取得不恰当(比如栅格地图上用了不满足一致性的距离函数),得到的路径不保证全局最优;第二,Dijkstra不需要调启发式权重,唯一的输入是邻域规则和代价定义,出问题容易排查,对新手尤其友好;第三,在中小尺寸栅格地图(比如500×500以下)上,Dijkstra的搜索耗时通常在一秒到几秒量级,对离线生成路径或者预先计算路网的场景完全够用。

用一句话概括我的选型习惯:如果地图是一次性的(离线算路)、尺寸不算夸张、对“严格最短路径”有硬性要求,Dijkstra是比A更省心的选择。A的优势在动态地图、多目标搜索和大规模栅格上才体现得明显,那些场景下Dijkstra的扩展节点数量会膨胀得很难看。

3. MATLAB实现主流程:地图生成、主循环与可视化

3.1 地图与起终点定义:把数据准备好

这一节直接进入可复现代码。我用一个随机生成的栅格地图来演示完整流程,实际使用中你把map替换成自己的SLAM建图结果或CAD导出的2D栅格即可。

% 地图尺寸与障碍率 rows = 80; cols = 80; obsRate = 0.3; map = zeros(rows, cols); % 随机撒障碍,并保证起点终点周围不生成障碍 obsCount = round(rows * cols * obsRate); for k = 1:obsCount r = randi(rows); c = randi(cols); map(r, c) = 1; end % 膨胀 inflateR = 2; se = strel('disk', inflateR, 0); mapInflated = imdilate(map, se); mapInflated = logical(mapInflated); % 起点和终点坐标 [row, col],注意这里用行列表示 startNode = [5, 5]; % row=5, col=5 goalNode = [75, 70]; % row=75, col=70

这一段里有几个容易引起混乱的细节。首先,startNode和goalNode我用的是[row, col]顺序,和MATLAB矩阵索引一致;但后面用plot画路径时,横轴对应col,纵轴对应row,转换时别搞反。其次,随机撒障碍必须检查起点终点是否落在障碍物上,否则主循环会直接失败,我这里偷懒没写校验,实际工程里一定要写,后面避坑章节会专门讲。

obsRate=0.3对80×80的图来说障碍密度不算低,膨胀2格之后可行区域会被进一步压缩。如果窄通道比较多,膨胀半径建议回退到1,或者对地图做一次连通域分析,确认起点和终点在同一个连通域里,不然Dijkstra必然返回“无路径”。

3.2 Dijkstra主循环:扩展、松弛与路径回溯

这是整篇文章的核心段落,我直接贴出最简版本。为了可读性,我用简单的min查找代替二叉堆,地图在几百乘几百量级时速度完全可以接受;如果地图超过1000×1000,再换成优先队列实现。

function path = dijkstra_grid(map, startNode, goalNode) [rows, cols] = size(map); % 初始化距离矩阵与访问标记 dist = inf(rows, cols); visited = false(rows, cols); parent = zeros(rows, cols, 2); % 记录每个节点的父节点坐标 sr = startNode(1); sc = startNode(2); dist(sr, sc) = 0; % 8邻域与对应代价 dxy = [1,0; -1,0; 0,1; 0,-1; 1,1; 1,-1; -1,1; -1,-1]; cost = [1; 1; 1; 1; sqrt(2); sqrt(2); sqrt(2); sqrt(2)]; while true % 在未访问节点中找到距离最小的节点 minDist = inf; cr = -1; cc = -1; for r = 1:rows for c = 1:cols if ~visited(r,c) && dist(r,c) < minDist minDist = dist(r,c); cr = r; cc = c; end end end % 如果没有可扩展节点,或已经到终点,退出 if cr == -1 || (cr==goalNode(1) && cc==goalNode(2)) break; end visited(cr, cc) = true; % 对当前节点的8个邻域做松弛 for k = 1:size(dxy, 1) nr = cr + dxy(k, 1); nc = cc + dxy(k, 2); % 越界、障碍、已访问,跳过 if nr < 1 || nr > rows || nc < 1 || nc > cols, continue; end if map(nr, nc) == 1, continue; end if visited(nr, nc), continue; end newDist = dist(cr, cc) + cost(k); if newDist < dist(nr, nc) dist(nr, nc) = newDist; parent(nr, nc, 1) = cr; parent(nr, nc, 2) = cc; end end end % 回溯路径 path = []; cr = goalNode(1); cc = goalNode(2); if isinf(dist(cr, cc)) return; % 返回空path表示无解 end while ~(cr == startNode(1) && cc == startNode(2)) path = [path; cr, cc]; pr = parent(cr, cc, 1); pc = parent(cr, cc, 2); if pr == 0 && pc == 0, break; end cr = pr; cc = pc; end path = [path; startNode(1), startNode(2)]; path = flipud(path); % 从起点到终点排序 end

这个主循环的逻辑分四段。第一段,dist矩阵初始化成inf,visited全false,parent用来存路径。第二段,主循环里每次暴力扫描所有未访问节点找最小距离,这是“简单但正确”的做法,复杂度是O(V²)级别,80×80的图总共6400个节点,扫描一次几毫秒,完全可以接受。第三段,松弛操作里最关键的是“先跳过已访问节点”和“只更新比当前距离更小的邻居”,这两条保证了Dijkstra算法的单调性和最终收敛。第四段,从终点回溯parent链到起点,得到路径坐标序列。

这里有一个很小的细节值得注意:我判断算法结束用了两个条件——没有可扩展节点,或已经扩展到终点。当目标是“求起点到终点的最短路径”时,扩展到终点就可以提前退出,不需要跑完整张图。这个提前退出是网格场景下的标准优化,效果是搜索范围缩成一个以起点终点连线为中心的椭圆区域。

3.3 可视化:地图、路径与代价叠加显示

路径算出来之后,光看坐标数字没有意义,必须叠加到地图上检查。常见做法是pcolor或者imagesc显示栅格,再把路径点折线画上去:

figure; imagesc(~mapInflated); % 显示时反色,白色区域是可通行区 colormap(gray); hold on; if ~isempty(path) % 注意转换: 矩阵row->y, col->x plot(path(:,2), path(:,1), 'r-', 'LineWidth', 2); plot(startNode(2), startNode(1), 'go', 'MarkerSize', 8); plot(goalNode(2), goalNode(1), 'b^', 'MarkerSize', 8); end axis equal; axis tight;

这里path(:,2)是列坐标(即x),path(:,1)是行坐标(即y),放在plot里顺序必须写成(x, y)。很多人第一次跑通算法但画出来路径是歪的,多半就是这个坐标映射没有对齐。如果画完之后发现路径压着障碍走,先不要怀疑算法,先用hold on把mapInflated的轮廓叠上去,看看是不是膨胀半径的问题。

4. 参数边界与不可达处理:跑通只是第一步

4.1 关键参数表:分辨率、膨胀半径与邻域选择

栅格Dijkstra能用的参数不多,但每个参数都直接影响路径质量和算法耗时。我按照调试优先级整理了一张表,供参考:

参数默认建议值影响路径的方式调参信号
栅格分辨率0.05~0.1 m/格分辨率越高,路径越平滑但计算量指数增长路径阶梯感严重时提高分辨率
膨胀半径2格控制路径离障碍的最近距离路径贴障碍太近调大,窄通道被堵死调小
邻域类型8邻域决定路径能否斜穿和对角移动代价有非完整约束时改用4邻域或限制转角
斜向代价√2影响路径走向偏好路径过度走斜线时检查斜向代价是否被误设为1
障碍值1(不可通行)参与扩展判定,不参与代价累加无
提前终止扩展到终点即停止缩短搜索时间约30%~50%需要全图距离场时关闭

这个表里的“默认建议值”不是拍脑袋定的,而是从多轮调试经验里来的。栅格分辨率如果定成0.2m/格,机器人直径0.6m左右的话,地图上只有3格宽,膨胀半径2格一算,通道直接不可行,这时候要么降分辨率,要么把膨胀半径降到1并配合实际避障策略,二选一。

4.2 起点终点碰撞与不可达判定

Dijkstra跑完返回空路径,很多人第一反应是“算法写错了”,其实最常见的原因是起点或终点本身就在障碍物上,或者两者不在同一个连通域。

我的习惯是在算法外面套一层防御性检查,而不是在Dijkstra内部处理:

% 起点终点有效性检查 if mapInflated(startNode(1), startNode(2)) == 1 error('起点在障碍物内,请重新选择起点'); end if mapInflated(goalNode(1), goalNode(2)) == 1 error('终点在障碍物内,请重新选择终点'); end path = dijkstra_grid(mapInflated, startNode, goalNode); if isempty(path) disp('无可通行路径,尝试减小膨胀半径或更换起终点'); end

这段检查的意义有两个。第一,把“输入数据有问题”和“算法有问题”快速区分开,省去对着算法调试半天最后发现是地图数据本身的坑;第二,当路径为空时,给一句明确提示而不是甩一个空数组,后续接上层调度逻辑时更友好。我在实际项目中还遇到过一种情况:起点终点都没问题,但路径确实不存在,这通常是障碍物形状把起点和终点隔成了两个连通区域。排查方法是用MATLAB的bwlabel对可行区域做连通域标记,检查起点和终点是否落在同一个label里,这一步可以在算法运行前直接判定无解,省掉一次无意义的全图搜索。

4.3 大规模地图的性能优化:记忆函数与稀疏矩阵

前面第3章的主循环为了可读性用了双重循环扫描,这个实现在图尺寸超过500×500之后会明显变慢。一个简单的优化是改用稀疏邻接矩阵 +dijkstra相关的向量化实现,另一个实用技巧是换用MATLAB自带的graph对象构造稀疏图:

% 把栅格地图转为稀疏图对象 G = graph(); % 遍历所有可通行节点,按邻域规则加边 for r = 1:rows for c = 1:cols if map(r,c) == 1, continue; end idx = sub2ind([rows, cols], r, c); % 只加右、下、右下、左上四个方向避免重复 for k = 1:4 nr = r + dxy(k,1); nc = c + dxy(k,2); if nr<1 || nr>rows || nc<1 || nc>cols, continue; end if map(nr,nc)==1, continue; end nidx = sub2ind([rows, cols], nr, nc); G = addedge(G, idx, nidx, cost(k)); end end end % 用graph内置的shortestpath pathIdx = shortestpath(G, startIdx, goalIdx);

graph对象的好处是shortestpath内部使用优化过的Dijkstra实现,而且天然支持稀疏存储,内存占用远低于5×5的dist矩阵。这个写法适合地图尺寸大、重复调用次数多的场景,缺点是把栅格转图需要额外一次遍历。我的建议是:地图边长500以下用第3章的纯矩阵实现,超过500或者需要频繁重规划时转成graph对象。两者算出来的路径在“最短”意义上是严格一致的,区别只在扩展效率。

5. 避坑自查:Dijkstra栅格实现的五个典型翻车点

5.1 地图生成阶段的坑:坐标轴、膨胀与对角穿墙

坑1:坐标轴搞反,路径画出来方向不对

现象:算法返回的路径坐标看起来合理,画在地图上却整体旋转了90度,或路径和障碍的位置对不上。

原因:[row, col]与[x, y]的映射关系没有统一。MATLAB的imagesc显示矩阵时,行方向是纵轴,列方向是横轴;但很多人习惯把第一个分量当成x,直接把path(:,1)放到plot的横坐标。

解决:所有涉及坐标转换的地方统一用一套规则——读取矩阵用map(row, col),画图用plot(col, row),起终点定义也用[row, col]。我还会在画图前打印一次path(1,:),肉眼确认第一个点和起点坐标一致,避免低级错误。

坑2:膨胀半径过大,把窄通道直接堵死

现象:地图上明明有一条肉眼可见的通道,Dijkstra却返回无路径;或者路径绕了很远的路才到终点。

原因:膨胀半径设置超过了通道宽度的1/2,膨胀后的障碍区把通道完全填满。这个问题在随机生成的栅格地图里尤其常见,窄通道多,一膨胀全堵死。

解决:膨胀前先统计地图连通域,确认起点终点连通;膨胀后再次检查连通性。如果膨胀后不可达,尝试把膨胀半径减半,或者改用旋转的方形结构元(对角方向膨胀半径小于轴向),这样既能保安全又不至于堵死通道。

坑3:8邻域移动时“穿墙而过”

现象:路径在折角位置斜着穿过一个看起来不应该穿过的障碍角。

原因:邻域规则允许对角移动,但障碍判定只检查了目标节点是否可通行,没有检查斜向移动经过的两个轴向相邻栅格是否也可通行。事实上,从(1,1)斜走到(2,2)时,机器人会同时经过(1,2)和(2,1)两个格的边缘,这两个格如果有任意一个是障碍,就应该禁止这次斜向移动。

解决:在松弛循环里增加一个“拐角障碍检查”:

% 斜向移动时额外检查两个相邻格 if dxy(k,1) ~= 0 && dxy(k,2) ~= 0 if map(cr + dxy(k,1), cc) == 1 || map(cr, cc + dxy(k,2)) == 1 continue; % 拐角被堵,禁止斜穿 end end

这个检查非常关键,很多项目中的“穿墙”问题都是它导致的。加上之后路径会变得更保守,但也更符合真实机器人的移动能力。

5.2 算法运行阶段的坑:代价、访问标记与回溯中断

坑4:斜向代价误设为1,路径“走近路”失真

现象:路径上一串斜线,看起来比预期“直”很多,折算成实际移动距离也不合理。

原因:8邻域的斜向代价被直接复制了直线代价1,导致算法认为斜向移动和横向移动消耗一样,于是路径尽量走斜线。用欧氏距离衡量,真实距离多了42%,但在代价矩阵里却没有反映。

解决:把代价数组改成[1,1,1,1,sqrt(2),sqrt(2),sqrt(2),sqrt(2)],并且做一次路径长度验算:对路径上每两个相邻点累加实际距离,对比Dijkstra返回的总代价,两者应该一致。如果不一致,一定是代价定义和邻域规则没对齐。

坑5:回溯路径时死循环或路径断裂

现象:路径回溯到一半中断,返回的路径只到某个中间节点;或者代码卡住不退出。

原因:parent数组的新节点距离在松弛循环里更新了,但只更新了父节点坐标,没有考虑目标节点被重复更新覆盖的情况;另一个常见原因是起点到终点不可达时,parent链在某个位置为0,回溯循环没有正确判断跳出。

解决:回溯循环里我先判断isinf(dist(cr, cc)),不可达直接返回空路径;在循环体内再判断parent等于0的情况,防止越界。更稳妥的做法是用一个显式的“路径长度计数”作为循环上限,超过节点总数就强制跳出,避免极端情况下的死循环。

6. 验证与进阶:把Dijkstra的价值榨干

6.1 验证方法:用全图距离场检查路径合理性

Dijkstra跑通后,第一个要做的验证不是看路径起点到终点通不通,而是检查“全图距离场”是否符合直觉。具体做法是把第3章主循环里的提前终止去掉,让算法跑完整张图,得到dist矩阵,然后用imagesc(dist)可视化。距离场应该呈现以起点为中心的“波纹状扩散”,波纹是椭圆形的,椭圆长短轴比例对应直线代价和斜向代价的比值,这个细节能直观验证代价设置是否正确。如果波纹在某个方向突然变形或者出现异常的“凹陷”,多半是障碍物周边的代价更新有Bug。

这个验证思路我在多个模拟项目里用过。某次调试一个基于模糊栅格代价的地图版本,路径看起来合理但总觉得绕路,一查距离场发现某块区域的波纹明显被拉长,最终找到问题出在代价矩阵更新时漏掉了一列索引。距离场验证比肉眼检查路径可靠得多——它把整个算法的行为暴露在眼前,路径只是它的一个截面。

6.2 进阶技巧:双向Dijkstra与路径抽稀

如果追求更快的规划速度,可以上双向搜索:起点和终点同时执行Dijkstra,两边相遇时路径就是全局最短路径。这个技巧在栅格地图上实现并不复杂,维护两个dist矩阵和两个parent矩阵,每轮各扩展一个新的节点,当某节点在两边的距离记录都被更新过时,拼接两段路径即可。MATLAB实现中需要额外写一个“反向邻域”描述,逻辑上跟正向完全对称,这里不展开代码了,核心思想是把搜索空间从椭圆再砍掉一半左右,在300×300以上的地图上收益比较明显。

路径平滑则是另一回事。Dijkstra给出的路径本质上是栅格节点组成的折线,转折角度步进45度(8邻域)或90度(4邻域),直接给下位机使用会有明显的不连续。常见做法是路径抽稀,用“拉窗帘”算法把共线的中间节点删掉,然后把剩余节点用三次样条或贝塞尔曲线拟合。我自己的习惯是两步走:先抽稀,再平滑。抽稀保证了路径不会偏离原始最短路径太远,平滑保证了下位机跟踪时曲率连续。这个过程可以在MATLAB里用polyshape判断抽稀后的线段是否穿过障碍物来验证正确性。

6.3 一个容易忽略的收尾技巧

最后分享一个我自己的使用习惯:在把路径交付给下一个模块之前,强制做三件事——检查起点终点是否有障碍物碰撞,逐段检查折线是否穿过障碍栅格,以及把代价矩阵和距离场存档。很多次我都是靠存档的距离场快速复现问题,而不是重新跑一遍完整规划。从那张距离场里,我可以直接读出每个栅格离起点有多远,这比事后看一段路径能提供的信息多得多。从那以后,我每次写栅格路径规划代码都会强制走一遍“完整距离场生成 + 路径抽查 + 参数存档”的流程。这一套流程稳住了我在多次项目中的排错时间,希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询