如何理解自动驾驶,SLAM,BEV,训练数据源常见术语?(4)
图29实际上是在照片坐标系(uv)上拓展了一个深度Z构成的新坐标系。由于LSS默认是5路摄像头,把5个Frustum送到get_geometry函数里,会输出5路Frustum构成的一个组合笼子,其张量尺寸变为:B x N x D x H x W x 3,其中B是batch_size,默认是4组训练数据,N是相机数量5。get_geometry里一开始要做一个
#undo post-transformation
这玩意是干啥的?这跟训练集有关,在深度学习里里,有一种增强现有训练样本的方法,一般叫做Augmentation(其实AR技术里这个A就是Augmentation,增强的意思),通过把现有的训练数据做一些随机的:翻转/平移/缩放/裁减,给样本添加一些随机噪音(Noise)。比如,在不做样本增强前,相机的角度是不变的,训练后的模型只认这个角度的照片,而随机增强后再训练,模型可以学习出一定角度范围变化内的适应性,也就是Robustness。
图30Augmentation技术也是有相关理论和方法的,这里就贴个图不赘述了。数据增强的代码一般都是位于DataLoader内:
class NuscData(torch.utils.data.Dataset):
def sample_augmentation(self):
回到刚才的get_geometry,数据增强会给照片增加一些随机变化,但相机本身是必须固定的,这样才能让DNN模型学习这些随机变化的规律并去适应它们。所以将5路Frustum的安置到车身坐标系时候要先去掉(undo)这些随机变化。然后通过:
# cam_to_ego
points = torch.cat((points[:, :, :, :, :, :2] * points[:, :, :, :, :, 2:3],
points[:, :, :, :, :, 2:3]
), 5)
combine = rots.matmul(torch.inverse(intrins))
points = combine.view(B, N, 1, 1, 1, 3, 3).matmul(points).squeeze(-1)
points += trans.view(B, N, 1, 1, 1, 3)
将各路Frustum从相机坐标系转入车辆自身坐标系,注意这里的intrins是相机内参,rots和trans是相机外参,这些都是nuScenes训练集提供的,这里只有intrincs用了逆矩阵,而外参没有,因为nuScenes是先把每个相机放在车身原点,然后按照各路相机的位姿先做偏移trans再做旋转rots,这里就不用做逆运算了。如果换个数据集或者自己架设相机采集数据,要搞清楚这些变换矩阵的定义和计算顺序。四视图大概就是这个样子:
图31
LSS中推理深度和相片特征的模块位于:
class CamEncode(nn.Module):
def __init__(self, D, C, downsample):
super(CamEncode, self).__init__()
self.D = D
self.C = C
self.trunk = EfficientNet.from_pretrained("efficientnet-b0")
self.up1 = Up(320+112, 512)
self.depthnet = nn.Conv2d(512, self.D + self.C, kernel_size=1, padding=0)
trunk用于同时推理原始的深度和图片特征,depthnet用于将trunk输出的原始数据解释成LSS所需的信息,depthnet虽是卷积网但卷积核(Kernel)尺寸只有1个像素,功能接近一个全连接网FC(Full Connected),FC日常的工作是:分类或者拟合,对图片特征而言,它这里类似分类,对深度特征而言,它这里类似拟合一个深度概率分布。EfficientNet是一种优化过的ResNet,就当做一个高级的卷积网(CNN)看吧。对于这个卷积网而言,图片特征和深度特征在逻辑上没有区别,两者都位于trunk上的同一个维度,只是区分了channel而已。这就引出了另外一个话题:从单张2D图片上是如何推理/提取深度特征的。这类问题一般叫做:Monocular Depth Estimation,单目深度估计。一般这类系统内部分两个阶段:粗加工(Coarse Prediction)和精加工(Refine Prediction),粗加工对整个画面做一个场景级别的简单深度推测,精加工是在这个基础上识别更细小的物体并推测出更精细的深度。这类似画家先用简笔画出场景轮廓,然后再细致勾勒局部画面。除了用卷积网来解决这类深度估计问题,还有用图卷积网(GCN)和Transformer来做的,还有依赖测距设备(RangeFinder)辅助的DNN模型,这个话题先不展开了,庞杂程度不亚于BEV本身。那么LSS这里仅仅采用了一个trunk就搞定深度特征是不是太儿戏了,事实上确实如此。LSS估计出的深度准头和分辨率极差,参看BEVDepth项目里对LSS深度问题的各种测试报告:https://github.com/Megvii-BaseDetection/BEVDepthBEVDepth的测试里发现:如果把LSS深度估计部分的参数换成一个随机数,并且不参与学习过程(Back Propagation),其BEV的总体测试效果只有很小幅度的降低。但必须要说明,Lift的机制本身是很强的,这个突破性的方法本身没问题,只是深度估计这个环节可以再加强。
LSS的训练过程还有另外一个问题:相片上大约有1半的数据对训练的贡献度为0,其实这个问题是大部分BEV算法都存在的:
图32右边的标注数据实际上只描述了照片红线以下的区域,红线上半部都浪费了,你要问LSS里的模型对上半部都计算了些什么,我也不知道,因为没有标注数据可以对应上,而大部分的BEV都是这么训练的,所以这是一个普遍现象。训练时,BEV都会选择一个固定面积范围的周遭标注数据,而照片一般会拍摄到更远的景物,这两者在范围上天生就是不匹配的,另一方面部分训练集只关注路面标注,缺乏建筑,因为眼下BEV主要解决的是驾驶问题,不关心建筑/植被。这也是为什么图17哪里的深度图和LSS内部真实的深度图是不一致的,真实深度图只有接近路面这部分才有有效数据:
图33所以整个BEV的DNN模型势必有部分算力被浪费了。目前没看到任何论文关于这方面的研究。
接着继续深入LSS的Lift-Splat计算过程:
def get_depth_feat(self, x):
x = self.get_eff_depth(x)
# Depth
x = self.depthnet(x)
depth = self.get_depth_dist(x[:, :self.D])
new_x = depth.unsqueeze(1) * x[:, self.D:(self.D + self.C)].unsqueeze(2)
return depth, new_x
def get_voxels(self, x, rots, trans, intrins, post_rots, post_trans):
geom = self.get_geometry(rots, trans, intrins, post_rots, post_trans)
x = self.get_cam_feats(x)
x = self.voxel_pooling(geom, x)
return x
这里的new_x是把深度概率分布直接乘上了图片纹理特征,为了便于直观理解,我们假设图片特征有3个channel:c1,c2,c3,深度只有3格:d1,d2,d3。我们从图片上取某个像素,那么它们分别代表的意义是:c1:这个像素点有70%的可能性是车子,c2:有20%的可能性是路,c3:有10%的可能性是信号灯, d1:这个像素有80%的可能是在深度1,d2:有15%的可能性是在深度2,d3:有%5的可能性是在深度3上。如果把它们相乘的到:
那么这个像素最大的概率是:位于深度1的一辆车子。这也就是LSS里:
公式的意义,注意它这里把图像特征叫做c(Context), a_d的意义是深度沿视线格子的概率分布,d是深度。new_x就是这个计算结果。前面说过,由于图像特征和深度都是通过trunk训练出来的,它们位于同一维度,只是占用channel不同,深度占用了前self.D(41)个channel,Context占用了后面self.C(64)个channel。由于new_x是分别按照每路相机的Frustum单独计算的,而5个Frustum有重叠区域,须要做作数据融合,所以在voxel_pooling里计算好格子的索引和对应的空间位置,通过这个对应关系,把new_x的内容一一装入指定索引的格子。LSS在voxel_pooling的计算力引入了cumsum这个机制,虽然有很多文章在解释它,但这里不建议花太多功夫,它只是一个计算上的小技巧,对整个LSS是锦上添花的事,不是必要的。
*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。