拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CenterPoint源码解析:从体素化到两阶段3D目标检测全流程

CenterPoint源码解析:从体素化到两阶段3D目标检测全流程

CenterPoint 在 CVPR 2021 发布之后,几乎成了 3D 目标检测实验里绕不开的 baseline。它最让我舒服的一点,是把 2D 检测里成熟的 center-based 思路搬到了点云上,同时用“点中心预测 + 两阶段优化”解决了稀疏点云带来的边界回归问题。最近我把它的源码通读了一遍,又从 VoxelNet 的体素化流程一路跟到两阶段 refine 的 ROI 聚合,踩了不少坑,也理清了整套实现脉络。这篇文章就从源码角度把 CenterPoint 从输入点云到最终输出的完整链路拆开讲,适合刚接触 3D 检测、准备跑通模型甚至二次开发的朋友。


1. 整体架构:从 VoxelNet 骨干到双阶段检测设计

1.1 为什么选 center-based 而不是 anchor-based

读源码之前,我建议先想清楚一个问题:CenterPoint 为什么要抛弃 3D 检测里统治了很久的 anchor-based 方案?

在 3D 空间里直接铺 anchor 是一件很奢侈的事。2D 图像上 anchor 只需要考虑 (x, y, w, h) 外加类别,到了 3D 场景,你还要管长宽高、绕 Z 轴旋转的角度,某些数据集里甚至要预测速度。空间范围一大,anchor 数量会爆炸,正负样本比例也会变得极其难看。纯点云数据不像图像那样有密集的像素纹理,背景点占了绝大多数,用滑动窗口式 anchor 去扫,大量计算都被浪费在空体素和背景点上。

CenterPoint 的思路和 CenterNet 一脉相承:不枚举候选框,而是让网络在 BEV(鸟瞰图)特征上预测“目标中心在哪里”。一个目标只要中心预测准了,其余属性都可以回归出来,这样就把 3D 检测从“大量候选里筛错框”变成了“先找点再补属性”。从我复现的体验看,这套思路在点云稀疏、遮挡严重的场景下尤其稳,因为中心点即使在遮挡情况下也往往有迹可循,而一个被挡掉大半的 anchor 框很容易让分类头迷失。

1.2 一条数据从点云到输出的完整链路

CenterPoint 的整体流水线并不复杂,对我来说更像是几个成熟模块的漂亮组合。我把它分成五个阶段,每个阶段对应源码里相对独立的部分:

  1. 体素化:把散乱点云分配到离散 voxel 格子中,并完成特征提取,这是 VoxelNet 留下的核心资产。
  2. 稀疏 3D 骨干网络:使用稀疏卷积在三维体素空间里逐步下采样,提取高维语义特征。
  3. BEV 特征生成:把三维稀疏特征压成鸟瞰图形式的密集特征图,方便后续用 2D 卷积继续处理。
  4. 中心点检测头:在 BEV 特征上预测类别热力图、中心偏移、尺寸、朝向、速度等信息,并通过局部峰值提取生成候选 3D 框。
  5. 两阶段优化:针对每个候选框,从原始点云中聚合局部几何特征,预测 IoU 置信度并修正边框参数。

源码里这五步对应的文件也比较清晰。不同开源版本的文件命名略有差异,但功能基本一致,我列一下我参考的实现里常见的对应关系:

模块常见源码位置主要职责
体素编码models/readers/voxel_encoder.py把 voxel 内点云编码成固定维度特征
稀疏 3D 骨干models/backbones/scn.pySparse 3D Conv,逐级下采样
BEV Neckmodels/necks/rpn.py恢复分辨率并融合多尺度特征
检测头models/dense_heads/center_head.pyHeatmap 与回归分支
两阶段优化models/roi_heads/roi_head.pyProposal 内点特征聚合与精修

这个结构说明一个问题:CenterPoint 并不算从零发明新东西,它把 VoxelNet 的体素化思想、CenterNet 的中心点范式、PointNet 类局部特征提取全部串了起来。理解它最好的方式,就是跟着数据 shape 一步步走。


2. 前置处理:VoxelNet 风格的体素化与稀疏编码

2.1 点云体素化参数的选择

体素化是把连续空间划分成规则格子的过程,你可以把它理解成把一整箱乐高积木按格子分类摆放。点云本来是无序、密集程度不均的,体素化之后才可能用规则的卷积处理。

CenterPoint 源码里对体素化的配置主要在数据集配置文件中。我见过比较典型的配置是:体素大小[0.1, 0.1, 0.15]米,检测范围在x0, y0, z0到x1, y1, z1之间,每个体素最多保留 10 个点,训练时最多保留 16000 个体素,推理时最多保留 40000 个体素。不同数据集范围差异很大,这里只是给一个量级感受。

这些参数没有一个是可以随便拍的。体素越小,空间分辨率越高,但 voxel 数量也会上升,稀疏卷积虽然高效,计算量和显存依然会涨。每个体素最多点数设太大,单个特征会被那些距离较远的点污染;设太小,有效信息又可能被丢掉。CenterPoint 的作者并没有把体素化玩出花,而是延续了 VoxelNet 时代已经验证过的配置,这说明对大多数激光雷达场景来说,这些经验值已经足够稳定。

2.2 体素特征编码的实现细节

体素化之后,每个 voxel 里通常有几个到几十个点。问题来了:怎么把这些数量不等的点变成一个固定维度的向量送入网络?

VoxelNet 当年的做法是用一个叫做 Voxel Feature Encoding(VFE)的小网络,对每个体素内的点做逐点全连接、BatchNorm、ReLU,再通过 max pooling 聚合得到体素级特征。CenterPoint 源码里其实做了一定简化,但思想完全一致。核心步骤是这样:

  1. 对体素内每个点,计算它相对于体素中心的偏移量。这一步非常关键,因为原始点云坐标是绝对世界坐标,直接送入网络会让模型难以适应不同位置的相同结构;换成相对坐标后,特征就变“平移不变”了。
  2. 将点云的反射强度等原始属性拼进特征向量。常见做法是把(x, y, z, intensity)扩展为带有体素中心偏移的更高维特征,比如 5 维或 7 维。
  3. 对每个体素内部的点做随机采样,数量不够就重复采样或补零,最终保证输出维度固定。
  4. 使用 MLP + max pooling 得到每个非空体素的编码向量,后续稀疏卷积只在这些非空体素上计算。

我在读这里时踩过一个坑:VFE 输出特征中,如果完全没有包含体素中心偏移,模型在定位上会明显变差。原因是体素区域的绝对位置信息虽然对单个 point 不重要,但对理解目标大致空间位置有帮助。VoxelNet 原始实现里会把体素中心偏移作为额外通道喂给网络,CenterPoint 也沿用了这一点。

另外要提醒的是,体素内点数太少时,均值或中心偏移计算可能不稳定。一个体素里如果只有一个点,它的中心偏移基本就是零;这种情况下特征的主要信息来源就变成了反射强度和绝对坐标。源码里通常会对体素内点做随机排序和采样,避免因为点序固定导致网络学到“第几个点是什么”这种伪规律。


3. 三维稀疏骨干网络与 BEV 生成

3.1 稀疏 3D 骨干 stage 设计

点云体素化之后,绝大多数体素都是空的。如果不对空体素做区分、硬用密集 3D 卷积,计算量会大到完全无法训练。CenterPoint 使用稀疏卷积来只处理非空体素,这是 VoxelNet 和 SECOND 系列网络的核心优势。

稀疏卷积不是魔改,它的逻辑很直观:常规卷积要对每个位置都做一次乘加运算,稀疏卷积则通过一张哈希表只记录非空位置,计算时只对非空位置做卷积,并把结果回填到输出位置。空间利用率上,点云稀疏性越高,稀疏卷积收益越大。

CenterPoint 的 3D 骨干网络通常包含多个 stage,每个 stage 由稀疏卷积下采样和若干子模块组成。常见配置是三个 stage,通道数依次类似[16, 32, 64],每个 stage 内再堆 1 到 4 个基础 block。整个过程把体素分辨率逐级降低,最终输出的 3D 特征图在空间分辨率上大概比原始体素缩小 8 倍。

这个设计延续了 VoxelNet 的多分辨率思想:低层 stage 捕捉小尺度几何细节,高层 stage 拥有更大的感受野,能理解目标的整体结构。读源码时建议重点关注每个 stage 输出的 stride,因为后面 BEV 特征生成时,需要知道当前特征图每个格子对应多少米的实际空间范围。

3.2 生成鸟瞰图特征:把高度维折叠进通道

3D 稀疏卷积输出的特征仍然是三维网格状的,CenterPoint 的检测头却工作在 BEV 平面上。这一步需要把高度维折叠掉。

折叠方式在源码里其实很简单:假设当前 3D 特征形状是(B, C, D, H, W),直接把 D 维移到通道维上,变成(B, C*D, H, W)的密集 BEV 特征。理论上你也可以对高度维做 max pooling 或 average pooling,但我看到的实现大多选择直接 reshape,因为这样能保留每个高度层的语义信息,交给后续 2D 卷积自行决定哪些层更重要。

值得注意的是,直接 reshape 会带来特征图通道数变多,后续网络的计算量也会上升。CenterPoint 的 BEV Neck 部分通常是一个类 FPN 结构:先用 2D 卷积继续下采样,再通过反卷积或上采样恢复空间分辨率,并把不同层级的特征在通道维拼接起来,最终输出一个分辨率较高、语义较丰富的 BEV 特征图。

为什么从 3D 特征转成 2D BEV 是合理的选择?一方面,自动驾驶场景中目标通常在地平面上分布,高度维的信息虽然重要,但不需要保留很高的空间分辨率;另一方面,2D 卷积的成熟组件、预训练模型和调参经验都远比 3D 卷积丰富,后续检测头可以复用大量 CenterNet 的技术积累。CenterPoint 的巧妙之处就在于它没有固执地留在三维空间一直做 3D 卷积,而是尽早转到 BEV,利用低算力代价处理中心点任务。


4. 中心点检测头与第一阶段输出

4.1 Heatmap 构造与 Gaussian Focal Loss

检测头最核心的输入是 BEV 特征图,最核心的输出是类别 heatmap。heatmap 的形状一般是(B, C, H, W),C 是目标类别数,H/W 是 BEV 特征图分辨率。

构造训练标签时,需要把每个 ground truth 3D 目标的中心投影到 BEV 特征图坐标上,以该位置为中心生成一个高斯核。高斯核的半径不是拍脑袋定的,通常取决于目标在 BEV 上的投影尺寸,尺寸越大,高斯圆的半径越大,这是为了保证相邻目标之间的热力分布不会互相干扰。我在中心点检测里踩过的经验是,如果高斯半径算小了,正样本区域太窄,训练初期会出现热力图上一点响应都没有的现象;如果算大了,两个挨得近的目标会被糊成一团,中心点会整体偏移。

CenterPoint 使用的分类损失是 Gaussian Focal Loss,来自 CenterNet。它跟 Focal Loss 的区别在于,它不是把高斯核覆盖区域当作难负样本,而是用高斯值作为软标签:中心点是 1,周围根据高斯衰减逐步降低到 0。这样网络不会对目标边缘的模糊区域产生强烈的错误惩罚,训练会更稳。

读代码时特别注意一个细节:heatmap 头在输出前通常要过 sigmoid,而损失函数里也做了对应的数值处理。如果数据分布很不均衡,某些类别样本特别少,可以考虑对类别做重加权。

4.2 回归分支:尺寸、朝向、速度与中心偏移

有了中心点位置,下一步是为每个中心点回归出完整 3D 框属性。CenterPoint 的回归分支主要输出这几项:

  • 中心偏移:由于 heatmap 的中心点坐标是离散化的格子位置,回归分支需要预测真实中心与格子中心之间的微小偏移。
  • 尺寸:目标的长度、宽度、高度,大多使用 L1 损失直接回归。
  • 朝向:绕 Z 轴的旋转角,CenterPoint 的常见做法是把角度转换为残差形式,与常见的角度分类+回归组合不同,直接使用 sin/cos 或残差角度。
  • Z 轴中心高度:因为 BEV 特征丢失了具体高度,需要单独回归中心点的 z 坐标。
  • 速度:在 nuScenes 这类需要预测运动属性的数据集上,还会输出速度值。

各分支损失加权一般可以在配置里调。我通常从大权重尺寸、中等权重角度、小权重偏移开始,因为尺寸直接决定框的物理大小,对最终 mAP 影响最明显。训练初期如果看到框尺寸偏移特别大,先检查是否是 reg loss 权度过大导致网络优先拟合回归,忽略了 heatmap 分类。

这里容易忽略的是角度回归的周期性问题。角度 0 度和 2π 其实是一样的,但 L1 损失会把它们当成巨大差异。源码里一般会把角度目标转换到[-π, π)区间再计算残差。调试时如果你发现很多框的角度“差了一个 180 度”,往往不是模型坏了,而是角度残差的规范化方法出了问题。

4.3 从 Heatmap 到 3D 候选框:峰值提取与解码

推理阶段,模型在 BEV heatmap 上得到一堆响应值,需要从中找出目标中心。这一步在 CenterNet 里叫 peak extraction,CenterPoint 也完全沿用。

具体做法通常分两步:

  1. 对 heatmap 做一个3x3的 max pooling,等价于只在局部最大值处保留响应。之后把响应值等于池化结果的点视为“峰值候选点”。这样做的目的是去除那些邻近但重复的响应,相当于一个简易的 NMS。
  2. 设定一个分数阈值,从峰值候选点中选出 top-k 个点。每个点对应一个中心位置,结合回归分支预测的尺寸、朝向等参数,解码出 3D 框。

解码过程要看清楚 BEV 特征图坐标与真实点云坐标的换算关系。常见源码逻辑是:特征图格子的像素坐标乘以 stride,再加上量化偏移,得到 BEV 平面上的真实坐标;Z 坐标和中心高度则来自回归分支的预测。这个坐标换算最容易出错,我建议先在单帧数据上可视化一遍,确认输出框和原始点云对齐后再跑全量测试。


5. 两阶段优化:从候选框到精细结果

5.1 为什么还需要第二阶段

很多第一次接触 CenterPoint 的人会问:第一阶段已经生成完整 3D 框了,后面再加一个阶段,有必要吗?

从实验结果看,非常有必要。第一阶段本质上是在 BEV 平面上做中心点检测,它看到的是一张俯视图,物体的高度信息已经被压扁到通道里。这就带来一个天然问题:网络对目标的精细边界、局部几何结构理解不足,尤其是行人、骑行者这类小目标,以及点云稀疏、形状不规则的目标。

第二阶段的思路非常直接:既然第一阶段框已经“大概在那个位置”了,那就把候选框内部的局部点云特征重新捞出来,用一个轻量网络仔细看一遍。这个过程相当于第一阶段的“复核”,重点关注框的边界是否精确、置信度是否合理。

我个人的体会是,两阶段带来的提升不仅体现在 mAP 上,更体现在输出的稳定性上。单阶段输出偶尔会出现置信度虚高但边界明显不对的框;加了两阶段之后,置信度分布更接近真实的 IoU,下游决策模块用起来更放心。

5.2 特征聚合:在候选框内采样并编码局部点云

两阶段的核心代码集中在 ROI Head 里,整个过程可以拆成采样、融合、编码三步。

采样阶段,对于每个候选框,需要从原始点云中提取框内或框附近一定范围内的点。常见做法是每个框采固定数量,比如 256 个点,不足就重复,多了就随机降采样。这里要强调一个细节:不能只取严格落在框内的点,因为第一阶段框的定位还不够准,边界点可能刚好被切掉。一般会对框做一点膨胀,把贴近边界的外部点也纳进来,让网络自己判断哪些点属于目标。

融合阶段是两阶段的精华。每个采样点不光有自己的原始坐标和反射强度,还要从 BEV 特征图里取一个“全局特征”。具体做法是把点投影到 BEV 平面上,通过双线性插值在对应位置的特征图上取值。这样每个点就变成了“局部几何特征 + 全局语义特征”的拼接体。

编码阶段通常会使用 PointNet 类的结构:对每个点做几层 MLP,然后对所有点做 max pooling,得到整个候选框的特征向量。最后接两个分支:一个输出 refine 后的框参数残差,一个输出预测的 IoU 置信度。我刚开始读这里有困惑,为什么不像第一阶段一样直接输出类别置信度,而要去预测 IoU?原因在于第二阶段的核心目标不是“这个框属于哪个类”,而是“这个框有多准”。用预测 IoU 作为最终置信度,能更好地对第一阶段分数进行校准,为后续 NMS 提供更有区分度的排序依据。

5.3 训练与推理中的两阶段配合

两阶段的训练标签不是直接来自 GT 框的类别,而是根据候选框与 GT 框之间的 IoU 来定义。常见做法是:用 GT 框做数据增强扰动,生成一组与 GT 有不同程度重叠的 proposal,然后计算每个 proposal 和对应 GT 的 3D IoU,作为置信度分支的监督目标。IoU 高的是正样本,低的是负样本。

Box refine 分支的监督信号则是 proposal 与 GT 之间的参数残差。计算残差前注意角度要处理好周期问题,尺寸要对数化或直接做差,中心偏移直接 L1。损失函数一般用 Smooth L1,权重可以根据数据集调。

推理时,第二阶段拿第一阶段已经解码出的 top-k 候选框作为输入,输出新的 confidence 和 refined box。源码里通常直接用第二阶段输出的 confidence 替换第一阶段的类别分数,作为最终 NMS 的排序分数。有一种常见错误是只对框做 refine,而忘了更新 confidence,导致精细后的框仍然用旧分数参与 NMS,排序效果会明显变差。

性能方面,两阶段增加的计算量其实很小。因为候选框数量通常只有几百个,每个框只用几百个点,整体耗时大约增加几毫秒。对比它带来的精度提升,这笔开销相当划算。


6. 训练配置、后处理与源码常见坑

6.1 点云数据增强的“正确姿势”

CenterPoint 训练效率高,很大程度上得益于点云增强策略。常用的增强包括全局旋转、全局缩放、随机翻转、随机平移,以及对目标的 GT 采样增强。

读代码时你会发现,点云增强与图像增强有个很大差异:图像增强只要改像素,点云增强必须同步把 3D 框的标注信息也做同样的变换。比如全局旋转激光雷达坐标系时,所有 GT 框的中心、朝向也要跟着旋转;随机缩放时,GT 框的尺寸也要按同样比例调整。如果只增强点云忘了改标注,模型会在一个完全错乱的数据上训练,效果必然惨不忍睹。

一个实用的调参建议:旋转角度范围可以设置在[-π/4, π/4]左右,缩放比例在[0.95, 1.05]左右。增强幅度太大会破坏目标结构,太小则提升有限。GT 采样增强虽然能把稀疏区域的训练样本丰富起来,但要注意两个目标如果被强行贴在一起,可能在 heatmap 上产生特征冲突,一般要设置最小距离限制。

6.2 训练超参与实验复现要点

CenterPoint 的训练超参并不复杂,典型的配置是使用 AdamW 优化器,学习率 1e-4 量级,配合 cosine 衰减。batch size 受显存限制,多卡训练时学习率可能需要随 batch size 调整。

对于复现实验,有两点我想专门提醒:

第一,训练和推理的体素化配置必须严格一致,包括体素大小、采样点数、最大 voxel 数量。如果推理时换了体素大小,网络看到的特征分布就变了,结果会大幅下降。我在这个坑上浪费过半天时间,最后发现是推理配置里检测范围写错了。

第二,初学阶段不要一上来就跑完整数据集,先用几帧数据过一遍 train step 和 eval step,确认 loss 能正常下降、框能正常输出。CenterPoint 这种多模块模型,编译和配置错误会在训练几小时后才暴露出来,排查成本很高。

6.3 3D NMS 与阈值选择

后处理阶段,CenterPoint 输出一堆候选框,需要经过 NMS 去重。3D NMS 和 2D NMS 算法思路一致,但需要针对旋转矩形框做 IoU 计算。

一个很多人没想到的点是:3D IoU 的阈值通常比 2D 低得多。2D 检测 NMS 阈值常用 0.5 或者 0.7,3D 检测里因为点云稀疏,两个不同目标的框在 BEV 上即使 IoU 达到 0.1,也可能是不同目标。所以实际使用中阈值经常设在 0.01 到 0.1 区间,具体数值要看类别和数据集。车辆类可以稍微高一点,行人这种密集小目标要调低,否则相邻行人的框会被错误抑制。

源码里如果实现了类别独立的 NMS 阈值配置,我建议跨类别调优,不要用统一阈值。

6.4 常见问题排查速查表

现象可能原因解决方案
Heatmap 全背景无响应学习率过大、正样本太少调小学习率、检查高斯核半径
框尺寸整体偏大或偏小回归权重大小失衡、GT 没有归一化调整损失权重、检查回归目标预处理
两阶段精度不升反降采样点过多过拟合、置信度分支训练不足减少采样点数、对 proposal 做数据扰动
训练时显存溢出最大 voxel 数设置过大降低训练时体素上限或 batch size
可视化框与点云不对齐特征图坐标 stride 换算错误检查解码流程与体素尺寸的一致性
多类别中某一类完全测不出类别样本太少、heatmap 权重不平衡重加权类别损失或增加该类增强

这张表是我实测过程中总结出来的,具体环境不同可能表现不同,但排查方向一般不会偏差太多。


7. 源码向调优:按需求精简或改造

7.1 如何适配自己的数据格式

很多人想把自己的数据集跑进 CenterPoint,最烦的是数据格式转换。CenterPoint 常见源码实现里已经支持 KITTI、Waymo、nuScenes 等主流数据集,但如果你用的是自采点云,就需要自己写数据加载器。

一个比较省事的办法是:把你自己的点云转成 KITTI 那样的 bin 文件,每帧保存x, y, z, intensity四维坐标,同时用 pkl 记录标定信息和标注框。数据加载器只需要读取点云文件、计算点云范围、生成 GT 框标签就够了。不太建议为了适配而是去修改 Voxelization 和检测头的核心代码,因为体素化和坐标处理逻辑是高度耦合的,随便动一个参数,整个特征链路都要跟着验一遍。

改类别数和检测范围相对简单。把配置文件里的类别列表改成你自己的,把检测范围point_cloud_range改成传感器实际覆盖范围,再把体素大小按目标尺度调整。比如目标普遍很小、距离又远,体素可以适当调小,代价是显存和推理时间上升。

7.2 先可视化,再谈优化

我最后想给的建议是:无论你想改结构还是换数据集,第一步永远是可视化,而且要可视化中间的 heatmap,不是只看最终检测框。

CenterPoint 的 heatmap 可视化能直观告诉你网络有没有找到正确位置。如果 GT 中心明明在,heatmap 上却只有一团模糊响应,说明分类分支训练有问题;如果 heatmap 响应很好但输出框全歪,说明回归分支或者坐标解码有 bug。只看最终 mAP 指标,你很难判断错误从哪里来。

读完这套源码后我最强烈的感受是,CenterPoint 的代码设计得相当克制,每个模块都尽量复用成熟组件,但组合起来又确实有效。两阶段部分初看复杂,实际实现里核心代码量并不大,却能把边界框质量明显提升。如果你正想在自己的点云数据上跑 3D 检测,与其漫无目的地试模型,不如先把 CenterPoint 这条链路吃透,后面的路会顺很多。

返回列表