资讯动态

PV-RCNN 3D目标检测实战:从体素到点的两阶段融合与复现指南

发布时间:2026/10/14 0:09:54 来源:尧图企业网站定制
1. 为什么PV-RCNN值得单独写一篇阅读笔记3D目标检测这个方向只要你在自动驾驶感知、机器人环境理解或者点云处理这几个圈子里待过一阵子就一定绕不开PV-RCNN这个名字。它算是把**点云体素化Voxel-based和点云直接处理Point-based**两条技术路线真正捏合到一起的代表作之一在KITTI、Waymo这些公开数据集上刷出过相当漂亮的成绩也是后续很多3D检测工作的基线。我最初读这篇论文的时候最大的困惑不是网络结构本身而是它为什么要这么设计——明明Voxel-based方法已经够快了Point-based方法精度也够高了为什么还要费劲把两者拼起来后来自己动手复现、跑通、调参踩了不少坑之后才慢慢理解PV-RCNN的核心价值不在于“堆模块”而在于它解决了一个非常具体的工程问题如何在保留体素卷积高效性的同时把细粒度的点云信息无损地喂给后续的refinement阶段。这篇笔记适合谁看如果你正在入门3D目标检测想找一个结构清晰、思路完整、代码可复现的baselinePV-RCNN是个很好的切入点如果你已经在做点云相关项目想理解“两阶段检测器”在3D场景下到底怎么落地这篇笔记里的细节拆解和实操心得应该能帮你少走一些弯路。我会尽量把论文里一笔带过的部分补全把我在复现过程中遇到的真实问题和解决思路都摊开讲。2. 整体架构拆解两条路线是怎么缝合的2.1 从Voxel到Point的动机分析先说清楚一个前提3D点云和2D图像最大的区别在于稀疏性和无序性。一张1080P的图像有200多万个像素排列整齐卷积核滑过去就行但一帧激光雷达点云可能只有几万个点撒在三维空间里密度极不均匀近处密密麻麻远处稀稀拉拉。Voxel-based方法比如VoxelNet、SECOND的思路很直接把三维空间切成规则的小格子voxel每个格子里如果有多个点就用一个小的PointNet或者简单的MLP提取特征然后整个体素网格就可以当成3D图像来做3D卷积。这样做的好处是计算效率高因为3D卷积有成熟的优化实现而且体素化之后数据是规则排列的GPU利用率很高。但问题也很明显量化误差。一个体素假设是0.05m×0.05m×0.1m那么落在这个格子里的所有点都被“平均”成了一个特征向量点的精确空间位置信息就丢了。对于大目标比如卡车影响不大但对于行人、自行车这类小目标量化误差可能是致命的。Point-based方法比如PointRCNN、PointNet则直接在每个点上做操作通过最远点采样FPS和球查询Ball Query来聚合邻域特征。这样做保留了原始点的精确坐标精度上限更高但计算量随点数增长很快而且FPS采样本身就很耗时很难做到实时。PV-RCNN的聪明之处在于它不纠结选哪条路而是让两条路各干各擅长的事。体素分支负责高效地提取多尺度特征并生成高质量的3D候选框proposal点分支负责在候选框内部做精细的特征聚合和框回归。两者通过一个叫**Voxel Set AbstractionVSA**的模块连接起来把体素特征“投影”到关键点上让点分支也能享受到体素卷积的感受野。2.2 整体流水线概览整个PV-RCNN的流水线可以分成四个阶段体素特征编码原始点云先体素化每个非空体素内的点通过一个小型PointNet提取特征得到一个体素特征图。3D稀疏卷积主干用3D稀疏卷积Sparse Convolution对体素特征图进行下采样得到不同尺度的特征图比如1×、2×、4×、8×下采样。这一步和SECOND基本一致。Voxel Set Abstraction这是PV-RCNN的第一个核心创新。从下采样后的体素特征图中通过FPS选取一组关键点比如2048个然后对每个关键点在多个尺度上做球查询把邻域内的体素特征聚合过来形成关键点的特征向量。RoI-grid Pooling与Refinement这是第二个核心创新。对于第一阶段生成的每个3D候选框在框内均匀采样一组网格点比如6×6×6216个然后用类似VSA的方式从关键点特征和体素特征中聚合信息最后通过一个小的MLP预测最终的框偏移和置信度。注意这里的关键点keypoints和网格点grid points是两个不同的概念。关键点是全局撒的用来“浓缩”整个场景的信息网格点是每个候选框内部撒的用来“浓缩”这个框内部的信息。论文里把前者叫VSA后者叫RoI-grid Pooling但本质上都是“在连续空间里采样一组点然后从离散的特征图里聚合信息”。2.3 为什么这样设计有效我自己的理解是PV-RCNN解决了一个信息瓶颈问题。在它之前的两阶段方法比如PointRCNN第一阶段生成的候选框只包含一个粗略的位置和尺寸第二阶段要从原始点云里重新提取特征。但原始点云是没有语义信息的你只能靠PointNet自己去学学出来的特征质量很不稳定。PV-RCNN的做法是第一阶段不仅生成候选框还生成了一组带有丰富语义信息的关键点特征。这些关键点特征是从体素卷积主干里来的已经经过了多尺度感受野的聚合包含了局部几何和全局上下文。第二阶段做refinement的时候直接从这些关键点特征里查询相当于站在巨人的肩膀上自然更容易回归准确。另一个设计亮点是多尺度特征融合。VSA模块不是只从一个尺度的特征图里查询而是从1×、2×、4×、8×四个尺度都查一遍然后把结果拼起来。这样做的好处是关键点既能感知到细粒度的局部形状来自高分辨率特征图又能感知到大范围的上下文来自低分辨率特征图。对于区分“站着的人”和“电线杆”这种任务局部形状很重要对于区分“卡车”和“公交车”这种任务全局上下文更重要。3. 核心模块深度解析与实操要点3.1 Voxel Set Abstraction的细节与参数选择VSA模块是PV-RCNN的灵魂但论文里对它的描述比较简略很多细节需要看代码才能理解。我结合自己的复现经验把关键点拆开讲。第一步关键点采样。论文里用的是FPS最远点采样从所有体素中心点里选2048个。为什么用FPS而不是随机采样因为FPS能保证采样出来的点在整个空间里分布均匀不会出现某个区域密集、某个区域空白的情况。但FPS的缺点是慢2048个点从几万个体素中心里选在GPU上大概要几毫秒对于实时性要求高的场景是个负担。我试过用随机采样替代发现精度会掉1-2个点所以如果追求精度还是老老实实用FPS。第二步球查询。对于每个关键点在半径r内找邻域体素。论文里用了多个半径比如0.4m、0.8m、1.2m等对应不同的特征图尺度。这里有个经验半径的设置要和体素大小匹配。如果体素大小是0.05m那么0.4m半径大概覆盖8个体素这个感受野对于捕捉局部形状是合适的如果半径设成0.1m可能只覆盖2个体素信息量太少。第三步特征聚合。对于查询到的邻域体素特征论文里用的是PointNet-style的聚合先对每个邻域特征做MLP然后取max。这里有个细节是否要加入相对位置信息论文里是加了的把邻域体素中心相对于关键点的坐标差也拼到特征里。这个操作很重要因为如果不加位置信息关键点就不知道邻域体素在它的哪个方向对于回归框的方向角会有影响。# 伪代码示意VSA的核心逻辑 def voxel_set_abstraction(keypoints, voxel_features, voxel_coords, radii, nsamples): keypoints: (N, 3) 关键点坐标 voxel_features: (M, C) 体素特征 voxel_coords: (M, 3) 体素中心坐标 radii: list of float, 每个尺度的查询半径 nsamples: list of int, 每个尺度采样的邻域点数 aggregated_features [] for r, n in zip(radii, nsamples): # 球查询找到每个关键点半径r内的体素 idx ball_query(r, n, keypoints, voxel_coords) # 获取邻域特征 neigh_features voxel_features[idx] # (N, n, C) # 计算相对位置 neigh_coords voxel_coords[idx] # (N, n, 3) rel_coords neigh_coords - keypoints.unsqueeze(1) # (N, n, 3) # 拼接特征和相对位置 combined torch.cat([neigh_features, rel_coords], dim-1) # MLP max pooling feat mlp(combined) # (N, n, C) feat feat.max(dim1)[0] # (N, C) aggregated_features.append(feat) # 多尺度拼接 return torch.cat(aggregated_features, dim-1)实操心得球查询的实现方式对性能影响很大。如果直接用PyTorch的广播机制做暴力查询2048个关键点×几万个体素显存直接爆炸。建议用CUDA写自定义kernel或者用现成的点云库比如OpenPCDet里的实现。我在复现时一开始偷懒用了暴力查询结果batch size只能设到1后来换成优化实现batch size能上到8。3.2 RoI-grid Pooling的设计哲学第一阶段结束后我们有了若干个3D候选框通常保留300个左右。传统做法是把每个候选框里的原始点拿出来重新跑一遍PointNet。但PV-RCNN不这么做它在候选框内部均匀撒网格点然后从关键点特征和体素特征里查询信息。为什么用网格点而不是原始点我理解有两个原因第一原始点在候选框内的分布是不均匀的可能全挤在一边导致特征聚合有偏第二网格点是规则排列的可以更好地覆盖整个候选框的空间对于回归框的尺寸和中心偏移更有利。具体操作对于每个候选框把它归一化到一个单位立方体里然后按6×6×6的网格采样216个点。每个网格点的坐标是固定的不随框的内容变化。然后对于每个网格点用类似VSA的方式从关键点特征半径0.4m和体素特征半径0.8m里查询邻域信息聚合后得到一个特征向量。最后把所有网格点的特征拼起来通过一个共享的MLP再max pooling得到这个候选框的最终特征。这里有个关键参数网格大小。论文里用的是6×6×6我试过4×4×4和8×8×8。4×4×4的精度会掉一点因为空间分辨率不够8×8×8的精度提升很有限但计算量翻倍。所以6×6×6是个比较平衡的选择。另外网格点的查询半径也要和候选框的尺寸挂钩。如果候选框很大比如卡车0.4m的半径可能只覆盖框内很小一块区域信息聚合不充分。论文里的做法是根据框的尺寸动态调整半径但代码里其实是用固定半径加多尺度来近似。3.3 损失函数与训练策略PV-RCNN的损失函数分两部分第一阶段的RPN损失和第二阶段的refinement损失。RPN损失和SECOND基本一致包括分类损失focal loss和框回归损失smooth L1。Refinement损失也是分类回归但回归的目标是相对于第一阶段候选框的偏移量而不是绝对坐标。这里有个训练技巧第一阶段和第二阶段是联合训练的但梯度不回传。也就是说第二阶段的损失不会去更新第一阶段的权重。这样做是为了避免训练不稳定因为第一阶段的候选框质量在训练初期很差如果让第二阶段的梯度回传会把第一阶段带偏。我试过让梯度回传发现收敛很慢而且最终精度还不如不回传。另一个技巧是数据增强。PV-RCNN用了大量的数据增强包括全局旋转、全局缩放、全局平移、以及针对每个ground truth box的局部旋转和平移。局部增强很重要因为它模拟了同一个目标在不同朝向下的样子对于方向角的回归很有帮助。我在复现时一开始只做了全局增强结果方向角的误差很大后来加上局部增强方向角的精度提升了大概15%。4. 完整实操流程与核心环节实现4.1 环境准备与依赖安装我复现PV-RCNN用的是OpenPCDet这个框架它把PV-RCNN、SECOND、PointRCNN等主流方法都集成好了省去了很多造轮子的时间。环境配置如下Ubuntu 20.04CUDA 11.3 cuDNN 8.2PyTorch 1.10Python 3.8spconv 2.x稀疏卷积库安装步骤# 创建虚拟环境 conda create -n pvrcnn python3.8 conda activate pvrcnn # 安装PyTorch pip install torch1.10.0cu113 torchvision0.11.0cu113 -f https://download.pytorch.org/whl/torch_stable.html # 安装spconv pip install spconv-cu113 # 安装OpenPCDet git clone https://github.com/open-mmlab/OpenPCDet.git cd OpenPCDet pip install -r requirements.txt python setup.py develop注意spconv的版本和CUDA版本必须严格匹配否则编译会报错。我一开始装了spconv 1.x结果和PyTorch 1.10不兼容折腾了半天才换成2.x。另外OpenPCDet的requirements.txt里有些包的版本比较老如果直接pip install可能会和现有环境冲突建议用conda单独建环境。4.2 数据准备与预处理我用的是KITTI数据集需要下载并整理成OpenPCDet要求的格式。KITTI的原始数据包括点云velodyne、图像image、标定文件calib和标签label。OpenPCDet提供了一个脚本来自动整理python -m pcdet.datasets.kitti.kitti_dataset create_kitti_infos tools/cfgs/dataset_configs/kitti_dataset.yaml这个脚本会生成几个.pkl文件包含训练集和验证集的索引信息。这里有个坑KITTI的标签里有些类别是“DontCare”这些样本在训练时应该忽略但OpenPCDet默认会把它当成背景类。我一开始没注意结果模型在DontCare区域也学了一些乱七八糟的东西后来在配置里把DontCare加到ignore_class里才解决。数据预处理还包括点云体素化。PV-RCNN的体素大小是0.05m×0.05m×0.1m点云范围是[0, -40, -3, 70.4, 40, 1]单位米。这个范围是KITTI的典型设置覆盖了前方70米、左右各40米、高度从-3米到1米。如果你的应用场景不同比如需要检测更远的目标可以把范围调大但体素数量会指数增长显存和计算量都会上去。4.3 模型配置与关键参数OpenPCDet的配置文件在tools/cfgs/kitti_models/pv_rcnn.yaml。我挑几个关键参数讲一下MODEL: NAME: PVRCNN VFE: NAME: PillarVFE # 体素特征编码PV-RCNN用的是PillarVFE的变体 NUM_FILTERS: [64] # 每个体素输出的特征维度 BACKBONE_3D: NAME: VoxelBackBone8x # 3D稀疏卷积主干 LAYER_NUMS: [1, 2, 2, 2] # 每个stage的卷积层数 LAYER_STRIDES: [1, 2, 2, 2] # 每个stage的下采样倍数 VOXEL_SET_ABSTRATION: NUM_KEYPOINTS: 2048 # 关键点数量 RADII: [[0.4], [0.8], [1.2], [1.6]] # 四个尺度的查询半径 NSAMPLES: [[16], [16], [16], [16]] # 每个尺度的采样点数 ROI_HEAD: GRID_SIZE: [6, 6, 6] # RoI网格大小 NUM_SAMPLED_POINTS: 216 # 6*6*6 RADII: [[0.4], [0.8]] # 两个尺度的查询半径实操心得NUM_KEYPOINTS设成2048是个经验值。我试过1024精度掉得不多但速度提升明显试过4096精度提升很小但显存占用翻倍。如果你的场景对实时性要求高可以降到1024如果追求极致精度可以上到4096但要确保显存够用。4.4 训练过程与显存优化训练命令python train.py --cfg_file tools/cfgs/kitti_models/pv_rcnn.yaml --batch_size 4 --epochs 80PV-RCNN的显存占用比较大batch size4在RTX 309024GB上大概占20GB。如果显存不够有几个优化方向减小体素范围把点云范围从70米缩到50米体素数量减少约30%。降低关键点数量从2048降到1024VSA模块的显存占用减半。使用混合精度训练OpenPCDet支持AMP开启后显存占用减少约40%速度提升约20%。我实测下来混合精度训练对精度的影响很小掉0.5个点以内但显存和速度的收益很大强烈推荐开启。训练过程中要监控几个指标分类损失、回归损失、以及验证集上的3D mAP。PV-RCNN在KITTI验证集上的典型收敛曲线是前10个epoch损失下降很快20-40个epoch逐渐平稳40个epoch之后基本收敛。如果发现验证集mAP不升反降可能是过拟合了可以增加数据增强或者加dropout。5. 常见问题与排查技巧实录5.1 训练不收敛或loss震荡这是最常见的问题我遇到过好几次。排查思路如下现象可能原因解决方法loss一开始就NaN学习率太大把学习率从0.01降到0.003loss震荡不下降batch size太小增大batch size或减小学习率loss下降但mAP不升过拟合增加数据增强加weight decay第一阶段loss正常第二阶段loss爆炸候选框质量太差先单独训练第一阶段再联合训练我踩过的一个坑是体素化的时候没有做坐标归一化。KITTI的点云坐标范围是[0, 70]米如果直接喂给网络数值太大梯度容易爆炸。正确的做法是把点云坐标减去一个均值或者除以一个尺度因子让数值落在[-1, 1]附近。5.2 显存溢出OOMPV-RCNN的显存占用主要来自三个地方体素特征图、VSA模块的邻域查询、RoI-grid Pooling。如果OOM可以按以下顺序排查检查batch size先从1开始试如果能跑通再往上加。检查体素范围范围越大体素数量越多显存占用越高。检查关键点数量2048个关键点每个关键点在4个尺度上查询16个邻域总共是2048×4×16131072个邻域特征如果每个特征是64维那就是8M个浮点数约32MB。看起来不大但中间变量会翻好几倍。检查RoI数量训练时保留300个候选框每个框216个网格点每个网格点查询2个尺度总共是300×216×2129600个查询和VSA差不多。避坑技巧如果实在跑不起来可以把VSA和RoI-grid Pooling的邻域查询改成在线查询也就是不预先算好所有邻域而是在forward的时候动态查询。这样显存占用会小很多但速度会慢一些。5.3 精度不达标如果训练完了发现mAP比论文里低很多可以从以下几个方面排查数据增强是否一致论文里用了大量的数据增强如果你没做或者做少了精度会差很多。学习率调度是否一致论文用的是cosine annealing如果你用的是step decay最终精度可能差1-2个点。NMS阈值是否合适PV-RCNN用的是旋转NMS阈值设0.01。如果设太大会有很多重复框设太小会漏掉一些真阳性。类别不平衡是否处理KITTI里Car类样本最多Pedestrian和Cyclist很少。如果不用focal loss或者类别权重模型会偏向Car类。我复现的时候第一次跑出来Car的mAP只有75%比论文低了5个点。后来发现是数据增强里的局部旋转没开加上之后Car的mAP到了80%基本和论文持平。5.4 推理速度优化PV-RCNN的推理速度在RTX 3090上大概是10 FPS左右对于实时性要求高的场景比如自动驾驶可能不够。优化方向减少关键点数量从2048降到1024速度提升约30%精度掉0.5个点。减少候选框数量从300降到100速度提升约20%精度掉1个点。使用TensorRT把模型转成TensorRT速度可以提升2-3倍但转换过程比较麻烦尤其是spconv的自定义算子。剪枝和量化对3D稀疏卷积做通道剪枝或者用INT8量化速度提升明显但需要重新训练和校准。我个人的经验是如果追求实时性减少关键点数量减少候选框数量是最简单有效的方案精度损失在可接受范围内。如果追求极致速度那就得上TensorRT但要做好折腾一周的准备。6. 一些零散但重要的经验补充6.1 关于代码复现的几点提醒OpenPCDet的PV-RCNN实现和原论文有一些细微差别。比如论文里的VSA模块用了4个尺度但OpenPCDet里只用了3个论文里的RoI-grid Pooling用了2个尺度OpenPCDet里也是2个但半径不同。这些差别对精度的影响不大但如果你要严格复现论文结果需要自己改配置。另外OpenPCDet的代码更新比较频繁不同版本的配置文件格式可能不一样。我建议fork一份代码到自己仓库固定一个版本避免因为上游更新导致代码跑不通。6.2 关于论文阅读的方法论PV-RCNN这篇论文我读了至少5遍每一遍都有新的收获。第一遍读的时候我只关注了网络结构第二遍读的时候我开始关注损失函数和训练细节第三遍读的时候我开始思考作者为什么这么设计有没有其他方案第四遍读的时候我对照代码看发现了很多论文里没写的细节第五遍读的时候我尝试自己改进比如换不同的关键点采样策略、不同的特征聚合方式。我的建议是读论文不要只读一遍也不要只读一篇。PV-RCNN的思想在后续很多工作里都有体现比如PV-RCNN、Part-A2、CenterPoint等。把相关的论文放在一起读你会发现很多设计是相通的理解也会更深。6.3 关于工程落地的思考PV-RCNN在学术数据集上表现很好但真正落地到产品里还有很多工程问题要解决。比如点云畸变激光雷达在扫描的时候车辆是在运动的所以一帧点云里的点其实不在同一个时刻。如果不做运动补偿高速场景下的检测精度会明显下降。多传感器融合PV-RCNN只用了点云如果能把图像信息也融合进来精度还能再提升。但融合的难点在于标定和时间同步。极端天气雨雪天气下激光雷达的点云会有很多噪声PV-RCNN对这种噪声的鲁棒性还有待验证。这些问题在论文里通常不会涉及但实际做项目的时候一定会遇到。我的经验是不要指望一个模型解决所有问题要根据具体场景做针对性的优化。比如如果你的场景主要是低速园区那点云畸变就不是大问题如果你的场景是高速公路那运动补偿就是必须的。6.4 一个容易被忽略的细节体素特征编码PV-RCNN的体素特征编码用的是PillarVFE也就是把每个体素内的点通过一个简单的MLP然后max pooling。这个操作看起来很简单但有几个细节需要注意体素内点数KITTI里每个体素平均只有2-3个点如果体素大小设得太大一个体素里可能有几十个点max pooling会丢失很多信息。所以体素大小要设得小一点保证每个体素里的点数不要太多。特征维度PillarVFE的输出维度是64这个维度不能太小否则后续的3D卷积提取不到足够的特征也不能太大否则显存和计算量都上去了。坐标归一化体素内点的坐标要相对于体素中心做归一化这样网络才能学到局部几何形状。如果不做归一化点的绝对坐标会引入噪声。这些细节在论文里都是一笔带过但实际写代码的时候每一个都会影响最终精度。我建议你在复现的时候先把这些细节都搞清楚再动手写代码否则调参的时候会很迷茫不知道是哪个环节出了问题。6.5 关于后续改进方向的个人看法PV-RCNN之后3D检测领域又出了很多新工作。我个人比较看好的方向有两个一个是基于Transformer的3D检测比如用注意力机制替代球查询可以更好地捕捉长距离依赖另一个是基于BEVBird‘s Eye View的检测把3D问题转化成2D问题计算效率更高也更容易和图像融合。但不管技术怎么演进PV-RCNN的核心思想——多尺度特征融合、两阶段refinement、体素与点的结合——是不会过时的。理解了这些思想再看新的论文你会发现很多都是在这些基础上做微创新。所以如果你刚开始入门3D检测我建议先把PV-RCNN吃透再去看其他方法会事半功倍。最后再分享一个小技巧如果你在复现PV-RCNN的时候遇到了问题先去GitHub的issue区搜一下大概率已经有人遇到过类似的问题。OpenPCDet的社区很活跃很多坑都已经有人踩过了。如果搜不到再自己排查。排查的时候从数据开始查先确认数据预处理没问题再查模型最后查损失函数。我遇到的大部分问题最后都发现是数据的问题而不是模型的问题。

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

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

免费获取报价 →
↑