【版权声明】
本文为博主原创文章,未经博主允许严禁转载,我们会定期进行侵权检索。
参考书籍:《人工智能点云处理及深度学习算法》
本文为专栏《Python三维点云实战宝典》系列文章,专栏介绍地址“https://blog.csdn.net/suiyingy/article/details/124017716”。配套书籍《人工智能点云处理及深度学习算法》提供更加全面和系统的解析。
1 模型总体结构
MVXNet是一种基于激光雷达和图像的多模态融合的三维目标检测模型,发表在CVPR 2019《MVX-Net: Multimodal VoxelNet for 3D Object Detection》,论文地址为“https://arxiv.org/abs/1904.01649”。从论文题目上看,该模型的基础结构仍然属于基于体素的目标检测算法,总体思想与VoxelNet保持一致。其核心在于提取体素特征时融合了图像特征,从而实现多模态数据融合。因此,该模型属于一种早期融合方法。
图MVXNet模型结构
从模型结构可以看出,MVXNet首先训练了一个二维图像目标检测网络,并将其特征层融入到点云当中,融合后的运算过程与VoxelNet一致。二维图像特征提取网络可替换成计算计算机视觉中等其它相关模型结构。融合步骤是通过将点云投影到图像平面,然后以特征插值的方式得到点的图像特征,最终拼接到点云特征完成融合。
MVXNet模型总体过程如下图所示。
图 MVXNet模型总体计算过程
2 模型详解
2.1 图像特征提取
MVXNet模型特征提取extract_feat包含图像特征提取extract_img和点云特征提取extract_pts_feat。如下程序所示,图像特征提取的入口函数为 self.extract_img_feat(img, img_metas)。图像输入维度为3x416x1344。
def extract_feat(self, points, img, img_metas):
"""Extract features from images and points."""
img_feats = self.extract_img_feat(img, img_metas)
pts_feats = self.extract_pts_feat(points, img_feats, img_metas)
return (img_feats, pts_feats)
图像特征提取的主干网络结果为ResNet,并且提取到4种不同尺度特征,维度分别为256x104x336、512x52x168、1024x26x84和2048x13x42。主干网路特征进一步采用特征金字塔Neck FPN结构得到5种不同尺度特征img_feats,维度分别为256x104x336、256x52x168、256x26x84、256x13x42、256x7x21。通常情况下,主干网络特征随着网络深度增加,通道数逐渐增加而特征尺度逐渐减少。FPN Neck结构在进行特征融合过程中将特征通道变换到相同数值。
2.2 点云特征提取
MVXNet点云特征提取的入口函数为self.extract_pts_feat(points, img_feats, img_metas)。与单模态三维目标检测相比,该函数输入不仅包含了点云数据,而且包含了图像特征。因而,该模型多模态融合是在这个阶段实现的。点云输入数据的维度为Nx4,其中N表示点云数量,
4个维度数据分别为空间坐标x、y、z和激光雷达反射强度r。
点云特征提取主要包括体素化、体素融合特征编码等步骤,下面分别进行介绍。
2.2.1 体素化
MVXNet模型的体素化步骤与之前所介绍的相关内容略有差异,主要表现在未对非空体素最大数量和体素内点数进行设置。程序中用于实现体素化的入口函数为self.voxelize(points),其参数设置为Voxelization(voxel_size=[0.05, 0.05, 0.1], point_cloud_range=[0, -40, -3, 70.4, 40, 1], max_num_points=-1, max_voxels=(-1, -1), deterministic=True)。函数输入分别为:
- points,Nx4,原始点云,N表示点云数量,4表示特征维度,特征为坐标x、y、z与反射强度r。
- voxel_size:单位体素的尺寸,x、y、z方向上的尺度分别为0.05m、0.05m、0.1m。
- point_cloud_range:x、y、z方向的距离范围,结合b)中体素尺寸可以得到总的体素数量为1408x1600x41,即180224000(41x1600x1408)。
- deterministic:取值为True时,表示每次体素化的结果是确定的,而不是随机的。
MVXNet体素化输出结果为点云中各点的原始4个维度属性及其所属于的体素坐标。
- points:原始点云,Nx4。
- coors_batch:各点所属的体素坐标,Nx4,[batch_id, x, y, z]
2.2.2 点云体素初步特征编码
MVXNet体素融合特征编码层(Voxel Encoder)的函数入口为self.pts_voxel_encoder(voxels, coors, points, img_feats, img_metas),输入不仅包含点云信息points,还包含了图像特征img_feats。其模型过程主要由三部分组成,分别是点云体素特征提取、点云图像特征提取和点云图像融合特征提取。
模型首先采用类似PointNet++数据预处理方法中对点云输入特征进行预处理。将点云坐标减去对应体素内点云的质心坐标得到特征f_cluster(Nx3)。将点云坐标减去对应体素中心坐标得到特征f_center(Nx3)。原始4个维度特征与这两种特征相拼接得到Nx10个维度特征,该特征作为点云的输入特征。关键程序如下所示。
features_ls = [features]#Nx4, x, y, z, r
voxel_mean, mean_coors = self.cluster_scatter(features, coors)#计算每个体素中点的平均值及平均坐标(体素中点的质心),以及独立的体素(原始的体素可能有重复,即多个点属于同一个体素)
points_mean = self.map_voxel_center_to_point(coors, voxel_mean, mean_coors)#计算每个点对应的体素均值
f_cluster = features[:, :3] - points_mean[:, :3]#点的空间坐标减去体素均值
features_ls.append(f_cluster)
f_center = features.new_zeros(size=(features.size(0), 3))
f_center[:, 0] = features[:, 0] - (coors[:, 3].type_as(features) * self.vx + self.x_offset)
f_center[:, 1] = features[:, 1] - (coors[:, 2].type_as(features) * self.vy + self.y_offset)
f_center[:, 2] = features[:, 2] - (coors[:, 1].type_as(features) * self.vz + self.z_offset)
features_ls.append(f_center)
features = torch.cat(features_ls, dim=-1)#拼接得到10维特征,Nx10
点云特征features(Nx10)经过VFE层的全连接层 Linear(in_features=10, out_features=64, bias=False)得到Nx64维新特征point_feats。模型对体素内的点采用最大池化操作得到Kx64维度体素特征voxel_feats,其中K表示非空体素的数量。Voxel_feats是单个体素特征,也是体素内所含点云的全局特征,而point_feats是每个点的特征,即点云的局部特征。点云全局特征与局部特征进行拼接融合得到新的特征features,特征维度为Nx128。关键程序如下所示。
point_feats = vfe(features)#Nx64
voxel_feats, voxel_coors = self.vfe_scatter(point_feats, coors)#对体素内的点采用最大池化得到体素特征
feat_per_point = self.map_voxel_center_to_point(coors, voxel_feats, voxel_coors)#将体素特征映射回点
features = torch.cat([point_feats, feat_per_point], dim=1)#特征拼接。Nx128
特征features(Nx128)再次经过VFE层的全连接层Linear(in_features=128, out_features=64, bias=False)得到Nx64维新特征point_feats。
2.2.3 点云图像特征提取(PointFusion)
上述5种不同尺度的图像特征分别经过PointFusion层的卷积Conv2d(256, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)将通道数量转换为128,维度分别为128x104x336、128x52x168、128x26x84、128x13x42和128x7x21。原始点云坐标投影到图像坐标系后,用图像特征插值即可得到对应点的图像特征。每种特征图分别进行插值,那么点云图像特征具有5组,每一组维度均为Nx128。该5组特征进行拼接融合得到Nx640维度特征img_pts。该特征即为点云的不同尺度图像特征,入口函数为self.obtain_mlvl_feats(img_feats, pts, img_metas)。关键程序解析如下所示。
for level in range(len(self.img_levels)):
mlvl_img_feats.append(self.sample_single(img_ins[level][i:i + 1], pts[i][:, :3], img_metas[i]))#将点云投影到图像坐标系,并用图像特征插值得到对应点的图像特征,Nx128
mlvl_img_feats = torch.cat(mlvl_img_feats, dim=-1)
img_feats_per_point.append(mlvl_img_feats)
img_pts = torch.cat(img_feats_per_point, dim=0)
2.2.4 点云图像融合特征提取(VoxelFusion)
当前img_pts是五组不同尺度特征堆叠而成的,经过全连接层Linear(in_features=640, out_features=128, bias=True)之后可实现特征融合,融合后的特征img_pre_fuse的维度为Nx128。另一方面,点云特征pts_feats经过全连接层Linear(in_features=64, out_features=128, bias=True)后也转变为Nx128维特征pts_pre_fuse。两组相同维度的特征直接求和得到了点云图像的融合特征fuse_out。关键程序解析如下所示。
img_pre_fuse = self.img_transform(img_pts)#不同尺度特征进行融合,维度转变为Nx128
pts_pre_fuse = self.pts_transform(pts_feats)#Nx64->Nx128
fuse_out = img_pre_fuse + pts_pre_fuse#相加融合,Nx128
fuse_out = F.relu(fuse_out)#Nx128
上述融合特征fuse_out作新的点云特征point_feats。这些点仍然分布在不同体素当中,并且通过最大池化得到融合后的体素特征voxel_feats,维度维Kx128。该特征即为整个体素编码层的最终输出,并且实现了图像和点云的信息融合,模型后续计算过程与单模态的激光雷达三维目标检测算法一致。
3 中间层特征提取
MVXNet中间层特征提取的入口函数为self.pts_middle_encoder(voxel_features, feature_coors, batch_size)。其采用了三维稀疏卷积的方式将体素编码特征维度变换为256x200x176维度特征,具体计算过程可参考之前所介绍的相关单模态激光雷达三维检测算法。
4 主干网络
MVXNet的主干网络采用的是SECOND结构,通过两条通路提取两种不同尺度的特征图。第一条通路是上一部分所提取的特征 256x200x176经连续6个3x3卷积得到128x200x176维度的特征,记为out1。第二条通路是out1继续经过连续6个3x3卷积(其中第一个步长为2)得到256x100x88维度的特征,记为out2。out1和out2为主干网络输出结果。主干网络关键入口函数为self.pts_backbone(x)。
输入:x = self.backbone(feats_dict['spatial_features'])
out1:256x200x176 -> 128x200x176
Sequential(
(0): Conv2d(256, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(1): BatchNorm2d(128, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): ReLU(inplace=True)
(3): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(4): BatchNorm2d(128, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(5): ReLU(inplace=True)
(6): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(7): BatchNorm2d(128, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(8): ReLU(inplace=True)
(9): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(10): BatchNorm2d(128, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(11): ReLU(inplace=True)
(12): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(13): BatchNorm2d(128, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(14): ReLU(inplace=True)
(15): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(16): BatchNorm2d(128, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(17): ReLU(inplace=True)
)
Out2:128x200x176 -> 256x100x88
Sequential(
(0): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(1): BatchNorm2d(256, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): ReLU(inplace=True)
(3): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(4): BatchNorm2d(256, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(5): ReLU(inplace=True)
(6): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(7): BatchNorm2d(256, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(8): ReLU(inplace=True)
(9): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(10): BatchNorm2d(256, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(11): ReLU(inplace=True)
(12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(13): BatchNorm2d(256, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(14): ReLU(inplace=True)
(15): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(16): BatchNorm2d(256, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(17): ReLU(inplace=True
)
Out = [out1, out2] [128x200x176, 256x100x88]
5 上采样拼接
Neck网络分别对out1、out2进行上采样,out1的维度从128x200x176转换为256x200x176,out2的维度也从256x2100x88转换为256x200x176,两者维度完全相同。out1和out2拼接后得到Neck网络的输出结果,即neck_feats,维度为512x200x176。
6 Head与损失计算
上述200x176维度特征图上每个位置对应三种尺寸、两种方向共6种候选框anchor。
分类head:512x200x176特征经过conv_cls(512, 18)得到18x200x176个预测结果,对应6个候选框和3种目标类别。
位置head:512x200x176特征经过conv_reg(512,42)得到42x200x176个预测结果,对应6个候选框和7个位置参数(x, y ,z, l, w, h, θ)。
方向head:512x200x176特征经过conv_reg(512,12)得到12x200x176个预测结果,对应6个候选框和两个方向参数。
MVXNet损失由分类损失、位置损失和方向损失三部分组成,损失函数分别为FocalLoss、SmoothL1Loss和 CrossEntropyLoss。
模型Head和损失函数配置如下所示。
cls_score conv_cls Conv2d(512, 18, kernel_size=(1, 1), stride=(1, 1)) 18x200x176
bbox_pred conv_reg Conv2d(512, 42, kernel_size=(1, 1), stride=(1, 1)) 42x200x176
dir_cls_preds conv_dir_cls Conv2d(512, 12, kernel_size=(1, 1), stride=(1, 1)) 12x200x176
Anchor3DHead(
(loss_cls): FocalLoss()
(loss_bbox): SmoothL1Loss()
(loss_dir): CrossEntropyLoss(avg_non_ignore=False)
(conv_cls): Conv2d(512, 18, kernel_size=(1, 1), stride=(1, 1))
(conv_reg): Conv2d(512, 42, kernel_size=(1, 1), stride=(1, 1))
(conv_dir_cls): Conv2d(512, 12, kernel_size=(1, 1), stride=(1, 1))
)
7 顶层结构
MVXNet模型的顶层结构主要由三部分组成。
- 图像特征提取:采用ResNet和特征金字塔网络结构提取5种不同尺度的图像特征。
- 点云图像融合特征提取:包括体素特征提取、图像特征插值、图像特征融合以及点云图像特征融合等。
- 目标预测与损失函数:包括中间层特征提取、主干网络、上采样拼接、Head、损失计算等。
def forward_train(self, points=None, img_metas=None, gt_bboxes_3d=None, gt_labels_3d=None, gt_labels=None, gt_bboxes=None, img=None, proposals=None, gt_bboxes_ignore=None):
img_feats, pts_feats = self.extract_feat(points, img=img, img_metas=img_metas)
losses = dict()
if pts_feats:
losses_pts = self.forward_pts_train(pts_feats, gt_bboxes_3d, gt_labels_3d, img_metas, gt_bboxes_ignore)
losses.update(losses_pts)
if img_feats:
losses_img = self.forward_img_train(img_feats, img_metas=img_metas, gt_bboxes=gt_bboxes, gt_labels=gt_labels, gt_bboxes_ignore=gt_bboxes_ignore, proposals=proposals)
losses.update(losses_img)
return losses
8 模型训练
模型训练命令为“python tools/train.py configs/mvxnet/dv_mvx-fpn_second_secfpn_adamw_2x8_80e_kitti-3d-3class.py”,采用KITTI作为输入数据集。运行训练命令可得到如下图所示训练结果。
9 【python三维深度学习】python三维点云从基础到深度学习_python3d点云从基础到深度学习-CSDN博客
【版权声明】
本文为博主原创文章,未经博主允许严禁转载,我们会定期进行侵权检索。
更多python与C++技巧、三维算法、深度学习算法总结、大模型请关注我的博客,欢迎讨论与交流:https://blog.csdn.net/suiyingy,或”乐乐感知学堂“公众号。Python三维领域专业书籍推荐:《人工智能点云处理及深度学习算法》。
本文为专栏《Python三维点云实战宝典》系列文章,专栏介绍地址“https://blog.csdn.net/suiyingy/article/details/124017716”。配套书籍《人工智能点云处理及深度学习算法》提供更加全面和系统的解析。