首页 > 编程语言 >BEV感知算法:LSS论文与代码详解

BEV感知算法:LSS论文与代码详解

时间:2024-07-01 14:31:01浏览次数:3  
标签:self 相机 详解 geom LSS 维度 BEV feats

BEV感知算法:LSS论文与代码详解

0. 前言

最近几年,BEV感知是自动驾驶领域中一个非常热门研究方向,其核心思想是把多路传感器的数据转换到统一的BEV空间中去提取特征,实现目标检测、地图构建等任务。如何把多路相机的数据从二维的图像视角转换到三维的BEV视角?LSS提出一种显示估计深度信息的方法,实现图像特征到BEV特征的转换,从而实现语义分割任务。

LSS是英伟达在ECCV2020上发表的文章《Lift, Splat, Shoot: Encoding Images from Arbitrary Camera Rigs by Implicitly Unprojecting to 3D》中提出的一个BEV感知算法,后续很多BEV感知算法如CaDDNBEVDet都是在LSS的基础上实现的。本文将结合论文和代码详细解读LSS的原理。

附赠自动驾驶最全的学习资料和量产经验:链接

1. 核心思想

作者提出一种新的端到端架构,该架构可以从任意数量的相机中直接提取给定图像数据场景中的鸟瞰图(bird’s-eye-view,BEV)表示,其核心思想是将每张图像单独“提升(Lift)”到每个相机的特征视锥体中,然后将所有视锥体“溅射(Splat)”到栅格化的BEV网格中。结果表明,这样训练的模型不仅能够学习如何表示图像特征,还能够学习如何将来自全部相机的预测结果融合到单个内聚的场景表示中,同时对标定误差具有鲁棒性。在基于BEV的目标分割和地图分割等任务中,该模型都比之前的模型表现得更出色。

image

2. 算法原理

image

2.1 Lift: 潜在的深度分布

这一步的目的是把每个相机的图像从局部2D坐标系Lift到全部相机共享的统一3D坐标系,这个操作过程每个相机是独立进行的。

image

image

image

2.2 Splat:Pillar池化

作者采用与pointpillars算法中一样的方式处理Lift操作生成的点云,一个Pillar定义为无限高度的体素。每个点被分配到与其最近的Pillar中然后执行求和池化,产生一个可以被标准CNN处理的�×�×�维度的张量。

image

为了提升效率,作者采用“累计求和”的方式实现求和池化,而不是等填充完每个Pillar后再来做池化。这种操作具有可分析的梯度,可以高效地计算以加速自动微分过程。由于Lift操作生成的点云坐标只与相机的内外参有关,因此可以预先给每个点分配一个索引,用于指示其属于哪个Pillar。对所有点按照索引进行排序,累积求和的具体实现过程如下:

image

图片来源于深蓝学院《BEV感知理论与实践》

2.3 Shoot:运动规划

这个操作是根据前面BEV空间的感知结果学习端到端的轨迹预测代价图用于运动规划。由于我们主要关注感知部分,这部分就不做过多介绍

3. 代码解析

如果只看论文,估计很多人看完论文后还是一头雾水,根本不知道LSS到底是怎么实现的。接下来我们就结合代码对LSS的每个步骤进行详细解析。

LSS模型被封装在src/model.py文件中的LiftSplatShoot类中,模型用Nuscense数据集进行训练,每次输入车身环视6个相机的图像。Nuscense数据集中的原始图像宽高为1600x900,在预处理的时候被缩放到352x128的大小,6个相机的图像经过预处理后组成一个维度为(B=1,N=6,C=3,H=128,W=352)的张量输入给LSS模型。前向推理时,LiftSplatShoot类的forward函数需要输入以下几个参数:

  • **x**6个相机的图像组成的张量,(1,6,3,128,352)

  • **rots**6个相机从相机坐标系到自车坐标系的旋转矩阵,(1,6,3,3)

  • **trans**6个相机从相机坐标系到自车坐标系的平移向量,(1,6,3)

  • **intrins**6个相机的内参矩阵,(1,6,3,3)

  • **post_rots**6个相机的图像因预处理操作带来的旋转矩阵,(1,6,3,3)

  • **post_trans**6个相机的图像因预处理操作带来的平移向量,(1,6,3)

LSS模型前向推理的大致流程如下图所示:

image

LiftSplatShoot类的初始化函数中,会调用create_frustum函数去为相机生成图像坐标系下的视锥点云,维度为(D=41,H=8,W=22,3),其中D表示深度方向上离散深度点的数量,3表示每个点云的坐标[h,w,d]。

def create_frustum(self):
    # make grid in image plane
    # 模型输入图片大小,ogfH:128, ogfW:352
    ogfH, ogfW = self.data_aug_conf['final_dim']
    # 输入图片下采样16倍的大小,fH:8, fW:22
    fH, fW = ogfH // self.downsample, ogfW // self.downsample
    # ds取值范围为4~44,采样间隔为1
    ds = torch.arange(*self.grid_conf['dbound'], dtype=torch.float).view(-1, 1, 1).expand(-1, fH, fW)
    D, _, _ = ds.shape
    # xs取值范围为0~351,在该范围内等间距取22个点,然后扩展维度,最终维度为(41,8,22)
    xs = torch.linspace(0, ogfW - 1, fW, dtype=torch.float).view(1, 1, fW).expand(D, fH, fW)
    # ys取值范围为0~127,在该范围内等间距取8个点,然后扩展维度,最终维度为(41,8,22)
    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维度为(41,8,22,3)
    frustum = torch.stack((xs, ys, ds), -1)
    return nn.Parameter(frustum, requires_grad=False)

在推理阶段,会根据相机的内外参把图像坐标系下的视锥点云转换到自车坐标系下,这个过程在get_geometry函数中实现:

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

    # 坐标系转换过程:图像坐标系 -> 相机坐标系 ->自车坐标系

    # points[:, :, :, :, :, :2]表示图像坐标系下的(h,w),points[:, :, :, :, :, 2:3]为深度d
    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

要想看懂这个函数中关于坐标系转换的代码,我们需要了解不同坐标系之间的关系。

image

图片来源于深蓝学院《BEV感知理论与实践》

imageBEV感知算法 | LSS论文与代码详解

image

说完了视锥点云的创建与变换过程,我们再来看一下模型对输入图像数据的处理。由6个相机的图像组成的张量x的维度为(1,6,3,128,352),推理时首先把维度变换为(1 * 6,3,128,352),然后送入camencode模块中进行处理。在camencode模块中,图像数据首先被送入EfficientNet-B0网络中去提取特征,该网络输出的两层特征x1x2的维度分别为(6,320,4,11)和(6,112,8,22)。接下来,x1x2被送入到Up模块中进行处理。在该模块中,对x1进行上采样把维度变为(6,320,8,22),然后与x2拼接到一起,最后经过两层卷积处理,输出维度为(6,512,8,22)的张量。这个张量再经过一个核大小为1x1的卷积层depthnet处理,输出的维度为(6,105,8,22)。在这105个通道中,其中前41个会用SoftMax函数求取表示41个离散深度的概率,另外64个通道则表示前面说过的上下文向量,这41个深度概率与64个上下文特征向量会做一个求外积的操作。整个camencode模块输出的张量维度为(6,64,41,8,22),最终这个张量的维度会被变换为(1,6,41,8,22,64)。(这段文字对照上面的流程图来看效果会更好

到这里,Lift这部分的操作就讲完了,接下来我们来看Splat

Splat操作的第一步是构建BEV空间下的特征,这个过程在voxel_pooling函数中实现。该函数有两个输入,一个自车坐标系下的视锥点云坐标点geom,维度为(1,6,41,8,22,3);另一个是camencode模块输出的图像特征点云x,维度为(1,6,41,8,22,64)。voxel_pooling函数的处理过程如下:

  1. x的维度变换为(1 * 6 * 41 * 8 * 22,64);

  2. geom转换到体素坐标下,得到对应的体素坐标,并将参数范围外的点过滤掉;

  3. 将体素坐标系下的geom的维度变换为(1 * 6 * 41 * 8 * 22,3),然后给每个点分配一个体素索引,再根据索引值对geomx进行排序,这样归属于同一体素的点geom及其对应的特征向量x就会被排到相邻的位置;

  4. 用累计求和的方式对每个体素中的点云特征进行求和池化;

  5. unbind对张量沿Z维度进行分离,然后将分离的张量拼接到一起进行输出。由于Z维度的值为1,这样做实际上是去掉了Z维度,这样BEV空间下的特征就构建好了。下图是对BEV特征做可视化的结果:

image

def voxel_pooling(self, geom_feats, x):
    B, N, D, H, W, C = x.shape
    Nprime = B*N*D*H*W

    # 将特征点云展平,共有B*N*D*H*W个点,每个点包含C维特征向量
    x = x.reshape(Nprime, C)

    # 把自车坐标系下的坐标转换为体素坐标,然后展平
    geom_feats = ((geom_feats - (self.bx - self.dx/2.)) / self.dx).long()
    geom_feats = geom_feats.view(Nprime, 3)
    # 求每个点对应的batch size
    batch_ix = torch.cat([torch.full([Nprime//B, 1], ix,
                            device=x.device, dtype=torch.long) for ix in range(B)])
    geom_feats = torch.cat((geom_feats, batch_ix), 1)

    # 过滤点范围外的点
    kept = (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]
    geom_feats = geom_feats[kept]

    # 求每个点对应的体素索引,并根据索引进行排序
    ranks = geom_feats[:, 0] * (self.nx[1] * self.nx[2] * B)\
        + geom_feats[:, 1] * (self.nx[2] * B)\
        + geom_feats[:, 2] * B\
        + geom_feats[:, 3]
    sorts = ranks.argsort()
    x, geom_feats, ranks = x[sorts], geom_feats[sorts], ranks[sorts]

    # 累计求和,对体素中的点进行求和池化
    if not self.use_quickcumsum:
        x, geom_feats = cumsum_trick(x, geom_feats, ranks)
    else:
        x, geom_feats = QuickCumsum.apply(x, geom_feats, ranks)

    # final:(B x C x Z x X x Y),(1 x 64 x 1 x 200 x 200)
    final = torch.zeros((B, C, self.nx[2], self.nx[0], self.nx[1]), device=x.device)
    # 把特征赋给对应的体素中
    final[geom_feats[:, 3], :, geom_feats[:, 2], geom_feats[:, 0], geom_feats[:, 1]] = x

    # 去掉Z维度
    final = torch.cat(final.unbind(dim=2), 1)

    # final:(1,64,200,200)
    return final

作者设置的自车坐标系下的感知范围(以米为单位)为:

  • x:[-50.0, 50.0]

  • y:[-50.0, 50.0]

  • z:[-10.0, 10.0]

在划分体素时,3个坐标轴方向分别以0.5,0.5,20.0的间隔进行划分,所以一共有200x200x1个体素。

在构建好BEV特征后,该特征会被送入bevencode模块进行处理,bevencode模块采用ResNet-18网络对BEV特征进行多尺度特征提取与融合。bevencode模块输出的特征被用于实现BEV空间下的语义分割任务,下图是对语义分割结果做可视化的效果:

image

5. 参考资料

  • 《Lift, Splat, Shoot: Encoding Images from Arbitrary Camera Rigs by Implicitly Unprojecting to 3D》

  • 深蓝学院《BEV感知理论与实践》课程

标签:self,相机,详解,geom,LSS,维度,BEV,feats
From: https://blog.csdn.net/NEON7788/article/details/140100135

相关文章

  • DWA(Dynamic Window Approach)局部路径规划算法详解及代码实现
    DWA(Dynamic Window Approach)局部路径规划算法详解及代码实现二、算法原理一句话概况,就是假定机器人当前以若干组容许范围内的速度(差速轮为例:线速度V,角速度W)进行移动,并对这若干组速度进行轨迹计算,得到若干组轨迹,再根据若干条评分机制选择最好的轨迹所对应的速度作为dwa输......
  • Dubbo 协议详解
    Solomon_肖哥弹架构跟大家“弹弹”分布式微服务Dubbo协议详解欢迎点赞,收藏,关注。关注本人的公众号Solomon肖哥弹架构获取更多的惊喜协议的概念协议是两个网络实体进行通信的基础,数据在网络上从一个实体传输到另一个实体,以字节流的形式传递到对端。在这个字节流的......
  • c指针详解(2)--- 指针与数组
    在大致了解了c语言中变量在内存中的分配、存活等方面后,我们再来看看数组在内存中又是如何呈现的。这里我们就只讨论静态数组,动态数组涉及到动态内存分配,这里就不详细展开了。那么什么是静态数组呢?要理解这个数据结构,我们可以将其切分为两个概念:静态与数组。数组:数组其实就是一......
  • Stable Diffusion之最全详解图解
    稳定扩散(StableDiffusion)是指在图论和网络科学领域中,一种基于随机漫步的扩散模型。该模型可以用来描述节点在网络上的扩散过程,例如信息传播、疾病传播等。稳定扩散模型的基本思想是,节点在网络上随机选择邻居节点进行转移,转移概率与节点之间的连接强度相关。具体来说,稳定扩散......
  • 54、Flink 测试工具测试 Flink 作业详解
    测试Flink作业a)JUnit规则MiniClusterWithClientResourceApacheFlink提供了一个名为MiniClusterWithClientResource的Junit规则,用于针对本地嵌入式小型集群测试完整的作业。叫做MiniClusterWithClientResource.要使用MiniClusterWithClientResource,需要添加......
  • 53、Flink 测试工具测试用户自定义函数详解
    1.测试用户自定义函数a)单元测试无状态、无时间限制的UDF示例:无状态的MapFunction。publicclassIncrementMapFunctionimplementsMapFunction<Long,Long>{@OverridepublicLongmap(Longrecord)throwsException{returnrecord+1;}......
  • SSE使用详解
    一、SSE简介SSE是一种在网页开发中使用的、基于HTTP长连接技术,允许服务器向客户端浏览器实时推送更新。客户端通过创建一个EventSource对象并指向服务器上的一个URL来发起请求,这个请求保持打开状态,服务器可以在这个单一的TCP连接上不断发送新的数据块。这些数据块被称为“......
  • Mysql表的增删改查详解
    3.表的增删改查创建一个学生表DROPTABLEIFEXISTSstudent;CREATETABLEstudent(idINT,snINTcomment'学号',nameVARCHAR(20)comment'姓名',qq_mailVARCHAR(20)comment'QQ邮箱');单行数据+全列插入插入两条记录,value_list数量必须......
  • CUDA编程的注意事项和使用流程详解
    目录一、背景二、CUDA编程的基本概念 2.1、CUDA线程(Thread):2.2、线程块(Block):2.3、网格(Grid):2.4、内存模型:三、CUDA编程流程3.1.环境配置3.2.编写CUDA代码 3.2.1、初始化和分配内存3.2.2、数据传输3.2.3、内核函数(Kernel)调用3.2.4、结果传回主机3.2.5、释放资源......
  • sqlmap注入详解
    免责声明:本文仅做分享...目录1.介绍2.特点3.下载4.帮助文档5.常见命令指定目标请求HTTPcookie头HTTPUser-Agent头HTTP协议的证书认证HTTP(S)代理HTTP请求延迟设定超时时间设定重试超时设定随机改变的参数值利用正则过滤目标网址避免过多的错误请求被屏......