☰
VFH避障算法详解:STM32也能跑的局部路径规划
2026/10/5 4:16:40 网站建设 项目流程

做机器人底盘的时候,避障永远比你想的更恶心。超声波临到跟前才刹停,连个平滑减速都做不出来;红外虽然便宜但阳光一晒就失灵。后来换到2D激光雷达,总算能看到周围一圈了,但真正的问题才刚开始:看到障碍物之后,往哪儿走?怎么走得顺?遇到一个过道,如何判断是绕着走还是直接穿过去?你需要的这层逻辑,就是局部路径规划算法。而这其中,VFH避障算法是我实测下来最适合中小型移动平台、STM32级别算力也能跑得动的方案。

这篇博客我会把VFH的原理、实现、调参、踩坑完整拆一遍。适合正在做避障小车、ROS导航入门、或者琢磨扫地机/服务机器人的朋友,就算没有激光雷达,直接用超声波阵列生成的占栅格信息,同样可以套这套思路。

1. VFH是什么:为什么局部路径规划选了它

1.1 避障算法家族里VFH的位置

避障这条路,大致可以分成两类:一类是全局路径规划,比如A*、Dijkstra、RRT,这些算法在已知地图上做搜索,算出来是一条从起点到终点的完整路径,理想情况下很优美,但缺点也很明显——地图一旦变了就失效,动态障碍物没法处理,而且计算量通常不低。另一类是局部路径规划,它不关心全局地图长什么样,只看传感器当前测得的那一圈数据,快速决定下一步往哪走。

VFH属于后者,而且是典型的反应式算法。它把传感器数据编码成一个直方图,然后从直方图里挑一个最合理的行进方向。你不需要预先知道完整地图,甚至不需要里程计精度很高,只要传感器不断扫描、计算、出方向,然后循环,小车就能一路避着障碍物往前摸。

在VFH之前,最常用的是两类思路:一类是Bug算法,就是碰到障碍物后贴边绕行,等能再次面向目标了再继续走。实现简单,但路径极其粗糙,会在障碍物边上走成锯齿形,而且遇到凹形障碍物容易绕不出来——就是死循环在同一面墙边上打转。另一类是人工势场法,目标点产生吸引力、障碍物产生排斥力,小车顺着合力方向走。算式简单,但痛点同样明显:容易陷入局部极小点,也就是在一个U形坑里来回震荡,出不来找不到路。

VFH的优势在于,它把二维障碍物信息降维成一维的极坐标直方图,再做决策。降维之后,很多麻烦就消失了。凹形障碍物不会把你困住,因为直方图上障碍物覆盖的范围一目了然,你能看出哪个扇区是干净的,直接穿过去就行,不会像势场法那样被局部力平衡困死。

1.2 为什么说VFH特别适合小车项目

我先说一个结论:如果你的主控是STM32F103这种入门级MCU,内存只有几十KB,主频72MHz,那VFH几乎是少数几个能在这种环境中跑得动的方案。人工势场法虽然计算简单,但调参调到怀疑人生;DWA动态窗口法效果很好,但要同时考虑速度和角速度采样,还得做轨迹仿真,MCU全负荷运转不一定扛得住。

VFH的计算量主要在栅格数据的遍历和直方图统计上,典型的处理流程如下:把激光雷达或声呐的数据映射到一个局部栅格地图,比如5x5米的区域,每格5cm,那就是100x100=10000个格子。对MCU来说,遍历一万个格子做加减法,一点点压力都没有。然后把这10000个格子投影到360°的极坐标扇区上,比如分成72个扇区,每个扇区5°,累加障碍物密度,再挑选最优方向。整个过程不涉及浮点大运算,定点数也能玩转,我甚至见过只靠C语言的整型变量就把VFH跑起来的开源项目。

VFH还有一种变体叫VFH+,增加了对机器人实际运动轨迹的考虑,会检查候选方向按当前最小转弯半径能否到达,避免把角度发下去结果机器人原地转弯造成拥堵。VFH+效果更好,但代码量翻倍。我建议先完整移植基础版VFH,跑通了再考虑VFH+。以下是VFH与常见局部规划算法的直观对比:

算法计算量平滑性动态避障实现难度典型场景
Bug算法极低差弱极低极简单环境
人工势场法低中中低空旷环境
VFH低中高中高中室内小车、扫地机
DWA中高高高高服务机器人、ROS小车

从表格里能看出,VFH在低算力和避障效果之间取得了相对均衡的平衡点。我很多项目都是先用VFH验证方案,后面如果实在需要更复杂的行为,再往上层叠加DWA或者全局规划器。

2. VFH核心原理与数学拆解

2.1 从传感器数据到栅格可信度

VFH的第一步不是直接做直方图,而是先建立一个局部的栅格地图。很多人看论文时容易跳过这个环节,觉得既然传感器直接给了距离信息,为什么不直接用?原因是传感器有噪声、有误差,直接对原始距离做直方图,输出会抖得非常厉害,今天看到左前方有个点,明天同一位置可能就没了,最终导致规划出的方向来回跳动。

所以VFH的做法是:在机器人周围设置一个固定大小的活动窗口,比如2m x 2m,每个格子存放一个置信度值。每当传感器数据进来,就把落在格子里的障碍物信息累加进去。这是一个典型的贝叶斯式更新思想——多次观测后,置信度会收敛到稳定值,单次测量的偶然误差就被平滑掉了。

置信度更新有一个经典公式:

C_new = min(C_max, C_old + p_hit)

也就是说,每次在该格子检测到障碍物,置信度增加p_hit,一般p_hit取2或3。反过来,如果某个格子之前有障碍物,但这次扫描没有检测到,就顺便减一点置信度:

C_new = max(0, C_old - p_miss)

这样做的目的是让栅格地图具备"记忆"和"遗忘"能力。动态障碍物离开了,栅格置信度逐渐下降,不会长期挡住路径;而静态障碍物被多次确认,置信度居高不下,规划器对它保持敬畏。这个机制是我认为VFH最聪明的设计之一,它把一个纯粹的传感器问题转换成了状态估计问题,大幅提高了算法的稳定性。

2.2 极坐标直方图:把二维地图压成一维向量

栅格地图建立好之后,接下来的工作是把它转换成一维的极坐标直方图。这是VFH的核心降维操作,也是整个算法最需要理解的部分。

先把机器人周围360°方向分成n个扇区。理论上分越多越精细,但扇区太多会导致单个扇区内点太少,直方图起伏大,输出角度不稳定。主流做法是分72个扇区,每个扇区对应5°。如果你有更高的角度精度要求,也可以用180个扇区,每个扇区2°,但MCU压力会变大,导航效果提升却不明显,不推荐。

对每个扇区k,逐个检查落在该扇区角度范围内的栅格(位于活动窗口内且有障碍物置信度的栅格),计算障碍物密度,公式为:

H(k) = Σ C_ij * W_ij

其中C_ij是第(i,j)个栅格的置信度,W_ij是权重系数,其值与该栅格到机器人中心的距离相关。距离越近,权重越高,因为近距离障碍物肯定比远距离的更紧迫。

权重函数怎么设计?最简单的方案是平方反比:

W_ij = 1 / d²

但平方反比会导致近距离权重过大,稍微有一点障碍物就把整个扇区堵满。我实际调下来,用1 / d更平顺,扇区直方图的峰谷更明显,阈值判断反而更容易。这个细节论文里写得比较粗略,真正实现时值得多试几种权重形式。

2.3 阈值、波谷与候选方向

直方图H(k)计算出来后,每个扇区对应一个障碍物密度值。接下来设置一个阈值τ,密度大于τ的扇区标记为“不可行”,密度小于τ的扇区标记为“可行”。把所有连续“可行”的扇区连起来,就是一个波谷区段。

每个波谷区段理论上都可以作为一个候选前进方向,但还需要判断这个波谷够不够宽。假设机器人物理宽度为w,当前波谷跨越的角度为α,那么该波谷对应的通道宽度大约为:

width ≈ 2 * r * sin(α / 2)

其中r是该波谷内距离最近的障碍物到机器人的距离。如果通道宽度小于机器人宽度,强行通过大概率撞车,必须舍弃这个波谷。这个判断非常关键——很多初版搞不定窄门的场景,都是忽略了波谷宽度的校验,把一根柱子旁边的缝隙当成了可通行路径。

筛选完波谷之后,每个波谷都会对应一个候选方向,一般是波谷的中心方向,或者更讲究一点,用波谷内代价最小的扇区方向。

2.4 代价函数:如何选择最优波谷

有多个波谷时,到底走哪个?VFH的做法是给每个候选方向算一个代价函数值,选代价最小的方向。

代价函数通常包含三个核心项:

cost = μ1 * Δ(θ_candidate, θ_target) + μ2 * Δ(θ_candidate, θ_current) + μ3 * Δ(θ_candidate, θ_prev)

第一项是候选方向与目标方向的角度差,目的是让机器人尽量朝着目标前进;第二项是候选方向与当前朝向的角度差,目的是避免急剧转向,保证运动的平稳性;第三项是候选方向与上一帧选择方向的角度差,目的是让方向输出有连贯性,防止抖振。

三个权重系数μ1、μ2、μ3的比例直接影响行为风格。μ1大,机器人会直奔目标,哪怕前方障碍密集也要冒险找缝钻;μ2大,路径更平滑,但可能过度绕路;μ3大时,路径连贯性好,适合运动惯性大的平台。一个可参考的初始值是μ1=5、μ2=2、μ3=2,给目标方向更高的权重,但通过后面两项抑制剧烈转动的冲动。

当遇到U形障碍物时,因为障碍物两侧的波谷都指向同一方向,而那个方向与目标方向差异极大,代价函数会把这两个方向都压得很低,机器人不会选择往里钻。这与势场法形成鲜明对比,势场法在U形坑里很容易陷入合力为零的局部极小点,VFH则天然免疫。

3. 实操:从零实现一个VFH模块

3.1 你需要的数据结构

实现VFH的第一步是定义好数据结构。这里不扯复杂架构,直接给一套在嵌入式环境用着很顺的结构。

机器人位姿信息可以用结构体表达:

typedef struct { float x; // 机器人x坐标,单位m float y; // 机器人y坐标,单位m float theta; // 机器人朝向,单位rad } pose_t;

栅格地图是核心存储体:

#define MAP_SIZE_X 120 // 活动窗口x方向格子数 #define MAP_SIZE_Y 120 // 活动窗口y方向格子数 #define CELL_SIZE 0.05 // 每个格子的物理尺寸,单位m,即5cm一个格子 #define CONF_MAX 15 // 置信度上限 uint8_t grid_map[MAP_SIZE_X][MAP_SIZE_Y];

活动窗口物理尺寸为120 * 0.05 = 6m x 6m,对一个中等偏大的室内机器人来说足够用了。如果你在狭窄走廊里跑,可以把窗口缩小到4m x 4m,分辨率不变,格子数相应减少,计算量进一步下降。

扇区直方图需要的数组:

#define SECTOR_COUNT 72 #define SECTOR_ANGLE (360.0f / SECTOR_COUNT) // 每个扇区5度 uint16_t histogram[SECTOR_COUNT];

另外再存一张布尔表,标记每个扇区是否可行:

bool sector_free[SECTOR_COUNT];

3.2 核心流程与关键代码

VFH主循环分四步:更新栅格、构建直方图、筛选波谷、计算最优方向。下面这个函数是核心循环的一个可运行骨架,我这里用C语言实现。

第一步,处理传感器数据,更新栅格可信度值:

void update_grid_map(pose_t *robot, sensor_scan_t *scan) { for (int i = 0; i < scan->point_count; i++) { float wx = robot->x + scan->points[i].range * cosf(robot->theta + scan->points[i].angle); float wy = robot->y + scan->points[i].range * sinf(robot->theta + scan->points[i].angle); int gx = (int)((wx - robot->x + MAP_SIZE_X * CELL_SIZE / 2.0f) / CELL_SIZE); int gy = (int)((wy - robot->y + MAP_SIZE_Y * CELL_SIZE / 2.0f) / CELL_SIZE); if (gx < 0 || gx >= MAP_SIZE_X || gy < 0 || gy >= MAP_SIZE_Y) continue; if (grid_map[gx][gy] < CONF_MAX) grid_map[gx][gy] += 2; // 命中,置信度+2 } // 对每个格子做衰减,模拟遗忘过程 for (int i = 0; i < MAP_SIZE_X; i++) { for (int j = 0; j < MAP_SIZE_Y; j++) { if (grid_map[i][j] > 0) grid_map[i][j]--; } } }

第二步,构建极坐标直方图:

void build_histogram(pose_t *robot) { memset(histogram, 0, sizeof(histogram)); for (int i = 0; i < MAP_SIZE_X; i++) { for (int j = 0; j < MAP_SIZE_Y; j++) { if (grid_map[i][j] == 0) continue; float dx = (i - MAP_SIZE_X / 2) * CELL_SIZE; float dy = (j - MAP_SIZE_Y / 2) * CELL_SIZE; float dist = sqrtf(dx*dx + dy*dy); if (dist > 3.0f) continue; // 只统计3m以内的栅格 float angle = atan2f(dy, dx) - robot->theta; if (angle < 0) angle += 2.0f * PI; int sector = (int)(angle / (2.0f * PI / SECTOR_COUNT)); if (sector >= SECTOR_COUNT) sector = 0; histogram[sector] += (uint16_t)(grid_map[i][j] / (dist + 0.1f)); } } }

这个函数里有个细节值得注意:dist + 0.1f中的0.1是一段很小的平滑项,防止距离为0时出现除零错误,也防止极近距离的噪点把直方图某个扇区抬得过于夸张。

第三步,根据阈值筛选可行扇区并寻找波谷:

void find_valleys(float threshold, valley_t *valleys, int *valley_count) { *valley_count = 0; for (int i = 0; i < SECTOR_COUNT; i++) { sector_free[i] = (histogram[i] < threshold); } int start = -1; for (int i = 0; i <= SECTOR_COUNT; i++) { bool is_free = (i < SECTOR_COUNT) ? sector_free[i] : false; if (is_free && start < 0) { start = i; } else if (!is_free && start >= 0) { valleys[*valley_count].start = start; valleys[*valley_count].end = i - 1; (*valley_count)++; start = -1; } } }

第四步,遍历波谷计算最优方向。需要注意的是,因为直方图是环形的,要处理扇区从71跳回0的跨越情况。可以在数组末尾再拼接一遍SECTOR_COUNT个扇区,或者在遍历时对index做模运算:

float find_best_direction(valley_t *valleys, int valley_count, float target_angle, float current_angle, float prev_angle) { float best_cost = 1e9f; float best_direction = current_angle; for (int i = 0; i < valley_count; i++) { int mid = (valleys[i].start + valleys[i].end) / 2; float candidate = mid * (2.0f * PI / SECTOR_COUNT); float cost = 5.0f * angle_diff(candidate, target_angle) + 2.0f * angle_diff(candidate, current_angle) + 2.0f * angle_diff(candidate, prev_angle); if (cost < best_cost) { best_cost = cost; best_direction = candidate; } } return best_direction; }

angle_diff这个函数要单独写好,注意处理角度差归一化到[-π, π]区间,否则两个方向一个在+170°、一个在-170°,实际只差20°,计算却会得到340°的错误差值。这是新手实现时最常掉的坑。

3.3 参数怎么调:典型配置与影响范围

VFH参数不算多,但每个都牵一发动全身。我用一个表格把参数的作用、初始值和调试方向列出来,照着调会少走很多弯路。

参数典型值作用调节方向
栅格大小CELL_SIZE0.05m分辨率,越小细节越精细过小则噪声大,过大则漏检小障碍物
活动窗口尺寸6m x 6m影响全局视野过小反应迟钝,过大引入远处无谓障碍物
置信度上限CONF_MAX15控制栅格记忆强度动态环境调低,静态环境调高
密度阈值τ800~1200决定扇区是否可行过高会忽略窄通道,过低会频繁绕行
扇区数量72方向分辨率一般固定5°就够用
权重μ1/μ2/μ35/2/2平衡目标性与平滑性全调大则整体反应迟钝

阈值τ是最考验调试经验的参数。它和传感器噪声水平、障碍物大小、栅格置信度上限都有关。我的调试思路是:先让机器人静止在障碍物前方,观察直方图在障碍物方向上的峰值是多少,然后取峰值的60%~70%作为初始阈值。比如峰值是2000,阈值就定在1200~1400左右。然后到走廊、窄门、杂物堆等环境下实测,看哪里会误判或漏判,再微调。

3.4 和底层运动控制的衔接

VFH的输出是一个目标角度,但实际下发给电机的不止角度。还需要一个速度规划器来做“方向盘”和“油门”的配合。这个环节处理不好,会有很多奇怪现象:比如明明VFH输出的是90°转向,小车却原地打转停在那儿;或者直行时速度忽快忽慢。

我用的方案是角度误差的比例控制。把VFH输出的方向和当前朝向的差值作为输入,计算角速度和线速度:

float angle_err = angle_diff(target_direction, robot->theta); float angular_speed = 1.5f * angle_err; float linear_speed = base_speed; if (fabsf(angle_err) > 1.2f) { linear_speed = base_speed * 0.3f; // 大角度转向时减速 } else { linear_speed = base_speed; }

这里的思路是:误差角度大时,降低前进速度,优先把朝向转过来,避免车辆冲入危险区域;误差小时正常前进,保障通行效率。如果你把线性速度维持在高位,会出现车头突然猛打方向、车身横滑的危险姿态,尤其是在室内地面摩擦力不太理想的场景。

还有一个容易忽略的点:VFH按固定频率运行还是按帧触发?建议固定频率运行,典型值是10Hz~20Hz。传感器数据来了就先存到缓冲区,定时器到了才拿最新一帧数据计算。这样做的好处是输出节奏稳定,运动控制的响应不会因传感器帧率波动而抖动。

4. 常见问题与排查技巧实录

4.1 翻车场景1:方向抖得像抽风

这是VFH最常见的问题,现象是机器人在原地方向来回跳,或者走一个明显的锯齿形路径。大概率原因有三个:

第一个是置信度更新太敏感,单帧数据就能让直方图峰谷形态大变,候选方向随之跳变。解决办法是增加置信度累加次数或调低命中增量,本质是加强滤波,让栅格状态更平滑。

第二个是阈值设置得太激进,很多扇区的密度值都在阈值附近来回波动,自由与障碍状态不稳定。给阈值设置一个滞后带可以解决:扇区由自由变为障碍需要达到较高的阈值τ_high,由障碍变为自由需要降到较低的τ_low。两个阈值之间留一个缓冲区间,扇形状态不会因为小的直方图波动而频繁翻转。

第三个是代价函数中平滑权重太低。μ2和μ3加起来至少要等于μ1的一半,否则算法过于“莽”,只为贴着目标走,方向震荡在所难免。

4.2 翻车场景2:明明有个缝,它就是不走

窄通道困境也很常见。VFH把栅格密度超过阈值的扇区全部划为不可行,但如果一个狭窄通道里的障碍物信号强度很高,哪怕通道宽度足够,直方图上那个区域的密度值也会超过阈值,导致所有通过该通道的扇区都被封住,机器人只能绕远路甚至停留不前。

处理办法之一是把阈值调低,但这会带来误入障碍区的风险。更稳妥的方式是做波谷宽度校验时直接计算通道的实际物理宽度,而不只是看直方图高度。把波谷附近的原始点云数据拿出来,计算最近障碍物之间的空隙是否大于机器人宽度,大于则可以强行打开这个波谷。这相当于给VFH增加了一个“目测”步骤,避开了纯阈值判断的盲区。

4.3 翻车场景3:动态障碍物闪避不及时

前面说的是静态环境,但实际场景中总有行人、推车、宠物。VFH对动态障碍物的反应速度取决于栅格置信度的更新速率。如果每次扫描只+2、每秒只跑10帧,一个快速移动的障碍可能从侧面冲过来,栅格还没累加到阈值,车就撞上了。

应对思路有两个方向:一是提高更新频率和命中的置信度增量,让障碍物第一帧出现就被直方图感知到;二是增加距离门控,在直方图计算时对近距离障碍物加权更多,让危险物在早期就主导决策。第二种方式效果更好,因为只需改权重函数W_ij,不用动整体框架。我可以把权重改为1 / (k * d),当距离越近时,k值越大,权重增加越快。实测下来,对0.5m以内的障碍物,响应速度能提升一倍以上。

4.4 问题排查速查表

现象可能原因快速定位方法解决方案
方向抖振置信度更新过快,阈值过窄打印直方图看峰谷是否大幅波动调低命中增量,加滞后带
窄通道不通过密度阈值过高读取窄通道对应的直方图峰值调低阈值,增加波谷宽度校验
动态障碍物反应慢权重函数平滑太强观察障碍物初现帧的直方图增幅改近距离权重公式
直行时蛇形走位平滑权重过低检查输出方向序列的稳定性增大μ3
原地卡死不长走所有波谷不满足宽度条件打印候选波谷的宽度是否都过小放宽最小宽度阈值或引入倒车行为

4.5 从VFH到VFH+、VFH*的进阶通道

基础版VFH够用,但如果你追求更极限的表现,可以考虑两步升级。VFH+在构建直方图时额外考虑了机器人的实际运动学约束——比如最小转弯半径,它会过滤掉那些理论上可行但机器人当前速度与转角组合无法达到的方向,输出更接近执行器可完成的结果。VFH*则是在VFH+基础上引入有限的树搜索,相当于对候选方向做几步前瞻模拟,选出未来几步内综合代价最小的方向,进一步提高路径质量。

我实际项目中用VFH+较多,因为它在基础版上只增加了一层空间状态转移检查,代码量增加约40%,但避障的可靠性提升明显。VFH*效果当然更好,但计算量暴涨,我一般只有在ROS环境、算力充足的平台上才会考虑。

如果你的平台是MCU,我建议先把基础版VFH调稳再升级;如果你直接上ROS,扫地机平台可以直接用move_base里集成的VFH+插件,但要读懂插件里的参数映射并不简单,最好还是先按本文的流程自己实现一遍,这样才能理解每个参数背后控制的物理含义。

5. 最后分享一点个人体会

做了一段时间VFH的工程落地之后,我最大的感受是:这个算法真正的难度不在数学,而在参数与物理世界的对齐。栅格大小、阈值、权重,这些数值都不是拍脑袋定的,它们反映了你对环境、传感器、机器人运动能力的理解。把栅格调大了,小车可能对细小的桌腿视而不见;把阈值调太低,又会在空旷区域频繁急转。这些都是在调试中一点点“喂”出来的手感,论文里不会告诉你。

如果只让我留一条建议,那就是:先把栅格地图和直方图可视化出来再调参。你可以把直方图输出到串口绘图工具,或者简单点,在调试屏上画一条折线——每一个峰代表一个障碍方向,谷代表候选方向,肉眼看到的和你调出来的参数是否合理,一目了然。闭着眼调参,等同于抽盲盒,运气好能跑,运气不好就卡在某个地方转圈。

还有个小技巧:在小车上贴一根细长的探针,用来模拟实际碰撞轮廓,然后让VFH的波谷宽度校验直接按探针长度来计算,而不只是考虑底盘宽度。这样在穿过极其狭窄且两侧障碍突出的场景时,安全性会明显提升,也算是我踩坑踩出来的偏方。

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

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

立即咨询