4、经典Pipeline解析:从多视图图像到3D体素特征,再到占据预测的完整流程

这一章,我们来拆解一下整个Occupancy Network的经典pipeline。

说实话,很多初学者一上来就被各种论文里的框图吓住了。什么“多视图特征提取”、“BEV空间变换”、“体素查询”…… 其实你把它拆开看,核心就三步:看图像 → 建体素 → 做预测。我当年刚接触这个方向时,也花了整整两周才把整个流程在脑子里串起来。今天咱们就用最直白的方式,把这条线捋清楚。

4.1 第一步:多视图图像特征提取

这一步说白了,就是让模型“看懂”每张图里有什么。

我们通常用2D backbone,比如ResNet、Swin Transformer之类的,对每张环视图像单独提取特征。输出是一个多尺度的特征图金字塔。我个人习惯用FPN(特征金字塔网络),因为小目标和大目标都能照顾到。

关键点: 多视图不是“多张图拼在一起”,而是每张图独立过backbone,最后在特征空间里融合。

举个例子,假设我们有6个环视相机,每张图分辨率是800×1600。经过backbone下采样32倍后,每张图会得到一个25×50的特征图。6张图就是6个这样的特征图。嗯,这里要注意:不同相机的内参外参不同,所以特征图上的每个像素,其实对应着3D空间中不同方向的一条射线。

4.2 第二步:从2D特征到3D体素特征

这是整个pipeline里最核心、也最容易出问题的一步。

我们有了2D特征,但Occupancy预测需要的是3D体素特征。怎么把2D映射到3D?主流做法有两种:

  1. 基于Lift-Splat的方法:把2D特征“提升”到3D空间,再“拍平”到BEV网格上。
  2. 基于Transformer的方法:用可变形注意力,让3D体素自己去2D特征图上“查”对应的特征。

我个人更倾向于第二种,因为它在处理遮挡和深度不确定性时更鲁棒。我在项目中遇到过一个问题:用Lift-Splat时,远处的物体因为深度估计不准,特征被投影到了错误的位置,导致预测结果出现“鬼影”。后来换成基于注意力的方案,这个问题就缓解了很多。

避坑指南: 我曾经在体素分辨率上吃过亏。体素太小(比如0.1m),显存直接爆掉;体素太大(比如0.5m),小物体又检测不到。建议从0.2m开始试,根据你的场景和GPU显存做调整。

具体来说,我们定义一个3D体素空间,比如前后40米、左右40米、高4米,体素大小0.2米,那么体素网格就是200×200×20 = 80万个体素。每个体素需要从2D特征图中聚合信息。Transformer的做法是:每个体素作为一个query,通过可变形注意力,在多个视图的特征图上采样特征,然后加权融合。

# 伪代码示意:体素特征查询
for each voxel in voxel_grid:
    # 将体素中心投影到每个相机坐标系
    for each camera in cameras:
        uv = project(voxel_center, camera.extrinsics, camera.intrinsics)
        if uv in image_bounds:
            # 在2D特征图上采样
            feat = bilinear_sample(feature_map[camera], uv)
            # 加权融合(注意力权重由query和key计算)
            voxel_feat += attention_weight * feat

你看,代码其实不复杂。但实际工程中,这步的并行化是个大坑。80万个体素,每个体素要查6个视图,还要做注意力计算。我建议用CUDA自定义算子来加速,或者用现成的库比如torchsparse。

4.3 第三步:占据预测

有了3D体素特征,剩下的就是分类了。

每个体素对应一个特征向量,我们把它送进一个轻量级的MLP或者3D卷积网络,输出每个体素的语义类别(比如:车、人、道路、背景等)。

这里有个细节:占据预测本质上是每个体素的分类任务。但和语义分割不同,它还要预测“这个体素是否被占据”。所以常见的做法是输出两个头:

  • Occupancy头:二分类,判断体素是否被占据。
  • Semantic头:多分类,判断占据的体素属于哪个类别。

我习惯把这两个头合并成一个多分类任务,把“背景”也作为一个类别。这样模型只需要一个输出,训练也更稳定。不过要注意类别不平衡问题——背景体素通常占90%以上。我在项目中用过focal loss,效果比交叉熵好不少。

注意: 预测是在3D体素空间做的,但最终输出需要转换到世界坐标系或者自车坐标系。别忘了做坐标变换!我曾经因为忘记把体素索引转成实际坐标,导致可视化结果完全对不上,排查了整整一天。

4.4 完整流程总结

好了,我们把整个pipeline串起来看:

步骤 输入 输出 关键操作
1. 图像特征提取 多视图图像 (N×H×W×3) 多尺度2D特征图 (N×C×H'×W') Backbone + FPN
2. 3D体素特征构建 2D特征图 + 相机参数 3D体素特征 (X×Y×Z×C) 可变形注意力 / Lift-Splat
3. 占据预测 3D体素特征 体素语义标签 (X×Y×Z×num_cls) MLP / 3D Conv + Softmax

你想想看,整个流程其实就是一个“编码-融合-解码”的过程。2D backbone是编码器,体素特征构建是融合模块,最后的MLP是解码器。理解了这一点,再看任何Occupancy网络的论文,你都能一眼看出它的pipeline属于哪一类。

我的建议: 初学者不要一上来就复现SOTA模型。先把这个经典pipeline用最小配置跑通——比如只用1个视图、10×10×5的体素网格、3个类别。跑通了,再慢慢加复杂度。我当年就是这么过来的,稳扎稳打比什么都重要。

下一章,我们会深入讲解体素特征构建的细节,包括坐标变换、投影、采样等核心操作。到时候我会分享一些我在工程落地时踩过的坑,保证让你少走弯路。