Simultaneous Localization and Mapping(SLAM):并发定位与地图测绘,相对于BEV的另外一种感知技术。
Perception:感知,SLAM和BEV在AD领域里都是协助控制系统了解车辆周围状况的感知技术:知道自己在哪,有哪些障碍物,障碍物在自己的什么方位,距离多远,哪些障碍物是静态的那些是移动的,等等相关信息,便于随后做出驾驶决策。
SLAM VS BEV:SLAM主要通过各种传感器扫描周围空间的物体结构,以3维数据来描述这些信息。BEV同样通过传感器扫描获知周边状况,主要以2维数据来描述这些信息。从应用范围来讲,目前SLAM更为广阔,在AD火起来之前主要应用在VR/AR等领域,BEV主要集中在AD行业里。从技术实现来看,SLAM偏向于传统数学工具,包括各种几何/概率论/图论/群论相关的软件包,而BEV基本上清一色的基于深度神经网络DNN。两者最好不要对立着看,很多情况下可以互补。
以下将侧重于BEV的基础介绍。
SLAM和BEV最基础和核心的传感器就是相机(Camera),所以两者在计算过程中有大量的算力都被消耗在了图像中信息提取/识别和变换计算。SLAM倾向于识别图像中的特征(Feature)点,属于特征信息里的低级信息,通过计算这些特征点在不同图像帧上的位置来获取场景结构以及相机自身的位姿(Position and Pose)。而BEV倾向于识别车辆/道路/行人/障碍物等等高级特征信息,这些是卷积网CNN和Transformer擅长的。
相机有两个最基础的数据:内参(Instrinsics)和外参(Extrinsics),内参主要描述的是相机的CCD/CMOS感光片尺寸/分辨率以及光学镜头的系数,外参主要描述的是相机在世界坐标系下的摆放位置和朝向角度。
其中内参的常见矩阵是:
其中fx和fy分别表示光学镜头的横向/纵向焦距长度(Focus),正常情况下焦距是不分横纵向的,但因为CCD/CMOS感光片上的像素单元不够正,如果这个像素是绝对的正方形,那么fx = fy,实际上很难做到,有微小的差异,导致光线经过镜头投射到感光片上后,横纵坐标在单位距离上出现不等距的问题,所以相机模块的厂家会测量这个差异并给出fx和fy来,当然开发者也可以利用标定(calibration)过程来测量这两个值。
图1
图2
另外,在传统的光学领域里,fx和fy的默认单位是毫米:mm,但在这个领域默认单位是像素:Pixel,导致很多有摄影经验的人看到fx和fy的值都挺纳闷,特别大,动不动就是大几千,这数值都远超业余天文望远镜了。为什么这里用像素?我们试着通过内参计算一下相机的FOV(Field of View,视场大小,通常以角度为单位)就明白了:
图3
这里fy是纵向焦距,h是照片高度。因为h的单位是像素,所以fy也必须是像素,这样才好便于计算机处理,所以fx和fy的单位就统一成了像素。其实都不用到计算机这步,CCD/CMOS感光片一般是要集成另外一块芯片ISP(Image Signal Processor)的,这块芯片内部就要把感光数据转成数字化的图片,这里就可以用像素单位了。
内参除了这个矩阵外还有一套畸变(Distortion)系数K,这个东西不详细说了,正常的镜头成像后都是居中位置的变形小,四周变形大,一般通过标定(Calibration)获得这个参数后,对照片做反畸变处理,恢复出一个相对“正常”的照片。SLAM算法里很强调这个反畸变的重要性,因为特征点在照片上的绝对位置直接关系到了定位和建图的准确性,而大部分的BEV代码里看不到这个反畸变处理,一方面是BEV注重物体级别的高级特征,像素级别的轻微偏移影响不大,另一方面是很多BEV项目都是为了写论文,采用了类似nuScenes/Argoverse这类训练数据,这些数据的畸变比较小而已,一旦你在自己的项目里用了奇怪的镜头还是老老实实得做反畸变预处理。
图4
外参就简单多了,一个偏移(Transform)系数加一个旋转(Rotation)系数。
三维空间里表述旋转的计算方式常见的有2种:矩阵(Matrix)和四元数(Quaternion),为了防止矩阵方式存在万向节死锁(Gimbal Lock)问题,通常采用四元数来计算旋转。但在AD领域里很少这么干,因为相机是固定在车子上,只有垂直于地面的轴(一般是Z轴)才会发生360度的旋转,根本无法引发万向节问题,总不至于用户坚持在翻车的阶段仍旧保持自动驾驶这个诡异的需求。所以BEV的代码里通常就是矩阵形式,SLAM因为还会用在AR和其它领域,相机不是相对固定的,所以会采用四元数。另外,AD领域里不考虑透视现象,所以外参都是仿射矩阵(Affine Matrix),这点和CG领域的3维渲染是不同的。
另外,一般文章里介绍内参时还会考虑旋转偏差,这是由于CCD/CMOS感光片在工厂里被机器给装歪了,但AD领域一般不会考虑它,误差太小,而相机安装在车辆上时本身外参就有很大的相对旋转,不如一并算了,最后交由DNN学习过滤掉,而AR领域里的SLAM更是要主动计算外参,这点毛毛雨就不考虑了。
内外参了解之后,下一个基础的重点就是坐标系。AD的坐标系有好几个,不事先理清楚就直接看代码有点晕。
1、世界坐标系(World Coordination),这个是真实世界空间里,车辆的位置和方位角,通常粗略的位置是由GNSS(Global Navigation Satellite System)卫星定位系统获取,GNSS包括了美国GPS/中国BDS/欧洲Galileo/毛子GLONASS/日本QZSS/印度IRNSS,各有千秋,定位精度一言难尽,一般标称的精度都是指:车辆在空旷地区,上面有好几颗定位卫星罩着你,车辆静止,定位设备天线粗壮,无其它信号源干扰的情况下的测试结果。
如果你处在城市内,四周高楼林立,各种无线电干扰源,卫星相对你时隐时现,车速还不慢,这种情况下给你偏个几十米都是对的起你了。为此有两种常见解决方案:差分基站纠偏和地图通行大数据纠偏。这能给你造成一种错觉:卫星定位还是蛮准的。不管怎么弄,最后得到的坐标位置是经纬度,但跟常规GIS(Geographic Information System)相比,AD的经纬度不是球面坐标系,而是展开成2维地图的坐标系,所以最终在系统内的坐标系也是有区别的,比如google会把WGS84的经纬度换算成它自家地图的矩形切片编码,Uber提出过一种六边形切片的H3坐标编码,百度则是在火星坐标的基础上叠加了一个BD09的矩形切片坐标,等等诸如此类。
这些都是绝对坐标位置,而通过类似SLAM技术扫描的高精度地图还会在这个基础上引入一些相对坐标。不管怎么样,最后在代码里看到的只剩下XY了。但这些系统都不能获取车辆朝向(地理正北为0度,地理正东为90度,依此类推,这仍旧是在2维地图上表示方式),所以AD里的车辆角度都是指“轨迹朝向”,用当前位置坐标减去上一时刻的坐标获得一个指向性的矢量。当然在高精度地图的加持下,是可以通过SLAM技术算出车辆的瞬时方位角。在缺失GNSS定位的时候,比如过隧道,需要用车辆的IMU(Inertial Measurement Unit)这类芯片做惯性导航补充,它们提供的数值是一个相对的坐标偏移,但随着时间的推移累积误差大,所以长时间没有GNSS信号的时候,IMU表示也没办法。
2、BEV训练数据集的世界坐标系(nuScenes World Coordination,其它训练集就不特别说明了),这个跟GNSS的绝对坐标系就不同了:
图5
这是一个nuScenes地图,它的世界坐标系是图片坐标系,原点在图片左下角,单位是米,因此在使用训练数据集时,是不用考虑经纬度的。数据集中会根据时间序列给出车辆的瞬时位置,也就是在这个图片上的XY。
3、Ego坐标系(Ego Coordination),在BEV里,这个Ego是特指车辆本身,它是用来描述摄像机/激光雷达(Lidar,light detection and ranging)/毫米波雷达(一般代码里就简称为Radar)/IMU在车身上的安装位置(单位默认都是米)和朝向角度,坐标原点一般是车身中间,朝向如图:
图6
所以车头正放的相机默认都是Yaw(Z轴)为0度,外参(Extrinsics Matrix)主要就是描述这个坐标系的。
4、相机坐标系(Camera Coordination),切记,这个不是照片坐标系,坐标原点在CCD/CMOS感光片的中央,单位是像素,内参(Intrinsics Matrix)主要就是描述这个坐标系的。
5、照片坐标系(Image Coordination),坐标原点在图片的左上角,单位是像素,横纵坐标轴一般不写成XY,而是uv。
图7
左中右三套坐标系分别为:Ego Coordination, Camera Coordination, Image Coordination。
所以,当在BEV中做LSS(Lift,Splat,Shoot)时,需要把照片中的像素位置转换到世界坐标系时,要经历:
Image_to_Camera, Camera_to_Ego, Ego_to_World,用矩阵表示:
Position_in_World = Inv_World_to_Ego * Inv_Ego_to_Camera * Inv_Camera_to_Image * (Position_in_Image)
其中Inv_表示矩阵的逆。实际代码里,Camera_to_Image通常就是Intrinsics参数矩阵,Ego_to_Camera就是Extrinsics参数矩阵。
这里要注意的一点是:fx,fy,它们实际上是这样计算得到的:
Fx和Fy分别是横向/纵向的镜头焦距,但单位是米,Dx和Dy分别是一个像素有几米宽几米高,得出fx和fy的单位就是像素。当使用(Ego_to_Camera * Camera_to_Image)矩阵乘上Ego空间的坐标,会以像素为单位投影到照片空间,当使用(Inv_Ego_to_Camera * Inv_Camera_to_Image)矩阵乘上照片空间的坐标,会以米为单位投影到Ego空间,不会有单位上的问题。
大部分的BEV是多摄像头的,意味着要一次性把多组摄像头拍摄的照片像素换算到Ego或者世界坐标系:
图8
在统一的坐标系下,多角度的照片才能正确得“环绕”出周边的景象。另外还有一些单目(Monocular)摄像头的BEV方案,它们有的不考虑Ego坐标系,因为只有一个朝向正前方(Yaw,Pitch,Roll全部为0)的摄像头,而且原点就是这个摄像头本身,所以直接从相机坐标系跳到世界坐标系。
Frustum,这个东西在3维渲染领域通常叫做“视锥体”,用来表示相机的可视范围:
图9
红面和绿面以及线框包围起来的空间就是视锥体,绿面通常叫做近平面(Near Plane),红面叫做远平面(Far Plane),线框构成的角度叫做FOV,如果CCD/CMOS成像的高宽相同,那么近平面和远平面就都是正方形,一个FOV就足以表示,反之,就要区分为FOVx和FOVy了,超出这个视锥体范围的物体都不考虑进计算。
图7中由6个三角面构成了组合的可视范围,实际上应该是6个俯视的视锥体构成,能看出视锥体之间是有交叠区域的,这些区域有利于DNN在训练/推理中对6组数据做相互矫正,提高模型准确性,在不增加相机数量的前提下,如果想扩大这个交叠区域,就必须选择FOV更大的相机,但FOV越大的相机一般镜头畸变就会越严重(反畸变再怎么做也只能一定程度上的矫正图片),物体在图片上的成像面积也越小,干扰DNN对图片上特征的识别和提取。
BEV是个庞大的算法族,倾向于不同方向的算法选择,粗略得看,有Tesla主导的以视觉感知流派,核心算法建立在多路摄像头上,另外一大类是激光雷达+毫米波雷达+多路摄像头的融合(Fusion)派,国内很多AD公司都是融合派的,Google的Waymo也是。
严格得讲,Tesla正在从BEV(Hydranet)过渡到一种新的技术:Occupancy Network,从2维提升到3维:
图10
无论是2维的还是3维的,都在试图描述周遭空间的Occupany(占用)情况,只是一个用2维棋盘格来表述这种占用情况,一个是用3维的积木方式表述占用。DNN在度量这种占用时采用的是概率,比如我们直观看到某个格子上是一辆车,而DNN给出的原始结果是:这个格子上,是车的可能性有80%,是路面的可能性为5%,是行人的可能性为3%,所以,在BEV代码里,一般将各种可能出现的物体分了类,通常是两大类:
- 不常变化的:车辆可通信区域(Driveable),路面(Road),车道(Lane),建筑(Building),植被(Foliage/Vegetation),停车区域(Parking),信号灯(Traffic Light)以及一些未分类静态物体(Static),它们之间的关系是可以相互包容的,比如Driveable可以包含Road/Lane等等。
- 可变的:也就是会发生移动的物体:行人(Pedestrian),小汽车(Car),卡车(Truck),锥形交通标/安全桶(Traffic Cone)等等
这样分类的目的是便于AD做后续的驾驶规划(Planning,有的翻译成决策)和控制(Control)。而BEV在感知(Perception)阶段就是按照这些物体在格子上出现的概率打分,最后通过Softmax函数将概率归一取出最大的那个可能性作为占用这个格子的物体类型。
但这有个小问题:BEV的DNN模型(Model)在训练阶段,是要指明照片中各个物体是啥?也就是要在标注数据(Labeled Data)上给各种物体打上类型标签的。
图11
右边的我们权当做是标注数据吧,左边是对应的相片,按照这个物体分类训练出来的DNN模型,真得跑上路面,如果遭遇了训练集里未出现的物体类型怎么办?如果模型效果不好,比如某个姿势奇葩的人体未被识别成行人和其它已知类型,又当如何?Occupancy Network为此改变的感知策略,不再强调分类了(不是不分类,只是重点变了),核心关注路面上是否有障碍物(Obstacle),先保证别撞上去就行了,别管它是什么类型。3维的积木方式表述这种障碍物更为贴切,有的地方借用了3维渲染(Rendering/Shading)领域的常见概念把这种3维表述叫做体素(Voxel),想象一下我的世界(MineCraft)就很简单了。
图12
以上是视觉流派的简述,混合派在干嘛?它们除了相机外,还侧重于激光雷达的数据,毫米波雷达由于数据品相太差逐渐退出,留守的去充当停车雷达了,也不能说它一无是处,Tesla虽然强调视觉处理,但也保留了一路朝向正前方的毫米波雷达,而且AD这个领域技术变化非常快,冷不丁哪天有新算法冒出又能把毫米波雷达的价值发扬光大一把。
激光雷达的好处是什么:可以直接测出物体的远近,精度比视觉推测出的场景深度要高很多,一般会转化为深度(Depth)数据或者点云(Point Cloud),这两者配套的算法有很长的历史了,所以AD可以直接借用,减少开发量。另外,激光雷达可以在夜间或糟糕的天气环境下工作,相机就抓瞎了。
但这几天出现了一种新的感知技术HADAR(Heat-Assisted Detection and Ranging),可以和相机/激光雷达/毫米波雷达并列的传感器级别感知技术。它的特点是利用特殊的算法把常规热成像在夜间拍摄的图片转化为周围环境/物体的纹理和深度,这个东西和相机配合能解决夜间视觉感知的问题。
以前的BEV为什么不提热成像/红外相机,因为传统算法有些明显的缺陷:只能提供场景的热量分布,形成一张灰度(Gray)图,缺乏纹理(Texture),原始数据缺乏深度信息,推算出的深度精度差,如果仅仅通过从灰度图上提取的轮廓(Contour)和亮度过渡(Gradient),很难精确还原场景/物体的体积信息,并且目前的2维物体识别是很依赖纹理和色彩的。这个HADAR的出现,恰好可以解决这个问题:在较暗的环境下提取场景的深度以及纹理:
图13
左列,自上而下:
- 基础的热成像,简称T
- 用常规热成像算法从T提取的深度
- 用HADAR算法从T提取的纹理图
- 用HADAR算法从T提取的深度
- 真实场景的深度
右列,自上而下:
- 这个场景在白天用可见光相机拍摄的照片
- 通过照片推理的深度
- 真实场景的深度
HADAR的这个深度信息老牛逼了,对比一下激光雷达的效果就知道了:
图14
激光雷达的扫描范围是有限的,一般半径100米,从上图可以看出,没有纹理信息,远处的场景也没有深度了,扫描线导致其数据是个稀疏(Sparse)结构,想要覆盖半径更大更稠密(Dense)就必须买更昂贵的型号,最好是停下来多扫一段时间。激光雷达模块厂家在展示产品时,当然得给出更好看的图了,只有AD研发人员才知道这里面有多苦。
以上都是基础的概念,作为BEV算法的入门,必须先提到LSS(Lift,Splat,Shoot):
https://link.zhihu.com/?target=https%3A//github.com/nv-tlabs/lift-splat-shoot
老黄家的,很多文章都把它列为BEV的开山(Groundbreaking)之作。它构建了一个简单有效的处理过程:
把相机的照片从2维数据投影成3维数据,然后像打苍蝇一样把它拍扁,再从上帝视角来看这个被拍扁的场景,特别符合人看地图的直觉模式。一般看到这里会有疑惑的:都已经建立了3维的场景数据,3维不香么?干嘛还要拍扁?不是不想要3维,是没办法,它不是一个完善的3维数据:
图15
看过这玩意吧,它就是LSS的本质,从正面看,能形成一张2维照片,这个照片被LSS拉伸到3维空间后就是上图,你从BEV的视角也就是正上方向下看会是啥?什么都看不出来,所以后续要拍扁(Splat),具体过程是这样:
图16
先提取图像特征和深度(Feature and Depth,LSS里是同时提取的,后面会具体解释),深度图类似:
图17
只能说类似,并不准确,后面也会具体说明的,这个深度信息可以构建一个伪3D模型(Point Cloud点云模式),类似图15:
图18
看着还行,但把这个3D模型转到BEV俯视角下,估计亲娘都认不出来了:
图19
拍扁后结合特征Feature再做一次语义识别,形成:
图20
这个就是喜闻乐见的BEV图了。以上是对LSS的直观认知,算法层面是如何实现的?
先给单个相机可拍摄的范围构建一个立方体模样的铁丝笼子(高8宽22深41),祭出大杀器Blender:
图21
这里是示意图,不要纠结于格子的数量和尺寸。这个3D网格代表的是一路相机的视锥体(Frustum),前面贴过视锥体的形状(图9),这里变形成立方体,在相机空间里看这个照片和这个立体网格的关系就是:
图22
右边是个正对着网格立方体的相机示意图,相片提取深度后(深度图的实际像素尺寸是高8宽22):
图23
把这个深度图按照每个像素的深度沿着红线方向展开(Lift)后:
图24
可以看到,部分深度像素已经超出了视锥体的范围,因为LSS一开始就假设了这么个有限范围的笼子,超出部分直接过滤掉。这里必须提醒一下:LSS并不是直接算出每个像素的深度,而是推理出每个像素可能处于笼子里每个格子的概率,图24是已经通过Softmax提取出每个像素最有可能位于哪个格子,然后把它装进对应格子的示意结果,便于理解,更准确的描述如下:
图25
在图25中选取深度图的某个像素(红色格子,事实上LSS的深度图分辨率是很小的,默认只有8*22像素,所以这里可以用一个格子当做一个像素),它隶属于笼子下方边沿的一条深度格子(这条格子其实就代表相机沿着深度看向远方的一条视线):
图26
图25中的那个红色的深度像素,沿着图26这条视线格子的概率分布就是:
图27
黄线的起伏表示2D深度图像素在Lift后沿着视线3D深度的概率分布(Depth Distribution,我这是示意性得画法,不是严格按照实际数据做的)。等价于LSS论文里的这张图:
图28
LSS中构建立方笼子的代码位于:
class LiftSplatShoot(nn.Module): def __init__(self, grid_conf, data_aug_conf, outC): self.frustum = self.create_frustum() def create_frustum(self): # D x H x W x 3 frustum = torch.stack((xs, ys, ds), -1) return nn.Parameter(frustum, requires_grad=False) def get_geometry(self, rots, trans, intrins, post_rots, post_trans): """Determine the (x,y,z) locations (in the ego frame) of the points in the point cloud. Returns B x N x D x H/downsample x W/downsample x 3 """ B, N, _ = trans.shape # undo post-transformation # B x N x D x H x W x 3 points = self.frustum - post_trans.view(B, N, 1, 1, 1, 3) points = torch.inverse(post_rots).view(B, N, 1, 1, 1, 3, 3).matmul(points.unsqueeze(-1)) # 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) return points
为了便于分析,我裁减了代码。单个相机的frustum尺寸为:D x H x W x 3(深度D:41,高度H:8,宽度W:22),也就是创建了一个D x H x W的容器,容器的每个格子里存储了这个格子的坐标值(X,Y,Z)。
图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。
图30
Augmentation技术也是有相关理论和方法的,这里就贴个图不赘述了。数据增强的代码一般都是位于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://link.zhihu.com/?target=https%3A//github.com/Megvii-
BaseDetection/BEVDepth
BEVDepth的测试里发现:如果把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是锦上添花的事,不是必要的。