Fast Planner——从一维抛物线到三维ESDF:EDT算法的实现与优化

张开发
2026/4/17 20:31:37 15 分钟阅读

分享文章

Fast Planner——从一维抛物线到三维ESDF:EDT算法的实现与优化
1. 从抛物线到三维空间EDT算法到底在算什么第一次看到EDT欧几里得距离变换算法时我盯着那一堆抛物线公式发呆了半小时。直到把三维问题拆解成一维场景才恍然大悟——原来这就是在解决每个点到最近障碍物的距离是多少这个看似简单的问题。想象你站在空地上闭着眼睛向前走EDT算法就是那个不断告诉你离前方围墙还有1.2米的智能提醒系统。在Fast Planner中这个距离场就是ESDF欧几里得符号距离场地图的核心。不同于传统栅格地图只记录有障碍/无障碍ESDF会精确存储每个自由空间点到最近障碍物的距离值这对无人机避障和路径规划至关重要。算法本质可以概括为用抛物线相交的特性来寻找最小距离。一维情况下每个障碍物点都会生成一条抛物线这些抛物线的下包络线就是我们要的距离函数。这个思路妙在将几何问题转化为代数计算使得计算机可以高效处理。当我第一次用Matlab可视化这个下包络过程时看着那些抛物线相互竞争形成最终的距离曲线突然就理解了文献中那些晦涩的数学符号。2. 一维到三维的维度魔法EDT的递推计算2.1 一维情况下的抛物线竞赛让我们用具体例子说明一维EDT。假设在10米长的走廊上有三个障碍物分别位于x2、x5和x8处。算法要为每个位置计算到最近障碍物的距离平方值# 一维EDT示例 import numpy as np obstacles [2, 5, 8] # 障碍物位置 query_points np.arange(0, 10, 0.1) # 需要计算距离的所有点 # 每个障碍物生成一条抛物线 def parabola(q, x): return (x - q)**2 # 计算下包络 distance_sq np.min([parabola(q, query_points) for q in obstacles], axis0)这个过程中每个障碍物点q都会对所有查询点x产生一条抛物线y(x-q)²。算法需要找出这些抛物线在每一点处的最小值——就像多个选手跳高我们只记录最低的那个高度。实际实现时Fast Planner采用了更聪明的办法通过维护抛物线的交点来避免全量计算这就是文献[1]中的核心技巧。2.2 二维扩展从列到行的接力计算当扩展到二维时EDT展现出其精妙之处。不是直接计算二维距离而是先对每一列做一维EDT然后将结果作为下一维计算的初始值。这就像先解决所有纵向距离问题再用这些结果解决横向问题。在Fast Planner的代码中这个过程体现为fillESDF函数的三次调用z→y→x维度。每次计算时上一维度的输出f(q)会成为下一维度的输入偏置量。这种维度递推策略将O(n³)的复杂度降到了O(n)是算法能实用的关键。我曾用棋盘格地图测试这个过程先对每列计算时障碍物下方的格子会形成距离波峰接着行计算时这些波峰又会相互影响最终形成精确的二维距离场。通过matplotlib动画展示这个传播过程能直观看到距离场像水波纹一样从障碍物向外扩散。3. 深入Fast Planner的EDT实现细节3.1 内存布局与维度遍历Fast Planner的EDT实现有几个工程亮点值得注意。首先是它的内存访问模式toAddress(x,y,z)这个函数将三维坐标转换为线性内存地址配合tmp_buffer1_和tmp_buffer2_两个中间缓冲区实现了维度计算的流水线作业。观察updateESDF3d()函数会发现它严格遵循z→y→x的计算顺序。这种特定顺序不是偶然的——由于内存是按x,y,z维度连续存储的这样的遍历顺序能最大化缓存命中率。我在i7-11800H处理器上测试发现相比乱序访问这种顺序访问能带来近3倍的性能提升。3.2 数值处理的工程技巧代码中几个细节处理体现了工程智慧使用std::numeric_limitsdouble::max()表示无限远距离避免魔法数字最后一步通过mp_.resolution_ * std::sqrt(val)将平方距离转为实际距离负距离场的并行计算确保能处理障碍物内外两种情况特别值得注意的是障碍物判断逻辑[](int z) { return md_.occupancy_buffer_inflate_[toAddress(x,y,z)] 1 ? 0 : std::numeric_limitsdouble::max(); }这个lambda函数精妙地实现了障碍物点距离为0自由空间点初始距离为无穷大的设定。我在实现类似功能时曾犯过错误——忘记给自由空间点赋初始最大值导致距离计算完全错误这个坑让我调试了整整一天。4. 实战用Python复现核心算法4.1 简化版EDT实现为了更好理解算法我用Python实现了简化版的二维EDT。去掉所有优化后核心算法不到50行代码import numpy as np def edt1d(f, start, end): v np.zeros(end-start1, dtypeint) z np.zeros(end-start2) k start v[0] start z[0], z[1] -np.inf, np.inf for q in range(start1, end1): while True: s ((f(q)q*q)-(f(v[k])v[k]*v[k]))/(2*q-2*v[k]) if s z[k]: break k - 1 k 1 v[k], z[k], z[k1] q, s, np.inf k start distance np.zeros(end1) for q in range(start, end1): while z[k1] q: k 1 distance[q] (q-v[k])**2 f(v[k]) return distance def edt2d(grid): h, w grid.shape # 第一遍处理列 col_edt np.zeros_like(grid, dtypefloat) for x in range(w): f lambda y: 0 if grid[y,x] else np.inf col_edt[:,x] edt1d(f, 0, h-1) # 第二遍处理行 result np.zeros_like(grid) for y in range(h): f lambda x: col_edt[y,x] result[y,:] edt1d(f, 0, w-1) return np.sqrt(result) # 转换为真实距离这个实现虽然效率不如C版本但完整展现了算法骨架。通过edt2d(np.array([[0,1,0],[0,0,0],[1,0,0]]))这样的调用可以快速验证算法正确性。4.2 可视化调试技巧在实现EDT时可视化是必不可少的调试手段。我常用的方法包括用matplotlib绘制抛物线包络线import matplotlib.pyplot as plt q_points [2, 5, 8] x np.linspace(0, 10, 100) for q in q_points: plt.plot(x, (x-q)**2, labelfq{q}) plt.plot(x, np.min([(x-q)**2 for q in q_points], axis0), k--, linewidth3, label下包络) plt.legend()用热力图显示二维距离场plt.imshow(edt2d(grid), cmaphot, interpolationnearest) plt.colorbar()制作三维曲面观察距离场变化from mpl_toolkits.mplot3d import Axes3D X, Y np.meshgrid(range(w), range(h)) fig plt.figure() ax fig.add_subplot(111, projection3d) ax.plot_surface(X, Y, edt2d(grid), cmapviridis)这些可视化手段帮我发现了实现中的多个边界条件错误比如没有正确处理地图边缘的情况或者抛物线交点计算时的浮点精度问题。

更多文章