简介:这份资源面向无人车路径规划方向的学习者与开发者,聚焦基于人工势场法(APF)的避障路径规划实现,适合已具备一定MATLAB基础、希望理解算法落地细节的中高级读者。压缩包共4个文件,均为m脚本文件,整体约3KB,涵盖主程序入口与引力、斥力、角度计算等核心模块,结构精简,便于直接运行与逐行研读。资源围绕目标势能与障碍势能的构建、合力方向求解、位置迭代更新及碰撞检测等关键环节展开,可帮助读者厘清吸引力场与排斥力场如何共同驱动无人车绕开障碍并逼近目标,同时理解局部最小值等典型问题的成因与改进思路。目前已有220人学习下载,适合作为课程设计、算法验证或工程原型的参考脚本,便于在此基础上调整参数、替换场景并扩展优化策略。
1. 从一份“最新版本程序.rar”说起:人工势场法凭什么是无人车避障的入门首选
如果你在搜索框里敲下“人工势场 避障 无人车 路径规划”,大概率是两种情况:要么正在做毕设或课程项目,需要让一台仿真小车绕开障碍物走到目标点;要么已经在实车上试过,发现车要么在障碍物前面来回抖,要么直接一头撞上去。这两种情况我都经历过,所以这篇笔记不打算从“势场是什么”这种教科书定义讲起,而是直接按“能跑起来、能调参数、能排错”的顺序,把人工势场法(Artificial Potential Field,APF)在无人车避障里的落地路径拆开讲。
人工势场法的核心思想其实很朴素:把目标点当成一个“吸引源”,把障碍物当成“排斥源”,无人车在两者叠加出来的虚拟力场里,沿着合力方向走。它的吸引力在于实现简单、计算量小、实时性好,特别适合算力有限的嵌入式平台或者需要快速验证避障逻辑的仿真环境。但它的坑也同样出名——局部极小值、目标不可达、窄通道震荡,这三个问题几乎每个用过 APF 的人都踩过。这篇笔记会围绕一份典型的“最新版本程序.rar”里可能包含的仿真工程结构,把建模、参数整定、代码实现和避坑经验串起来,适合刚接触路径规划的新手照着复现,也适合已经跑通过但效果不理想的熟手对照排查。
2. 人工势场法建模:引力、斥力与合力到底怎么算
2.1 引力场与斥力场的数学形式选择
人工势场法最常见的引力场形式是二次型:当无人车距离目标点较远时,引力大小与距离成正比;距离很近时为了避免引力过大导致超调,也有用锥形势场的做法。我一般会先用二次型引力场把基本逻辑跑通,因为它的梯度连续,求导后得到的引力方向稳定,不会在目标点附近产生突变。
斥力场则要复杂一些。经典形式是当无人车进入障碍物影响半径之后,斥力随距离减小而急剧增大,距离越近斥力越大,直到无穷大——这在数学上保证了不会撞上障碍物,但在离散仿真里会直接导致数值爆炸。所以实际写代码时,几乎所有人都会给斥力设一个上限,或者把障碍物影响半径设得比安全距离大一圈,让车在真正贴近之前就已经被推开。
下面这段 Python 代码展示了一个最简二维引力场和斥力场的计算方式,参数都是可调的,后面会逐个解释。
import numpy as np def attractive_force(pos, goal, k_att=1.0): """ 二次型引力场:F_att = k_att * (goal - pos) pos: 无人车当前位置 [x, y] goal: 目标点位置 [x, y] k_att: 引力增益系数 """ return k_att * (goal - pos) def repulsive_force(pos, obstacles, k_rep=100.0, rho_0=2.0): """ 经典斥力场:F_rep = k_rep * (1/rho - 1/rho_0) * (1/rho^2) * grad_rho obstacles: 障碍物位置列表 [[x1,y1], [x2,y2], ...] k_rep: 斥力增益系数 rho_0: 障碍物影响半径 """ f_rep = np.zeros(2) for obs in obstacles: diff = pos - obs rho = np.linalg.norm(diff) if rho < rho_0 and rho > 1e-6: # 斥力方向为障碍物指向无人车 direction = diff / rho magnitude = k_rep * (1.0/rho - 1.0/rho_0) * (1.0/rho**2) f_rep += magnitude * direction return f_rep def total_force(pos, goal, obstacles, k_att=1.0, k_rep=100.0, rho_0=2.0): """合力 = 引力 + 斥力""" return attractive_force(pos, goal, k_att) + repulsive_force(pos, obstacles, k_rep, rho_0)这段代码里,k_att控制车往目标点走的“急切程度”,k_rep控制避障的“强硬程度”,rho_0决定障碍物在多远开始影响车辆。三个参数没有绝对最优值,必须结合地图尺度和车辆速度来调。我一般会先把k_att设为 1.0,rho_0设为障碍物最大外接圆半径的 1.5 到 2 倍,然后从小到大调k_rep,直到车能稳定绕开障碍物又不至于在远处就被“弹开”。
2.2 合力方向与运动学约束的衔接
算出合力之后,下一步是把力方向转成车辆能执行的控制量。仿真里常见两种做法:一种是直接把合力方向当作速度方向,速度大小固定或按距离目标远近调整;另一种是把合力当作加速度,再积分出速度和位置。前者简单,适合差速小车;后者更接近真实动力学,但需要处理速度限幅和转向角限制。
我一般会在仿真里用第一种方式快速验证势场参数,因为它的因果关系最直接——力指向哪,车就往哪走。等参数稳定了,再换成加速度模型,加上最大速度和最大角速度约束,看避障效果是否还能保持。这一步很关键,很多在纯运动学模型下跑得很好的参数,一加动力学约束就翻车,原因通常是斥力变化太快,车辆转向跟不上,导致在障碍物边缘来回震荡。
提示:如果你的无人车模型有最小转弯半径,斥力方向突变时车可能来不及响应,建议对合力方向做一阶低通滤波,或者限制相邻两帧之间的方向变化率。
3. 用 Python 在本地跑通人工势场避障的最小闭环
3.1 搭建仿真环境与障碍物地图
要复现一份“最新版本程序.rar”里的避障效果,第一步不是急着看主程序,而是先把地图和障碍物配置搞清楚。常见的地图有两种:栅格地图和连续坐标地图。人工势场法在连续坐标下计算更自然,所以我一般用连续坐标定义障碍物,再用 matplotlib 画出来做可视化。
下面这段代码定义了一个 20x20 的仿真区域,目标点在 (18, 18),障碍物用圆形表示,位置和半径都可以改。
import matplotlib.pyplot as plt # 仿真区域 map_size = 20.0 goal = np.array([18.0, 18.0]) start = np.array([1.0, 1.0]) # 障碍物列表:[x, y, radius] obstacles = [ [5.0, 5.0, 1.5], [8.0, 12.0, 2.0], [12.0, 6.0, 1.8], [14.0, 14.0, 1.2], [10.0, 10.0, 1.0], ] def plot_scene(ax, path=None): ax.set_xlim(0, map_size) ax.set_ylim(0, map_size) ax.set_aspect('equal') ax.plot(goal[0], goal[1], 'g*', markersize=15, label='Goal') ax.plot(start[0], start[1], 'bo', markersize=10, label='Start') for obs in obstacles: circle = plt.Circle((obs[0], obs[1]), obs[2], color='r', alpha=0.5) ax.add_patch(circle) if path is not None: path = np.array(path) ax.plot(path[:, 0], path[:, 1], 'b-', linewidth=1.5, label='Path') ax.legend() ax.grid(True)这段代码只做可视化,不参与势场计算。把障碍物画出来之后,你就能直观看到哪些位置容易形成局部极小值——比如两个障碍物中间夹着的区域,或者障碍物正好挡在目标点正前方的场景。
3.2 主循环:从起点到目标点的迭代逻辑
主循环要做的事情很固定:每一步计算当前位置的合力,沿合力方向移动一个步长,判断是否到达目标点或是否超时。步长和最大迭代次数是两个关键参数,步长太大会在障碍物附近震荡,步长太小则收敛慢。
def run_apf(start, goal, obstacles, step_size=0.2, max_iter=2000, k_att=1.0, k_rep=80.0, rho_0=2.5, goal_threshold=0.3): pos = np.array(start, dtype=float) path = [pos.copy()] for i in range(max_iter): if np.linalg.norm(pos - goal) < goal_threshold: print(f"Reached goal in {i} steps") break f_total = total_force(pos, goal, obstacles, k_att, k_rep, rho_0) if np.linalg.norm(f_total) < 1e-6: print(f"Stuck at {pos} due to zero net force") break direction = f_total / np.linalg.norm(f_total) pos = pos + step_size * direction # 边界处理:撞到地图边界就停住 pos = np.clip(pos, 0, map_size) path.append(pos.copy()) else: print("Max iterations reached without reaching goal") return path这里step_size=0.2是我在 20x20 地图上常用的值,对应大约 100 步走完全程。goal_threshold=0.3表示距离目标点 0.3 米以内就算到达,避免在目标点附近无限震荡。max_iter=2000是保险丝,防止陷入局部极小值后死循环。
跑完主循环后,用前面的plot_scene把路径画出来,你就能看到车是怎么走的。如果路径在某个位置开始打转或者停住,那就是局部极小值出现了,下一章会专门讲怎么处理。
3.3 参数整定:k_att、k_rep、rho_0 的联动关系
这三个参数不是独立的。k_att越大,车越“想”往目标点冲,但遇到障碍物时如果k_rep不够大,就会撞上去;k_rep越大,避障越强硬,但太大又会让车在障碍物影响范围边缘就被弹开,走出一条绕远路的轨迹。rho_0决定障碍物影响范围,设得太小车发现障碍物太晚,设得太大则多个障碍物的斥力场会叠加,把车推离目标方向。
我一般用下面这个顺序调参:
- 固定
k_att=1.0,rho_0设为最大障碍物半径的 2 倍。 - 从
k_rep=20开始,每次翻倍,观察路径。如果车撞障碍物,继续加大;如果车绕得太远,减小。 - 找到能稳定避障的
k_rep后,再微调rho_0,让车在障碍物附近的行为更平滑。 - 最后调
step_size,步长太大会震荡,太小会慢,一般取rho_0的 1/10 左右。
注意:如果你的障碍物半径差异很大,建议对每个障碍物单独设置
rho_0,否则小障碍物可能被大障碍物的影响范围淹没。
4. 局部极小值与目标不可达:人工势场法最出名的两个坑怎么绕
4.1 局部极小值的三种典型场景与检测方法
局部极小值是人工势场法最常被诟病的问题。简单说,就是车走到某个位置时,引力和斥力刚好抵消,合力为零,车就停在那里不动了。最常见的三种场景是:障碍物正对目标点、两个障碍物形成窄通道、U 型障碍物包围目标点。
检测局部极小值的方法很直接:如果连续多步位置变化小于某个阈值,或者合力模长持续小于某个阈值,就判定为陷入局部极小值。下面这段代码在主循环里加了检测逻辑。
def run_apf_with_detection(start, goal, obstacles, step_size=0.2, max_iter=2000, k_att=1.0, k_rep=80.0, rho_0=2.5, goal_threshold=0.3, stuck_threshold=0.01, stuck_patience=20): pos = np.array(start, dtype=float) path = [pos.copy()] stuck_count = 0 for i in range(max_iter): if np.linalg.norm(pos - goal) < goal_threshold: print(f"Reached goal in {i} steps") break f_total = total_force(pos, goal, obstacles, k_att, k_rep, rho_0) if np.linalg.norm(f_total) < 1e-6: print(f"Zero force at step {i}, pos={pos}") break direction = f_total / np.linalg.norm(f_total) new_pos = pos + step_size * direction new_pos = np.clip(new_pos, 0, map_size) if np.linalg.norm(new_pos - pos) < stuck_threshold: stuck_count += 1 else: stuck_count = 0 if stuck_count >= stuck_patience: print(f"Stuck detected at step {i}, pos={pos}") break pos = new_pos path.append(pos.copy()) return pathstuck_threshold=0.01表示一步移动距离小于 1 厘米就认为没动,stuck_patience=20表示连续 20 步没动就判定卡住。这两个值可以根据步长调整,一般stuck_threshold取步长的 1/10 到 1/20。
4.2 用虚拟目标点法跳出局部极小值
检测到局部极小值之后,常见的处理方式有几种:随机扰动、模拟退火、虚拟目标点法。我比较常用的是虚拟目标点法,因为它实现简单,效果也稳定。思路是:当检测到卡住时,在车当前位置附近找一个临时目标点,让车先朝那个方向走一段,脱离极小值区域后再切回真实目标点。
下面是一个简化版的虚拟目标点生成逻辑,沿垂直于“车到目标点”方向随机选一侧,距离取rho_0的 2 倍。
def generate_virtual_goal(pos, goal, rho_0, side=None): """ 在垂直于 pos->goal 的方向上生成虚拟目标点 side: 1 或 -1,None 表示随机选 """ to_goal = goal - pos dist = np.linalg.norm(to_goal) if dist < 1e-6: return goal to_goal_unit = to_goal / dist perp = np.array([-to_goal_unit[1], to_goal_unit[0]]) if side is None: side = np.random.choice([-1, 1]) virtual_goal = pos + side * perp * rho_0 * 2.0 virtual_goal = np.clip(virtual_goal, 0, map_size) return virtual_goal在主循环里,一旦stuck_count超过阈值,就把goal临时替换成virtual_goal,同时把goal_threshold调大一些,让车更容易“到达”虚拟目标。到达虚拟目标后,再切回真实目标。这个逻辑需要加一个状态标志,避免在虚拟目标和真实目标之间反复切换。
4.3 目标不可达问题的成因与改进步长策略
目标不可达通常发生在目标点附近有障碍物的情况。因为斥力在障碍物附近趋于无穷大,如果目标点离障碍物太近,车在接近目标点时会被斥力推走,永远到不了。解决思路有两种:一是修改斥力函数,让它在目标点附近衰减;二是当车进入目标点一定范围后,直接忽略斥力,只受引力控制。
我一般用第二种,简单粗暴但有效。在total_force里加一个判断:如果np.linalg.norm(pos - goal) < goal_threshold * 3,就只返回引力。这样车在最后接近阶段不会被障碍物斥力干扰,能稳定到达目标点。
提示:如果你的应用场景要求车必须精确到达目标点,建议把目标点附近的障碍物影响半径单独调小,或者把目标点选在障碍物影响范围之外。
5. 避坑与排查:人工势场避障最常见的五个翻车现场
5.1 车在障碍物前面高频抖动
现象:车在障碍物影响范围边缘来回小幅震荡,路径画出来像锯齿。
原因:斥力随距离变化太剧烈,步长又相对较大,导致车一步进入斥力区、下一步又被推出来,反复横跳。
解决:减小步长,或者对合力方向做低通滤波。我一般会加一个方向平滑:direction = 0.7 * direction_prev + 0.3 * direction_new,再归一化。这样车不会因为单步斥力突变而猛打方向。
5.2 车绕过第一个障碍物后直接冲向第二个
现象:避障逻辑只对最近的一个障碍物生效,车绕开一个之后没有及时响应下一个。
原因:斥力计算时只取了最近的障碍物,或者rho_0设得太小,车发现第二个障碍物时已经太晚。
解决:斥力要对所有在影响范围内的障碍物求和,不能只取最近的一个。另外rho_0要至少覆盖车辆一个步长能移动的距离,否则车会在“发现障碍物”和“撞上障碍物”之间没有足够的反应时间。
5.3 路径在目标点附近画圈
现象:车接近目标点后不停止,绕着目标点转圈。
原因:引力在目标点附近变小,但斥力仍然存在,合力方向偏离目标点,车就绕着走。
解决:在目标点附近切换控制策略,比如距离小于某个阈值时直接朝目标点直线走,忽略斥力。或者把goal_threshold调大,让车在进入目标点一定范围后就判定到达。
5.4 参数换一张地图就失效
现象:在 A 地图上调好的k_rep和rho_0,换到 B 地图后车要么撞障碍物,要么绕远路。
原因:人工势场法对地图尺度敏感,障碍物密度和半径变化后,原来的参数不再适用。
解决:把参数和地图特征绑定。我一般会按最大障碍物半径来设rho_0,按障碍物平均间距来调k_rep。如果地图变化频繁,建议加一个简单的自适应逻辑:根据当前障碍物密度动态调整k_rep。
5.5 仿真能跑但实车不动
现象:仿真里路径很漂亮,烧到实车上之后车要么不动,要么乱转。
原因:仿真里用的是位置控制,实车需要速度或加速度控制,合力方向没有转成车辆能执行的指令。另外实车有通信延迟和传感器噪声,势场计算用的位置可能滞后。
解决:在仿真阶段就加上速度限幅和转向角限制,把合力转成线速度和角速度。实车调试时先把速度降到很低,确认避障逻辑正确后再逐步提速。传感器噪声大的话,对障碍物位置做滑动平均滤波。
6. 从仿真到实车:人工势场法还能怎么用得更稳
如果你已经把前面的仿真跑通,并且用虚拟目标点法解决了局部极小值,下一步可以考虑几个进阶方向。第一个是动态障碍物处理:把障碍物速度纳入斥力计算,让斥力方向不仅考虑当前位置,还考虑相对运动趋势。这需要在斥力函数里加一个速度项,实现起来不复杂,但能明显改善对移动障碍物的避让效果。
第二个是势场参数的自适应调整。固定参数很难同时适应空旷区域和密集障碍物区域,我一般会用一个简单的规则:统计车辆前方一定范围内的障碍物数量,数量多时增大k_rep、减小rho_0,数量少时反过来。这样车在空旷区域走得快,在密集区域走得稳。
第三个是把人工势场法和其他规划器结合。纯 APF 适合做局部避障,全局路径可以用 A* 或 RRT 先规划一条粗略路径,再用 APF 做局部修正和动态避障。这种混合方案在实车上比纯 APF 稳定得多,也是目前比较常见的做法。
验证方法上,我习惯用一组固定场景做回归测试:单障碍物、双障碍物窄通道、U 型障碍物、动态障碍物横穿。每次改完参数或逻辑,都跑一遍这组场景,看路径长度、是否到达目标、是否撞障碍物。这三个指标能覆盖大部分问题。
最后说一个我自己的教训:早期做 APF 的时候,我总想把参数调到“完美”,结果在一个场景下调好了,换个场景就翻车。后来才明白,人工势场法本身就是一个局部方法,它的边界就在这里——不要指望它解决所有避障问题,把它放在合适的场景里,配合全局规划和动态调整,才能发挥最大价值。希望帮到你。
本文还有配套的精品资源,点击获取