自动驾驶BEV感知系列算法整理总结

BEV感知技术解析:从多视角融合到自动驾驶环境建模 计算机视觉中的多视角感知与特征融合是自动驾驶环境理解的基础技术。其核心原理在于将多个摄像头采集的二维图像信息,通过深度估计与空间转换算法,统一映射到三维鸟瞰视角下的特征表示空间。这种技术价值在于解决了传统感知方案中视角不统一、信息割裂的问题,为自动驾驶系统提供了无盲区、标准化的环境表征。在应用场景上,该技术广泛应用于自动驾驶的实时环境建模、障碍物检测与轨迹预测等关键任务。通过BEV(鸟瞰视角)感知框架,系统能够实现多摄像头数据的特征级前融合,显著提升感知精度与系统鲁棒性。当前技术路线主要分为基于Transf 阅读详情

序论

BEV系列的感知算法是指在BEV上进行预测分割,这样对于下游任务更加友好,而BEV算法类别之间最大的区别就是在图像特征到BEV转换这一块(img2bev),根据这一块不同,我们将算法分为三类:

第一类显示转换:根据像素坐标以及模型预测的像素深度分布得到(x,y,z),然后通过相机内外参转换到BEV上得到BEV下的(X,Y,Z)

第二类隐式转换:首先根据BEV上的分辨率进行位置编码同时加上lidar特征,然后让其作为transformer里的Q查询向量往图像特征,图像位置编码和内外参编码组成的KV里面查询,经过交叉注意力机制最终输出查询后得到的BEV特征。

第三类显示逆向转换:显式转换是将图像向BEV转,那我直接先指定好BEV空间的体素位置,根据体素的位置(X,Y,Z)这样根据内外参可以查询得到像素空间的(x,y,z)直接把对应的像素特征赋给BEV的体素。

一、显示转换的BEV感知

1.LSS

论文链接:https://arxiv.org/pdf/2008.05711.pdf

代码链接:GitHub - nv-tlabs/lift-splat-shoot: Lift, Splat, Shoot: Encoding Images from Arbitrary Camera Rigs by Implicitly Unprojecting to 3D (ECCV 2020)

概述:LSS的思想是对每个像素预测一个D维的深度分布,然后HWD与HWC外积,得到一个HWDC的特征,给定一个hwd,我们根据相机内外参可以将其转化到BEV空间里的一个XYZ,这样把所有像素空间的HWD都转到BEV,最终按照体素pooling池化一下,就得到了BEV特征

优点:实现了相机特征到BEV特征的转换

缺点:极度的依赖depth预测的准确性,同时矩阵外积过于耗时,要想在depth维度有较高的精度,HWD计算量特别大。

A:Lift操作

LSS中的L表示lift,升维的意思,表示把H*W*C的图像特征升维到H*W*D*C,D表示在每个像素点都预测了一个D维的深度信息,比如D是50维,相当于这个像素点在0-50m里出现的概率。D的一个维度表示在这一米出现的概率。

第一步:get_geometry(得到特征图与3D空间的索引对应)

torch.arange生成D的列表,view加expand变成(D,H,W)的维度

torch.linespace生成特征图长宽HW的列表分布,里面的值是像素坐标,同样view+expand变为(D,H,W),然后用torch.stack将这三个在-1维度拼接。得到(D,H,W,3)的矩阵,相当于给定一个D,H,W的特征点,我们可以得到三维的像素坐标表示,这个像素坐标需要转换到真实车身坐标系。

将图像数据增强进行抵消,同时根据相机内外参矩阵与上面的相乘,得到(D,H,W,3)这个3表示的就是车身坐标系的3D位置。此时我们就可以根据一个D,H,W索引的特征点,得到其在真实空间的位置。这一步是接下来投影的关键。

第二步:get_cam_feats(得到相机的D,H,W型feature)

图像经过骨干网络处理之后,用一个1*1的卷积,将channel变为D+C,在channel维度选取前D维,进行softmax操作,相当于说这个H,W的特征点在D的深度上的分布概率。将(N,1,D)和(N,C,1)做乘法,相当于矩阵外积,得到(N,C,D)的张量,N表示的是H*W,所以我们就得到了(D,H,W,C)的图像特征。

B:Splat操作

splat表示拍平的意思,上一步我们得到了像素空间与真实3D空间的坐标索引,同时也得到了(D,H,W,C)的图像特征高维表示。这一步就是把特征转换到BEV空间中,并拍平为2D,相当于pointpillar的操作。

第一步:预处理操作

传进来的就是geom_feats维度为(D,H,W,3)和X图像高维特征(D,H,W,C),首先geom_feats里3这个维度,里面的值是车身坐标系的3D位置,有正有负,我们先把他变为从0开始的长整形分布,比如x是0-100m,y是0-60m,z是0-5m这样,全为正的表示,相当于做个平移。然后view成(Nprime,3),然后循环batch,用torch.cat以及torch.full生成一个(Nprime,1)的batch索引,再和geom_feats用torch.cat一下,得到新的geom_feats维度为(Nprime,4),里面是真实空间的xyzb,然后用xyz的范围进行过滤一下。

第二步:拍平操作

得到维度为(Nprime,4)的geom_feats,这里说的也不准确,因为过滤了范围外的,第一维度就不再是Nprime,大家理解就行。然后根据BEV分辨率对geom_feats进行排序,geom_feats[:,0]*Y*Z*B+geom_feats[:,1]*Z*B+geom_feats[:,2]*B+geom_feats[:,3],然后argsort排序,对X以及geom_feats进行排序。X维度为(Nprime,C),对其进行cumsum操作,也叫前缀和操作,然后找到网格变化的节点,用当前变化节点减去前面的变化节点,就是中间这个网格的和,起到了一个sum_pooling操作,最后根据geom_feats的索引,把X放入到(B,C,Z,X,Y)里面,再把Z用unbind和cat操作压缩即可。

代码分析

"""
Copyright (C) 2020 NVIDIA Corporation.  All rights reserved.
Licensed under the NVIDIA Source Code License. See LICENSE at https://github.com/nv-tlabs/lift-splat-shoot.
Authors: Jonah Philion and Sanja Fidler
"""

import torch
from torch import nn
from efficientnet_pytorch import EfficientNet
from torchvision.models.resnet import resnet18

from .tools import gen_dx_bx, cumsum_trick, QuickCumsum


class Up(nn.Module):
    def __init__(self, in_channels, out_channels, scale_factor=2):
        super().__init__()

        self.up = nn.Upsample(scale_factor=scale_factor, mode='bilinear',
                              align_corners=True)

        self.conv = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        )

    def forward(self, x1, x2):
        x1 = self.up(x1)
        x1 = torch.cat([x2, x1], dim=1)
        return self.conv(x1)


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)

    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)
        # Depth
        x = self.depthnet(x) #得到(B*N, D+C, fH,fW)

        depth = self.get_depth_dist(x[:, :self.D]) #在深度维度进行softmax操作
        new_x = depth.unsqueeze(1) * x[:, self.D:(self.D + self.C)].unsqueeze(2) #(B*N,1,D,fH,fW) *(B*N,C,1,fH,fW)

        return depth, new_x #返回维度为 (B*N,C,D,fH,fW)

    def get_eff_depth(self, x):
        # adapted from https://github.com/lukemelas/EfficientNet-PyTorch/blob/master/efficientnet_pytorch/model.py#L231
        endpoints = dict()

        # Stem
        x = self.trunk._swish(self.trunk._bn0(self.trunk._conv_stem(x)))
        prev_x = x

        # Blocks
        for idx, block in enumerate(self.trunk._blocks):
            drop_connect_rate = self.trunk._global_params.drop_connect_rate
            if drop_connect_rate:
                drop_connect_rate *= float(idx) / len(self.trunk._blocks) # scale drop connect_rate
            x = block(x, drop_connect_rate=drop_connect_rate)
            if prev_x.size(2) > x.size(2):
                endpoints['reduction_{}'.format(len(endpoints)+1)] = prev_x
            prev_x = x

        # Head
        endpoints['reduction_{}'.format(len(endpoints)+1)] = x
        x = self.up1(endpoints['reduction_5'], endpoints['reduction_4'])
        return x

    def forward(self, x):
        depth, x = self.get_depth_feat(x)

        return x


class BevEncode(nn.Module):
    def __init__(self, inC, outC):
        super(BevEncode, self).__init__()

        trunk = 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.bn1
        self.relu = trunk.relu

        self.layer1 = trunk.layer1
        self.layer2 = trunk.layer2
        self.layer3 = trunk.layer3

        self.up1 = Up(64+256, 256, scale_factor=4)
        self.up2 = nn.Sequential(
            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 = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)

        x1 = self.layer1(x)
        x = self.layer2(x1)
        x = self.layer3(x)

        x = self.up1(x, x1)
        x = self.up2(x)

        return x


def cumsum_trick(x, geom_feats, ranks):
    x = x.cumsum(0)  #(N,C) 0表示后面的行累加前面的行。
    kept = torch.ones(x.shape[0], device=x.device, dtype=torch.bool)
    kept[:-1] = (ranks[1:] != ranks[:-1])#当后面一位与前面的值不一样时,为1.相当于得到voxel变化时的边界点

    x, geom_feats = x[kept], geom_feats[kept] #得到变化时的特征和坐标映射
    x = torch.cat((x[:1], x[1:] - x[:-1]))#后面的减去前面的得到当前voxel的特征和。

    return x, geom_feats #此时得到每个voxel的特征和


class LiftSplatShoot(nn.Module):
    def __init__(self, grid_conf, data_aug_conf, outC):
        super(LiftSplatShoot, self).__init__()
        self.grid_conf = grid_conf
        self.data_aug_conf = data_aug_conf

        dx, bx, nx = gen_dx_bx(self.grid_conf['xbound'],
                                              self.grid_conf['ybound'],
                                              self.grid_conf['zbound'],
                                              )
        self.dx = nn.Parameter(dx, requires_grad=False)
        self.bx = nn.Parameter(bx, requires_grad=False)
        self.nx = nn.Parameter(nx, requires_grad=False)

        self.downsample = 16
        self.camC = 64
        self.frustum = self.create_frustum()
        self.D, _, _, _ = self.frustum.shape
        self.camencode = CamEncode(self.D, self.camC, self.downsample)
        self.bevencode = BevEncode(inC=self.camC, outC=outC)

        # toggle using QuickCumsum vs. autograd
        self.use_quickcumsum = True
    
    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  #得到图片特征图上的分辨率
        #预设的深度范围,如4-45m,每隔1m取个点,得到ds维度 [D,fH,fW]
        ds = torch.arange(*self.grid_conf['dbound'], dtype=torch.float).view(-1, 1, 1).expand(-1, fH, fW)
        D, _, _ = ds.shape
        # 在原始分辨率上取fW个x,fH个y,他们的维度是[D,fH,fW]
        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)

    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
        # self.frustum是一个图像像素3D坐标,维度为B x N x D x H x W x 3,B是batch_size,N是相机个数。
        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)) #数据增强旋转矩阵的逆与像素3D坐标相乘

        # cam_to_ego 这一块看像素坐标系到车身坐标系的转换公式。λ(x,y,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) #加上外参平移矩阵得到BEV车身坐标系下的xyz

        return points

    def get_cam_feats(self, x):
        """Return B x N x D x H/downsample x W/downsample x C
        """
        B, N, C, imH, imW = x.shape #得到原始图片

        x = x.view(B*N, C, imH, imW) #把batch_size和N个相机合并
        x = self.camencode(x) #先送入骨干,然后把特征维度变为D+C,前D维softmax,然后外积得到(B*N,C,D,fH,fW)
        x = x.view(B, N, self.camC, self.D, imH//self.downsample, imW//self.downsample)
        x = x.permute(0, 1, 3, 4, 5, 2)

        return x  #返回特征维度 [B,N,D,fH,fW,C]

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

        # flatten x
        x = x.reshape(Nprime, C) #将所有的维度压缩在一起,除了C

        # B x N x D x H x W x 3将车身坐标系的坐标变为从000开始的
        geom_feats = ((geom_feats - (self.bx - self.dx/2.)) / self.dx).long()
        geom_feats = geom_feats.view(Nprime, 3)
        #得到[Nprime,1]的batch_idx下标。
        batch_ix = torch.cat([torch.full([Nprime//B, 1], ix,
                             device=x.device, dtype=torch.long) for ix in range(B)])
        #得到(Nprime, 4)里面4维是三维的BEV坐标和一个batch_idx
        geom_feats = torch.cat((geom_feats, batch_ix), 1)

        # 过滤掉不在BEV范围里的下标。
        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)
自动驾驶感知——BEV感知(学习笔记) 最后,MLP自下而上,而基于 Transformer 的方法采用了自上而下的策略,通过注意力机制构造查询和搜索相应的图像特征。 阅读详情

相关推荐

自动驾驶感知——视觉感知经典算法

定义(狭义):给定图像III, 输出图像中所有目标实例的包围框集合BBkk1KBBk​k1K​,并提供对应包围框中目标实例的类别。

sinat_52032317的博客 8895

极坐标BEV:自动驾驶感知的空间建模新范式

BEV(鸟瞰图)是自动驾驶感知的核心空间表征,而传统直角坐标系BEV因等距网格设计,在近场信息过载与远场特征坍缩之间难以兼顾。极坐标BEV通过角度θ与对数径向距离r的非均匀采样,实现对真实驾驶场景几何特性的主动适配,显著提升bev轨迹预测稳定性与bevfusion多传感器融合精度。其技术价值在于降低单模态投影信息损失、压缩推理延迟、兼容现有标注体系,并已在Orin-X等车规芯片上实现15ms级实时部署。本文深入解析该范式的数学原理、光栅化实现及量产落地路径。

weixin_33719619的博客 507

BEV视觉3D感知算法梳理

这部分的实现逻辑与传统的Transformer的Decoder的逻辑类似,利用Cross-Attention模块将生成的3D空间下的Object Query和具有3D空间位置的语义特征进行交互,得到Output Embedding,然后利用FFN网络充当3D检测头实现最终的3D检测结果。

@bangbang的博客 6395

第二十四章 BEV感知系列一(车道线感知)

近期参与到了手写AI的车道线检测的学习中去,以此系列笔记记录学习与思考的全过程。车道线检测系列会持续更新,力求完整精炼,引人启示。所需前期知识,可以结合手写AI进行系统的学习。BEV感知系列是对论文的翻译整理,有多处瑕疵,敬请谅解。​ 自动驾驶中的感知识别任务是本质上是对物理的三维几何重建世界。随着传感器的多样性和数量变得自动驾驶的装备越来越复杂车辆(SDV),表示从不同角度的特征统一的观点至关重要。众所周知的鸟瞰图(BEV)是一种自然而直接的候选视图,可以作为统一的表示。

专注于人工智能学习,总结 1124

YOLOv8n模型导出ONNX的避坑指南:解决版本差异,正确生成TensorRT/RKNN所需输出节点

本文详细介绍了YOLOv8n模型导出ONNX格式的实战避坑指南,涵盖版本差异处理、代码修改技巧及TensorRT/RKNN部署优化方案。针对不同推理框架需求,提供输出节点生成、动态轴处理和后处理划分等核心问题的解决方案,帮助开发者高效完成模型部署。

weixin_33247358的博客 350

SAP成本计划BAPI接口BAPI_COSTACTPLN_POSTPRIMCOST深度解析与实战指南

在SAP ERP系统中,成本计划是财务与控制模块的核心功能,它涉及对企业未来成本支出的预测与规划。其原理是通过结构化数据模型,将成本要素分配到具体的成本对象上,以实现预算编制和成本控制。这一技术的核心价值在于实现成本数据的标准化、自动化处理,从而提升财务流程效率与数据准确性。在应用场景上,成本计划广泛应用于年度预算编制、项目成本预测、系统间数据集成以及历史数据迁移等关键业务环节。本文聚焦于实现上述流程自动化的关键技术——SAP标准接口BAPI_COSTACTPLN_POSTPRIMCOST,并深入探讨其在高

weixin_29657209的博客 342

3DGS实战笔记:从相机坐标系到屏幕像素,手把手推导高斯椭球的投影变换矩阵

本文详细解析了3D高斯散射(3DGS)技术中从相机坐标系到屏幕像素的几何变换过程,重点推导了高斯椭球的投影变换矩阵。通过视图变换、投影变换和视口变换的完整流程,结合雅可比矩阵处理协方差矩阵,实现了高效的三维重建与渲染。文章提供了Python代码示例,帮助开发者深入理解3DGS的核心几何变换机制。

weixin_28691741的博客 435

手把手教你用Python+GDAL处理0.5米吉林一号卫星影像(附代码)

本文详细介绍了如何使用Python和GDAL处理吉林一号0.5米高分辨率卫星影像,包括环境配置、数据预处理、分辨率融合、植被指数计算及地物分类等关键步骤。通过实战代码示例,帮助开发者高效处理大尺寸遥感数据,解决资源崩溃和算法选择问题,提升遥感影像分析能力。

weixin_30892037的博客 117

把对讲门铃(控制底楼铁门)接入米家服务器

今天讲个跟SDR或者无人机无关的东西,技术含量不高,但是方便了我的生活。 现在家门有智能锁,汽车也可以远程解锁发动,NFC门禁也可以用某些手机复制,唯独楼下的铁门,必须用老式对讲门铃控制。 也就是说其它钥匙都可以不带,就剩这个钥匙还必须随身携带。 另外有时候人不在家,不能开楼下铁门,快递员也没法把东西送上楼。 ......

老邵的开源世界 6534

保姆级教程:用K210麦克风阵列模块实现声源定位(附完整Python代码与坐标计算)

本教程详细介绍了如何使用K210麦克风阵列模块实现高精度声源定位,包括硬件连接、算法解析与实战代码。通过Python代码示例,展示了从原始数据获取到三维空间坐标计算的完整流程,适用于智能家居、机器人导航等场景。

weixin_42558093的博客 219

LDRA Testbed软件静态分析_常见问题及处理

通过前面的几篇文章,LDRA Testbed软件静态分析的内容基本讲解完了,本篇文章将针对使用Testbed进行软件静态分析的常见问题进行介绍。 到此为止,LDRA Testbed软件静态分析的内容已经全部讲完,如果有问题,欢迎私信提问(不经常登录,回复可能不及时,见谅)。下一篇文章将开始讲解如何使用Testbed进行单元测试。

文章主要涉及嵌入式软件开发、测试、项目管理等,欢迎交流,共同进步。 2574

canoe安装完成开启之后因为VectorDeviceParams30_64.dll报错崩溃打不开...如何解决?

🏆本文收录于 《全栈 Bug 调优(实战版)》 专栏。专栏聚焦真实项目中的各类疑难 Bug,从成因剖析 → 排查路径 → 解决方案 → 预防优化全链路拆解,形成一套可复用、可沉淀的实战知识体系。无论你是初入职场的开发者,还是负责复杂项目的资深工程师,都可以在这里构建一套属于自己的「问题诊断与性能调优」方法论,助你稳步进阶、放大技术价值。

**My Coding Family** 1051

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

上一篇: DETR论文解读
下一篇: CBGS论文与代码解析
CVplayer111
博客等级 码龄5年 236粉丝 75原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CVplayer111

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值