BEV感知:BEV开山之作LSS(lift,splat,shoot)原理代码串讲 您所在的位置:网站首页 traefik源码解析 BEV感知:BEV开山之作LSS(lift,splat,shoot)原理代码串讲

BEV感知:BEV开山之作LSS(lift,splat,shoot)原理代码串讲

2023-03-16 09:15| 来源: 网络整理| 查看: 265

自动驾驶:BEV开山之作LSS(lift,splat,shoot)原理代码串讲 前言Lift参数创建视锥CamEncode Splat转换视锥坐标系Voxel Pooling 总结

前言

在这里插入图片描述

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

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

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

Lift 参数

我们先介绍一下一些参数: 感知范围 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] ybound=[-50.0, 50.0, 0.5] zbound=[-10.0, 10.0, 20.0] dbound=[4.0, 45.0, 1.0] fH, fW = ogfH // 16, ogfW // 16

在这里插入图片描述

创建视锥

什么是视锥?

代码:

def create_frustum(self): # make grid in image plane ogfH, ogfW = self.data_aug_conf['final_dim'] fH, fW = ogfH // self.downsample, ogfW // self.downsample ds = torch.arange(*self.grid_conf['dbound'], dtype=torch.float).view(-1, 1, 1).expand(-1, fH, fW) D, _, _ = ds.shape xs = 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 3 frustum = 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,视锥中的每个点都是长方体的坐标。

CamEncode

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

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) # 输出通道数为D+C,D为可选深度值个数,C为特征通道数 self.depthnet = nn.Conv2d(512, self.D + self.C, kernel_size=1, padding=0) def get_depth_dist(self, x, eps=1e-20): return x.softmax(dim=1) def get_depth_feat(self, x): # 主干网络提取特征 x = self.get_eff_depth(x) # 输出通道数为D+C x = self.depthnet(x) # softmax编码,相理解为每个可选深度的权重 depth = 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 def forward(self, x): 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。 demo代码如下:

a = torch.ones(36*4).resize(4,6,6)+1 demo1 = a.unsqueeze(1) print(demo1.shape) b = torch.ones(36*4).resize(4,6,6)+3 demo2 = b.unsqueeze(0) print(demo2.shape) c = demo1*demo2 print(c.shape) torch.Size([4, 1, 6, 6]) torch.Size([1, 4, 6, 6]) torch.Size([4, 4, 6, 6])

在这里插入图片描述 我们观察右面的网格图,首先解释一下网格图的坐标,其中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映射到视锥坐标下。

转换视锥坐标系

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

代码如下:

def get_geometry(self, rots, trans, intrins, post_rots, post_trans): 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 Voxel Pooling

代码:

def voxel_pooling(self, geom_feats, x): # geom_feats;(B x N x D x H x W x 3):在ego坐标系下的坐标点; # x;(B x N x D x fH x fW x C):图像点云特征 B, N, D, H, W, C = x.shape Nprime = B*N*D*H*W # 将特征点云展平,一共有 B*N*D*H*W 个点 x = x.reshape(Nprime, C) # flatten indices geom_feats = ((geom_feats - (self.bx - self.dx/2.)) / self.dx).long() # ego下的空间坐标转换到体素坐标(计算栅格坐标并取整) geom_feats = geom_feats.view(Nprime, 3) # 将体素坐标同样展平,geom_feats: (B*N*D*H*W, 3) batch_ix = torch.cat([torch.full([Nprime//B, 1], ix, device=x.device, dtype=torch.long) for ix in range(B)]) # 每个点对应于哪个batch geom_feats = torch.cat((geom_feats, batch_ix), 1) # geom_feats: (B*N*D*H*W, 4) # filter out points that are outside box # 过滤掉在边界线之外的点 x:0~199 y: 0~199 z: 0 kept = (geom_feats[:, 0] >= 0) & (geom_feats[:, 0] = 0) & (geom_feats[:, 1] = 0) & (geom_feats[:, 2]


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有