当前位置: 首页 > news >正文

BEV学习---LSS-1:论文原理及代码串讲

在这里插入图片描述

目前在自动驾驶领域,比较火的一类研究方向是基于采集到的环视图像信息,去构建BEV视角下的特征完成自动驾驶感知的相关任务。所以如何准确的完成从相机视角向BEV视角下的转变就变得由为重要。目前感觉比较主流的方法可以大体分为两种:

1、显式估计图像的深度信息,完成BEV视角的构建,在某些文章中也被称为自下而上的构建方式;
2、利用transformer中的query查询机制,利用BEV Query构建BEV特征,这一过程也被称为自上而下的构建方式;

LSS最大的贡献在于:提供了一个端到端的训练方法,解决了多个传感器融合的问题。传统的多个传感器单独检测后再进行后处理的方法无法将此过程损失进行反向传播而调整相机输入,而LSS则省去了这一阶段的后处理,直接输出融合结果。

LSS提出了一种将多视角的相机图像融合在BEV空间下的编码方法,
其中:
Lift:通过预测深度信息,将2D图像编码到3D空间;
Splat:将3D特征的高度拍扁为BEV特征;
Shoot:运动规划。

一、Lift:
1.1、参数:
我们先介绍一下一些参数。

感知范围:
x轴方向的感知范围 -50m ~ 50m;y轴方向的感知范围 -50m ~ 50m;z轴方向的感知范围 -10m ~ 10m;

BEV单元格大小:
x轴方向的单位长度 0.5m;y轴方向的单位长度 0.5m;z轴方向的单位长度 20m;

BEV的网格尺寸:
200 x 200 x 1;
深度估计范围:
由于LSS需要显式估计像素的离散深度,论文给出的范围是 4m ~ 45m,间隔为1m,也就是算法会估计41个离散深度,也就是下面的dbound。

why dbound:
因为二维像素可以理解为现实世界中的某一个点到相机中心的一条射线,我们如果知道相机的内外参数,就是知道了对应关系,但是我们不知道是射线上面的那一个点(也就是不知道depth),所以作者在距离相机5m到45m的视锥内,每隔1m有一个模型可选的深度值(这样每个像素有41个可选的离散深度值)。

在这里插入图片描述
代码如下:

ogfH=128  
ogfW=352
xbound=[-50.0, 50.0, 0.5]		# x方向网格
ybound=[-50.0, 50.0, 0.5]		# y方向网格
zbound=[-10.0, 10.0, 20.0]		# z方向网格
dbound=[4.0, 45.0, 1.0]			# 深度方向网格
fH, fW = ogfH // 16, ogfW // 16	# 16倍下采样

1.2、创建视锥
建立特征图在世界坐标系下坐标位置的视锥点云,也就是深度特征每个点的在相机坐标系的位置。

 def create_frustum(self):# make grid in image planeogfH, ogfW = self.data_aug_conf['final_dim']fH, fW = ogfH // self.downsample, ogfW // self.downsampleds = torch.arange(*self.grid_conf['dbound'], dtype=torch.float).view(-1, 1, 1).expand(-1, fH, fW)D, _, _ = ds.shapexs = torch.linspace(0, ogfW - 1, fW, dtype=torch.float).view(1, 1, fW).expand(D, fH, fW)ys = torch.linspace(0, ogfH - 1, fH, dtype=torch.float).view(1, fH, 1).expand(D, fH, fW)# D x H x W x 3frustum = torch.stack((xs, ys, ds), -1)return nn.Parameter(frustum, requires_grad=False)

根据代码可知,它的尺寸是根据一个2dimage构建的,它的尺寸为D H W * 3,维度3表示:【 x,y,depth】。我们可以把这个视锥理解为一个长方体,长x,宽y 高depth,视锥中的每个点都是长方体的坐标。

1.3、CamEncode
这部分主要是通过Efficient Net来提取图像的features,首先看代码:

class CamEncode(nn.Module):			# 提取图像特征,进行图像深度编码def __init__(self, D, C, downsample):super(CamEncode, self).__init__()self.D = D	# 41 深度区间【4-45】self.C = C	# 64 点的特征向量维度# efficientnet 提取特征self.trunk = EfficientNet.from_pretrained("efficientnet-b0")self.up1 = Up(320+112, 512)	# 上采样模块,输入320+112(多尺度融合),输出通道512# 1x1卷积调整通道数,输出通道数为D+C,D为可选深度值个数,C为特征通道数self.depthnet = nn.Conv2d(512, self.D + self.C, kernel_size=1, padding=0)# 深度维计算softmax,得到每个像素不同深度的概率def get_depth_dist(self, x, eps=1e-20):return x.softmax(dim=1)def get_depth_feat(self, x):# 使用efficientnet提取特征  x: BN x 512 x 8 x 22x = self.get_eff_depth(x)# 1x1卷积变换维度,输出通道数为D+C,x: BN x 105(C+D) x 8 x 22x = self.depthnet(x)# 第二个维度的前D个作为深度维,进行softmax  depth: BN x 41 x 8 x 22depth = self.get_depth_dist(x[:, :self.D])# 将深度概率分布和特征通道利用广播机制相乘# 深度值 * 特征 = 2D特征转变为3D空间(俯视图)内的特征new_x = depth.unsqueeze(1) * x[:, self.D:(self.D + self.C)].unsqueeze(2)return depth, new_x	#  new_x: BN x 64 x 41 x 8 x 22def get_eff_depth(self, x):  # 使用efficientnet提取特征# adapted from https://github.com/lukemelas/EfficientNet-PyTorch/blob/master/efficientnet_pytorch/model.py#L231endpoints = dict()# Stemx = self.trunk._swish(self.trunk._bn0(self.trunk._conv_stem(x)))  #  x: BN x 32 x 64 x 176prev_x = x # Blocksfor idx, block in enumerate(self.trunk._blocks):drop_connect_rate = self.trunk._global_params.drop_connect_rateif drop_connect_rate:drop_connect_rate *= float(idx) / len(self.trunk._blocks) # scale drop connect_ratex = block(x, drop_connect_rate=drop_connect_rate)if prev_x.size(2) > x.size(2):endpoints['reduction_{}'.format(len(endpoints)+1)] = prev_xprev_x = x# Headendpoints['reduction_{}'.format(len(endpoints)+1)] = x  # x: BN x 320 x 4 x 11x = self.up1(endpoints['reduction_5'], endpoints['reduction_4'])  # 对endpoints[4]上采样,然后和endpoints[5] concat 在一起return x  # x: 24 x 512 x 8 x 22def forward(self, x):# depth: B*N x D x fH x fW(24 x 41 x 8 x 22)  x: B*N x C x D x fH x fW(24 x 64 x 41 x 8 x 22)depth, x = self.get_depth_feat(x)return x

起初与以往的相同,到了init函数的最后一句,把feature的channel下采样到了 D +C,D与上面的视锥的D一致,用来储存深度特征,C为图像的语义特征,然后对channel为D的那部分在执行softmax 用来预测depth的概率分布,然后把D这部分与C这部分单独拿出来让二者做外积,就得到了shape为BNDCHW的feature。

在这里插入图片描述
观察右面的网格图,首先解释一下网格图的坐标,其中a代表某一个深度softmax概率(大小为H * W),c代表语义特征的某一个channel的feature,那么ac就表示这两个矩阵的对应元素相乘,于是就为feature的每一个点赋予了一个depth 概率,然后广播所有的ac,就得到了不同的channel的语义特征在不同深度(channel)的feature map,经过训练,重要的特征颜色会越来越深(由于softmax概率高),反之就会越来越暗淡,趋近于0。

二、Splat

得到了带有深度信息的feature map,那么我们想知道这些特征对应3D空间的哪个点,我们怎么做呢?

由于我们的视锥对原图做了16倍的下采样,而在上面得到feature map的感受野也是16,那么我们可以在接下来的操作把feature map映射到视锥坐标下。

2.1 转换视锥坐标系
首先我们之前得到了一个2D的视锥,现在通过相机的内外参数把它映射到车身(以车中心为原点)坐标系。

代码如下:

def get_geometry(self, rots, trans, intrins, post_rots, post_trans):"""    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  # B: batch size N:环视相机个数# 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))# 图像坐标系 -> 归一化相机坐标系 -> 相机坐标系 -> 车身坐标系# 但是自认为由于转换过程是线性的,所以反归一化是在图像坐标系完成的,然后再利用# 求完逆的内参投影回相机坐标系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)# (bs, N, depth, H, W, 3):其物理含义# 每个batch中的每个环视相机图像特征点,其在不同深度下位置对应# 在ego坐标系下的坐标return points	#维度不变,坐标值从相机坐标系->世界坐标系

2.2、BEV池化

至此,我们将6个环视摄像头拍摄的图片,通过显式的计算41个深度下的特征,将这【6,41 ,fW, fH】个特征点一一放置在了早先准备好的BEV特征空间下。

在这张像棋盘一样的BEV特征空间中,有的位置放了多个特征点,有的位置没放特征点,因此需要在每个栅格进行BEV池化,得到尺寸统一的BEV特征。

官方代码的实现思路是:
1、先根据特征点的XYZ坐标和batch,计算每个点的索引值;索引值相同的点位于同一个栅格中;【代码26-30】
2、根据索引值排序,则索引值变为有序数组,形如【1.2,1.4,2,4,4,5 … 】;
3、只需“遍历”索引值,将相同索引值的位置求和,完成池化,形如【1.2+1.4,2,4+4,5 … 】

下方voxel_pooling函数准备索引,BEV池化在QuickCumsum类中进行;

def voxel_pooling(self, geom_feats, x):# geom_feats: B x N x D x H x W x 3 (4 x 6 x 41 x 8 x 22 x 3):在ego坐标系下的坐标点;# x: B x N x D x fH x fW x C(4 x 6 x 41 x 8 x 22 x 64):图像点云特征B, N, D, H, W, C = x.shape  # B: 4  N: 6  D: 41  H: 8  W: 22  C: 64Nprime = B*N*D*H*W  # Nprime: 173184# flatten xx = x.reshape(Nprime, C)   # 将特征点云展平,一共有 B*N*D*H*W 个点 个点# flatten indicesgeom_feats = ((geom_feats - (self.bx - self.dx/2.)) / self.dx).long()  # 将ego下的空间坐标[-50,50] [-10 10]的范围平移转换到体素坐标[0,100] [0,20],计算栅格坐标并取整geom_feats = geom_feats.view(Nprime, 3)  # 将体素坐标同样展平  geom_feats: B*N*D*H*W x 3 (173184 x 3)batch_ix = torch.cat([torch.full([Nprime//B, 1], ix,device=x.device, dtype=torch.long) for ix in range(B)])  # 每个点对应于哪个batchgeom_feats = torch.cat((geom_feats, batch_ix), 1)  # geom_feats: B*N*D*H*W x 4(173184 x 4), geom_feats[:,3]表示batch_id# filter out points that are outside box# 过滤掉在边界线之外的点 x:0~199  y: 0~199  z: 0kept = (geom_feats[:, 0] >= 0) & (geom_feats[:, 0] < self.nx[0])\& (geom_feats[:, 1] >= 0) & (geom_feats[:, 1] < self.nx[1])\& (geom_feats[:, 2] >= 0) & (geom_feats[:, 2] < self.nx[2])x = x[kept]  # x: 168648 x 64geom_feats = geom_feats[kept]# get tensors from the same voxel next to each otherranks = geom_feats[:, 0] * (self.nx[1] * self.nx[2] * B)\+ geom_feats[:, 1] * (self.nx[2] * B)\+ geom_feats[:, 2] * B\+ geom_feats[:, 3]  # 给每一个点一个rank值,rank相等的点在同一个batch,并且在在同一个格子里面sorts = ranks.argsort()x, geom_feats, ranks = x[sorts], geom_feats[sorts], ranks[sorts]  # 按照rank排序,这样rank相近的点就在一起了# x: 168648 x 64  geom_feats: 168648 x 4  ranks: 168648# cumsum trickif not self.use_quickcumsum:x, geom_feats = cumsum_trick(x, geom_feats, ranks)else:x, geom_feats = QuickCumsum.apply(x, geom_feats, ranks)  # 一个batch的一个格子里只留一个点 x: 29072 x 64  geom_feats: 29072 x 4# griddify (B x C x Z x X x Y)final = torch.zeros((B, C, self.nx[2], self.nx[0], self.nx[1]), device=x.device)  # final: 4 x 64 x 1 x 200 x 200final[geom_feats[:, 3], :, geom_feats[:, 2], geom_feats[:, 0], geom_feats[:, 1]] = x  # 将x按照栅格坐标放到final中# collapse Zfinal = torch.cat(final.unbind(dim=2), 1)  # 消除掉z维return final  # final: 4 x 64 x 200 x 200

torch.autograd.Function是自定义的算子,能在python层面自定义算子的前向和后向过程。代码中前向过程使用前缀和区间求和;向量错位比较前后元素是否一致,这些计算步骤都使计算过程向量化;前向过程存储了保留下特征的位置,反向过程再使用前缀和技巧,将特征保留位置的梯度分给求和的位置。

class QuickCumsum(torch.autograd.Function):@staticmethoddef forward(ctx, x, geom_feats, ranks):# x: 168648 x 64  geom_feats: 168648 x 4  ranks: 168648 xx = x.cumsum(0) # 求前缀和  x: 168648 x 64kept = torch.ones(x.shape[0], device=x.device, dtype=torch.bool)  # kept: 168648 xkept[:-1] = (ranks[1:] != ranks[:-1])  # rank错位比较,rank[0]!=rank[1],则留下rank[1]x, geom_feats = x[kept], geom_feats[kept]  # rank值相等的点只留下最后一个,即一个batch中的一个格子里只留最后一个点 x: 29072  geom_feats: 29072 x 4x = torch.cat((x[:1], x[1:] - x[:-1]))  # x错位相减,还原前缀和之前的x,此时点的feature是rank相同点之和,相当于把同一个格子的点特征进行了sum# save kept for backwardctx.save_for_backward(kept)# no gradient for geom_featsctx.mark_non_differentiable(geom_feats)return x, geom_feats@staticmethoddef backward(ctx, gradx, gradgeom):kept, = ctx.saved_tensorsback = torch.cumsum(kept, 0)back[kept] -= 1val = gradx[back]return val, None, None

以上为LSS的主要创新点。

三、BEV空间下特征再编码

如下为BEV特征编码部分代码,利用卷积堆了些计算量,并做了多尺度融合,因为在此之前所有特征的计算是相机之间独立的,现在所有特征都统一到BEV视角,做特征融合以便于更好的利用相机之间的语义特征信息。

class BevEncode(nn.Module):def __init__(self, inC, outC):  # inC: 64  outC: 1super(BevEncode, self).__init__()# 使用resnet的前3个stage作为backbonetrunk = resnet18(pretrained=False, zero_init_residual=True)self.conv1 = nn.Conv2d(inC, 64, kernel_size=7, stride=2, padding=3,bias=False)self.bn1 = trunk.bn1self.relu = trunk.reluself.layer1 = trunk.layer1self.layer2 = trunk.layer2self.layer3 = trunk.layer3self.up1 = Up(64+256, 256, scale_factor=4)self.up2 = nn.Sequential(  # 2倍上采样->3x3卷积->1x1卷积nn.Upsample(scale_factor=2, mode='bilinear',align_corners=True),nn.Conv2d(256, 128, kernel_size=3, padding=1, bias=False),nn.BatchNorm2d(128),nn.ReLU(inplace=True),nn.Conv2d(128, outC, kernel_size=1, padding=0),)def forward(self, x):  # x: 4 x 64 x 200 x 200x = self.conv1(x)  # x: 4 x 64 x 100 x 100x = self.bn1(x)x = self.relu(x)x1 = self.layer1(x)  # x1: 4 x 64 x 100 x 100x = self.layer2(x1)  # x: 4 x 128 x 50 x 50x = self.layer3(x)  # x: 4 x 256 x 25 x 25x = self.up1(x, x1)  # 给x进行4倍上采样然后和x1 concat 在一起  x: 4 x 256 x 100 x 100x = self.up2(x)  # 2倍上采样->3x3卷积->1x1卷积  x: 4 x 1 x 200 x 200return x

四、总结:

优点:

1、LSS的方法提供了一个很好的融合到BEV视角下的方法。基于此方法,无论是动态目标检测,还是静态的道路结构认知,甚至是红绿灯检测,前车转向灯检测等等信息,都可以使用此方法提取到BEV特征下进行输出,极大地提高了自动驾驶感知框架的集成度。

2、虽然LSS提出的初衷是为了融合多视角相机的特征,为“纯视觉”模型而服务。但是在实际应用中,此套方法完全兼容其他传感器的特征融合。如果你想融合超声波雷达特征也不是不可以试试。

缺点:

1、极度依赖Depth信息的准确性,且必须显示地提供Depth 特征。当然,这是大部分纯视觉方法的硬伤。如果直接使用此方法通过梯度反传促进Depth网络的优化,如果Depth 网络设计的比较复杂,往往由于反传链过长使得Depth的优化方向比较模糊,难以取得较好效果。当然,一个好的解决方法是先预训练好一个较好的Depth权重,使得LSS过程中具有较为理想的Depth输出。

2、外积操作过于耗时。虽然对于机器学习来说,这样的计算量不足为道,但是对于要部署到车上的模型,当图片的feature size 较大, 且想要预测的Depth距离和精细度高时,外积这一操作带来的计算量则会大大增加。这十分不利于模型的轻量化部署,而这一点上,Transformer的方法反而还稍好一些。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 智谱携基座大模型 GLM-4-Plus 亮相 KDD,清言全新升级视频通话功能
  • 面试题集锦:数据库
  • 治愈系风景视频素材下载网站有哪些?令人治愈美景素材库网站分享
  • 使用安信可Ai-WB2-12F开启wifi与手机通信TCP-IP(AT指令)
  • C++系列-泛型编程概念及函数模板
  • Vue3其他Api
  • React中实现antd自定义图标,鼠标悬浮变色
  • RecyclerView网格布局如何动态设置item的显示个数
  • 【虚拟化】使用packer手搓安装linux,windows镜像并导入virsh进行管理(含Kickstart安装与linux内核参数配置)
  • SqlHelper 使用EF-Core框架 连接池处理并发
  • 数字化转型升级探索(二)
  • 算法中常用的排序
  • TMPDIR在pip|pip3 install时的作用以及tmp只有noexec权限的解决方法
  • Java笔试面试题AI答之面向对象(8)
  • C++(Qt)-GIS开发-QGraphicsView显示在线瓦片地图
  • 《剑指offer》分解让复杂问题更简单
  • Android路由框架AnnoRouter:使用Java接口来定义路由跳转
  • ES2017异步函数现已正式可用
  • ES6--对象的扩展
  • ES6核心特性
  • hadoop集群管理系统搭建规划说明
  • Hibernate最全面试题
  • IP路由与转发
  • JS+CSS实现数字滚动
  • Linux链接文件
  • Mac 鼠须管 Rime 输入法 安装五笔输入法 教程
  • node 版本过低
  • node入门
  • TypeScript实现数据结构(一)栈,队列,链表
  • 分享一份非常强势的Android面试题
  • 人脸识别最新开发经验demo
  • 如何学习JavaEE,项目又该如何做?
  • 手写双向链表LinkedList的几个常用功能
  • 小程序开发中的那些坑
  • 扩展资源服务器解决oauth2 性能瓶颈
  • ‌JavaScript 数据类型转换
  • #### golang中【堆】的使用及底层 ####
  • #我与Java虚拟机的故事#连载03:面试过的百度,滴滴,快手都问了这些问题
  • (1/2)敏捷实践指南 Agile Practice Guide ([美] Project Management institute 著)
  • (delphi11最新学习资料) Object Pascal 学习笔记---第5章第5节(delphi中的指针)
  • (el-Transfer)操作(不使用 ts):Element-plus 中 Select 组件动态设置 options 值需求的解决过程
  • (附源码)springboot 基于HTML5的个人网页的网站设计与实现 毕业设计 031623
  • (附源码)springboot炼糖厂地磅全自动控制系统 毕业设计 341357
  • (附源码)ssm高校运动会管理系统 毕业设计 020419
  • (附源码)ssm考试题库管理系统 毕业设计 069043
  • (附源码)计算机毕业设计SSM智能化管理的仓库管理
  • (亲测)设​置​m​y​e​c​l​i​p​s​e​打​开​默​认​工​作​空​间...
  • (全部习题答案)研究生英语读写教程基础级教师用书PDF|| 研究生英语读写教程提高级教师用书PDF
  • (十二)Flink Table API
  • (四) Graphivz 颜色选择
  • (学习总结16)C++模版2
  • (转)PlayerPrefs在Windows下存到哪里去了?
  • (转)负载均衡,回话保持,cookie
  • (转贴)用VML开发工作流设计器 UCML.NET工作流管理系统
  • ****Linux下Mysql的安装和配置