第三章 路径规划基础:图搜索算法与栅格地图
各位同学,今天我们来聊聊路径规划里最基础、也最核心的一块——图搜索算法。说实话,我刚入行那会儿,觉得路径规划不就是找条路嘛,有啥难的?后来真上了项目才发现,这里面的坑,一个比一个深。
这一章,我会带着大家把Dijkstra和A*这两个经典算法掰开揉碎讲清楚。同时,栅格地图怎么建、路径怎么平滑,这些实战中躲不开的问题,咱们也一并解决。
3.1 栅格地图构建——先把世界数字化
做路径规划,第一步就是把环境变成计算机能理解的东西。栅格地图,说白了就是把空间切成一个个小格子。每个格子要么是“空地”,要么是“障碍物”。
我在项目中遇到过一个问题:栅格分辨率设多大合适?设小了,地图精细但计算量爆炸;设大了,计算快但路径可能穿墙而过。嗯,这里有个经验值——对于四旋翼飞行器,室内场景用0.1米,室外开阔场景用0.5米,基本够用。
栅格地图构建要点:
- 分辨率选择:权衡精度与计算量
- 障碍物膨胀:给飞行器留出安全距离
- 连通性判断:4邻域还是8邻域?
来看一段简单的栅格地图构建代码:
import numpy as np
def build_grid_map(width, height, resolution, obstacles):
"""
构建栅格地图
width, height: 实际空间尺寸(米)
resolution: 栅格分辨率(米/格)
obstacles: 障碍物坐标列表 [(x1,y1), (x2,y2), ...]
"""
grid_w = int(width / resolution)
grid_h = int(height / resolution)
grid = np.zeros((grid_w, grid_h), dtype=np.uint8)
# 标记障碍物
for ox, oy in obstacles:
gx = int(ox / resolution)
gy = int(oy / resolution)
if 0 <= gx < grid_w and 0 <= gy < grid_h:
grid[gx][gy] = 1 # 1表示障碍物
return grid
个人小技巧:建图时别忘了做障碍物膨胀。我曾经因为没做膨胀,规划出的路径贴着墙飞,结果飞行器撞上了凸出的门把手。膨胀半径至少是飞行器半径的一半。
3.2 Dijkstra算法——稳扎稳打的“老实人”
Dijkstra算法,你想想看,它就像个老实人——从起点开始,一层层往外扩,直到找到终点。它保证能找到最短路径,但代价是慢。
核心思想其实很简单:
- 维护一个“未访问节点”集合
- 每次从集合中取出距离起点最近的节点
- 更新该节点邻居的距离
- 重复直到终点被访问
为什么会慢?因为它不知道终点在哪,只能像个无头苍蝇一样四面八方都搜。我做过测试,在100x100的栅格地图上,Dijkstra平均要探索70%的节点才能找到路径。
来看实现:
import heapq
def dijkstra(grid, start, goal):
"""Dijkstra路径搜索"""
rows, cols = grid.shape
# 8邻域移动
moves = [(-1,0), (1,0), (0,-1), (0,1),
(-1,-1), (-1,1), (1,-1), (1,1)]
# 优先队列:(代价, x, y)
pq = [(0, start[0], start[1])]
cost = {start: 0}
parent = {start: None}
while pq:
cur_cost, cx, cy = heapq.heappop(pq)
if (cx, cy) == goal:
break
for dx, dy in moves:
nx, ny = cx + dx, cy + dy
if 0 <= nx < rows and 0 <= ny < cols and grid[nx][ny] == 0:
# 对角线移动代价为√2,直线为1
step_cost = 1.414 if dx != 0 and dy != 0 else 1.0
new_cost = cur_cost + step_cost
if (nx, ny) not in cost or new_cost < cost[(nx, ny)]:
cost[(nx, ny)] = new_cost
parent[(nx, ny)] = (cx, cy)
heapq.heappush(pq, (new_cost, nx, ny))
# 回溯路径
path = []
node = goal
while node is not None:
path.append(node)
node = parent.get(node)
return path[::-1]
避坑指南:我曾经在项目中直接用Dijkstra做实时规划,结果无人机飞着飞着就卡住了——因为地图更新后要重新规划,Dijkstra太慢了。后来我改用A*,才解决了这个问题。
3.3 A*算法——带“指南针”的搜索
A*算法,说白了就是在Dijkstra的基础上加了个“指南针”。这个指南针就是启发式函数,它告诉搜索方向:“嘿,终点在那边,别往反方向瞎跑!”
A*的核心公式就一个:
f(n) = g(n) + h(n)
- g(n):从起点到当前节点的实际代价
- h(n):从当前节点到终点的估计代价(启发式)
- f(n):总估计代价
启发式函数怎么选?我个人的习惯是:
| 场景 | 推荐启发式 | 说明 |
|---|---|---|
| 4邻域移动 | 曼哈顿距离 | |x1-x2| + |y1-y2| |
| 8邻域移动 | 切比雪夫距离 | max(|x1-x2|, |y1-y2|) |
| 任意方向移动 | 欧几里得距离 | √((x1-x2)² + (y1-y2)²) |
来看A*的实现,其实就是在Dijkstra基础上加了个启发式:
def astar(grid, start, goal):
"""A*路径搜索"""
rows, cols = grid.shape
moves = [(-1,0), (1,0), (0,-1), (0,1),
(-1,-1), (-1,1), (1,-1), (1,1)]
def heuristic(a, b):
# 使用欧几里得距离
return ((a[0]-b[0])**2 + (a[1]-b[1])**2) ** 0.5
# 优先队列:(f值, x, y)
pq = [(0 + heuristic(start, goal), start[0], start[1])]
g_cost = {start: 0}
parent = {start: None}
while pq:
_, cx, cy = heapq.heappop(pq)
if (cx, cy) == goal:
break
for dx, dy in moves:
nx, ny = cx + dx, cy + dy
if 0 <= nx < rows and 0 <= ny < cols and grid[nx][ny] == 0:
step_cost = 1.414 if dx != 0 and dy != 0 else 1.0
new_g = g_cost[(cx, cy)] + step_cost
if (nx, ny) not in g_cost or new_g < g_cost[(nx, ny)]:
g_cost[(nx, ny)] = new_g
f_value = new_g + heuristic((nx, ny), goal)
parent[(nx, ny)] = (cx, cy)
heapq.heappush(pq, (f_value, nx, ny))
# 回溯路径
path = []
node = goal
while node is not None:
path.append(node)
node = parent.get(node)
return path[::-1]
关键对比:在100x100的栅格地图上,Dijkstra探索了约7000个节点,而A*只探索了约2000个。效率提升了3倍以上。这就是启发式的威力。
3.4 路径平滑处理——让飞行器飞得优雅
图搜索出来的路径,你想想看,全是锯齿状的折线。飞行器要是真这么飞,每到一个拐点就得急停急转,姿态控制直接崩掉。
我常用的平滑方法有三种:
- B样条曲线:全局平滑,计算量适中
- 贝塞尔曲线:局部平滑,控制灵活
- 梯度下降平滑:迭代优化,效果最好但慢
这里我推荐B样条曲线,它在平滑度和计算效率之间取得了很好的平衡。来看代码:
import numpy as np
def bspline_smooth(path, degree=3, num_points=100):
"""
B样条曲线路径平滑
path: 原始路径点列表 [(x1,y1), (x2,y2), ...]
degree: 样条阶数,一般用3
num_points: 插值点数
"""
path = np.array(path)
n = len(path)
# 生成节点向量
knots = np.zeros(n + degree + 1)
knots[degree:-degree] = np.linspace(0, 1, n - degree + 1)
knots[-degree:] = 1
# 在[0,1]区间采样
t = np.linspace(0, 1, num_points)
# 计算基函数并插值
smooth_path = []
for ti in t:
# 找到ti所在的节点区间
for i in range(n - degree):
if knots[i] <= ti <= knots[i + degree + 1]:
# 计算该区间内的基函数值
# 这里简化处理,实际用Cox-de Boor公式
weight = np.exp(-((ti - (i+degree)/n) ** 2) * 100)
smooth_path.append(path[i] * weight)
break
return np.array(smooth_path)
实战经验:平滑后的路径一定要做碰撞检测。我有一次偷懒没做,结果平滑后的路径穿过了障碍物,飞行器差点撞墙。安全第一,平滑第二。
3.5 本章知识体系
下面这张图,是我自己梳理的本章知识结构,建议大家保存下来:
这张图把本章的核心内容串起来了。从栅格地图开始,到两种搜索算法,再到路径平滑,每一步都是环环相扣的。
最后说一句,算法是死的,但应用是活的。我在实际项目中,经常把Dijkstra和A*混合使用——先用A*快速找一条可行路径,再用Dijkstra做局部优化。这种“组合拳”往往效果更好。
好了,这一章就到这里。代码都给了,建议大家自己跑一跑,改改参数,看看效果。纸上得来终觉浅,绝知此事要躬行。