尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

VFH避障算法详解:STM32也能跑的局部路径规划

VFH避障算法详解:STM32也能跑的局部路径规划 做机器人底盘的时候避障永远比你想的更恶心。超声波临到跟前才刹停连个平滑减速都做不出来红外虽然便宜但阳光一晒就失灵。后来换到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那就是100x10010000个格子。对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大时路径连贯性好适合运动惯性大的平台。一个可参考的初始值是μ15、μ22、μ32给目标方向更高的权重但通过后面两项抑制剧烈转动的冲动。当遇到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的波谷宽度校验直接按探针长度来计算而不只是考虑底盘宽度。这样在穿过极其狭窄且两侧障碍突出的场景时安全性会明显提升也算是我踩坑踩出来的偏方。
返回列表