
简介基于Dijkstra的障碍物规避路线规划MATLAB仿真配套GUI界面与操作演示录像面向本科与硕士阶段需要开展路径规划、栅格地图建模或MATLAB界面开发教研工作的读者。压缩包共3个文件其中fig文件保存GUI界面布局m文件为可运行的MATLAB主程序avi文件为操作演示录屏整体仅2.84MB结构精简、聚焦核心功能。源码基于MATLAB 2021a编写通过GUI可生成不同尺寸的栅格地图直观演示从起点到终点绕过障碍物的最优路径搜索过程avi录像完整录制了从打开界面、设置地图到运行得到仿真结果的操作流程便于对照复现。已有414人学习使用适合快速理解Dijkstra算法在移动机器人路径规划中的具体实现也可作为课程设计或毕业设计的参考基础。无论是验证算法思路还是学习MATLAB界面编程都能从中获得直接可用的参考。1. 为什么Dijkstra是障碍物规避中值得先跑的路线规划算法在静态栅格地图上做障碍物规避很多人一上来就写A*却忽略了一个事实如果地图更新慢、路径可以离线计算Dijkstra的“无启发式”反而能避开设计估值函数的坑并且保证每一步扩展的都是当前代价最小的节点。A_GUI.m 和 A_GUI.fig 就是这样一个教学上很好用的Dijkstra仿真你可以在MATLAB里生成不同大小的随机栅格地图点击按钮就能看到最短路径从起点向终点“推进”的完整过程。特别适合毕设中需要把算法跑通、再对比实验的场景。2. 栅格地图建模与Dijkstra核心算法在MATLAB中的实现2.1 栅格地图的数据结构与障碍物生成栅格地图的本质就是一个二维矩阵每个格子要么可通行要么是障碍。项目里“生成不同大小地图”的需求落在代码上就是控制矩阵的行列数再按一定密度把格子置为障碍。常见做法是让grid(r,c)1表示障碍0表示可通行同时把最外圈留成空白避免起点或终点直接落在障碍物里无法规划。function grid randomGrid(rows, cols, density) grid false(rows, cols); % 中间区域随机撒障碍 grid(2:rows-1, 2:cols-1) rand(rows-2, cols-2) density; % 强制起点和终点可通行 grid(1, 1) 0; grid(rows, cols) 0; endrand(rows-2, cols-2) density生成逻辑矩阵true的位置就是障碍。density一般控制在 0.1 到 0.5 之间超过 0.5 后随机地图大概率会出现不可达区域。外层留一圈还有一个好处Dijkstra在扩展边界时不需要额外判断是否越界减少数组索引错误。项目中GUI读取用户输入的行列数和滑块密度后通常就是用类似上面的函数生成handles.grid再交给规划函数使用。2.2 Dijkstra邻域扩展与最短路径回溯Dijkstra在栅格上的核心是维护三个数组距离表dist、访问标记visited、父节点表anc。这里我给出一种适合MATLAB教学的实现方式8邻域方向按代价区分正交方向步长为1对角方向为 \( \sqrt{2} \)。这样得到的路径长度更贴近真实距离而不是简单地把每个格子都算成1。function [path, cost] dijkstra_plan(grid, start, goal) [rows, cols] size(grid); dist inf(rows, cols); visited false(rows, cols); anc zeros(rows, cols, 2); % 记录父节点坐标 dist(start(1), start(2)) 0; dirs [-1 -1; -1 0; -1 1; 0 -1; 0 1; 1 -1; 1 0; 1 1]; path []; while true minDist inf; cur []; % 找到未访问节点中距离最小的一个 for r 1:rows for c 1:cols if ~visited(r, c) dist(r, c) minDist minDist dist(r, c); cur [r, c]; end end end if isempty(cur) || isequal(cur, goal) break; end visited(cur(1), cur(2)) true; for k 1:size(dirs, 1) nr cur(1) dirs(k, 1); nc cur(2) dirs(k, 2); if nr 1 || nr rows || nc 1 || nc cols continue; end if grid(nr, nc) 1 || visited(nr, nc) continue; end if dirs(k, 1) 0 || dirs(k, 2) 0 stepCost 1; else stepCost sqrt(2); end if dist(cur(1), cur(2)) stepCost dist(nr, nc) dist(nr, nc) dist(cur(1), cur(2)) stepCost; anc(nr, nc, 1) cur(1); anc(nr, nc, 2) cur(2); end end end if isempty(cur) || ~isequal(cur, goal) cost inf; return; end % 从终点回溯到起点 path goal; cur goal; while ~isequal(cur, start) pr anc(cur(1), cur(2), 1); pc anc(cur(1), cur(2), 2); if pr 0 || pc 0 path []; cost inf; return; end cur [pr, pc]; path [cur; path]; end cost dist(goal(1), goal(2)); end这里dirs的每一行代表一个相邻方向从左上角开始顺时针排列。cur的格式统一为[row, col]避免在二维数组里用线性索引时把行和列搞混。anc是一个三维数组anc(r,c,1)存父节点的行号anc(r,c,2)存父节点的列号回溯时只要不断跳到父节点就能从终点一路拿到起点。步长代价的分配可以通过下面这张表直观看出方向行增量列增量步长代价上-101下101左0-11右011左上-1-1sqrt(2)右上-11sqrt(2)左下1-1sqrt(2)右下11sqrt(2)如果希望移动代价更简单可以把所有步长都改成1路径会倾向于绕路如果希望禁止从墙角斜穿过去需要在进入对角方向前额外检查相邻的正交格子是否都是可通行的。这个细节在第四章的排错部分会再展开。2.3 路径规划函数与GUI的数据衔接上面的函数不关心界面只负责输入输出这样在GUI回调里调用时就很干净。start和goal在默认情况下取地图左上角和右下角也就是[1,1]和[rows,cols]。GUI里会先把这两个值存入handles结构体再传给dijkstra_plan执行。handles.grid randomGrid(rows, cols, density); handles.start [1, 1]; handles.goal [rows, cols]; handles.path []; guidata(hObject, handles);用guidata(hObject, handles)保存数据而不是用全局变量是GUIDE回调之间共享数据最标准的做法。后面生成地图按钮里修改的handles.grid路径规划按钮里直接就能读出来不会出现变量覆盖问题。3. 基于GUIDE的GUI界面设计与回调逻辑3.1 A_GUI.fig与A_GUI.m的协作关系项目里的A_GUI.fig是GUIDE保存的界面文件A_GUI.m是自动生成的逻辑代码。.fig里只存放控件的位置、颜色、Tag属性真正处理鼠标点击的代码都在.m文件里。两个文件必须同名且放在同一目录否则运行GUI时会出现找不到回调函数的问题。一个完整的Dijkstra路线规划GUI通常包含下面这些控件Tag名控件类型作用editRowsEdit Text输入地图行数editColsEdit Text输入地图列数sliderDensitySlider调整障碍物密度范围0~0.6btnGeneratePush Button生成随机栅格地图btnPlanPush Button运行Dijkstra并绘制路径axesMapAxes显示栅格地图和规划结果txtStatusText显示路径长度和计算耗时在GUIDE里右键每个控件把Tag改成上面这些名字保存后MATLAB会自动在.m文件里生成对应的空回调函数比如btnGenerate_Callback。这里的核心是每个回调函数的第一行都是handles guidata(hObject)操作完后再用guidata(hObject, handles)把更新后的结构体写回去。3.2 生成地图按钮从随机矩阵到可视化生成地图按钮要做两件事根据控件输入创建栅格矩阵然后把矩阵画到axesMap上。这里有三个容易出错的地方滑块读出来的是浮点数要先归一化imshow默认把白色当成1、黑色当成0障碍是1时显示成白色会非常反直觉坐标轴行列方向与plot的 xy 方向需要对齐。function btnGenerate_Callback(hObject, eventdata, handles) rows round(str2double(get(handles.editRows, String))); cols round(str2double(get(handles.editCols, String))); density get(handles.sliderDensity, Value); if isnan(rows) || isnan(cols) || rows 5 || cols 5 set(handles.txtStatus, String, 行数列数需为不小于5的整数); return; end handles.grid randomGrid(rows, cols, density); handles.start [1, 1]; handles.goal [rows, cols]; handles.path []; guidata(hObject, handles); imshow(~handles.grid, Parent, handles.axesMap); colormap(handles.axesMap, [1 1 1; 0 0 0]); % 0显示白1显示黑 hold(handles.axesMap, on); plot(handles.axesMap, handles.start(2), handles.start(1), rs, MarkerSize, 10, LineWidth, 2); plot(handles.axesMap, handles.goal(2), handles.goal(1), g^, MarkerSize, 10, LineWidth, 2); hold(handles.axesMap, off); endimshow(~handles.grid)把可通行区域变成白色障碍变成黑色。colormap接收一个两行的颜色矩阵第一行对应值0第二行对应值1这样地图看起来就是白底黑块符合大多数人看栅格图的直觉。在plot里第一个参数是 x 轴坐标所以传start(2)也就是列号第二个参数传start(1)行号。因为imshow的显示区域里行号向下增加正好对应 y 轴正向不需要额外翻转坐标轴。如果后面路径画错位了优先检查这里是不是行和列写反了。3.3 路径规划按钮调用Dijkstra并叠加路径btnPlan的回调要处理三种情况没有生成地图就点规划、路径不可达、正常找到路径。正常找到时把path的列号作为 x行号作为 y画一条蓝色折线覆盖在地图上。function btnPlan_Callback(hObject, eventdata, handles) if ~isfield(handles, grid) set(handles.txtStatus, String, 请先生成地图); return; end tic; [path, cost] dijkstra_plan(handles.grid, handles.start, handles.goal); elapsed toc; if isempty(path) set(handles.txtStatus, String, 路径不可达请降低障碍密度); return; end hold(handles.axesMap, on); plot(handles.axesMap, path(:,2), path(:,1), b-, LineWidth, 2); hold(handles.axesMap, off); set(handles.txtStatus, String, sprintf(长度: %.2f 耗时: %.3fs, cost, elapsed)); handles.path path; guidata(hObject, handles); endtic和toc是MATLAB自带的计时工具放在dijkstra_plan外面测得是从调用到返回的总时间。对于 100x100 的栅格矩阵这套双层循环实现的Dijkstra通常能在 1 秒内跑完如果超过 3 秒就要考虑地图里是否有大量不可达区域导致搜索遍历了全部节点。需要注意的是绘图前如果没有调用hold on新的plot会把之前的起点终点标记擦掉。这里每次规划前都重新hold on所以重复点击“路径规划”不会出现旧路径残留也不需要手动cla清空坐标轴。4. 仿真录像复现与参数调优地图大小、障碍密度和搜索代价4.1 按录像完整跑通一次仿真项目附带的“操作录像0032.avi”展示的就是从打开GUI到生成路径的完整过程。如果你不想看录像按下面步骤操作也能得到一致结果把A_GUI.fig和A_GUI.m放到同一目录在MATLAB中双击.fig文件打开界面。命令行窗口切换到该目录点击GUIDE工具栏上的绿色运行按钮。在editRows输入30editCols输入40把滑块拖到中间位置。点击“生成地图”等待栅格随机地图显示。点击“路径规划”观察蓝色路径是否连通起点和终点。如果双击.fig后只有一个空窗口没有显示任何控件通常是MATLAB版本里Java Swing组件加载失败。重开一次MATLAB或者在命令行输入guide后选择打开现有FIG一般能解决。4.2 地图大小与障碍密度对规划结果的影响栅格地图的行列数和障碍密度共同决定Dijkstra的搜索空间。搜索空间是rows * cols所以地图从 30x30 增加到 100x100节点数翻了十倍而双层循环找最小距离节点的耗时会更明显地上升。下面是一组适合课程演示的参考参数参数建议范围说明行数20 到 80超过100后开始卡顿适合小规模演示列数20 到 80与行数保持一致可避免路径过度水平或垂直障碍密度0.1 到 0.4密度超过0.5容易出现不可达区域起点坐标默认(1,1)建议留在外圈空白区域终点坐标默认(rows,cols)与起点形成对角线路径长度更有说服力同样的障碍密度不同随机种子生成的地图难度完全不同。比如密度 0.35 时有的地图用120步走完有的地图直接被障碍墙隔成两半。课程设计里如果要做多组对比实验建议固定随机种子比如在randomGrid调用前执行rng(42)保证每次结果可复现。4.3 常见报错与排查这类项目里最常遇到的问题并不是算法本身写错而是坐标索引和版本兼容问题。我在实测过程中遇到过下面这些情况现象原因分析处理方式提示索引超出矩阵维度起点终点坐标与地图维度不一致打印size(grid)检查 start/goal路径明显穿过黑色障碍块8邻域对角移动时没有检查斜穿墙角在对角移动前判断相邻两个正交格子点击规划后没有任何反应没有先生成地图或回调中读取了空handles.grid加isfield判断打开.fig后提示版本过高旧版本MATLAB无法读取高版本保存的FIG在R2021a或更高版本中打开Undefined function dijkstra_plan自定义函数未保存成同名文件或不在当前路径确认函数名与文件一致检查当前路径斜穿墙角的问题是我单独的。比如从格子(r,c)走到(r1,c1)如果只有(r1,c1)是空格而(r1,c)和(r,c1)都是障碍那路径实际上是从两面墙的尖角滑过去的视觉上很丑也不符合真实移动体的转弯约束。修正办法是在对角扩展条件里增加两个邻域的检查if dirs(k,1) ~ 0 dirs(k,2) ~ 0 if grid(cur(1)dirs(k,1), cur(2)) 1 || grid(cur(1), cur(2)dirs(k,2)) 1 continue; end end加这个判断后路径会更保守长度代价也会略大但看起来会比斜穿墙角可信得多。代价参数这里可以统一调整把sqrt(2)改成1.5或2等效于惩罚对角移动规划出来的路径会优先走正方向。5. 验证Dijkstra路径最优性的三个自查技巧拿到路径后第一步要确认它真的是最短路径而不是“看起来像”。一个最简单有效的校核办法是用切比雪夫距离计算下界因为每个节点到相邻节点的最小移动代价是 1所以任意两点之间的最短路径代价不可能小于max(abs(endRow - startRow), abs(endCol - startCol)) * 1。如果返回的cost比这个下界还小说明dist更新逻辑有bug。更进一步的验证是手动把起点和终点放在同一行中间一路通畅这时Dijkstra的路径代价应该正好等于列数差。如果结果多出sqrt(2)或者少了一段那就要检查回溯时path的头部拼接是否正确。我在项目调试时习惯把dist矩阵也返回然后在命令行里clc后打印地图、路径和距离表几行代码就能看出是哪一步有问题。第二个技巧是给算法加一个观察模式。在dijkstra_plan的while循环里每访问完 30 个节点就执行一次drawnow limitrate屏幕上能看到灰色区域从起点一圈圈向外扩散最终覆盖到终点。这个动画对教学演示特别有用也能直观判断搜索是不是被障碍墙引流到了错误方向。原项目的GUI其实很适合扩展这个功能只要在axesMap上叠加显示visited矩阵即可。第三个技巧是把Dijkstra原地扩展成A*。因为Dijkstra在每次循环里取的是dist最小的节点A* 取的是dist heuristic最小的节点只需要在我们给出的dijkstra_plan函数里增加一个hScore数组把选择条件从dist(r,c) minDist改成dist(r,c) hScore(r,c) minFhScore inf(rows, cols); for r 1:rows for c 1:cols if grid(r,c) 0 hScore(r,c) sqrt((r-goal(1))^2 (c-goal(2))^2); end end end这样修改后优先扩展的是“按照欧几里得距离更接近终点”的节点搜索速度会明显提升但代价是失去了Dijkstra对每个节点距离的全局最优保证。把两份代码放在同一个项目里通过一个标志位切换就能同时满足算法对比实验和真实使用需求。本文还有配套的精品资源点击获取