资讯动态

Occupancy后处理实战:从概率图到干净占用栅格的完整流程

发布时间:2026/10/26 4:17:43 来源:尧图企业网站定制
自动驾驶感知后处理里的 Occupancy 环节是这两年被讨论得越来越多的一块。前几年大家聊感知焦点基本都在检测框和分割掩码上直到 Occupancy Network 这类范式出来把场景里每个体素被占没被占变成一等公民后处理才真正被推到台前。我接触这块是从一个模拟项目开始的当时模型输出的 occupancy 栅格噪声大得离谱直接拿去做规划根本没法用于是硬着头皮把后处理这一整套流程从头捋了一遍。这篇就聊聊 Occupancy 后处理到底在做什么、为什么难、怎么落地适合刚接触占用栅格感知的算法同学也适合做下游规划、想搞清楚上游输出质量边界的朋友。核心关键词会围绕占用栅格、体素、后处理、连通域、栅格滤波这些展开尽量把每个环节的为什么讲透而不是只丢一堆参数。1. 先搞清楚 Occupancy 后处理到底在解决什么问题1.1 从模型原始输出说起它为什么不能直接用Occupancy Network 这类模型的典型输出是一个三维体素栅格每个体素带一个占用概率或者占用 logits有的还会附带语义类别、流场、特征向量。听起来很规整但真正拿到手你会发现它离能用差着十万八千里。最直接的问题是概率图是稠密且带噪的模型在物体边缘、远处稀疏区域、遮挡边界附近会给出大量模棱两可的中间概率比如 0.4、0.55 这种你按 0.5 一刀切切出来的表面坑坑洼洼像被虫蛀过一样。第二个问题是孤立噪点。体素空间里经常冒出一些和主体不相连的零星占用体素可能是模型对反光、雨雾、稀疏点云的误判。这些点如果直接喂给规划规划器会把它当成真实障碍物轻则绕路重则急刹。第三个问题是空洞和断裂真实物体内部本该是实心的模型却可能因为观测不足在中间留个洞或者一个连续墙面被切成好几段连通性被破坏。所以后处理的第一层使命很朴素把一张毛坯概率图打磨成结构干净、连通合理、语义一致的占用栅格。它不是锦上添花而是决定这套感知能不能进下游的关键闸门。我见过不少团队模型指标刷得很漂亮但后处理没做扎实实车一跑全是幽灵障碍最后返工的还是后处理。1.2 后处理在整个链路里的位置与边界得先把边界划清楚。Occupancy 后处理处在模型推理输出和下游建图/规划/碰撞检测之间它的输入是体素级的概率或 logits输出应该是干净的占用体素集合通常还会带上语义标签和置信度。它不负责提升模型的原始召回也不该去凭空补出模型完全没看到的结构——那是模型和时序融合的活。后处理能做的是去噪、补洞、连通、平滑、裁剪感兴趣区域、做时序一致性维护。这个边界很重要因为很多新手一上来就想用后处理救模型的漏检结果越补越错把真实空隙填成障碍规划直接卡死。我的经验是后处理只做基于已有证据的合理整理凡是需要无中生有的操作都要极度克制宁可留空也不要乱填。下面这张表是我梳理的职责划分实际项目里拿它对齐上下游预期特别管用。环节该做的事不该做的事模型推理输出体素概率、语义、特征保证结构干净后处理去噪、连通、补洞、平滑、裁剪、时序维护凭空补全未观测区域下游规划基于占用做碰撞检测与路径搜索承担去噪职责1.3 适合谁来读、需要什么前置知识这篇面向的是已经大致知道体素、栅格、占用概率这些概念的同学最好动手跑过一点三维数据处理。如果你完全没接触过也不影响理解整体思路但涉及连通域、形态学这些操作时建议先补一下三维图像处理的基础。代码层面我会用 Python 加 NumPy 为主偶尔提一下 SciPy 的 ndimage因为这套组合上手快、调试直观做原型足够真要上量产再考虑换成更高效的实现。整篇的基调是能抄作业每个环节我都会给出可运行的思路和参数选择的理由而不是停留在概念层面。2. 核心思路拆解为什么是这套处理顺序2.1 处理流水线的整体设计逻辑后处理不是一堆操作的随意堆叠顺序错了效果会差很多。我最终稳定下来的流水线大致是阈值化 → 感兴趣区域裁剪 → 体素级去噪 → 连通域分析 → 补洞 → 表面平滑 → 时序融合 → 输出结构化结果。这个顺序背后有明确的因果链我逐个说理由。先阈值化再裁剪是因为裁剪本身要先知道哪些体素是候选占用在概率图上裁剪没有意义而且先裁剪能大幅减少后续操作的体素数量三维形态学在稠密栅格上是很吃算力的先砍掉无关区域能省一大截时间。去噪放在连通域之前是因为孤立噪点会干扰连通域统计——你统计出来的连通域数量里混着一堆单个体素的噪声块阈值就不好定。连通域之后再补洞是因为补洞需要知道洞是被谁包围的只有先确定了主体连通块才能判断某个空腔是不是被同一个物体包住的内部空洞。平滑放在补洞之后避免平滑把刚补上的结构又抹掉。时序融合放最后是因为它需要当前帧已经整理干净的占用结果作为输入。提示这个顺序不是唯一解但每一步的依赖关系是硬的。你可以合并某些步骤但不要颠倒先确定主体、再修补细节这个大原则。2.2 阈值策略固定阈值为什么经常翻车最直觉的做法是概率大于 0.5 就算占用。实测下来这个做法在近处表现还行远处和边缘就崩。原因是模型输出的概率分布并不是校准良好的不同距离、不同语义类别的置信度分布差异很大。远处体素观测稀疏概率普遍偏低固定 0.5 会把远处真实障碍整片漏掉而近处反光地面又容易给出虚高概率。我试过几种改进。第一种是分类别阈值地面、墙面、车辆、行人各自用不同阈值因为它们的观测特性和误判模式不一样。第二种是基于距离的自适应阈值距离越远阈值适当放低补偿稀疏观测带来的置信度衰减。第三种是双阈值滞回高阈值确定铁定占用低阈值确定候选占用再用连通性把候选里和铁定占用相连的拉进来。滞回这招在抑制边缘抖动上效果明显代价是要多存一份候选掩码。参数上我一般把高阈值设在 0.6 到 0.7低阈值设在 0.3 到 0.4中间地带交给连通性裁决。这个区间不是拍脑袋是我在几个模拟场景里扫参数扫出来的经验带高阈值再往上召回掉得厉害低阈值再往下噪声涌进来。具体数值一定要在你的数据上重新扫因为模型校准程度不同最优带会平移。2.3 为什么用连通域而不是简单的中值滤波有人会问去噪用三维中值滤波不就行了何必搞连通域。中值滤波确实能压掉椒盐噪声但它对成块的孤立噪声无能为力——一个 3x3x3 的噪声团中值滤波照样保留。而连通域分析能直接从结构是否与主体相连这个维度判断把整块孤立噪声一锅端这是中值滤波做不到的。连通域的核心参数是最小连通体素数。小于这个数的连通块直接丢弃。这个阈值怎么定取决于你关心的最小真实物体。比如你希望至少能保留一个行人在远处的占用那就要估算行人在最远关注距离处大概占多少体素。假设体素分辨率 0.2 米一个行人粗略 0.6x0.6x1.8 米那大约占 3x3x9 个体素考虑远处观测不全打个对折也有几十个。所以最小连通体素数设在 20 到 50 之间比较合理设太小噪声去不干净设太大远处小目标被误删。这个计算过程我建议每个人都自己走一遍因为体素分辨率一变结论就变。2.4 补洞与平滑的取舍别把证据抹掉补洞的诱惑很大看到空洞就想填。但我踩过的坑是把真实空隙填成障碍。比如两辆车之间的缝隙、栏杆之间的空隙如果补洞算法太激进会把它们连成一片实心墙规划器直接判定不可通行。所以补洞必须限定在被单一连通块完全包围的内部空腔而且空腔体积要小于某个上限。实现上可以用连通域对空体素做标记只有不连通到栅格边界的空腔才算内部洞这个技巧叫背景连通标记非常实用。平滑同理。三维形态学的开运算能去掉毛刺闭运算能填小缝但开运算会削掉细小真实结构比如细杆、天线闭运算会粘连邻近物体。我的做法是平滑只作用在表面一层体素上且用较小的结构元素3x3x3并且平滑前后做一次体积变化检查体积变化超过阈值就回退。这个改完要验的习惯帮我避免了好几次把场景改坏的事故。3. 实操落地从概率图到干净占用栅格的完整流程3.1 环境与数据准备原型阶段我用 Python 3.10 加 NumPy、SciPy可视化用 Open3D 或者直接 matplotlib 切片看。数据方面你需要一份体素概率张量形状通常是(D, H, W)或者(X, Y, Z)外加可选的语义标签张量。如果没有真实模型输出可以自己造一个带噪声的模拟栅格来验证流程我一开始就是这么干的——造一个球体加几个立方体再撒上随机噪点和随机空洞专门用来调试后处理参数。import numpy as np from scipy import ndimage # 模拟一个带噪声和空洞的占用概率体素栅格 rng np.random.default_rng(42) shape (64, 64, 64) prob np.zeros(shape, dtypenp.float32) # 造一个球体作为主体 xx, yy, zz np.mgrid[0:64, 0:64, 0:64] sphere ((xx-32)**2 (yy-32)**2 (zz-32)**2) 12**2 prob[sphere] 0.9 # 撒噪点 noise_mask rng.random(shape) 0.995 prob[noise_mask] rng.random(noise_mask.sum()) * 0.8 # 挖几个空洞模拟观测缺失 for _ in range(5): cx, cy, cz rng.integers(20, 44, 3) hole ((xx-cx)**2 (yy-cy)**2 (zz-cz)**2) 3**2 prob[hole] 0.1这段造数据的代码看着简单但它能覆盖后处理最常遇到的几类问题边缘模糊、孤立噪点、内部空洞。拿它当回归测试集每次改参数都跑一遍看输出比在真实数据上盲调高效得多。3.2 阈值化与感兴趣区域裁剪的实现阈值化我推荐用双阈值加滞回。先算高阈值掩码和低阈值掩码然后对低阈值掩码做连通域凡是连通块里含有高阈值体素的整块保留。这样既保住了弱证据连成的结构又过滤了纯噪声。def hysteresis_threshold(prob, high0.65, low0.35): high_mask prob high low_mask prob low # 对低阈值掩码做连通标记 structure np.ones((3, 3, 3), dtypebool) labeled, num ndimage.label(low_mask, structurestructure) keep np.zeros(num 1, dtypebool) # 找出包含高阈值体素的连通块 hit_labels np.unique(labeled[high_mask]) keep[hit_labels] True keep[0] False return keep[labeled]感兴趣区域裁剪这块实际项目里通常只关心自车周围一定范围比如前后各 40 米、左右各 20 米、高度 0 到 4 米。裁剪不只是省算力更重要的是把远处那些观测极稀疏、误判率极高的区域排除掉避免它们污染连通域统计。裁剪边界建议留一点余量别卡得死死的否则物体刚好在边界上会被切断反而制造出假边缘。3.3 体素级去噪与连通域过滤去噪我分两步走。第一步用三维中值滤波压椒盐噪声核大小 3第二步用连通域按体积过滤。中值滤波放在连通域之前是因为它能把一些即将形成孤立小块的噪点先打散减少连通域的数量让后面的体积阈值更好定。def denoise_and_filter(mask, min_voxels30): # 中值滤波去椒盐 filtered ndimage.median_filter(mask.astype(np.uint8), size3).astype(bool) # 连通域体积过滤 structure np.ones((3, 3, 3), dtypebool) labeled, num ndimage.label(filtered, structurestructure) sizes ndimage.sum(filtered, labeled, range(1, num 1)) keep_labels np.where(sizes min_voxels)[0] 1 clean np.isin(labeled, keep_labels) return clean这里有个细节值得说structure用全 1 的 3x3x3 意味着 26 邻接连通性判定更宽松适合物体表面有细微断裂的场景如果你发现不同物体被错误地连成一块可以改成 6 邻接只认面相邻连通更严格。这个选择直接影响连通域数量和后续补洞行为值得单独调。3.4 内部空洞填补与表面平滑补洞用背景连通标记法。思路是把空体素做连通域凡是连通到栅格边界的空腔是外部背景不连通的才是内部洞只填内部洞且限制洞的体积上限。def fill_internal_holes(mask, max_hole_voxels200): empty ~mask structure np.ones((3, 3, 3), dtypebool) labeled, num ndimage.label(empty, structurestructure) # 找出接触边界的标签外部背景 border_labels set() for axis in range(3): border_labels.update(np.unique(labeled.take(0, axisaxis))) border_labels.update(np.unique(labeled.take(-1, axisaxis))) filled mask.copy() for lbl in range(1, num 1): if lbl in border_labels: continue hole labeled lbl if hole.sum() max_hole_voxels: filled[hole] True return filled平滑我只在表面做用 3x3x3 的开运算去毛刺然后做体积变化检查。体积变化超过 5% 就回退到平滑前这个保险丝很关键我靠它拦下过好几次过度平滑。3.5 时序融合与输出结构化单帧再干净帧间抖动也会让下游难受。时序融合最简单的做法是维护一个衰减的占用置信度体素当前帧结果按权重融合进去再重新阈值化。权重一般当前帧给 0.6 到 0.7历史给剩下的衰减系数控制历史记忆长度。要注意自车运动带来的坐标系变换融合前必须把历史栅格对齐到当前自车坐标系否则融合出来全是拖影。输出结构化时我建议除了占用掩码再输出每个连通块的包围盒、体素数、平均置信度、语义众数。下游规划拿包围盒做粗筛拿体素做精检比只给一张掩码好用得多。这一步经常被忽略但它是后处理和下游之间的接口契约定好了能省大量沟通成本。4. 常见问题与排查技巧实录4.1 幽灵障碍反复出现怎么查幽灵障碍是最高频的问题。排查顺序我总结成先看是单帧就有还是时序融合后才出现。单帧就有多半是阈值太低或去噪不够重点查低阈值区域和最小连通体素数时序融合后才出现八成是坐标系对齐有误或者历史权重给太高导致旧帧的误判被记住了。我遇到过一次融合后总在固定位置出现障碍最后发现是历史栅格对齐时漏了一个平移分量物体被复制到了错误位置。这类问题用可视化逐帧对比最快别硬看数字。4.2 远处小目标被误删的处理远处小目标被连通域体积阈值误删是另一个典型矛盾。我的处理是分区域用不同阈值近处用大一点的体积阈值远处用小阈值因为远处体素本来就少。同时配合距离自适应阈值远处降低占用阈值保住召回。如果还不行可以考虑对远处单独做一次不做体积过滤的保底通道把高置信度的小连通块强行保留。这个保底通道要谨慎容易放噪声进来建议只在远距离区域开。4.3 补洞把真实缝隙填死怎么办这就是前面强调的补洞要克制。除了限制只填内部洞、限制洞体积还可以加一条洞的长宽比检查。真实缝隙往往细长内部空洞往往接近球形。算一下洞的包围盒长宽比超过阈值比如 3:1就判定为缝隙不填。这招对栏杆、车缝特别有效。另外补洞后一定要做一次可通行性抽查挑几个已知该通的缝隙看有没有被填形成回归用例。4.4 性能扛不住时的优化方向三维形态学和连通域在稠密栅格上很吃资源。优化优先级我建议先做感兴趣区域裁剪这是收益最大的再把体素分辨率在远处降采样近处保持精细然后考虑把连通域换成基于并查集的增量实现最后才是上多线程或 GPU。别一上来就想着并行化先把算法层面的冗余砍掉往往能省一半以上时间。下面这张速查表是我平时排查用的直接对着查能省不少时间。现象可能原因排查动作幽灵障碍阈值低/去噪弱/时序对齐错分单帧与时序对比远处漏检体积阈值大/固定阈值分区域阈值保底通道缝隙被填补洞过激加长宽比检查帧间抖动无时序融合/权重不当调融合权重与衰减运行慢未裁剪/分辨率过高先裁剪再降采样4.5 参数调试的实操心得最后分享几个调参上的实在经验。第一永远先固定其他参数只动一个三维后处理参数之间耦合很强一起动你根本不知道是谁的功劳。第二建立小规模回归集十几帧覆盖典型场景近处、远处、缝隙、空洞、噪声每次改参数全跑一遍看指标比凭感觉靠谱。第三把中间结果都存下来阈值后、去噪后、补洞后各存一份出问题能快速定位是哪一步引入的。第四别迷信一套参数打天下不同场景地下车库、开阔道路、雨雾天最优参数带不一样条件允许就做场景自适应。我个人在实际操作中的体会是后处理这活儿七分靠对数据的理解、三分靠算法把数据看透了参数自然就有方向了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑