KITTI SOTA新秀VirConv解析:如何用虚拟稀疏卷积提升3D目标检测精度(附代码实战)
突破KITTI瓶颈:深度解析VirConv如何用虚拟稀疏卷积重塑3D目标检测
在自动驾驶感知系统的演进中,3D目标检测的精度与效率始终是技术攻坚的核心。当激光雷达(LiDAR)点云在远距离物体上变得稀疏时,性能便会急剧下降。近年来,通过深度补全技术,从RGB图像生成“虚拟点”来增强稀疏点云,已成为提升远距离检测能力的关键路径。然而,这条路径上横亘着两大障碍:海量虚拟点带来的计算冗余,以及深度补全不准确引入的噪声污染。2023年,一项名为VirConv(虚拟稀疏卷积)的工作,通过其独创的StVD(随机体素丢弃)与NRConv(抗噪声子流形卷积)机制,不仅巧妙地绕过了这些障碍,更在权威的KITTI数据集上刷新了排行榜,为我们提供了一个极具启发性的技术范本。这篇文章,我们将深入其算法内核,并结合代码实践,看看它是如何重新定义多模态融合的效率与精度边界的。
1. 虚拟点融合的困境与VirConv的破局思路
传统的多模态3D检测,无论是早期融合(将LiDAR点与虚拟点直接合并)还是后期融合(分别提取特征后再融合),在面对由图像生成的虚拟点时,都显得有些力不从心。以KITTI数据集为例,一张图像能生成超过46万个虚拟点,是原始LiDAR点数量的近30倍。这种数据量的爆炸式增长,直接拖垮了检测速度。
更棘手的是噪声问题。深度补全网络并非完美,其预测误差会导致虚拟点的位置出现系统性偏差。这些噪声点并非随机分布,而是高度集中在物体的2D投影边界附近。在3D空间中,这些噪声点与真实物体表面点混杂在一起,传统的3D卷积算子难以区分,严重干扰了边界框的回归精度。
VirConv的提出,正是基于两个深刻的观察:
- 计算冗余的分布不均:对于近处的物体,LiDAR点云本身已相对稠密,附加的大量虚拟点带来的几何信息增益微乎其微,却贡献了绝大部分的计算开销。
- 噪声的跨模态可辨识性:虽然在3D空间难以处理,但这些由深度估计错误产生的噪声点,当其投影回2D图像平面时,往往会形成不符合真实物体边缘规律的异常模式,在2D视觉上下文中更容易被识别。
基于此,VirConv设计了两大核心组件:StVD负责“做减法”,智能地丢弃冗余计算;NRConv负责“做加法”,在特征编码阶段引入2D上下文信息来抑制噪声。下面,我们就来逐一拆解。
2. StVD:以空间智能采样换取计算效率
StVD(Stochastic Voxel Discard)的核心思想不是盲目地对所有体素进行均匀下采样,而是执行一种基于距离的、保留关键信息的策略性丢弃。其流程包含输入端采样和网络层内随机丢弃两部分。
2.1 输入端基于距离分桶的采样策略
在将点云体素化后,StVD首先根据每个体素中心到传感器原点的距离,将其划分到不同的距离桶(bin)中。例如,可以将0到80米的距离范围均匀划分为10个桶。
其采样规则简洁而有效:
- 对于近距离的桶(如前几个桶):随机保留固定数量的体素(例如,每桶保留约1000个)。因为近处物体体素密集,大量是冗余的,随机保留一部分足以表征其几何形状。
- 对于远距离的桶:保留桶内的所有体素。因为远处物体本身点云就稀疏,每一个体素都可能包含关键的形状线索,丢弃任何一点都可能导致信息丢失。
通过这种策略,可以丢弃掉近处约90%的冗余体素,而几乎完整保留了远处的所有信息。实验表明,这一操作能将网络前向推理速度提升近2倍,同时精度损失极小。
import torch
import numpy as np
def stochastic_voxel_discard_by_bin(voxel_coords, voxel_features, ranges, bin_edges, keep_near_num):
"""
模拟StVD的基于距离桶的采样过程。
Args:
voxel_coords: (N, 3) 体素网格坐标
voxel_features: (N, C) 体素特征
ranges: 每个体素到原点的距离 (N,)
bin_edges: 距离桶的边界,例如 [0, 10, 20, ..., 80]
keep_near_num: 对近距离桶,每个桶保留的体素数量
Returns:
kept_coords, kept_features
"""
kept_indices = []
# 将体素分配到各个距离桶
bin_indices = np.digitize(ranges.cpu().numpy(), bin_edges) - 1
num_bins = len(bin_edges) - 1
for bin_idx in range(num_bins):
mask = (bin_indices == bin_idx)
indices_in_bin = np.where(mask)[0]
if len(indices_in_bin) == 0:
continue
# 判断是否为近距离桶(例如前30%的桶)
if bin_idx < int(num_bins * 0.3):
# 近距离桶:随机保留固定数量
if len(indices_in_bin) > keep_near_num:
selected = np.random.choice(indices_in_bin, size=keep_near_num, replace=False)
else:
selected = indices_in_bin
else:
# 远距离桶:全部保留
selected = indices_in_bin
kept_indices.extend(selected.tolist())
kept_indices = torch.LongTensor(kept_indices)
return voxel_coords[kept_indices], voxel_features[kept_indices]
# 示例调用
# 假设已有体素坐标和特征
# kept_coords, kept_features = stochastic_voxel_discard_by_bin(voxel_coords, voxel_features, ranges, bin_edges, keep_near_num=1000)
2.2 网络层内的随机丢弃与数据增强
除了输入端的采样,StVD还将“丢弃”作为一种数据增强手段,应用于网络训练过程中的每个VirConv块。在每个块的前向传播中,会以一定的概率(如15%)随机丢弃一部分非空的体素。
注意:此处的“丢弃”并非真正移除数据,而是在计算该层的稀疏卷积时,将被丢弃体素的激活置零或将其视为“空”体素,使其不参与卷积运算。
这一做法的妙处在于,它在训练阶段主动构造了各种稀疏程度的点云样本,相当于让模型见识了从极度稠密到相对稀疏的各种情况。这极大地增强了模型对于输入点云密度变化的鲁棒性。在实际部署中,即使遇到比训练集更稀疏的点云(例如传感器部分遮挡或极端天气),模型也能保持稳定的性能。
3. NRConv:在2D图像空间中为3D特征“降噪”
NRConv(Noise-Resistant Submanifold Convolution)是VirConv在精度提升上的核心贡献。它创新地将2D图像的空间上下文信息引入到3D稀疏卷积中,从而让网络能够“看见”噪声在图像上的分布模式。
3.1 从3D体素到2D投影的坐标变换
NRConv的第一步,是将3D体素的坐标映射回2D图像平面。这个过程需要精确的传感器标定和坐标变换。
- 体素坐标到3D世界坐标:根据体素化的参数(如体素大小、点云范围),将体素的网格索引转换为3D空间中的实际坐标。
- 考虑数据增强的逆变换:在训练时,点云通常会经过随机旋转、平移、缩放等增强。为了将体素对齐到原始的、与图像对应的坐标系,需要对这些增强变换进行逆操作。
- 3D到2D投影:利用相机内参和外参(LiDAR到相机的变换矩阵),将3D世界坐标投影到2D图像像素坐标。
def project_voxels_to_image(voxel_grid_coords, voxel_size, point_cloud_range, aug_transform, cam_intrinsic, lidar2cam):
"""
将体素网格坐标投影到2D图像平面。
Args:
voxel_grid_coords: (N, 3) 体素在网格中的索引 (z, y, x)
aug_transform: 数据增强的变换矩阵(4x4),需要对其求逆
cam_intrinsic: 相机内参矩阵 (3,3)
lidar2cam: LiDAR坐标系到相机坐标系的变换矩阵 (4,4)
Returns:
img_coords: (M, 2) 有效投影的像素坐标 (u, v), M <= N
valid_mask: (N,) 布尔张量,指示哪些投影是有效的(在图像范围内)
"""
# 1. 体素索引 -> 3D LiDAR坐标 (中心点)
# 公式: 真实坐标 = 点云起始范围 + (索引 + 0.5) * 体素大小
voxel_centers = (voxel_grid_coords.float() + 0.5) * voxel_size + torch.tensor(point_cloud_range[:3], device=voxel_grid_coords.device)
# 转换为齐次坐标 (N, 4)
ones = torch.ones((voxel_centers.shape[0], 1), device=voxel_centers.device)
points_lidar_homo = torch.cat([voxel_centers, ones], dim=1) # (N, 4)
# 2. 逆数据增强变换: 回到原始LiDAR坐标系
# aug_transform 是施加到原始点云上的变换
inv_aug = torch.inverse(aug_transform)
points_original_lidar = torch.matmul(points_lidar_homo, inv_aug.T)
# 3. LiDAR坐标系 -> 相机坐标系
points_cam_homo = torch.matmul(points_original_lidar, lidar2cam.T) # (N, 4)
# 4. 相机坐标系 -> 图像像素坐标系
# 首先过滤掉相机后面的点 (z <= 0)
valid_depth = points_cam_homo[:, 2] > 0.1
points_cam = points_cam_homo[valid_depth, :3] # (M, 3)
# 投影
points_img_homo = torch.matmul(points_cam, cam_intrinsic.T) # (M, 3)
img_coords = points_img_homo[:, :2] / points_img_homo[:, 2:3] # (M, 2), 除法广播
# 5. 检查是否在图像边界内
height, width = 375, 1242 # KITTI图像尺寸
valid_u = (img_coords[:, 0] >= 0) & (img_coords[:, 0] < width)
valid_v = (img_coords[:, 1] >= 0) & (img_coords[:, 1] < height)
valid_uv = valid_u & valid_v
final_img_coords = img_coords[valid_uv].round().long()
# 构建完整的有效掩码
full_valid_mask = torch.zeros(voxel_grid_coords.shape[0], dtype=torch.bool, device=voxel_grid_coords.device)
# 需要将 valid_depth 和 valid_uv 的索引映射回原始索引
# 此处简化处理,实际需仔细索引映射
# ...
return final_img_coords, full_valid_mask
3.2 双路径特征编码与融合
获取了每个3D体素对应的2D像素坐标后,NRConv并行执行两条特征提取路径:
- 3D几何路径:使用标准的3D子流形稀疏卷积(例如3x3x3卷积核)在原始的3D体素网格上操作。这一步专注于提取局部三维几何结构特征,例如表面的曲率、法向等。
- 2D上下文路径:在2D图像平面上,以投影得到的像素坐标为中心,构建一个2D邻域(例如3x3)。然后,将投影到同一个2D像素位置(或其邻域)的所有3D体素特征进行聚合(通常使用最大池化),得到一个代表该2D位置的特征。接着,对这个2D特征图使用2D子流形卷积进行编码。这一步的关键在于,它让3D体素特征能够“看到”其在2D图像上的上下文。噪声点由于其深度估计错误,在2D图像上往往与真实的物体边缘、纹理模式不协调,这种不协调性可以被2D卷积核捕捉到。
最后,将3D路径和2D路径输出的特征在通道维度上进行拼接,形成最终的抗噪声特征。
import spconv.pytorch as spconv
class NRConv(spconv.SubMConv3d):
"""
简化的NRConv层实现思路。
实际NRConv需要自定义算子,这里展示其概念流程。
"""
def __init__(self, in_channels, out_channels, kernel_size=3, image_feat_map=None, img_coords_map=None):
super().__init__(in_channels, out_channels, kernel_size, indice_key='nr_conv')
# 假设 image_feat_map 是来自2D backbone的图像特征图
# img_coords_map 是3D体素到2D像素坐标的映射关系
self.img_feat_map = image_feat_map
self.img_coords_map = img_coords_map
# 一个简单的2D卷积,用于处理投影后的特征
self.conv2d = torch.nn.Conv2d(in_channels, out_channels//2, kernel_size=3, padding=1)
def forward(self, input):
# 1. 标准的3D子流形卷积路径
features_3d = super().forward(input) # 输出通道为 out_channels//2
# 2. 2D上下文路径
batch_size = input.batch_size
all_2d_feats = []
for b in range(batch_size):
# 获取属于当前batch的体素索引和特征
batch_mask = input.indices[:, 0] == b
indices_b = input.indices[batch_mask]
feats_b = input.features[batch_mask] # (N_b, in_channels)
# 根据 img_coords_map 获取这些体素对应的2D像素坐标
# coords_2d_b = self.img_coords_map[b][indices_b] # (N_b, 2)
# 简化:从图像特征图中通过双线性插值采样对应位置的特征
# sampled_img_feat = F.grid_sample(self.img_feat_map[b].unsqueeze(0),
# coords_2d_b.unsqueeze(0).unsqueeze(0),
# align_corners=False).squeeze().T # (N_b, C_img)
# 这里用一个占位符代替复杂的采样和2D卷积过程
# 实际中,需要将投影到同一2D邻域的体素特征先做池化,再应用2D卷积
# pooled_2d_feat = pool_features_by_2d_grid(feats_b, coords_2d_b)
# feat_2d = self.conv2d(pooled_2d_feat)
# 假设我们得到了2D路径的特征 feat_2d_b
feat_2d_b = torch.randn(feats_b.shape[0], self.out_channels//2, device=feats_b.device) # 模拟
all_2d_feats.append(feat_2d_b)
# 合并批次并拼接特征
features_2d = torch.cat(all_2d_feats, dim=0) # (N_total, out_channels//2)
# 3. 特征拼接
combined_features = torch.cat([features_3d.features, features_2d], dim=1) # (N_total, out_channels)
# 返回新的稀疏张量
output = spconv.SparseConvTensor(combined_features, features_3d.indices,
features_3d.spatial_shape, features_3d.batch_size)
return output
这种设计使得NRConv能够同时利用3D空间的几何精确性和2D图像的丰富上下文,在不增加显式去噪模块的情况下,隐式地学习到抑制噪声的特征表示。
4. 构建实战:从VirConv-L到VirConv-T的演进
基于VirConv算子,研究者构建了三个不同侧重点的检测器,体现了该基础模块的灵活性与强大潜力。
4.1 VirConv-L:追求极致的效率
VirConv-L采用了早期融合策略,目标是实现高速推理。其流程非常直接:
- 数据准备:将原始LiDAR点云
P和由图像生成的虚拟点云V直接合并为P*。为了区分点源,可以为每个点添加一个通道标识(例如,LiDAR点为1,虚拟点为0)。 - 主干网络:使用VirConvNet完全替换掉Voxel-RCNN等检测器原有的稀疏卷积主干。VirConvNet由多个VirConv块堆叠而成,每个块依次执行StVD、NRConv和常规的3D稀疏卷积(带下采样)。
- 检测头:沿用成熟的二级检测头(如Voxel-RCNN的RPN和RoI Head),对VirConvNet提取的BEV特征或体素特征进行目标分类与边框回归。
这种设计的优势在于管道简洁,计算集中。由于在输入端和网络内部都应用了StVD,计算量得到大幅削减。论文中报告VirConv-L在KITTI数据集上仅用56毫秒即可完成一帧推理,同时保持了85%以上的AP,在速度-精度权衡上表现卓越。
4.2 VirConv-T:攀登精度的巅峰
VirConv-T则转向后期融合,并集成了两种先进的优化策略——多变换(TED) 与多阶段细化(CasA),旨在冲击最高精度。
其核心流程更为复杂:
- 多变换数据增强:对同一帧数据,应用多种不同的几何变换(如不同的旋转、翻转),生成多个增强视图。关键点在于,所有变换共享同一个主干网络的权重。
- 双分支特征提取:LiDAR点云
P和虚拟点云V分别通过不同的主干网络(例如,P用标准的VoxelNet,V用VirConvNet)进行特征编码。 - 多阶段级联细化:
- 阶段一:在第一个变换视图下,使用RPN生成初步的3D提案(Proposals)。
- 阶段二:将提案投影到其他变换视图下的特征图上,分别提取RoI特征并进行细化。
- 阶段三:融合来自不同变换视图、不同细化阶段的RoI特征(例如,通过边框投票机制),最终经过NMS得到检测结果。
下表对比了VirConv-L和VirConv-T的主要设计差异:
| 特性 | VirConv-L (高效版) | VirConv-T (高精度版) |
|---|---|---|
| 融合策略 | 早期融合 (点级别) | 后期融合 (特征级别) |
| 主干网络 | 单VirConvNet | 双分支 (VoxelNet + VirConvNet) |
| 核心优化 | StVD加速 | 多变换(TED) + 多阶段细化(CasA) |
| 计算开销 | 低 | 高 |
| 精度目标 | 实时性优先 | 极限精度优先 |
| 适用场景 | 车载嵌入式平台 | 离线评测、算法研究 |
VirConv-T通过这种复杂的、集成式的设计,充分挖掘了多模态和多视图信息的潜力,最终在KITTI测试集上达到了86.3%的AP,位列榜单第二。而在此基础上,结合半监督学习(VirConv-S),利用大量无标签数据生成伪标签进行训练,更是将性能推至87.2%的榜首水平。
5. 代码实战关键:实现自定义的稀疏卷积算子
想要在现有框架(如OpenPCDet, MMDetection3D)中复现或借鉴VirConv的思想,最大的挑战在于实现自定义的稀疏卷积算子。下面以PyTorch和spconv库为例,探讨几个关键实现细节。
环境准备与依赖:
# 基础环境
pip install torch torchvision
# 稀疏卷积库,注意版本兼容性
pip install spconv-cu113 # 对应CUDA 11.3,请根据自身环境选择
pip install open3d numpy
自定义NRConv层的骨架代码:
虽然spconv提供了基础的SubMConv3d,但NRConv需要访问2D图像特征和坐标映射关系。这通常需要修改底层C++/CUDA内核以实现高效计算。这里给出一个在Python层进行概念验证的简化流程:
import torch
import torch.nn as nn
import torch.nn.functional as F
import spconv.pytorch as spconv
from typing import Dict, Tuple
class VirtualSparseConvBlock(spconv.SparseModule):
"""一个完整的VirConv块,包含StVD、NRConv和下采样卷积。"""
def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, drop_rate=0.15):
super().__init__()
self.drop_rate = drop_rate # Layer StVD的丢弃率
# NRConv层 (此处用标准SubMConv3d示意,实际需实现双路径)
self.nr_conv = spconv.SubMConv3d(in_channels, out_channels, kernel_size, bias=False, indice_key='subm')
self.norm1 = nn.BatchNorm1d(out_channels)
self.relu1 = nn.ReLU()
# 下采样卷积层
self.downsample_conv = spconv.SparseConv3d(out_channels, out_channels*2, kernel_size=3,
stride=stride, padding=1, bias=False, indice_key='spconv')
self.norm2 = nn.BatchNorm1d(out_channels*2)
self.relu2 = nn.ReLU()
def stochastic_voxel_dropout(self, x: spconv.SparseConvTensor):
"""模拟Layer StVD:在训练时随机丢弃部分非空体素。"""
if self.training and self.drop_rate > 0:
indices = x.indices
features = x.features
# 生成丢弃掩码
num_voxels = indices.shape[0]
drop_mask = torch.rand(num_voxels, device=features.device) > self.drop_rate
# 创建新的稀疏张量,丢弃的体素不参与计算(通过索引过滤实现)
new_indices = indices[drop_mask]
new_features = features[drop_mask]
# 注意:丢弃后,稀疏张量的空间结构(spatial_shape)不变,但非空体素集合改变
# 实际实现中,需要更精细地处理稀疏张量的索引重建
# 此处为概念展示
return spconv.SparseConvTensor(new_features, new_indices, x.spatial_shape, x.batch_size)
return x
def forward(self, x, image_feats=None, voxel_to_pixel_map=None):
# 1. 应用Layer StVD (训练时)
x = self.stochastic_voxel_dropout(x)
# 2. 通过NRConv进行特征编码
# 注意:此处简化了NRConv,实际需要传入image_feats和映射关系
identity = x
out = self.nr_conv(x)
out = out.replace_feature(self.norm1(out.features))
out = out.replace_feature(self.relu1(out.features))
# 3. 下采样
out = self.downsample_conv(out)
out = out.replace_feature(self.norm2(out.features))
out = out.replace_feature(self.relu2(out.features))
return out
集成到现有检测框架:
将上述自定义模块集成到如Voxel-RCNN的流程中,主要需要修改其体素特征编码部分(通常位于MiddleExtractor)。
class VirConvMiddleExtractor(nn.Module):
"""替换Voxel-RCNN中的MiddleExtractor,使用VirConv块。"""
def __init__(self, input_channels, output_channels):
super().__init__()
self.conv1 = spconv.SparseSequential(
spconv.SubMConv3d(input_channels, 16, 3, indice_key='subm0'),
nn.BatchNorm1d(16),
nn.ReLU()
)
# 使用多个VirConv块
self.virconv_block1 = VirtualSparseConvBlock(16, 32, stride=2)
self.virconv_block2 = VirtualSparseConvBlock(64, 64, stride=2) # 注意通道数变化
self.virconv_block3 = VirtualSparseConvBlock(128, 128, stride=2)
def forward(self, voxel_features, voxel_coords, batch_size, **kwargs):
# 初始稀疏张量构造
input_sp_tensor = spconv.SparseConvTensor(
voxel_features, voxel_coords.int(),
self.sparse_shape, batch_size
)
# 通过主干网络
x = self.conv1(input_sp_tensor)
x = self.virconv_block1(x, kwargs.get('image_feats'), kwargs.get('voxel_to_pixel'))
x = self.virconv_block2(x, kwargs.get('image_feats'), kwargs.get('voxel_to_pixel'))
x = self.virconv_block3(x, kwargs.get('image_feats'), kwargs.get('voxel_to_pixel'))
# 转换为密集的BEV特征图供后续RPN使用
spatial_features = x.dense().permute(0, 1, 3, 2) # 调整维度顺序
return spatial_features
实现过程中的几个坑点需要特别注意:
- 坐标映射的维护:NRConv需要精确的3D体素到2D像素的映射表。这个映射需要在数据预处理阶段提前计算好,并以查找表或张量的形式传递给网络。在数据增强(如随机翻转)时,这个映射也需要同步变换。
- 稀疏张量的索引一致性:StVD的随机丢弃会改变非空体素的集合,导致稀疏张量的
indices发生变化。后续所有稀疏卷积层的indice_key需要妥善管理,确保在推理时能够重用预计算的规则,否则会严重影响性能。 - 2D特征对齐:从图像主干网络(如ResNet)提取的2D特征图,需要与投影后的体素坐标在空间上精确对齐。双线性插值是常用的方法,但要处理好坐标归一化和越界问题。
VirConv的成功并非仅仅源于某个孤立的创新点,而在于它针对多模态3D检测中的核心矛盾——密度与噪声——提出了一套系统性的、可解释的解决方案。StVD和NRConv像是一对默契的搭档,一个从数据层面精简计算,一个从特征层面净化信息。对于从事自动驾驶感知研发的工程师而言,理解其背后的设计哲学,比单纯复现其网络结构更为重要。它启示我们,在追求更高性能的路上,有时需要跳出单一模态或单一空间的思维定式,通过巧妙的跨模态信息交互,来解锁那些隐藏在数据深处的关键线索。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)