PointPillars 是自动驾驶 3D 目标检测领域里一篇里程碑式的工作,发表于 CVPR 2019,作者来自 nuTonomy。它的核心贡献是提出了一种极其简洁但高效的点云编码方式,在 KITTI benchmark 上以 62Hz 的推理速度打败了当时所有方法,包括同时使用激光雷达和摄像头的融合方法。


一:为什么要有 Pillar,而不是 Voxel

要理解 PointPillars,先要理解它在解决什么问题。

LiDAR 产生的点云是一堆三维空间中的点,每个点带有 (x,y,z) 坐标和反射强度 r。这份数据有两个根本性的特点:极度稀疏,以及无序

在 PointPillars 之前,VoxelNet 是最有代表性的端对端方法:把三维空间划分成均匀的体素(voxel)格子,对每个体素内的点用 PointNet 提特征,然后用 3D 卷积来聚合垂直方向的信息,最后接 2D backbone 和检测头。这个思路没问题,但 3D 卷积极其昂贵,VoxelNet 只能跑到 4.4Hz,完全不满足自动驾驶的实时要求。SECOND 改进了 VoxelNet,用稀疏卷积替代了部分 3D 卷积,跑到了 20Hz,但瓶颈仍然是 3D 卷积。

PointPillars 的出发点是:z 轴方向的信息,是否 3D 卷积来处理。

回答是不需要。把空间在 x-y 平面划分成二维格子,每个格子在 z 轴方向无限延伸,形成一根"柱子"(Pillar)。柱子内的所有点,不管它们的 z 坐标如何,都用同一套 PointNet 来提特征。PointNet 本身就是排列不变的,天然能处理无序点集。提完特征之后,每根柱子输出一个向量,所有柱子向量铺回到 x-y 平面,形成一张"伪图像"(Pseudo-image)——这是一张普通的二维特征图,可以直接送进任何 2D CNN。

这样做的好处显而易见:整个网络只有 2D 卷积,没有任何 3D 卷积。GPU 对 2D 卷积的优化已经极度成熟,推理速度自然大幅提升。

代价是:显式地丢弃了 z 轴方向的空间结构信息。但论文通过实验证明,这个代价是值得的——在 KITTI 上精度反而更高,速度高出 3 倍以上。


二:Pillar Feature Network——点云变伪图像

2.1 点从 4 维到 9 维

原始 LiDAR 点云里,每个点是 (x, y, z, r)  四维的,其中 r 是反射强度(reflectance)。

PointPillars 在把点喂进网络之前,会给每个点额外附加 5 个维度的信息,把点从 4 维装饰成 9 维。论文把这一步叫做 Point Decoration

这 9 个维度是:

$\tilde{p} = (x,\ y,\ z,\ r,\ x_c,\ y_c,\ z_c,\ x_p,\ y_p)$

前四个 (x, y, z, r) 是原始点的坐标和反射强度,后五个是额外添加的:

  • $x_c, y_c, z_c$:该点与其所在 Pillar 内所有点的算术均值(质心)之间的偏移量。形式上,若 Pillar 内有 n 个点,质心 $(\bar{x}, \bar{y}, \bar{z})$ 是所有点坐标的均值,则 $x_c = x - \bar{x}$,以此类推。
  • $x_p, y_p$:该点与其所在 Pillar 的 x-y 中心(即格子中心点)之间的偏移量。注意这里只有 x 和 y 两个方向,没有 z,因为 Pillar 在 z 方向是无限延伸的,没有 z 方向的"中心"概念。

为什么要加这 5 个维度?

  • $(x_c, y_c, z_c)$ 告诉网络这个点在 Pillar 内部的相对位置——它是靠近质心还是处于边缘?这对于学习局部几何形状至关重要,类似于 PointNet++ 里的局部坐标系。
  • $(x_p, y_p)$ 告诉网络这个点相对于 Pillar 中心的位置,帮助网络隐式地恢复精确的绝对空间位置——因为后续 Scatter 步骤会丢失 Pillar 内部的精细位置信息,这两个维度是一种补偿机制。

2.2 稀疏性的利用:(D, P, N) Tensor 的构建

Pillar 的划分大概会产生数万个格子(以 KITTI 车辆检测范围 x: [0, 70.4]m, y: [-40, 40]m 和 0.16m 的格子大小为例,一共有 $\frac{70.4}{0.16} \times \frac{80}{0.16} = 440 \times 500 = 220,000$ 个格子),但其中绝大多数是空的——论文指出稀疏度约为 97%,真正有点的 Pillar 只有 6000 到 9000 个。

如果对全部 22 万个格子都建 Tensor,内存和计算量都极大。PointPillars 的做法是只处理非空 Pillar,通过两个超参数来固定 Tensor 的尺寸:

  • P:每帧点云最多处理多少个非空 Pillar(默认值 P=12000)
  • N:每个 Pillar 里最多放多少个点(默认值 N=100)

这样就能构建一个形状固定的密集 Tensor:$\mathbf{T} \in \mathbb{R}^{D \times P \times N}$,其中 D=9。

构建规则如下:

  • 如果一个非空 Pillar 内的点数 < N:对不足的位置用全零填充(zero padding)
  • 如果点数 > N:随机采样 N 个点,丢弃其余的
  • 如果非空 Pillar 总数 < P:对不足的位置继续用全零填充
  • 如果非空 Pillar 总数 > P:随机丢弃部分非空 Pillar

同时,系统会记录每个非空 Pillar 对应的二维网格坐标 $(i_x, i_y)$,即这个 Pillar 在 x-y 画布上的位置索引。这个索引被称为 Pillar Index,后续 Scatter 步骤要用到它。

2.3 简化版 PointNet

拿到 (9, 12000, 100) 的 Tensor 之后,需要对每个点做特征提取。论文用的是一个简化版 PointNet,只有一层线性变换 + BatchNorm + ReLU,没有 PointNet 原版中的那些 T-Net(变换网络)。

操作流程是:

  1. Linear 层:对每个点施加一个权重矩阵 $W \in \mathbb{R}^{C \times D}$(C=64),把每个点从 D=9 维映射到 C=64 维

  2. 1×1 卷积实现:论文特别指出,这个线性变换可以等价地用 1×1 卷积来实现。把 Tensor 的维度排列成 (D, P, N) = (9, 12000, 100),可以把 $P \times N = 1,200,000$ 个点视作一张 $P \times N$ 的"特征图",每个"像素"有 D=9 个通道。1×1 卷积在每个像素位置独立地做 $9 \to 64$ 的映射,没有空间交互——这和对每个点独立做线性变换是完全等价的,但利用了 GPU 对卷积操作的高度优化,比显式的矩阵乘法更快。

  3. 经过 Linear + BN + ReLU 之后,Tensor 从 (9, P, N) 变成了 (C, P, N) = (64, 12000, 100)

  4. Channel-wise Max Pooling:对每个 Pillar 内的 N=100 个点,沿 N 这个维度取最大值(Max Pool over points),把每个 Pillar 从 N 个特征向量压缩成 1 个特征向量

经过 MaxPool:$(C, P, N) = (64, 12000, 100) \to (C, P) = (64, 12000)$

2.4 Scatter:把稀疏特征铺回密集画布

现在有了 (64, 12000) 的 Pillar 特征,以及 (12000, 2) 的 Pillar Index(记录每个 Pillar 对应画布上的哪个位置)。Scatter 步骤就是把这些稀疏的 Pillar 特征,按照各自的索引放回到对应的画布位置,没有 Pillar 的位置填零。

输出是一张形状为 (C, H, W) = (64, H, W) 的伪图像,其中 H 和 W 是画布的高度和宽度。

以 KITTI 车辆检测为例:

  • x 范围 [0, 70.4]m,步长 0.16m:W = 70.4 / 0.16 = 440
  • y 范围 [-40, 40]m,步长 0.16m:H = 80 / 0.16 = 500

所以 Pseudo-image 的形状是 (64, 500, 440)。

Scatter 的实现细节

在代码层面,Scatter 的实现非常简洁,本质上是一个 index_put 操作:

# pillar_features: (C, P)  = (64, 12000)
# pillar_indices:  (P, 2)  = (12000, 2) ← 每行是 (y_idx, x_idx)
# canvas: (C, H, W) 初始化为全零

# 把 P 个 pillar 的特征向量,放到 canvas 对应位置
# 等价于:
for p in range(P):
    y_idx = pillar_indices[p, 0]
    x_idx = pillar_indices[p, 1]
    canvas[:, y_idx, x_idx] = pillar_features[:, p]

实际实现用向量化的 scatter 操作完成,不需要循环:

# 展平 canvas 的空间维度,利用 pillar 的线性索引
# indices = y_idx * W + x_idx  ← 把 (y, x) 合并成一维线性索引
canvas = canvas.reshape(C, H * W)
indices = y_indices * W + x_indices  # (P,)
canvas[:, indices] = pillar_features  # (C, P)
canvas = canvas.reshape(C, H, W)


三:Backbone——从伪图像提取多尺度特征

拿到 (64, H, W) 的伪图像之后,就可以用标准的 2D CNN 了。PointPillars 的 backbone 设计和 VoxelNet 相同,是一个先下采样、再上采样拼接的结构,和 FPN(Feature Pyramid Network)思路一脉相承。

3.1 Top-Down 网络:三个 Block 逐步下采样

Backbone 的第一部分是一个自顶向下(从高分辨率到低分辨率)的特征提取网络,由三个 Block 串联组成。

论文把每个 Block 记为 $\text{Block}(S, L, F)$,其中:

  • S:这个 Block 相对于原始伪图像的 stride(步长)
  • L:这个 Block 内部堆叠的 3×3 卷积层数量
  • F:这个 Block 的输出通道数

以汽车检测网络为例,C=64,S=2:

Block1(S=2, L=4, C=64):
  输入:  (64, 500, 440)   ← 伪图像,stride=1
  第一层 3×3 conv,stride=2(下采样)
  后续 3 层 3×3 conv,stride=1
  每层都有 BN + ReLU
  输出:  (64, 250, 220)   ← stride=2 相对于原始图

Block2(S=4, L=6, 2C=128):
  输入:  (64, 250, 220)   ← stride=2
  第一层 3×3 conv,stride=2(再次下采样)
  后续 5 层 3×3 conv,stride=1
  每层都有 BN + ReLU
  输出:  (128, 125, 110)  ← stride=4 相对于原始图

Block3(S=8, L=6, 4C=256):
  输入:  (128, 125, 110)  ← stride=4
  第一层 3×3 conv,stride=2(再次下采样)
  后续 5 层 3×3 conv,stride=1
  每层都有 BN + ReLU
  输出:  (256, 63, 55)    ← stride=8 相对于原始图

 

注意每个 Block 内部的第一层卷积步长是 $S/S_{in}$(当前 stride 除以输入 stride),其余层步长均为 1。这是为了保证整个 Block 在原始伪图像上的有效 stride 恰好是 S。

3.2 Upsampling 网络:三个 Up 上采样并拼接

三个 Block 产生了三个不同分辨率的特征图。为了融合多尺度信息,论文对每个 Block 的输出做上采样,统一到相同分辨率,然后拼接。

上采样模块记为 $\text{Up}(S_{in}, S_{out}, F)$,使用转置卷积(transposed convolution)实现,其中:

  • $S_{in}$:输入特征图的 stride
  • $S_{out}$:输出特征图的 stride
  • $F$:输出通道数

论文设定三个上采样模块的输出通道数均为 2C = 128:

Up1(S_in=2, S_out=2, 2C=128):
  输入:  Block1 输出 (64, 250, 220)
  转置卷积(upscale factor=1,实际上是步长为1的conv)
  BN + ReLU
  输出:  (128, 250, 220)    ← 保持 stride=2

Up2(S_in=4, S_out=2, 2C=128):
  输入:  Block2 输出 (128, 125, 110)
  转置卷积(upscale factor=2)
  BN + ReLU
  输出:  (128, 250, 220)    ← 从 stride=4 上采样回 stride=2

Up3(S_in=8, S_out=2, 2C=128):
  输入:  Block3 输出 (256, 63, 55)
  转置卷积(upscale factor=4)
  BN + ReLU
  输出:  (128, 250, 220)    ← 从 stride=8 上采样回 stride=2

三个 Up 模块的输出形状全部统一为 (128, 250, 220),然后沿通道维度拼接(concat):

cat([Up1, Up2, Up3], dim=0)
  输出:  (128+128+128, 250, 220) = (384, 250, 220) = (6C, H/2, W/2)

这个 (6C, H/2, W/2) = (384, 250, 220) 的特征图就是送给检测头的最终特征。

一个原论文没有明确说清楚的细节:Up1 的上采样倍数实际上是 1($S_{in} = S_{out} = S$),它并没有改变空间分辨率,只是做了一个通道变换(64 → 128)。真正做上采样的是 Up2(×2)和 Up3(×4)。这个细节从论文的符号定义 $\text{Up}(S_{in}, S_{out}, F)$ 可以推断,但论文没有显式说明,容易让读者以为三个 Up 模块都在扩大分辨率。


四:SSD 检测头——从特征图到 3D 框

拿到 (384, H/2, W/2) 的特征图之后,检测头需要输出每个 Anchor 的类别概率和边界框回归量。

4.1 Anchor 的设计

PointPillars 直接使用 SSD 的 Anchor 机制,但做了 3D 化的适配。

在 2D SSD 里,Anchor 是一个矩形框;在 PointPillars 里,Anchor 是一个预定义的 3D 长方体,由 $(x, y, z, w, l, h, \theta)$ 七个量描述:

  • (x, y, z):Anchor 的中心坐标
  • (w, l, h):宽度、长度、高度
  • $\theta$:偏航角(yaw angle),绕 z 轴的旋转角

每个类别的 Anchor 尺寸是预设的,来自该类别物体的平均尺寸,每种尺寸在每个位置放两个,分别对应 $\theta = 0°$$\theta = 90°$ 两个朝向。

Anchor 匹配是用2D IoU(在 BEV 俯视图上,忽略高度)来做的,而不是 3D IoU。这意味着 z(高度位置)和 h(高度尺寸)不参与 Anchor 匹配,而是作为额外的回归目标。

这样做有两个好处:

  1. 2D IoU 计算比 3D IoU 快很多
  2. LiDAR 在 z 方向的测量本来就更准确,模型可以直接回归而不依赖先验

以汽车检测为例:检测范围 $x \in [0, 70.4]$$y \in [-40, 40]$,伪图像尺寸 $500 \times 440$,特征图(经过 backbone stride=2 下采样)为 $250 \times 220$。在这张特征图的每个位置放 2 个 Anchor(0° 和 90°),总共 $250 \times 220 \times 2 = 110,000$ 个 Anchor。

Anchor 的正负匹配规则:

  • 正样本:2D IoU 最高的 Anchor, IoU 超过正样本阈值的 Anchor
  • 负样本:IoU 低于负样本阈值的 Anchor
  • 忽略:介于正负阈值之间的 Anchor,不参与损失计算

4.2 检测头的输出结构

检测头是一个简单的 1×1 卷积,直接在 (384, H/2, W/2) 的特征图上预测每个位置每个 Anchor 的输出。每个 Anchor 需要预测:

  • 类别概率$N_{cls}$ 个(对于汽车单类,$N_{cls} = 1$;对于行人+骑行者,$N_{cls} = 2$
  • 边界框回归量:7 个(对应 $\Delta x, \Delta y, \Delta z, \Delta w, \Delta l, \Delta h, \Delta\theta$
  • 方向分类:2 个(这是 SoftMax 二分类,后面详解)

检测头的输出通道数:以汽车检测(每位置 2 个 Anchor,单类别)为例:

cls 头输出:     1×1 conv → (2×1, H/2, W/2) = (2, 250, 220)
reg 头输出:     1×1 conv → (2×7, H/2, W/2) = (14, 250, 220)
dir 头输出:     1×1 conv → (2×2, H/2, W/2) = (4, 250, 220)


五:Loss

PointPillars 的损失函数来自 SECOND,论文本身写得比较简略。我们来逐条把细节讲清楚。

5.1 边界框的回归编码方式

真实框 GT 和 Anchor 都用 $(x, y, z, w, l, h, \theta)$ 表示。回归目标不是直接预测 GT 的绝对坐标,而是预测 GT 和 Anchor 之间的残差

$\Delta x = \frac{x^{gt} - x^a}{d^a},\quad \Delta y = \frac{y^{gt} - y^a}{d^a},\quad \Delta z = \frac{z^{gt} - z^a}{h^a}$

$\Delta w = \log\frac{w^{gt}}{w^a},\quad \Delta l = \log\frac{l^{gt}}{l^a},\quad \Delta h = \frac{h^{gt}}{h^a}$

$\Delta\theta = \sin(\theta^{gt} - \theta^a)$

其中 $d^a = \sqrt{(w^a)^2 + (l^a)^2}$ 是 Anchor 在俯视图上的对角线长度,用于归一化 $\Delta x$$\Delta y$

角度的 sin 编码——以及论文里一个隐藏的问题

$\Delta\theta = \sin(\theta^{gt} - \theta^a)$ 这个编码方式乍看很合理:用 sin 函数把角度差映射到 [-1, 1] 之间,易于回归。

但这里有一个被很多人忽视的细节:sin 函数无法区分相差 $\pi$ 的两个角度,因为 $\sin(\theta) = \sin(\pi - \theta)$,所以 $\theta^{gt} - \theta^a = 30°$$\theta^{gt} - \theta^a = 150°$ 会产生相同的 $\Delta\theta$ 值。这意味着网络只预测这一个量,无法区分"车头朝前"和"车头朝后"。

这正是论文中单独引入方向分类损失 $\mathcal{L}_{dir}$ 的原因——用一个二分类来补偿 sin 编码丢失的方向信息。

更进一步,PointPillars 源码的一个实现中存在一个关于角度损失的重大缺陷:

原论文写的是 $\Delta\theta = \sin(\theta^{gt} - \theta^a)$,但在计算回归损失时,正确的做法(来自 SECOND)应该是用 $\sin(\theta_{pred} - \theta^{gt})$ 来作为损失,即把 sin 函数应用在预测值和目标值的差上,而非直接把 $\sin(\theta^{gt} - \theta^a)$ 作为目标,然后让网络直接用 L1 回归这个 sin 值。

具体来说,正确的角度损失应该是:

$\mathcal{L}_\theta = \text{SmoothL1}\left(\sin(\hat{\Delta\theta} - \Delta\theta_{target})\right)$

而不是简单地:

$\mathcal{L}_\theta = \text{SmoothL1}\left(\hat{\Delta\theta} - \sin(\theta^{gt} - \theta^a)\right)$

两者的区别在于:前者在预测和目标都接近 $\pm\pi/2$ 时仍然能正确传播梯度,后者会因为 sin 函数的非线性而产生梯度饱和。一些开源实现里存在这个 bug,导致角度回归性能下降。

正确的写法:

# sin(a - b) = sin(a)*cos(b) - cos(a)*sin(b)
bbox_pred[:, -1] = (
    torch.sin(bbox_pred[:, -1].clone()) * torch.cos(batched_bbox_reg[:, -1].clone())
)
batched_bbox_reg[:, -1] = (
    torch.cos(bbox_pred[:, -1].clone()) * torch.sin(batched_bbox_reg[:, -1].clone())
)

这个展开利用了 sin 差角公式,把 $\sin(\hat{\theta} - \theta^{gt})$ 展开成 $\sin(\hat{\theta})\cos(\theta^{gt}) - \cos(\hat{\theta})\sin(\theta^{gt})$,这样就不需要在计算图中出现$\hat{\theta} - \theta^{gt}$ 这个操作,避免了直接减法带来的梯度问题。

5.2 方向分类损失

如上所述,$\Delta\theta = \sin(\theta^{gt} - \theta^a)$ 无法区分相差 $\pi$ 的两个方向。网络单凭这一个回归量,判断出的是车的轴向(车体朝哪个轴),但无法判断车头朝哪边

方向分类损失 $\mathcal{L}_{dir}$ 是一个 softmax 二分类,把方向离散化为两个 bin:

  • bin 0:角度在 $[0, \pi)$ 范围内(即车头朝"正向")
  • bin 1:角度在 $[-\pi, 0)$ 范围内(即车头朝"反向")

在推理时,先用角度回归量恢复出 $\hat{\theta}$(取 $\arcsin(\Delta\theta_{pred}) + \theta^a$),再用方向分类结果决定是否翻转(加 $\pi$)来还原最终朝向。

这样的设计把一个难以直接回归的连续角度问题,分解成"回归小范围偏移 + 分类大方向"两个子问题,各自的优化都更容易。

5.3 类别分类损失:Focal Loss

分类损失使用 Focal Loss

$\mathcal{L}_{cls} = -\alpha_a (1 - p^a)^\gamma \log p^a$

其中 $p^a$ 是某个 Anchor 被分类为正样本的概率。

Focal Loss 的引入是因为 Anchor-based 方法面临严重的正负样本不均衡问题:110,000 个 Anchor 里,真正对应目标物体(正样本)的可能只有几十个,绝大多数都是背景(负样本)。如果用普通交叉熵,负样本的损失会淹没正样本的损失,训练极不稳定。

Focal Loss 通过 $(1 - p^a)^\gamma$ 这个因子,让置信度高的简单样本(容易判断的背景,$p^a \approx 0$,即 $1 - p^a \approx 1$,但已经快收敛所以梯度权重可以稍小)贡献更小的损失权重,让困难样本(分类边界上的样本)贡献更大权重,从而把优化资源集中在真正有挑战性的样本上。

5.4 总损失

$\mathcal{L} = \frac{1}{N_{pos}} \left(\beta_{loc} \mathcal{L}_{loc} + \beta_{cls} \mathcal{L}_{cls} + \beta{dir} \mathcal{L}_{dir}\right)$

其中:

  • \mathcal{L}_{loc} = \sum_{b \in {x,y,z,w,l,h,\theta}} \text{SmoothL1}(\Delta b) :对 7 个回归量分别算 SmoothL1 Loss 然后求和
  • $N_{pos}$:正样本 Anchor 的数量,用来归一化(避免不同帧目标数量不同导致损失量级波动)
  • 权重:$\beta_{loc} = 2$$\beta_{cls} = 1$$\beta_{dir} = 0.2$

六:从特征图到三维框——推理阶段的完整还原

训练阶段输出的是损失值。推理阶段,检测头输出了三张特征图(cls、reg、dir),需要把它们还原成有实际物理意义的 3D 边界框。

第一步:筛选正样本候选

对 cls 头的输出取 sigmoid,得到每个 Anchor 的类别概率 $p^a$。设置一个置信度阈值(一般 0.05 到 0.3 之间),只保留概率高于阈值的 Anchor 作为候选框。这一步过滤掉了绝大多数背景 Anchor。

第二步:还原 3D 框坐标

对于每个通过阈值的候选 Anchor,从 reg 头读取 7 个预测的残差量 $(\hat{\Delta x}, \hat{\Delta y}, \hat{\Delta z}, \hat{\Delta w}, \hat{\Delta l}, \hat{\Delta h}, \hat{\Delta\theta})$,通过逆变换还原出预测框的绝对坐标:

$x^{pred} = \hat{\Delta x} \cdot d^a + x^a$

$y^{pred} = \hat{\Delta y} \cdot d^a + y^a$

$z^{pred} = \hat{\Delta z} \cdot h^a + z^a$

$w^{pred} = w^a \cdot e^{\hat{\Delta w}}$

$l^{pred} = l^a \cdot e^{\hat{\Delta l}}$

$h^{pred} = h^a \cdot e^{\hat{\Delta h}}$

$\theta^{pred} = \arcsin(\hat{\Delta\theta}) + \theta^a$

第三步:角度方向修正

从 dir 头读取方向分类结果,如果分类结果表明角度应该翻转(车头朝"反方向"),则将 $\theta^{pred}$$\pi$

第四步:NMS 去重

同一个物体可能被多个 Anchor 检测到,需要用 NMS(Non-Maximum Suppression)去除重复框。PointPillars 使用轴对齐 NMS(axis-aligned NMS),即把旋转框投影成轴对齐矩形后再计算 IoU,而不是用精确的旋转 IoU。这样计算速度更快,论文指出性能相差不大。

NMS 阈值:IoU > 0.5 的框认为重复,保留概率高的,删除重复的。


七:细节

7.1 车辆和行人/骑行者用的是两个独立网络

PointPillars 在 KITTI 上训练了两个独立的网络:一个专门检测汽车,一个同时检测行人和骑行者。

两个网络的架构差异只在 backbone 第一个 Block 的 stride:

  • 汽车网络:S = 2(因为汽车体积大,下采样 2 倍不会丢失关键信息,且能加速计算)
  • 行人/骑行者网络:S = 1(因为行人体积小,需要保持更高的分辨率才能检测)

两者推理速度相近,原因如论文所述:行人/骑行者网络虽然不做第一步下采样,但其感知范围更小,所以整体计算量接近。

7.2 Data Augmentation 的三层策略

论文花了相当篇幅描述数据增强,这是 KITTI 上性能的重要来源。三层增强策略依次是:

第一层:GT Sampling(来自 SECOND)

预先建立一个包含所有训练帧里 GT 框及其对应点云的数据库(每个 GT 物体及其内部的点云单独存储)。训练时,从这个数据库里随机挑选一定数量的 GT 物体(汽车 15 个,行人 0 个,骑行者 8 个),插入当前帧的点云中。

这个操作人为地增加了训练帧里的物体密度,让网络见到更多不同背景下的目标,是最有效的增强手段(据论文,带来约 3% mAP 提升)。

第二层:Per-Box Augmentation(个体增强)

对每个 GT 框(含采样进来的)独立做小幅随机旋转($\pm \pi/20$$\pm 9°$)和小幅随机平移(三个方向各从 $\mathcal{N}(0, 0.25)$ 采样),模拟目标物体的姿态变化。

第三层:Global Augmentation(全局增强)

对整帧点云(含所有框)做:

  1. 沿 x 轴随机镜像翻转(50% 概率)
  2. 全局随机旋转和缩放
  3. 全局平移(三个方向各从 $\mathcal{N}(0, 0.2)$ 采样,模拟定位噪声)

八:完整的维度变换

原始点云输入:         (M, 4)         M ≈ 数万个点,每点 (x,y,z,r)
      ↓  Point Decoration
装饰后点云:           (M, 9)         增加 xc,yc,zc,xp,yp 五个维度
      ↓  分 Pillar + 填充/采样
Stacked Pillar:       (9, P, N)      P=12000, N=100(汽车网络)
Pillar Index:         (P, 2)         每个 Pillar 在画布上的 (y,x) 位置
      ↓  1×1 Conv + BN + ReLU(Linear: 9→64)
                      (64, P, N)
      ↓  MaxPool over N
Pillar Features:      (64, P)        = (64, 12000)
      ↓  Scatter(按 Index 放回画布)
Pseudo-Image:         (64, 500, 440)  H×W 取决于检测范围和网格分辨率
      ↓  Block1(S=2, L=4, C=64)
                      (64, 250, 220)
      ↓  Block2(S=4, L=6, 2C=128)
                      (128, 125, 110)
      ↓  Block3(S=8, L=6, 4C=256)
                      (256, 63, 55)
      ↓  Up1(S=2→2, 2C=128): Block1特征 → (128, 250, 220)
      ↓  Up2(S=4→2, 2C=128): Block2特征 ×2上采样 → (128, 250, 220)
      ↓  Up3(S=8→2, 2C=128): Block3特征 ×4上采样 → (128, 250, 220)
      ↓  Concatenate along channel
Detection Feature:    (384, 250, 220)  = (6C, H/2, W/2)
      ↓  Detection Head(1×1 conv × 3 路)
cls 输出:             (2, 250, 220)   每位置2个Anchor的分类概率
reg 输出:             (14, 250, 220)  每位置2×7=14个回归量
dir 输出:             (4, 250, 220)   每位置2×2=4个方向分类 logit
      ↓  阈值筛选 + 逆变换 + 方向修正 + NMS
最终输出:             (K, 7)          K个检测框,每框 (x,y,z,w,l,h,θ)


结语

PointPillars 成功的原因并不神秘,它的贡献在于把正确的思路落地得极其干净:用 Pillar 代替 Voxel 消除 3D 卷积,用稀疏 Tensor 处理点云稀疏性,用伪图像把点云问题转化成成熟的 2D 检测问题。每一个设计决策都有清晰的工程动机,整体结构简洁到可以在几百行代码里完整实现。

这篇文章没有涉及的是 PointPillars 的局限性——它在高密度区域的小目标检测(行人、骑行者)上表现较差,也没有利用 z 轴的结构信息,在复杂遮挡场景下表现不如 3D 方法。后续工作如 CenterPoint、VoxelNeXt、BEVFusion 等都在这些方向上做了改进,但 PointPillars 作为一个极度工程化的基线方案,至今仍然是大量工业系统的首选起点。

理解 PointPillars 的每一个细节,是深入 3D 目标检测领域的最好入口。

更多推荐