☰
基于Python的自动驾驶路径规划:从源码跑通到动态避障实战
2026/10/1 13:19:26 网站建设 项目流程

简介:一套基于Python的自动驾驶路径规划系统源码,面向自动驾驶、智能车竞赛和机器人导航方向的学习者与开发者。项目将路径规划与控制算法融为一体,涵盖RRT、A*、PRM等采样类规划方法,以及PID、纯跟踪、动态窗口法、Frenet最优轨迹规划、模型预测控制(MPC)等经典控制策略,并包含三次样条平滑、LQR速度控制等辅助模块,覆盖从局部避障到全局路径跟踪的完整技术链路。资源共34个文件,其中15个Python脚本呈现核心算法,9个C++源文件与5个头文件构成工程实现版本,另有仿真图与说明文档,压缩包仅800KB,目录分层清晰,便于对照阅读。已有66人学习浏览,可快速获取各类算法的可运行代码与实现思路,不同算法模块相互独立,便于单独提取学习或二次开发应用。

1. 基于Python的自动驾驶路径规划系统:先把它跑起来,再谈怎么改

做自动驾驶的路径规划,最劝退的不是算法本身,而是你打开一个源码包,发现几十个.py文件互相 import,地图数据是二进制,跑起来界面全黑,还不知道改哪里。近几年这类基于 Python 的路径规划系统源码在课程设计、课题预研和面试项目里出现频率极高,核心套路却很固定:一张栅格地图、一个搜索或采样算法、一辆带运动学约束的小车模型,再加上一段可视化。把这三层拆开,这套系统就再没有什么黑匣子。

这个方向适合谁?适合正在做自动驾驶算法岗笔试面试的人,适合毕设选了路径规划题目的人,也适合想把手里的静态 A* 升级成动态避障小车的移动机器人从业者。下面是这套源码最常见的组织方式,以及我个人反复用到的落地步骤。

2. 路径规划源码里藏的算法骨架:栅格搜索、采样与轨迹后处理

这类 Python 路径规划系统一般不会从传感器数据开始,而是从"已知地图 + 起点终点"出发,先解决几何路径,再逐步加约束。理解它的骨架,比直接跑通更有价值。

2.1 地图的表示方式:栅格图与代价地图的差异

打开源码大概率会看到两类地图文件:一类是.png+.yaml,另一类是纯文本矩阵。前者是给 ROS 的map_server用的,后者是教学项目常用的numpy二维数组。这两类地图在源码里的处理路径完全不同。

教学版源码常用的是一个GridMap类,内部是一个numpy.ndarray,0 表示可通行,1 表示障碍物。读取代码常见长这样:

import numpy as np from PIL import Image class GridMap: def __init__(self, map_path, resolution=0.05): # resolution 单位:米/像素,栅格地图的物理精度 img = Image.open(map_path).convert('L') self.resolution = resolution self.data = np.array(img) # 常见约定:白色(255)为可通行,黑色(0)为障碍 self.data = np.where(self.data > 128, 0, 1) self.height, self.width = self.data.shape

注意self.height, self.width = self.data.shape这一行,numpy数组第一维是行,对应栅格图的 Y 轴,第二维是列,对应 X 轴。这是一个极容易翻车的坐标顺序问题,后面避坑章会单独展开。

代价地图则多一层灰色地带,值在 0 到 255 之间,越靠近障碍物数值越大。Dijkstra 和 A* 在这类地图上不再只找"路径是否可达",而是计算"累计代价最小"。如果你手上的源码用的是代价地图,启发函数和邻域遍历的写法会不一样。

2.2 搜索式算法与采样式算法的选型判断

定位一套源码的算法水平,最快的方式是看它的planner目录下有哪些文件。

搜索式算法包括 Dijkstra、A*、D* Lite,它们的特点是显式维护一个开放列表,在小尺寸栅格图上效率高,路径质量稳定。采样式算法包括 RRT、RRT*、PRM,它们的特点是在高维空间或大范围地图上避免栅格化带来的内存爆炸,但路径质量波动大。

在这类 Python 系统中,A* 和 RRT 通常并存,因为它们的适用场景正好互补:A* 在 200x200 以内的栅格图上毫秒级出结果,RRT 适合越野场景的大门幅地图。下面是一个精简 A* 的核心循环,保留了源码里最常见的写法:

import heapq def astar_search(grid_map, start, goal): open_heap = [] heapq.heappush(open_heap, (0, start)) came_from = {} g_score = {start: 0} f_score = {start: heuristic(start, goal)} while open_heap: current = heapq.heappop(open_heap)[1] if current == goal: return reconstruct_path(came_from, current) for neighbor in get_neighbors(grid_map, current): tentative_g = g_score[current] + move_cost(current, neighbor) if tentative_g < g_score.get(neighbor, float('inf')): came_from[neighbor] = current g_score[neighbor] = tentative_g f_score[neighbor] = tentative_g + heuristic(neighbor, goal) heapq.heappush(open_heap, (f_score[neighbor], neighbor)) return None # 表示无可行路径

2.3 路径后处理:为什么规划出来的折线根本没法开

搜索类算法输出的是一串栅格中心点,直接交给下游控制模块,车辆会在每个拐点停下来转向,原地打转。因此一般会在规划层和控制层之间加一个后处理模块,常见做法有两种。

第一种是轨迹平滑,用三次样条插值或者共轭梯度法把折线变成连续曲线。第二种是运动学约束,把转折点替换成最小转弯半径约束的圆弧段。后者在泊车路径规划算法里非常关键,例如用 Dubins 曲线处理无倒车场景。

后处理代码通常长这样:

from scipy.interpolate import splprep, splev def smooth_path(waypoints, smoothing=3): # 输入是 Nx2 的数组,输出是采样更密的平滑轨迹 x = waypoints[:, 0] y = waypoints[:, 1] tck, u = splprep([x, y], s=smoothing) new_points = splev(np.linspace(0, 1, 500), tck) return np.vstack(new_points).T

smoothing参数是这条路线的"松紧度",值越大越顺滑但越容易切割障碍物的尖角膨胀层,一般取 1 到 5 之间。超过 5 容易直接穿过膨胀层,产生与障碍物碰撞的假轨迹。

3. 用最小配置跑通规划 demo:依赖、地图与启动参数

如果说算法骨架是这套系统的灵魂,那么跑通 demo 就是它的入门考试。多数人倒在环境依赖上,而不是代码逻辑上。

3.1 环境准备:Python 版本与三件套依赖

这类 Python 自动驾驶路径规划系统通常依赖numpy、matplotlib、scipy三件套,部分源码会额外引入opencv-python或pygame做可视化。我一般建议在一个干净的虚拟环境里安装,不要直接装在系统 Python 上。

python3 -m venv venv source venv/bin/activate pip install numpy scipy matplotlib opencv-python

这里有个细节:如果源码是两年前写的,numpy新版本可能不兼容旧的 API。遇到module 'numpy' has no attribute 'bool'这类报错,不要急着改源码,先降到numpy==1.23.5试试。这种兼容性坑在开源源码包里出现频率极高,属于最常见的翻车现场。

3.2 启动流程:从 main.py 到可视化窗口

绝大多数这类源码都会提供一个main.py或demo.py,入口逻辑是:加载地图,实例化一个规划器,给定起终点,然后循环刷新可视化窗口。

python main.py \ --map maps/map_demo.png \ --planner astar \ --start 120 60 \ --goal 40 180 \ --resolution 0.1

启动后能看到一条规划路径画在地图上。如果你的源码没有命令行参数,通常会在main.py顶部有一组全局变量,直接改START_POINT和GOAL_POINT即可。

命令行的四个参数是这套系统最常用的调优入口:

  • --map:替换成自己的地图,注意地图里障碍物要是实心黑色,否则二值化会把灰斑当可通行区域
  • --planner:切换算法,多数源码支持astar、rrt、dijkstra,三者之间路径形态差异明显
  • --start/--goal:单位是像素坐标,不是世界坐标,写反 Y 轴会直接导致规划失败
  • --resolution:规划结果会乘以它换算成米,改错会导致路径长度显示异常

我第一次跑这种包的时候,直接用了默认参数,地图是能显示,但规划按钮点了毫无反应。后来加了日志才发现读地图时把0和255的含义搞反了,障碍全被当成可通行区域,A* 直接穿过墙体,因为起点和终点在同一个连通域里。把np.where(self.data > 128, 0, 1)改成np.where(self.data > 128, 1, 0)就正常了。

3.3 三个必调的参数:步长、邻居数和膨胀半径

跑通只是第一步,要让路径质量看起来"像自动驾驶系统",三个参数必须调。

第一个是 RRT 的扩展步长step_size,单位是栅格数。步长太小,算法会在大地图上龟速探索,半天够不到终点;步长太大,轨迹会频繁撞击障碍物边缘,碰撞检测直接不通过。推荐初始值为地图短边的 1% 到 2%。

第二个是 A* 的邻居数。四邻域速度慢但路径安全,八邻域速度快但可能在墙角斜穿。如果你发现规划路径贴着障碍物对角线切过去,多半是邻居定义里没有排除对角穿越的情况。

第三个是地图膨胀半径inflation_radius,单位通常也是栅格数。车辆是有宽度的,按照质点去规划,生成路径会让实际车体刮墙。这类源码一般不会默认开启膨胀层,需要自己给障碍物矩阵做一次距离变换扩展。

from scipy.ndimage import binary_dilation def inflate_obstacles(grid_map, radius=3): # 把障碍物向外扩 radius 个栅格,生成安全边界 struct = np.ones((2 * radius + 1, 2 * radius + 1)) return binary_dilation(grid_map, structure=struct).astype(np.int8)

radius=3对应物理尺寸是3 * resolution米,如果车辆宽度是 0.4 米、栅格分辨率是 0.05 米,膨胀半径至少要 4 个栅格,否则地图上的"安全距离"小于实际车身半径。

4. 避坑清单:这类源码最常见的五个翻车点

这套源码的方向盘后面藏着不少玄学问题,很多现象看似是环境问题,其实根源在数据约定上。以下是我在复现不同版本源码时反复踩过的坑,按出现频率从高到低整理。

4.1 规划结果在 Y 轴上镜像翻转

现象:规划出来的路径在可视化窗口里是正常的,但把坐标输出成文本后,在别的软件里画图发现路径上下颠倒。

原因:栅格图的坐标系和常规笛卡尔坐标系不一致。图片的原点在左上角,Y 轴向下;路径规划算法按数学惯例使用 Y 轴向上。源码里的可视化模块做了翻转,但保存路径时没做。

解决:在最终输出路径前统一坐标转换,常见的代码是对 Y 轴取负并加上地图高度:

def grid_to_world(point, grid_map): row, col = point x = col * grid_map.resolution y = (grid_map.height - row) * grid_map.resolution return x, y

4.2 RRT 算法卡死在高维空间

现象:RRT 跑起来后,可视化窗口里的树一直在起点附近反复生长,终点方向几乎没有树枝,甚至跑了十几秒都没产出路径。

原因:采样是均匀随机采样,当起点和终点之间有一条窄长走廊时,随机点落在走廊里的概率极低,树扩张速度慢。

解决:给采样函数加上goal_bias机制,以一定概率直接把终点作为采样目标点。常见的做法是每 10 次采样中固定一次取终点。下面的实现片段可以自己加到源码里:

def sample_random_point(goal, goal_bias=0.2): if np.random.random() < goal_bias: return tuple(goal) else: return (np.random.randint(0, width), np.random.randint(0, height))

如果你改完goal_bias还是看不到效果,检查迭代上限max_iterations。很多源码默认只给 500 次迭代,在 500x500 的地图上就是在碰运气。调到 5000 次以上会明显改观。

4.3 路径频繁与障碍物"擦边"

现象:路径目标点是可达的,但路径上某些线段距离障碍物只有 1 个栅格,视觉上像是硬挤过去的。

原因:规划器把栅格中心点当作路径的几何位置,没有考虑车辆宽度和定位误差。碰撞检测用的是二值化的障碍层,没有用膨胀层。

解决:在规划前对地图做膨胀处理,并且碰撞检测也要基于膨胀后的地图。不要在路径生成后再去"修正",那是事后修补,效果不好。

4.4 代价函数权重不合适导致路径绕着走

现象:A* 能出路径,但路径明显绕远,或者贴障碍物很近,明显不符合直觉走向。

原因:多数源码沿用最朴素的f = g + h,其中h是欧氏距离,g是累计移动代价。问题出在move_cost:如果它只区分邻域是横竖还是对角,而没有引入障碍物距离惩罚项,路径就会贴着墙走。如果惩罚项加得过大,路径又会在空旷区域绕大圈。

def move_cost(current, neighbor): base_cost = 1.0 # 惩罚项:根据邻居邻域内的障碍物密度加权 obstacle_penalty = 1.0 + 2.0 * local_obstacle_density(neighbor) return base_cost * obstacle_penalty

这个函数的取值属于调参玄学区,没有标准答案。我的习惯是先以 2.0 的系数起步,看路径是否避开了墙面,再微调到 1.5 到 3.0 之间。

4.5 可视化刷新卡顿与内存泄漏

现象:规划完成之后拖动地图窗口,帧率急剧下降,长时间运行时内存占用不断攀升。

原因:多数源码在while循环里不断调用plt.clf()重建整个画布,旧画布没有被真正释放,内存泄漏。

解决:改用matplotlib的FuncAnimation模式,或者手动ax.clear()而不是plt.clf()。如果只关心规划结果,可以直接把可视化循环去掉,输出结果到numpy文件,再用外部工具回放。

5. 从静态全局规划到动态避障:加感知、重规划与行为决策

拿到一套可运行的路径规划源码只是开始,它的真正实用价值在于能不能从"静态全局规划"升级成"动态避障小车路径规划"。这个升级路径在嵌入式小车、ROS 仿真和课程设计里是主线任务。这里讲下我一般怎么做,以及这套源码应该改哪几个位置。

5.1 给栅格地图加一个"动态障碍层"

静态全局规划的地图是不变的,动态避障要求每帧刷新局部地图,把实时感知到的障碍物标到新的图层上。常见做法是在原来障碍矩阵之外维护一个同样大小的动态矩阵,每次规划前做一次叠加,而不是修改原始地图。

def update_dynamic_map(static_map, obstacles): dynamic = static_map.copy() for point in obstacles: x, y = point dynamic[x, y] = 1 return dynamic

障碍物来源可以是激光雷达的 occupancy grid,也可以是视觉的栅格化结果。关键是动态层只在局部窗口生效,全局层保留静态地图数据。这套源码如果本身地图对象是全局唯一的,建议把叠加操作放在planner.plan()之前,这样做不至于污染地图缓存。

5.2 重规划循环:局部规划器的刷新频率

动态场景下的路径规划不可能指望全局规划一次跑完,必须做重规划。重规划分为两种:定时重规划和事件触发重规划。前者的典型频率是 5 Hz 到 10 Hz,在嵌入式平台上会适当降低。后者的触发条件一般是感知模块发现前方路径上新增了障碍物。

下面的伪代码结构是我在这类源码基础上加动态避障时用的模板:

while running: sensor_data = perception.get_obstacles() local_map = update_dynamic_map(static_map, sensor_data) if is_path_blocked(local_map, current_plan): new_plan = planner.plan( map_data=local_map, start=vehicle.get_pose(), goal=global_goal ) if new_plan is None: emergency_stop() continue current_plan = new_plan controller.follow_path(current_plan)

注意is_path_blocked这一步,它比直接重规划效率高得多。如果车辆只前进了 1 米,而全局路径剩余还有 100 米,不需要对整个路径重新规划,只要检查最近 10 米的路径段是否被新障碍物覆盖。这个局部检查实现很简单:遍历近端路径点,看它在 local_map 上是不是落在障碍物栅格里。

5.3 决策层:路径规划之上还有一个"行为选择"

如果源码里只有Planner类,没有行为状态机,那么在遇到"无路可走"的情况时,代码会直接返回None。这是很多动态避障方案的薄弱点:规划器不懂语义,只知道"没有可行路径"。

一个低成本的做法是在规划器之上增加一个简单状态机,区分直行、绕行、停车等待、倒车重试四个状态。绕行模式下给规划器加一个"目标偏移"参数。当 A* 返回None时,把目标点向垂直于当前前进方向的一侧偏移 N 个栅格再规划一次。这在停车场和窄通道场景中非常管用,相当于给规划器提供了自救手段。

def plan_with_retry(planner, start, goal, offset_step=5): path = planner.plan(start, goal) if path is not None: return path # 左右各尝试偏移,最多偏移 5 次 for direction in [-1, 1]: for i in range(1, 6): shifted_goal = (goal[0] + direction * i * offset_step, goal[1]) path = planner.plan(start, shifted_goal) if path is not None: return path return None

这种"规划失败再偏移目标"的思路在部分文献里叫目标松弛法,虽然在理论上不保证最优,但在工程上能大幅提高任务完成率。我在移动底盘上的调试经验是:偏移上限限制在 1 米以内,偏移步长取 0.2 米,效果最稳定。偏移太大会让机器人完全脱离原目标走廊,走出一条和全局规划完全无关的路径。

5.4 与嵌入式平台的衔接:把路径点序列下发给底盘

Python 规划系统跑出来的路径,最终要下发给嵌入式端去执行。常见的接口是一个 JSON 文件或者共享内存结构体,内容就是有序路径点序列。这里有一个高频问题:路径点的间隔太密,嵌入式端 PID 控制跟不上,车辆会出现抖动;间隔太疏,车辆在各点之间走直线,精度下降。

我一般会在下发给底盘之前做一次抽稀:

def simplify_path(path, min_gap=0.2): simplified = [path[0]] last_pt = path[0] for pt in path[1:]: if np.linalg.norm(np.array(pt) - np.array(last_pt)) >= min_gap: simplified.append(pt) last_pt = pt simplified.append(path[-1]) return simplified

min_gap单位是米,在室内移动机器人上取 0.1 到 0.2,在自动驾驶仿真场景里取 0.5 到 1.0。抽稀原则是保证相邻路径点之间的线段不会短于底盘一帧控制周期内可执行的移动距离。

6. 用标准 benchmark 验证你的改进参数

这套系统改得差不多了,有一个问题随之而来:怎么判断你调参后的成果真的变好了?路径规划领域没有统一的公开测评集,但有一组评价指标在学术界和工程界基本达成共识。它们是规划成功率、平均规划耗时、路径长度和路径平滑度。

规划成功率表示在 100 次随机起点终点中,规划器返回有效路径的次数占比。平均规划耗时反映在线重规划的可承受能力,如果你做的是动态避障小车,单次规划超过 50 毫秒就要警惕。路径长度直接对比改进前后,路径平滑度用相邻轨迹点之间的航向角变化量的方差来量化,方差越小越好。

我习惯把这四项指标封装成一个测试函数,每次改动完立刻跑一遍,而不是靠肉眼目测路径图:

def evaluate_planner(planner, maps, start_goal_pairs, trials=100): success = 0 total_time = 0.0 path_lengths = [] for _ in range(trials): map_id = np.random.randint(len(maps)) start, goal = start_goal_pairs[_] t0 = time.time() path = planner.plan(map_id, start, goal) total_time += time.time() - t0 if path is not None: success += 1 path_lengths.append(compute_path_metric(path)) return { "success_rate": success / trials, "avg_time_ms": total_time * 1000 / trials, "path_length_avg": np.mean(path_lengths) }

跑这个基准函数时要注意:起点终点必须放在可通行区域,否则规划失败会污染成功率数据。你可以做一个is_free(map, point)函数去过滤掉非法起终点,这样测试结果才是算法能力的真实反映。

最后一个技巧是保存规划结果快照。无论是 RRT 还是 A*,随机种子不同,路径就不同。对比实验时必须固定随机种子,否则你无法分辨指标变化是算法改进还是采样随机性导致的。在main.py开头加上以下两行,结果才可复现:

import random random.seed(42) np.random.seed(42)

这是我的血泪经验。最初我做 RRT* 算法对比实验,连续三天的测试数据都在上下波动,一直以为是新代码有问题,最后发现是随机种子没固定。从那以后,任何规划算法改动,我都先固定随机种子再跑基准。希望这个习惯也能帮到你,少走一段无谓的弯路。

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

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

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

立即咨询