文章目录
- PillarVFE模块
- 1. PillarVFE初始化
- 2. PillarVFE数据处理
- 2.1 特征构造
- 2.2 掩码构造
- 2.3 特征编码
OpenPCDet的整个结构图:
PillarVFE模块属于VFE结构的其中一种,所以可以在PCDet中的backbone_3d目录下,可以找到vfe目录结构。在OpenPCDet\pcdet\models\backbones_3d\vfe\__init__.py
文件中,可以查看所有可供选择的vfe模型结构。
# 根据MODEL中的VFE_NAME确定选择的模块
__all__ = {
'VFETemplate': VFETemplate,
'MeanVFE': MeanVFE,
'PillarVFE': PillarVFE, # PointPillar
'ImageVFE': ImageVFE,
'DynMeanVFE': DynamicMeanVFE,
'DynPillarVFE': DynamicPillarVFE,
'DynamicPillarVFESimple2D': DynamicPillarVFESimple2D
}
PillarVFE模块
这里先简单的介绍一下PointPillars算法中的PillarVFE结构。其中的VFE其实就是voxel feature encoding,而backbone3d目录下的另外一个文件pfe,其实表示的point feature encoding。前者是对voxel进行特征编码,而后者是对point进行特征编码。而在pointpillars中,不是简单的voxel表示,而是更加极端的pilllars表示。
1. PillarVFE初始化
PillarVFE的初始化具体是在Detector3DTemplate类中的build_vfe函数中实现,build_vfe在build_network中进行调用。PillarVFE模块相对于的模型设置通过传入cfg的VFE配置部分即可。使用NUM_FILTERS: [64]来设置模型的层数以及channels大小。假设这里的设置的就是NUM_FILTERS: [64],那么由于这个列表只有一个数值,所以FE网络层数只有一层。一层的VFE层一般是Linear+BN的结构(是否有BN层取决于是有设置USE_NORM)。设置后VFE层的输入channels为10,输出的channels就是64.
# 构造线性层:10 -> 64
pfn_layers = []
for i in range(len(num_filters) - 1):
in_filters = num_filters[i]
out_filters = num_filters[i + 1]
pfn_layers.append(
PFNLayer(in_filters, out_filters, self.use_norm, last_layer=(i >= len(num_filters) - 2))
)
self.pfn_layers = nn.ModuleList(pfn_layers) # 加入网络结构
具体结构如下所示:
所以,PointPillars模型初始的点云特征编码结构还是比较简单的,就是单纯的只有一层全连接来对特征进行升维操作。
2. PillarVFE数据处理
在之前已经介绍到,数据经过collect_data函数进行数据的批处理后,其传入模型的数据是一个batch_dict的形式,当batch处理后首先就是传入到了PillarVFE模块的forward函数进行处理。这里可以注意到,points的特征维度还是5(后面4维的点特征,第一维是确定当前点属于哪个点云帧),voxels的特征维度还是4(这个4就是点特征,32表示每个voxel选择的32个点,最大只有32个点,非点用0来填充)。此时与模型设置的初始维度10还是不一样的,此时还不能直接对数据进行处理。
在voxel点特征处理的具体过程化分化几个大概的步骤:
2.1 特征构造
对于给定的点特征以及voxel特征与每个点的网格具体位置,这里会分别构造每个voxel内每个点离voxel中心点的相对距离以及离网格的相对距离。代码如下所示:
# 1)计算voxel内每个点离voxel中心点的相对距离
# 求每个voxle的平均值(102483, 1, 3) / (102483, 1, 1) = (102483, 1, 3)
# 被求和的维度,在求和后会变为1,如果没有keepdim=True的设置,python会默认压缩该维度
points_mean = voxel_features[:, :, :3].sum(dim=1, keepdim=True) / voxel_num_points.type_as(voxel_features).view(-1, 1, 1) # 计算每个voxel内有效点的中心坐标(无效点部分用已0表示)
f_cluster = voxel_features[:, :, :3] - points_mean # voxel内的每个点与中心坐标的偏移量 (102483, 32, 3)
# 2) 计算voxel内每个点离网格的相对距离
# coords是网格点坐标,不是实际坐标,乘以voxel大小再加上偏移量是恢复网格中心点实际坐标
f_center = torch.zeros_like(voxel_features[:, :, :3]) # voxel内每个点与当前点网格的相对距离 (102483, 32, 3)
f_center[:, :, 0] = voxel_features[:, :, 0] - (coords[:, 3].to(voxel_features.dtype).unsqueeze(1) * self.voxel_x + self.x_offset)
f_center[:, :, 1] = voxel_features[:, :, 1] - (coords[:, 2].to(voxel_features.dtype).unsqueeze(1) * self.voxel_y + self.y_offset)
f_center[:, :, 2] = voxel_features[:, :, 2] - (coords[:, 1].to(voxel_features.dtype).unsqueeze(1) * self.voxel_z + self.z_offset)
随后,如何还选择使用点的绝对位置,那么voxel的特征就是点特征+离中心店相对距离+离网格相对距离 = 10维的特征,这里没有选择额外的使用xyz离原点的距离特征。
2.2 掩码构造
由于每个voxel中最大的有效点数量为32,一般都达不到这个数值,所以实际上的voxel是存在很多的0,也就是空值点。对于这些控制点,由于在构造特征时对其进行赋值,但是实际上不应该使用这些特征,十一要构造一个掩码矩阵抑制其作用,来与构造好的特征进行相乘。
# 3) 每个voxel点的掩码构造
voxel_count = features.shape[1] # 32 每个voxel内最多有32个有效点
mask = self.get_paddings_indicator(voxel_num_points, voxel_count, axis=0) # (102483, 32)
mask = torch.unsqueeze(mask, -1).type_as(voxel_features) # (102483, 32, 1)
features *= mask
2.3 特征编码
在掩码处理后的特征就可以正式的进行升维编码处理。但是在具体的实现上时,由于一个batch的数据可能会过大,比如10w+以上的点。那么,如此庞大的点全部就进行linear处理会造成极大的随机性。这里涉及了点工程上的优化,就是对点进行分批的linear升维处理再对升维后的数据拼接在一起。随后再进行BN层+ReLU激活函数层。然后,进行一个maxpool来提取当前批次点的全局特征,作为每个voxel特征。
class PFNLayer(nn.Module):
def __init__(self,
in_channels,
out_channels,
use_norm=True,
last_layer=False):
super().__init__()
......
self.part = 50000 # 如果当前的batch点数量太大,需要进行特殊处理
def forward(self, inputs):
"""
如果当前batch的点数量太大,需要进行分批进行linear处理,否则效果随机性较大,这里一个小批次的点数量设置为50000
"""
if inputs.shape[0] > self.part:
# nn.Linear performs randomly when batch size is too large
num_parts = inputs.shape[0] // self.part
part_linear_out = [self.linear(inputs[num_part*self.part:(num_part+1)*self.part]) # 分part来进行模型的升维处理
for num_part in range(num_parts+1)]
x = torch.cat(part_linear_out, dim=0) # [(50000, 32, 64), (50000, 32, 64), (2716, 32, 64)]
else:
x = self.linear(inputs) # 如果点数量少于5w可以直接linear处理
torch.backends.cudnn.enabled = False # 取消配置最高效率算法
x = self.norm(x.permute(0, 2, 1)).permute(0, 2, 1) if self.use_norm else x # 对channels=64的维度进行norm
torch.backends.cudnn.enabled = True # 配置最高效率算法
x = F.relu(x) # 激活函数
x_max = torch.max(x, dim=1, keepdim=True)[0] # 相当于是maxpool操作(pointnet中提出)
......
所以其实在PillarVFE模块中,具体的模型处理是分批次的Linear+BN层归一化+ReLU激活函数+maxpool提取全局特征,对每个voxel内的32个点(包含有效点+控制)提取了全局特征就是voxel特征。随后保留当前模块的特征处理结果在batch_dict字典中,以供后续模块的继续处理。
# 4) 特征编码
# (102483, 32, 10) -> (102483, 32, 64) -> (102483, 1, 64) -> (102483, 64)
for pfn in self.pfn_layers:
features = pfn(features)
features = features.squeeze() # 降维
batch_dict['pillar_features'] = features # 保留特征处理的结果
return batch_dict
至此,获取到了每个voxel的特征,也就是每个pillars的特征,batch_dict字典更新情况如下: