VoxelNet: End-to-End Learning for Point Cloud Based 3D Object Detection
推荐阅读:
代码复现: VoxelNet论文和代码解析 pytorch版本
代码复现: VoxelNet论文和代码解析 pytorch版本(一) train.py
代码复现: VoxelNet论文和代码解析 pytorch版本(二) Dataloader.py
论文阅读:VoxelNet(3D-detection)+代码复现
论文链接:
https://paperswithcode.com/paper/voxelnet-end-to-end-learning-for-point-cloud
非官网代码:
https://github.com/hongxiaoy/VoxelNet-detectron2
https://github.com/hongxiaoy/VoxelNet-pytorch
VoxelNet Architecture
VoxelNet包含三个功能块:
-
Feature Learning Network
以原始点云作为输入,将空间分为体素,然后将每个体素内的点转换为向量表示来刻画形状信息。空间表示为稀疏4D张量。
-
Convolutional middle layers
处理4D张量来聚合空间上下文信息。
-
Region proposal network
产生3D检测。
关于代码实现:
class VoxelNet(nn.Module):
def __init__(self):
super(VoxelNet, self).__init__()
self.svfe = SVFE() # Stacked Voxel Feature Encoding
self.cml = CML() # Convolutional Middle Layers
self.rpn = RPN() # Region Proposal Network
# 下述代码尚未理解
def voxel_indexing(self, sparse_features, coords):
dim = sparse_features.shape[-1]
dense_feature = Variable(torch.zeros(dim, cfg.N, cfg.D, cfg.H, cfg.W).cuda())
dense_feature[:, coords[:,0], coords[:,1], coords[:,2], coords[:,3]] =
sparse_features
return dense_feature.transpose(0, 1)
def forward(self, voxel_features, voxel_coords):
# feature learning network
vwfs = self.svfe(voxel_features)
vwfs = self.voxel_indexing(vwfs, voxel_coords)
# convolutional middle network
cml_out = self.cml(vwfs)
# region proposal network
# merge the depth and feature dim into one,
# output probability score map and regression map
psm, rm = self.rpn(cml_out.view(cfg.N, -1, cfg.H, cfg.W))
return psm, rm
Feature Learning Network
Voxel Partition
将点云所在三维空间(在Z、Y、X方向范围是D、H、W)分成等空间的体素(大小为 v D v_D vD、 v H v_H vH、 v W v_W vW),最后得到的3D体素格大小为 D ′ = D / v D D'=D/v_D D′=D/vD、 H ′ = H / v H H'=H/v_H H′=H/vH、 W ′ = W / v W W'=W/v_W W′=W/vW。
关于代码实现:
# utils.py
def get_filtered_lidar(lidar, boxes3d=None):
# lidar: [N_lidar_points, >=3]
# boxes_3d:
# get the lidar's xs, ys, zs
pxs = lidar[:, 0]
pys = lidar[:, 1]
pzs = lidar[:, 2]
# get the filter of xs, ys, zs and compose them to get the all valid points index
filter_x = np.where((pxs >= cfg.xrange[0]) & (pxs < cfg.xrange[1]))[0]
filter_y = np.where((pys >= cfg.yrange[0]) & (pys < cfg.yrange[1]))[0]
filter_z = np.where((pzs >= cfg.zrange[0]) & (pzs < cfg.zrange[1]))[0]
filter_xy = np.intersect1d(filter_x, filter_y)
filter_xyz = np.intersect1d(filter_xy, filter_z)
# 对于boxes3d的筛选暂不解释
if boxes3d is not None:
box_x = (boxes3d[:, :, 0] >= cfg.xrange[0]) & (boxes3d[:, :, 0] < cfg.xrange[1])
box_y = (boxes3d[:, :, 1] >= cfg.yrange[0]) & (boxes3d[:, :, 1] < cfg.yrange[1])
box_z = (boxes3d[:, :, 2] >= cfg.zrange[0]) & (boxes3d[:, :, 2] < cfg.zrange[1])
box_xyz = np.sum(box_x & box_y & box_z,axis=1)
return lidar[filter_xyz], boxes3d[box_xyz>0]
return lidar[filter_xyz] # 返回范围内的lidar点
汽车检测:
对于汽车检测任务,我们考虑点云的范围沿Z、Y、X轴方向是 [ − 3 , 1 ] m × [ − 40 , 40 ] m × [ 0 , 70.4 ] m [-3,1]m\times[-40,40]m\times[0,70.4]m [−3,1]m×[−40,40]m×[0,70.4]m。投影在图像边界外的点被去掉。对于体素大小的选择 v D = 0.4 m v_D=0.4m vD=0.4m、 v H = 0.2 m v_H=0.2m vH=0.2m、 v W = 0.2 m v_W=0.2m vW=0.2m。这样就得到 D ′ = 10 D'=10 D′=10、 H ′ = 400 H'=400 H′=400、 W ′ = 352 W'=352 W′=352。
# config.py
class config:
# voxel size
vd = 0.4
vh = 0.2
vw = 0.2
# points cloud range
xrange = (0, 70.4)
yrange = (-40, 40)
zrange = (-3, 1)
# voxel grid
W = math.ceil((xrange[1] - xrange[0]) / vw)
H = math.ceil((yrange[1] - yrange[0]) / vh)
D = math.ceil((zrange[1] - zrange[0]) / vd)
行人和自行车检测:
对于行人和自行车检测任务,我们考虑点云的范围沿Z、Y、X轴方向是 [ − 3 , 1 ] m × [ − 20 , 20 ] m × [ 0 , 48 ] m [-3,1]m\times[-20,20]m\times[0,48]m [−3,1]m×[−20,20]m×[0,48]m。(实验观察表面超过这个范围行人和自行车的激光雷达回波会变得很稀疏所以检测结果不可靠。)对于体素大小的选择仍然为 v D = 0.4 m v_D=0.4m vD=0.4m、 v H = 0.2 m v_H=0.2m vH=0.2m、 v W = 0.2 m v_W=0.2m vW=0.2m。这样就得到 D ′ = 10 D'=10 D′=10、 H ′ = 200 H'=200 H′=200、 W ′ = 240 W'=240 W′=240。
Grouping
我们将点根据它们所在的体素进行分组。这样,在分组后,一个体素会包含不同数量的点。
Random Sampling
考虑到直接处理所有点
- 增加计算平台的存储/效率负担
- 空间中高度变化的点云密度会使检测产生偏差
所以从包含多于T个点的体素中随机采样固定数量T个点。
目的是:
- 节省计算资源
- 减少体素之间点的不平衡,减少采样偏差,对训练增加多样性。
汽车检测:
随机选取 T = 35 T=35 T=35个点。
行人和自行车检测:
随机选取 T = 45 T=45 T=45个点,获取更多的点来更好捕捉形状信息。
Stacked Voxel Feature Encoding
这是一系列的VFE层,图中只展示了一个体素的层级特征编码过程。后面只讲解VFE Layer-1的细节,VFE Layer-n同理。
具体的VFE Layer-1结构如下图:
一个包含 T T T个点的非空体素可以表示为 V = { p i = [ x i , y i , z i , r i ] T ∈ R 4 } i = 1 , . . . , t \bold{V}=\{\bold{p}_i=[x_i,y_i,z_i,r_i]^T\in\mathbb{R}^4\}_{i=1,...,t} V={pi=[xi,yi,zi,ri]T∈R4}i=1,...,t
然后计算 V \bold{V} V中所有点的均值作为中心,记为 ( v x , v y , v z ) (v_x,v_y,v_z) (vx,vy,vz),并我们对每个点 p i \bold{p}_i pi增广从而获得输入特征 V i n = { p ^ i = [ x i , y i , z i , r i , x i − v x , y i − v y , z i − v z ] T ∈ R 7 } i = 1 , . . . , t \bold{V}_{in}=\{\hat{\bold{p}}_i=[x_i,y_i,z_i,r_i,x_i-v_x,y_i-v_y,z_i-v_z]^T\in\mathbb{R}^7\}_{i=1,...,t} Vin={p^i=[xi,yi,zi,ri,xi−vx,yi−vy,zi−vz]T∈R7}i=1,...,t。
将每一个 p ^ i \hat{\bold{p}}_i p^i送入全连接网络,得到新的特征,我们可以把这个特征 f i ∈ R m \bold{f}_i\in\mathbb{R}^m fi∈Rm聚合来编码体素内所包含的表面形状。全连接网络为 R e L U ( B N ( L i n e a r ( i n p u t ) ) ) ReLU(BN(Linear(input))) ReLU(BN(Linear(input)))。
在得到点级的特征表示之后,我们对 V \bold{V} V中的所有 f i \bold{f}_i fi执行元素级 M a x P o o l i n g MaxPooling MaxPooling得到 V \bold{V} V的局部聚合特征 f ~ ∈ R m \tilde{\bold{f}}\in\mathbb{R}^m f~∈Rm。
最后将 f i \bold{f}_i fi和 f ~ \tilde{\bold{f}} f~拼接形成点级拼接特征 f i o u t = [ f i T , f ~ T ] T ∈ R 2 m \bold{f}_i^{out}=[\bold{f}_i^T,\tilde{\bold{f}}^T]^T\in\mathbb{R}^{2m} fiout=[fiT,f~T]T∈R2m。
从而我们得到输出特征 V o u t = { f i o u t } i = 1 , … , t \bold{V}_{out}=\{\bold{f}_i^{out}\}_{i=1,…,t} Vout={fiout}i=1,…,t。
所有非空体素都以相同方式编码,并共享FCN的参数。
VFE-i ( c i n , c o u t ) (c_{in},c_{out}) (cin,cout)用来表示第i个VFE Layer,它将 c i n c_{in} cin维的输入特征转换到 c o u t c_{out} cout维的输出特征。 L i n e a r ( ) Linear() Linear()层会学习一个大小 c i n × ( c o u t / 2 ) c_{in}\times(c_{out}/2) cin×(cout/2)为的矩阵,然后点级的拼接会产生 c o u t c_{out} cout维的输出。
体素级的特征是通过将VFE-n的输出通过FCN和元素级 M a x P o o l i n g MaxPooling MaxPooling转换为 R C \mathbb{R}^C RC, C C C是体素级特征的维度。
高效执行方法:GPU在处理密集张量结构时会被优化。使用下述方法将点云转换为密集张量结构使得堆叠的VFE运算可以在点和体素之间并行处理:
我们初始化一个 K × T × 7 K\times T\times7 K×T×7维度的张量结构来存储体素输入特征缓冲区,其中 K K K是非空体素最大数量, T T T是每个体素中点的最大数量,7是每个点输入的编码维度。
点在处理之前会被随机打乱。
对于点云中每个点,我们检查对应的体素是否已经存在。这个查询操作可以使用哈希表在 O ( 1 ) O(1) O(1)内高效完成,在哈希表中,体素坐标作为哈希的键。如果体素已经被初始化,我们就插入点到体素位置,如果它少于 T T T个点,否则点被忽略。如果体素没有被初始化,我们初始化一个新的体素,存储它的坐标到体素坐标缓冲器,然后插入点到这个体素位置。
体素输入特征和坐标缓冲器可以通过单次遍历点列表构建,所以复杂度为 O ( n ) O(n) O(n)。
为了进一步提高存储/计算效率,可以只存储有限数的体素,如 K K K个,然后忽略来自于只有几个点的体素的点。
在构建了体素输入缓冲器后,堆叠的VFE层只包括了点级和体素级的密集操作,这个就可以在GPU上并行计算。
注:在VFE的拼接操作后,我们将对应空点的特征重置为0,这样它们就不影响体素特征的计算。
最后,使用存储的坐标缓冲器,我们将计算的稀疏体素结构重构为密集体素网格。
关于代码实现:
# voxelnet.py
# Fully Connected Network
class FCN(nn.Module):
"""
FCN实现的就是将Point-wise Input转换为Point-wise Feature的Fully Connected Neural
Net, 以及VFE Layer-n后面的那个Fully Connected Neural Net. 其输入维度我们按照论文
中的高效执行方法安排, 即[K, T, M_in].
输出维度为[K, T, M_out].
"""
def __init__(self, cin, cout):
super(FCN, self).__init__()
self.cout = cout
self.linear = nn.Linear(cin, cout)
self.bn = nn.BatchNorm1d(cout)
def forward(self, x):
# x: shape [K, T, M_in], K: num of valid voxels, T: max points number per voxel,
# M_in: features of each point, [x, y, z, r, dx, dy, dz] at the beginning.
# KK is the stacked k across batch
kk, t, _ = x.shape
x = self.linear(x.view(kk*t, -1)) # shape [kk*t, M_in]
x = F.relu(self.bn(x))
return x.view(kk, t, -1) # shape [kk, t, M_out]
# Voxel Feature Encoding layer
class VFE(nn.Module):
"""
VFE实现了VFE Layer-i的功能. 需要指定输出的维度cout, 且这个cout应该为偶数以获得VFE层
中FCN层的正确输出维度。
"""
def __init__(self, cin, cout):
super(VFE, self).__init__()
assert cout % 2 == 0
self.units = cout // 2
self.fcn = FCN(cin, self.units)
def forward(self, x, mask):
# x: [K, T, cin] shape
# mask: [K, T] shape
# point-wise feauture
pwf = self.fcn(x) # [K, T, cout//2] shape
#locally aggregated feature
laf = torch.max(pwf, 1)[0].unsqueeze(1).repeat(1, cfg.T, 1) # [K, T, cout//2]
# point-wise concat feature
pwcf = torch.cat((pwf, laf),dim=2) # [K, T, cout]
# apply mask
mask = mask.unsqueeze(2).repeat(1, 1, self.units * 2) # [K, T, cout]
pwcf = pwcf * mask.float()
return pwcf # [K, T, cout]
这一部分具体应用的参数如下:
汽车检测:
首先对于每一个非空体素,随机采样点最大数量设定为 T = 35 T=35 T=35。
其次我们用了两个VFE层:VFE-1(7, 32)、VFE-2(32, 128)。
在VFE-2后面的全连接网络将VFE-2的输出映射到 R 128 \mathbb{R}^{128} R128。
# voxelnet.py
# Stacked Voxel Feature Encoding
class SVFE(nn.Module):
"""
SVFE实现了多个VFE Layer-i的堆叠.
"""
def __init__(self):
super(SVFE, self).__init__()
self.vfe_1 = VFE(7, 32)
self.vfe_2 = VFE(32, 128)
self.fcn = FCN(128, 128)
def forward(self, x):
# x: [K, T, 7]
mask = torch.ne(torch.max(x, 2)[0], 0) # [K, T]
x = self.vfe_1(x, mask) # [K, T, 32]
x = self.vfe_2(x, mask) # [K, T, 128]
x = self.fcn(x) # [K, T, 128]
# element-wise max pooling
x = torch.max(x, 1)[0] # [K, 128]
return x
行人和自行车检测:
Feature Learning Network同汽车检测。
Sparse Tensor Representation
只处理非空体素,我们获得一系列体素特征,每一个都关联一个非空体素的空间坐标。体素级特征可以表示为稀疏4D张量,形状如上。尽管点很多,但大部分体素都是空的。
将非空体素特征表示为稀疏张量极大减少了反向传播中计算消耗和内存使用。
汽车检测:
经过Feature Learning Network后,产生的稀疏张量为 128 × 10 × 400 × 352 128\times10\times400\times352 128×10×400×352。
Convolutional Middle Layers
我们用ConvMD( c i n c_{in} cin, c o u t c_{out} cout, k \bold{k} k, s \bold{s} s, p \bold{p} p)表示一个M维卷积操作,其中 c i n c_{in} cin, c o u t c_{out} cout为输入输出通道数,后三个是M维向量,表示kernel size,stride size,padding size。如果各维度的size是相同的,就用标量表示size。
每一个卷积中间层顺序使用3D卷积、BN层、ReLU层。
卷积中间层在逐渐扩大的感受野中聚集体素级特征,为形状描述添加更多上下文信息。
关于代码实现:
# voxelnet.py
# conv3d + bn + relu
class Conv3d(nn.Module):
"""
Conv3d实现了Convolutional Middle Layers中的一个Conv层.
"""
def __init__(self, in_channels, out_channels, k, s, p, batch_norm=True):
super(Conv3d, self).__init__()
self.conv = nn.Conv3d(in_channels,
out_channels,
kernel_size=k,
stride=s,
padding=p)
if batch_norm:
self.bn = nn.BatchNorm3d(out_channels)
else:
self.bn = None
def forward(self, x):
x = self.conv(x)
if self.bn is not None:
x = self.bn(x)
return F.relu(x, inplace=True)
这一部分具体应用的参数如下:
汽车检测:
为了聚合体素级特征,我们顺序使用三个卷积中间层:
- Conv3D(128, 64, 3, (2, 1, 1), (1, 1, 1))
- Conv3D(64, 64, 3, (1, 1, 1), (0, 1, 1))
- Conv3D(64, 64, 3, (2, 1, 1), (1, 1, 1))
# voxelnet.py
# Convolutional Middle Layer
class CML(nn.Module):
"""
CML类实现了Convolutional Middle Layer.
"""
def __init__(self):
super(CML, self).__init__()
self.conv3d_1 = Conv3d(128, 64, 3, s=(2, 1, 1), p=(1, 1, 1))
self.conv3d_2 = Conv3d(64, 64, 3, s=(1, 1, 1), p=(0, 1, 1))
self.conv3d_3 = Conv3d(64, 64, 3, s=(2, 1, 1), p=(1, 1, 1))
def forward(self, x):
x = self.conv3d_1(x)
x = self.conv3d_2(x)
x = self.conv3d_3(x)
return x
经过Convolutional Middle Layers后,产生的4D张量为 64 × 2 × 400 × 352 64\times2\times400\times352 64×2×400×352。
行人和自行车检测:
Convolutional middle layers同汽车检测。
Region Proposal Network
根据Faster R-CNN中的RPN网络进行修改,我们将它和特征学习网络以及卷积中间层相结合来形成端到端训练。
RPN网络的输入为卷积中间层提供的特征图。
网络有三块全卷积层:每块的第一层通过步长为2的卷积下采样特征图到1/2,后面是一系列的q个步长为1的卷积。在每一卷积层后,都跟着BN和ReLU操作。
然后我们上采样每个块的输出到一个固定大小并拼接在一起,构成一个高分辨率特征图。
最后这个特征图映射到需要的学习目标:(1)概率分数图,(2)回归图。
关于代码实现:
# voxelnet.py
# conv2d + bn + relu
class Conv2d(nn.Module):
"""Conv2d根据论文将卷积层、BN层、ReLU层合并."""
def __init__(self,
in_channels,
out_channels,
k,
s,
p,
activation=True,
batch_norm=True):
super(Conv2d, self).__init__()
self.conv = nn.Conv2d(in_channels,
out_channels,
kernel_size=k,
stride=s,
padding=p)
if batch_norm:
self.bn = nn.BatchNorm2d(out_channels)
else:
self.bn = None
self.activation = activation
def forward(self, x):
x = self.conv(x)
if self.bn is not None:
x=self.bn(x)
if self.activation:
return F.relu(x, inplace=True)
else:
return x
# Region Proposal Network
class RPN(nn.Module):
def __init__(self):
super(RPN, self).__init__()
self.block_1 = [Conv2d(128, 128, 3, 2, 1)]
self.block_1 += [Conv2d(128, 128, 3, 1, 1) for _ in range(3)]
self.block_1 = nn.Sequential(*self.block_1)
self.block_2 = [Conv2d(128, 128, 3, 2, 1)]
self.block_2 += [Conv2d(128, 128, 3, 1, 1) for _ in range(5)]
self.block_2 = nn.Sequential(*self.block_2)
self.block_3 = [Conv2d(128, 256, 3, 2, 1)]
self.block_3 += [nn.Conv2d(256, 256, 3, 1, 1) for _ in range(5)]
self.block_3 = nn.Sequential(*self.block_3)
self.deconv_1 = nn.Sequential(
nn.ConvTranspose2d(256, 256, 4, 4, 0),
nn.BatchNorm2d(256)
)
self.deconv_2 = nn.Sequential(
nn.ConvTranspose2d(128, 256, 2, 2, 0),
nn.BatchNorm2d(256)
)
self.deconv_3 = nn.Sequential(
nn.ConvTranspose2d(128, 256, 1, 1, 0), # 这里在论文中有误
nn.BatchNorm2d(256)
)
self.score_head = Conv2d(768, cfg.anchors_per_position, 1, 1, 0,
activation=False, batch_norm=False)
self.reg_head = Conv2d(768, 7 * cfg.anchors_per_position, 1, 1, 0,
activation=False, batch_norm=False)
def forward(self,x):
x = self.block_1(x)
x_skip_1 = x
x = self.block_2(x)
x_skip_2 = x
x = self.block_3(x)
x_0 = self.deconv_1(x)
x_1 = self.deconv_2(x_skip_2)
x_2 = self.deconv_3(x_skip_1)
x = torch.cat((x_0, x_1, x_2),1)
return self.score_head(x),self.reg_head(x)
汽车检测:
经过reshaping操作,RPN输入的特征图大小为 128 × 400 × 352 128\times400\times352 128×400×352,对应是3D张量的通道数、高度、宽度。
对于锚框大小,我们只使用了: l a = 3.9 m l^a=3.9m la=3.9m, w a = 1.6 m w^a=1.6m wa=1.6m, h a = 1.56 m h^a=1.56m ha=1.56m,中心为 z c a = − 1.0 m z_c^a=-1.0m zca=−1.0m,旋转为 0 ° 0\degree 0°和 90 ° 90\degree 90°。
锚框匹配原则如下:
- 一个锚框如果和ground truth有最大的IoU,或在鸟瞰图中与ground truth的IoU高于0.6,则被认定为是正锚框。
- 一个锚框如果它和所有ground truth框的IoU低于0.45,则被认定为是负锚框。
- 如果它和任意ground truth框的IoU在 [ 0.45 , 0.6 ] [0.45,0.6] [0.45,0.6],则忽略它。
行人和自行车检测:
我们对第一个块进行了一项修改:将第一个2D卷积的步长大小从2变为1,这可以在锚框匹配上获得更精细的分辨率,这对于行人和自行车检测十分必要。
对于行人检测的锚框大小,我们只使用了: l a = 0.8 m l^a=0.8m la=0.8m, w a = 0.6 m w^a=0.6m wa=0.6m, h a = 1.73 m h^a=1.73m ha=1.73m,中心为 z c a = − 0.6 m z_c^a=-0.6m zca=−0.6m,旋转为 0 ° 0\degree 0°和 90 ° 90\degree 90°。
对于自行车检测的锚框大小,我们只使用了: l a = 1.76 m l^a=1.76m la=1.76m, w a = 0.6 m w^a=0.6m wa=0.6m, h a = 1.73 m h^a=1.73m ha=1.73m,中心为 z c a = − 0.6 m z_c^a=-0.6m zca=−0.6m,旋转为 0 ° 0\degree 0°和 90 ° 90\degree 90°。
锚框匹配原则如下:
- 一个锚框如果它和ground truth有最高的IoU,或它和ground truth的IoU高于0.5,则被认定为是正锚框。
- 一个锚框如果它和每个ground truth的IoU都低于0.35,则被认定为是负锚框。
- 如果它和任意ground truth的IoU在 [ 0.35 , 0.5 ] [0.35, 0.5] [0.35,0.5],则忽略它。
Loss Function
用 { a i p o s } i = 1... N p o s \{a_i^{pos}\}_{i=1...N_{pos}} {aipos}i=1...Npos表示 N p o s N_{pos} Npos个正锚框集合,用 { a j n e g } j = 1... N n e g \{a_j^{neg}\}_{j=1...N_{neg}} {ajneg}j=1...Nneg表示 N n e g N_{neg} Nneg个正锚框集合。
ground truth三维框表示为 ( x c g , y c g , z c g , l g , w g , h g , θ g ) (x_c^g,y_c^g,z_c^g,l^g,w^g,h^g,\theta^g) (xcg,ycg,zcg,lg,wg,hg,θg),前三个表示中心位置,之后三个表示框的长宽高,最后一个表示绕Z轴旋转的角度。
为了从匹配的正锚框获得ground truth框,我们定义一个残差向量 u ∗ ∈ R 7 \bold{u}^*\in\mathbb{R}^7 u∗∈R7,包含了7个回归目标,对应中心位置 Δ x \Delta x Δx, Δ y \Delta y Δy, Δ z \Delta z Δz,三个维度的 Δ l \Delta l Δl, Δ w \Delta w Δw, Δ h \Delta h Δh,旋转角度 Δ θ \Delta\theta Δθ。
计算如下:
其中 d a = ( l a ) 2 + ( w a ) 2 d^a=\sqrt{(l^a)^2+(w^a)^2} da=(la)2+(wa)2,是锚框底面对角线长度。我们的目标是用对角线 d a d^a da同时直接估计有方向的3D包围盒和归一化 Δ x \Delta x Δx, Δ y \Delta y Δy。
损失函数定义如下:
其中, p i p o s p_i^{pos} pipos和 p j n e g p_j^{neg} pjneg表示正锚框 a i p o s a_i^{pos} aipos和负锚框 a j n e g a_j^{neg} ajneg的softmax输出, u i ∈ R 7 \bold{u}_i\in\mathbb{R}^7 ui∈R7和 u i ∗ ∈ R 7 \bold{u}_i^*\in\mathbb{R}^7 ui∗∈R7是正锚框 a i p o s a_i^{pos} aipos的回归ground truth和回归输出。
损失函数前两项是 { a i p o s } i = 1... N p o s \{a_i^{pos}\}_{i=1...N{pos}} {aipos}i=1...Npos和 { a j n e g } j = 1... N n e g \{a_j^{neg}\}_{j=1...N_{neg}} {ajneg}j=1...Nneg的归一化分类损失,其中 L c l s L_{cls} Lcls表示二分类交叉熵损失(Binary Cross Entropy Loss)。 α \alpha α和 β \beta β是正常数来平衡相对重要性,文中设定为 α = 1.5 \alpha=1.5 α=1.5, β = 1 \beta=1 β=1。
最后一项是回归损失,使用SmoothL1损失函数。
Hyper Parameters
使用随机梯度下降SGD算法,前150个epoch的学习率lr为0.01,最后10个epoch的学习率lr减到0.001。
batchsize为16个点云。
Data Augmentation
防止训练点云数目过少出现的过拟合现象,引入三种不同形式数据增强方法(增强的训练数据在线生成无需存储在磁盘上)。
集合 M = { p i = [ x i , y i , z i , r i ] T ∈ R 4 } i = 1 , . . . , N \bold{M}=\{\bold{p}_i=[x_i,y_i,z_i,r_i]^T\in\mathbb{R}^4\}_{i=1,...,N} M={pi=[xi,yi,zi,ri]T∈R4}i=1,...,N表示整个点云,包含N个点。
3D边界框 b i \bold{b}_i bi为 ( x c , y c , z c , l , w , h , θ ) (x_c,y_c,z_c,l,w,h,\theta) (xc,yc,zc,l,w,h,θ)。
定义 b i \bold{b}_i bi包含的所有激光雷达点的集合 Ω i = { p ∣ x ∈ [ x c − l / 2 , x c + l / 2 ] , y ∈ [ y c − w / 2 , y c + w / 2 ] , z ∈ [ z c − h / 2 , z c + h / 2 ] , p ∈ M } \Omega_i=\{\bold{p}|x\in[x_c-l/2,x_c+l/2],y\in[y_c-w/2,y_c+w/2],z\in[z_c-h/2,z_c+h/2],\bold{p}\in\bold{M}\} Ωi={p∣x∈[xc−l/2,xc+l/2],y∈[yc−w/2,yc+w/2],z∈[zc−h/2,zc+h/2],p∈M},其中 p = [ x , y , z , r ] \bold{p}=[x,y,z,r] p=[x,y,z,r]表示集合 M \bold{M} M中一个具体的点。
方式一:独立对每个ground truth 3D边界框以及框内的激光雷达点应用扰动(旋转+平移+碰撞检测)
具体来说,相对于 [ x c , y c , z c ] [x_c,y_c,z_c] [xc,yc,zc]绕着Z轴旋转3D边界框 b i \bold{b}_i bi和框内所有点 Ω i \Omega_i Ωi,旋转角度为均匀分布的随机变量 Δ θ ∈ [ − π / 10 , + π / 10 ] \Delta\theta\in[-\pi/10,+\pi/10] Δθ∈[−π/10,+π/10]。
然后加上一个平移 ( Δ x , Δ y , Δ z ) (\Delta x,\Delta y,\Delta z) (Δx,Δy,Δz)到 b i \bold{b}_i bi和 Ω i \Omega_i Ωi的XYZ坐标,这三个值是从均值为0,标准差为1的高斯分布中独立取得的。
为了避免物理上不可能的结果,我们在扰动后加入了两个边界框之间的碰撞检测,如果检测到碰撞就恢复原样。
由于对每个ground truth框和相应的激光雷达点独立应用扰动,网络可以从比原始训练数据更多的变换中学习。
方式二:对所有ground truth框 b i \bold{b}_i bi和整个点云 M \bold{M} M应用全局缩放(乘以缩放因子)
具体来说,我们把每个边界框 b i \bold{b}_i bi的x,y,z,l,w,h和 M \bold{M} M的x,y,z都乘以一个从 [ 0.95 , 1.05 ] [0.95, 1.05] [0.95,1.05]均匀分布中抽取的随机变量。
引入全局缩放增加了网络检测不同大小和距离的物体的鲁棒性。
方式三:对所有ground truth框 b i \bold{b}_i bi和整个点云 M \bold{M} M应用全局旋转(沿Z轴绕(0,0,0)旋转)
旋转的实现是沿着Z轴绕 ( 0 , 0 , 0 ) (0,0,0) (0,0,0)。
旋转的角度从均匀分布 [ − π / 4 , + π / 4 ] [-\pi/4,+\pi/4] [−π/4,+π/4]中采样。
通过整体点云旋转模拟了车辆转弯情况。
Experiments
在KITTI 3D Object Detection benchmark上评估,7481个训练图像/点云,7518个测试图像/点云,包含Car,Pedestrian,Cyclist三类。每一个类别的检测结果基于三个难度等级评估。
VoxelNet的推理时间在一个TitanX GPU和1.7Ghz CPU上是225ms,其中体素输入特征计算用时5ms,特征学习网络用时20ms,卷积中间层用时大约170ms,RPN用时30ms。