← 课程首页 | M0 理论地基 论文深度拆解
Bird’s-Eye-View Perception · 深度拆解

六路相机
如何变成一张鸟瞰世界图

把 BEVFormer 掰开揉碎:从 6 张平视照片,到一张俯视特征图。空间交叉注意力、时序自注意力、和雷达的关系——每一步数据怎么流动,全部讲清楚。

ECCV 2022UniAD 的眼睛200×200 BEV 网格面向实战
00 / 开篇

先想清楚:为什么非要"压"成鸟瞰图?

车上 6 个摄像头拍的是平视照片——跟你眼睛看到的一样:近处的车大、远处的车小,还互相遮挡。这种"透视图"对人脑友好,但对规划模块是噩梦。

规划模块要回答的是:"前方 15 米偏左 3 米有没有车"。它需要的是俯视的、带真实距离的地图,而不是一张"越远越小"的照片。所以必须把 6 张平视图"拼接 + 压平 + 摆正"成一张俯视图。这张俯视图,就是 BEV(Bird’s-Eye-View)。

◆ 一个比方

你玩赛车游戏,屏幕角落那个"小地图"就是 BEV。你的第一视角(挡风玻璃视角)能看清细节但判断不了全局位置;小地图用俯视角告诉你"对手在你左后方 20 米"。BEVFormer 做的事,就是实时把 6 个第一视角摄像头,转成那张小地图。

难点在于:从"平视"到"俯视"这一步,不是简单的图像旋转。一个像素在照片里的位置,要对应到现实世界俯视图的哪个格子,取决于这个像素到底是远处的物体还是近处的物体——而单张照片丢失了深度信息。BEVFormer 用一套巧妙的注意力机制解决了这个问题,下面一步步看。

01 / 全局架构

先看整张地图,再钻细节

BEVFormer 整体是一个"喂进 6 张图,吐出 1 张 BEV 特征图"的管道。中间叠了 6 层结构一样的编码器。先记住这张总图,后面每一节都是在放大其中一块。

6 路相机输入 前视 CAM_F 前左 FL 前右 FR 后视 B 后左 BL 后右 BR 骨干网络 ResNet-101 提取图像特征 6 视角 图像特征 F_t BEVFormer 编码器 × 6 层 ① 时序自注意力 TSA 融合上一帧 BEV(记忆) ② 空间交叉注意力 SCA 从 6 视角图像采特征 ③ 前馈网络 FFN + 残差 ↑ 这一层结构重复 6 次,逐层精炼 ↑ 每层输出喂给下一层作为新的 BEV query BEV 特征图 200×200×256 自车 3D 检测 / 地图分割头 本帧 BEV 存下来,下一帧当"记忆" 数据流向 → ━ 相机特征 ┄ 时序记忆回传 ━ BEV 输出
图 1 · BEVFormer 全局管道。6 张图经骨干网络变成图像特征,再被 6 层编码器反复"询问",最终输出 200×200 的 BEV 特征图,供检测/分割/规划使用。

三个关键部件,后面三节逐一拆解:

02 / 数据形状

跟着张量形状走一遍

作为工程师,看懂数据的 shape 怎么变,比看懂公式更重要。下面是一帧数据从输入到输出的完整形状变化(以 nuScenes 标准配置为例)。

原始图像 6×3×900×1600 6 相机·RGB·高·宽 骨干 图像特征 F_t 6×256×23×40 下采样后的特征图 SCA BEV query 40000×256 200×200 个格子 ×6层 BEV 特征 256×200×200 俯视特征图 关键尺度换算(把抽象数字落到现实): BEV 网格 = 200 × 200 每格代表现实 0.512 米 → 覆盖范围 ≈ 102m × 102m → 自车四周各约 51 米 自车永远在这张图的正中心 每个格子 = 256 维向量 这 256 个数字编码了"这块区域 有没有车、是什么、朝哪开" 40000 格 × 256 维 = 完整场景 下游检测头从这里读出 3D 框
图 2 · 张量形状变化全程。注意 BEV query 既可写成 200×200×256(网格视角),也可拉平成 40000×256(序列视角)——Transformer 内部按后者计算。
▲ 工程提示

你在 Project 2 第 3 章训练时,显存爆掉八成是栽在这里:6 路图像特征 + 40000 个 query + 6 层编码器,中间激活值非常大。这就是为什么我们用 batch_size=1 + 梯度累积 + FP16。看懂上图你就知道显存到底被谁吃了。

03 / 核心概念

BEV Query:一张会主动提问的空白地图

这是整个 BEVFormer 最反直觉、也最关键的设计。传统做法是"把像素推到 BEV 空间"(从图像出发)。BEVFormer 反过来:先准备一张空白的俯视格子地图,让每个格子主动去图像里找自己需要的信息(从 BEV 出发)。

每个格子是一个"提问者"

BEV query 是一组 可学习的参数,形状 200×200×256。你可以把它想成 40000 个小探头,每个探头钉在俯视地图的一个格子上(比如"自车正前方 10 米"那一格),它的任务是:"我负责的这块地,现实里有什么?我去 6 张照片里问问。"

◆ 一个比方

想象一张大棋盘(200×200 格)铺在马路上空,自车停在棋盘正中。每个格子里站着一个小侦察兵,手里拿着 6 张照片(6 个摄像头拍的)。每个侦察兵只关心自己脚下这块地:他抬头在 6 张照片里找"我这块地对应照片的哪个位置",然后把那里的信息记下来。40000 个侦察兵同时工作,填满整张棋盘,就得到了 BEV 特征图。

关键一步:一个格子要采样多个高度

现实世界是 3D 的,而 BEV 格子只有 2D 坐标 (x, y)。一个格子上方可能是路面、可能是车顶、可能是红绿灯。所以每个格子不是只取 1 个点,而是沿高度方向取 4 个参考点(论文中从 -5m 到 3m 每隔约 2m 取一个),组成一根"柱子(pillar)"。

BEV 俯视网格(取1格) Q 查询点 p=(x,y) 沿高度立一根柱子,取 4 点 z=+3m (高处/信号灯) z=+1m (车身) z=-1m (车轮) z=-5m (路面) 每个点投影到对应相机 前视相机图像 前左相机图像 …只投到能看见的相机 投不到的相机直接跳过 用相机的内参+外参矩阵,把每个 3D 点精确投影到 2D 照片上,在落点附近采样特征 这一步叫 "lifting" —— 从 2D 图像把信息"抬"到 3D BEV 空间
图 3 · BEV query 的工作方式:每个格子沿高度取 4 个 3D 点 → 用相机标定参数投影到能看见它的相机图像上 → 在投影落点附近采样图像特征。
● 这就回答了你的核心疑问

"6 个摄像头怎么融合成 BEV?"——答案就在这里:不是把图像拼接,而是每个 BEV 格子拿着自己的 3D 坐标,用相机标定参数反向投影到各个相机,去采样对应位置的特征。一个格子可能同时落在 2 个相机的重叠视野里,那就两个相机的特征加权平均。融合是在"每个格子各自查询"这个过程中天然完成的。

03.5 / 表示的本质

那 256 维到底装了什么?和 Transformer 是一回事吗?

先给结论:是的,完全是一回事。BEVFormer 的 256 维,和 Transformer / BERT / GPT 里的 d_model(隐藏维度)是同一个概念——只不过把"句子里的词"换成了"地图上的格子"。下面把这件事讲透。

先对齐概念:256 是"隐藏维度",不是算出来的

这个 256 不是某个公式的结果,而是作者设计选定的超参数,在 BEVFormer 代码里叫 embed_dims。它的作用是:让模型内部所有东西都统一成 256 维向量,这样它们才能互相做注意力运算——query、key、value 维度必须对齐,否则矩阵乘法都做不了。这是注意力机制能跑起来的硬前提。

同一个 Transformer 骨架,两种"token" NLP:一句话 = 一串词向量 "猫 坐 在 垫 子 上" 猫 坐 在 … 0.2 -0.8 0.5 ⋮ 0.1 每个词 = 768 维向量 (BERT-base) 形状:[句子长度 × 768] 序列 = 几百个词 每个向量编码:词义/语法/上下文角色 BEV:一张图 = 一串格子向量 200×200 格 0.4 0.9 -0.3 ⋮ 0.2 每个格子 = 256 维向量 (embed_dims) 形状:[40000 × 256] 序列 = 40000 个格子 每个向量编码:有无物体/类别/朝向/速度…
图 3.5 · 同一个 Transformer 骨架的两种用法。NLP 把句子拆成词向量序列;BEVFormer 把俯视图拆成格子向量序列。Transformer 内部只看到"一串等长向量",根本不在乎它们是词还是地图格子。

对照表:它们就是同一个东西

概念原版 TransformerBERT-baseGPT 类BEVFormer
隐藏维度的名字d_modelhidden_sizen_embdembed_dims
典型取值512768768~12288256
序列里每个"token"是一个词一个词片一个词片一个 BEV 格子
序列长度句子长度≤512上下文长度40000 (200×200)

把这张表横着读:200×200×256 摊平就是 40000×256——和 BERT 处理一个 [序列长度 × 768] 的句子,在数据结构上一模一样。

核心问题:词的 768 维 vs 格子的 256 维,各装了什么"语义"?

这是你问得最关键的一点。先说共同的本质:无论 768 还是 256,这些维度不是人为规定每一维代表什么的。没有"第 5 维 = 是不是动物""第 200 维 = 颜色"这种事先约定。它们是训练中自己学出来的分布式表示——语义被"打散"编码在所有维度的组合里。

▲ 什么叫"分布式表示"

不是一维管一个意思,而是一个语义由很多维度协同表达,一个维度也参与表达很多语义。就像"猫"的含义不写在某一维上,而是散落在 768 个数的整体模式里。这就是为什么你没法指着某一维说"这维是啥意思"——它对人不可读,但对模型是高效的。

维度NLP 词向量(768维)隐式编码BEV 格子向量(256维)隐式编码
语义内容词义、近义/反义关系、一词多义的当前义这块地有没有物体
语法角色(主语/谓语/宾语)是什么类别(车/人/锥桶/路面)
上下文关系(它指代谁、修饰谁)物体朝向、尺寸
情感、语气、领域风格运动速度(来自时序融合)
为何更宽/更窄语言信息密度极高,768 起步,大模型上万单格语义相对简单,256 够用

那这 256 维的语义到底"从哪来"?

这是理解表示学习的关键。词向量和 BEV 格子向量,语义来源路径不同,但机制相同——都来自"任务监督下的梯度下降"。

词向量的语义从哪来

初始时每个词就是一串随机数(毫无意义)。训练时模型被要求完成任务(预测下一个词 / 完形填空)。为了把任务做对,模型被迫调整这些数字——逐渐让"猫"和"狗"的向量靠近(都是动物)、让"猫"和"汽车"的向量拉远。语义是任务逼出来的副产品。

BEV 格子语义从哪来:第一步,采集

每个格子的 256 维,初始也是随机的可学习参数(就是第 3 节那个"空白探头")。前向传播时,它通过空间交叉注意力从 6 路图像采样特征,通过时序自注意力融合上一帧记忆。这一步把"原始视觉信息"灌进了这 256 维。

BEV 格子语义从哪来:第二步,监督

光采集还不够,模型不知道该把信息编码成什么样。下游任务的损失函数是真正的老师:检测头要求"这格有车就得能解码出车的框",分割头要求"这格是车道线就得能分出来"。这些 loss 反向传播回来,逼着这 256 维去编码"对检测/分割有用的语义"。

结果:语义是"被任务塑造"出来的

训练收敛后,这 256 维就自发地组织出了"有无物体、类别、朝向、速度"这些信息——不是因为我们规定它这么做,而是因为只有这么编码,下游任务才能做对、loss 才能降下来。

● 一句话讲透"语义来源"

词向量的语义来自语言任务(预测词)塑造;BEV 格子的语义来自感知任务(检测/分割)塑造。两者机制完全相同:随机初始化 → 前向采集信息 → 任务 loss 反向传播 → 维度被逼着编码出有用的语义。维度本身只是"容器",装什么由任务决定。这就是"表示学习"四个字的真正含义——表示不是设计的,是学出来的。

✓ 呼应你 Project 2 的那个坑

还记得你 VLA 调试时栽过的"hidden_size 必须动态解析,硬编码会出错"吗?那个 hidden_size 和这里的 256 是同一个东西——模型的隐藏维度。不同模型这个值不同(Qwen-3B 一个值、7B 另一个值、BEVFormer 是 256),因为它是每个模型自己选的超参数,不是固定常数。你当时踩的坑,根源就是没意识到"隐藏维度是模型相关的设计选择"。现在这个概念彻底通了。

04 / 空间维度

空间交叉注意力:格子去图像里"采购"

上一节讲了"投影采样"的直觉,这一节讲它的正式机制:Spatial Cross-Attention(SCA)。名字拆开看就懂——"空间"指的是把图像空间信息搬到 BEV 空间;"交叉"指 query 来自 BEV、key/value 来自图像(两个不同来源,所以叫 cross);"注意力"指用注意力机制决定采哪里。

为什么用"可变形注意力"而不是普通注意力?

普通注意力(global attention)会让每个 query 跟图像里所有像素算一遍关系。6 路图像特征有几万个像素,40000 个 query 全连一遍——计算量爆炸,根本跑不动。

BEVFormer 用可变形注意力(Deformable Attention):每个 query 不看全图,只在"投影落点"附近采几个点(稀疏采样)。这就是为什么图 3 里每个格子只投影到几个相机的几个位置。

STEP 1 · 确定参考点

格子 (x,y) 沿高度取 4 个 3D 点。这 4 个点是这个 query 的"关注锚点"。

STEP 2 · 投影到相机

用相机内参 K 和外参 [R|t],把每个 3D 点投影成某张照片上的 2D 坐标 (u,v)。一个点可能投到前视相机,另一个点可能落在前左相机。投不到的相机(背对着的)直接忽略。

STEP 3 · 稀疏采样特征

在每个 (u,v) 落点附近,用双线性插值采样图像特征。可变形注意力还会学习一个偏移量,自动微调采样位置(因为标定不完美、物体有形变)。

STEP 4 · 加权汇总

把所有命中相机、所有高度点采到的特征,按注意力权重加权求和,写回这个格子的 256 维向量。一个格子的"采购"就完成了。

用公式说一句(看不懂可跳过)

# 空间交叉注意力的本质(伪代码)
def spatial_cross_attention(Q_p, image_features):
    # Q_p: 某个 BEV 格子的 query 向量 (256,)
    total = 0
    hit_cameras = 0
    for cam in all_6_cameras:           # 遍历 6 个相机
        for j in range(4):                 # 遍历柱子上 4 个高度点
            uv = project(p, cam, height_j)  # 3D点 → 2D像素坐标
            if uv in cam.image_bounds:    # 这个相机能看见才采
                feat = deform_attn(Q_p, uv, image_features[cam])
                total += feat
                hit_cameras += 1
    return total / hit_cameras          # 命中的相机做平均(融合!)
▲ 工程提示

注意最后一行 total / hit_cameras——这就是多相机融合发生的地方。一个格子如果同时被前视和前左两个相机看到,它的特征就是这两个相机的平均;只被一个相机看到,就用那一个。这种"按可见性自适应融合"远比硬拼接鲁棒。

04.5 / 交互演示

动手试试:点一个格子,看它去哪几张图找数据

下面这个演示把前面讲的"投影找特征"完全跑活了。点击(或拖动)左边俯视网格里的任意格子,右边会实时显示:这个格子命中了哪几个相机、在每张照片上的具体像素落点、以及它为什么跳过另几个相机。自车永远在网格正中,车头朝上。

BEV 俯视网格 · 点击任意格子
选中格子 — 点击网格 —
6 路相机 · 高亮=命中
命中相机 — —
1
格子的 3D 坐标点一个格子开始
2
投影到 6 相机用内参 K · 外参 [R|t] 反算像素
3
命中判定落在画面内→采样;画面外/背后→跳过
4
加权融合命中多个相机→特征加权平均
图 4.5 · 交互式投影演示。这是真实 BEVFormer 里每个格子都在做的事 —— 几何投影决定"去哪几张图的哪个点",注意力只负责采到后怎么加权。
● 玩的时候注意这几个现象

① 正前方的格子只命中前视相机一张;② 斜前方的格子常常同时命中"前视+前左/前右"两张(视野重叠区,这里就是融合发生的地方);③ 把格子点到自车正侧面或正后方,会看到前视相机变灰(跳过),后方相机亮起。这正是"该用哪张图由几何自动决定,不是人为分配"的直观体现。

05 / 时间维度

时序自注意力:给模型装上记忆

只看当前一帧有两个致命问题:① 算不出速度(一张静止照片看不出车在动多快);② 怕遮挡(前车被卡车挡住这一帧,模型就"瞎了")。人类开车靠的是连续观察——上一秒看到的车,这一秒就算被挡住,你也知道它还在。BEVFormer 用时序自注意力(Temporal Self-Attention, TSA)复现这个能力。

机制:把"上一帧的 BEV"当成可查询的记忆

BEVFormer 不重新处理历史所有帧(太贵),而是只保留上一帧算好的 BEV 特征,递归地往前传。当前帧的每个 query 会同时看两个东西:① 自己当前帧的初始 query;② 上一帧对应位置的 BEV 特征。

上一帧 BEV (t-1) 车在这个位置 运动对齐 按自车位移 SE(3) 补偿 时序自注意力 当前 query + 对齐后的 历史 BEV 注意力融合 ↓ 当前帧 6 相机 (经空间交叉注意力) 当前帧 BEV (t) 车移动了 → 算出速度! 本帧结果存下来,成为下一帧的"上一帧记忆" → 递归循环
图 4 · 时序自注意力。先把上一帧 BEV 按自车运动对齐(车自己也在动),再与当前 query 做注意力融合。对比两帧同一物体的位置变化,模型就能推断速度,也能"记住"被临时遮挡的物体。
◆ 一个比方

就像你开车进隧道,前车被大货车挡了一下。你不会慌——因为0.5 秒前你看到它在那,你的大脑自动"补"出它现在大概的位置。时序自注意力就是模型的这个"短期记忆 + 脑补"能力。论文里专门强调:它对速度估计和重度遮挡物体检测帮助巨大,而几乎不增加计算量。

▲ 一个容易忽略的细节

对齐那一步很关键:自车本身在移动。上一帧的 BEV 是以"上一帧自车位置"为中心的,这一帧自车往前开了几米。所以要先用自车位移(里程计/IMU 给的 SE(3) 变换)把历史 BEV"挪正",再做注意力。否则两帧坐标系对不齐,记忆就成了噪声。这也是你 Project 2 里"驾驶指令要用帧间 yaw 差,不是绝对 yaw"那个教训的同源逻辑。

06 / 多模态

那激光雷达、毫米波到底掺不掺进来?

这是你问得最好的一个点,也是最容易被各种博客讲混的地方。直接给结论:

● 一句话讲清

原版 BEVFormer(2022,ECCV)是纯相机方案——只吃 6 路图像,不碰任何雷达。但 BEVFormer 的作者后来出了期刊扩展版(2024,T-PAMI),把它升级成了多模态:激光雷达也能通过"空间交叉注意力"融进同一个 BEV 空间。

为什么 BEV 这个表示天然适合融合多种传感器?因为BEV 是一个统一的"世界坐标"空间。不管信息来自相机、激光雷达还是毫米波,只要能投影到"自车周围 100m×100m 的俯视网格",就能在同一张图上叠加。

传感器给 BEV 贡献什么怎么融进去谁在用
相机 ×6 颜色、纹理、语义(红绿灯、车道线、是轿车还是卡车) 空间交叉注意力,投影采样(本文主线) Tesla、小鹏(纯视觉)
激光雷达 精确的 3D 距离和形状(几何信息最准) 点云先编码成 BEV 特征,再和相机 BEV 拼接/交叉注意力 华为、理想、Waymo
毫米波雷达 直接测速度(多普勒)、全天候(雨雾不怕)、远距离 稀疏点投影到 BEV 网格,作为额外通道或 query 补充 几乎所有量产车的兜底
相机 ×6 语义/纹理/颜色 激光雷达 精确 3D 几何 毫米波雷达 速度/全天候 投影采样→BEV 体素化→BEV 点投影→BEV 统一 BEV 空间 三种信息叠在同一张俯视网格 检测/分割/规划 下游任务不关心来源 关键洞察:BEV 是"传感器无关"的中间表示 —— 这正是它成为行业标准的根本原因
图 5 · 多模态融合的统一性。无论相机、激光雷达还是毫米波,各自编码成 BEV 后,在同一张俯视网格上叠加。下游任务读的是融合后的统一特征,不需要关心信息来自哪个传感器。
✓ 对你 Project 2 / Project 3 的意义

你 Project 2 跑的是纯相机 BEVFormer(对标小鹏/Tesla 路线),这是入门最干净的起点。Project 3 第 5-6 章会加 LiDAR(PointPillars)+ Camera 融合,本质就是上图——把点云也编码成 BEV,和相机 BEV 拼起来。看懂这张图,你 Project 3 的融合章节就有了全局认知,不会一头雾水。

07 / 产业实战

现在的自动驾驶,到底怎么用它?

BEVFormer 不是终点,而是现代自动驾驶感知栈的"地基层"。它输出的那张 BEV 特征图,是后面一切任务的共同输入。

在量产系统里的位置

# 现代端到端自动驾驶的典型数据流(以 UniAD 为例)
6路相机 ─► BEVFormer ─► BEV特征 ┬─► TrackFormer  # 检测+跟踪
                                  ├─► MapFormer    # 在线建图(车道线)
                                  ├─► MotionFormer # 预测别人怎么动
                                  ├─► OccFormer    # 占据栅格(哪有障碍)
                                  └─► Planner      # 规划自车轨迹
# BEVFormer 是"眼睛",上面 5 个模块都靠它喂的 BEV 特征工作

三条产业路线怎么用它

厂商路线BEV 怎么用
Tesla FSD纯视觉 + Occupancy Network8 相机 → BEV → 占据网络,完全不用 BEV 高精地图,端到端到控制
小鹏 XNGP纯视觉 + 轻地图BEV 感知 + 神经网络规划,"无图"全国可开
华为 ADS / 理想激光雷达 + 相机融合多模态 BEV,几何最准,城区复杂场景更稳
● 2025-2026 的趋势

纯检测框正在被占据网络(Occupancy)取代——不再只输出"框",而是输出整个 3D 空间的占据情况(连没见过的异形障碍物也能识别)。同时端到端把感知-预测-规划拧成一个网络。但无论怎么演进,"把多传感器压成统一 BEV 表示"这个核心思想没变,BEVFormer 开创的范式仍是地基。

面试官最爱问的 5 个点(你现在应该能答了)

✓ 下一步

这套理解打通后,回去看你 Project 2 第 3 章的 BEVFormer 训练脚本,那些 bev_h=200、bev_w=200、num_points_in_pillar=4、encoder layers=6 的配置项,现在每一个你都知道它对应本文的哪张图了。带着这张"地图"去读代码,比硬啃快 10 倍。