BEV:DETR3D
一、论文核心思想(30秒版)
DETR3D将DETR的集合预测范式引入多视角3D检测:用可学习的3D参考点代替2D
anchor,通过3D→2D投影从多视角图像中采样特征,实现端到端的NMS-free 3D检测。
核心创新:不在图像空间做检测,而是在3D空间设置query
→ 每个query学习一个3D参考点
→ 将3D参考点投影到各相机图像上
→ 在投影位置采样特征
→ Transformer Decoder解码出3D框
关键区别于LSS/BEVDepth:
BEVDepth: 图像 → 显式深度 → BEV特征 → 检测 (自底向上)
DETR3D: 3D query → 投影到图像 → 采样特征 → 解码 (自顶向下)
二、整体架构(Pipeline)
输入: 6路环视相机图像 [B, 6, 3, H, W]
┌─────────── 图像编码 ──────────────────┐
│ GridMask数据增强 (prob=0.7) │
│ ResNet101-DCN Backbone │
│ FPN Neck → 4级多尺度特征 │
│ 输出: list of [B, 6, 256, Hi, Wi] │
└──────────────────┬───────────────────┘
│
┌──────── Detr3DTransformer ─────────────┐ ← 注意: 只有Decoder, 没有Encoder!
│ │
│ Object Queries: 900个可学习query │
│ ┌──────────────────────────────────┐ │
│ │ query_embed [900, 512] │ │
│ │ → split → query_pos [900, 256] │ │
│ │ query [900, 256] │ │
│ │ │ │
│ │ query_pos → Linear → sigmoid │ │
│ │ → reference_points [B, 900, 3] │ │ ← 3D参考点(归一化到[0,1])
│ └──────────────────────────────────┘ │
│ │
│ Decoder × 6层: │
│ ├─ Self-Attention (query间交互) │
│ ├─ Cross-Attention ← 核心创新 │
│ │ ├─ 3D参考点 → lidar2img投影 │
│ │ ├─ 6相机×4尺度 grid_sample采样 │
│ │ ├─ 学习注意力权重 + 有效性mask │
│ │ └─ 加权求和 + 位置编码 │
│ ├─ FFN │
│ └─ [可选] Box Refinement更新参考点 │
│ │
│ 输出: hs [6, B, 900, 256] │
│ reference_points每层更新 │
└──────────────────┬───────────────────┘
│
┌──────── 检测头(每层独立)──────────────┐
│ cls_branch: Linear×2 + LN + ReLU → 10 │ ← 10类
│ reg_branch: Linear×2 + ReLU → 10 │ ← cx,cy,w,l,cz,h,sin,cos,vx,vy
│ │
│ 输出坐标解码: │
│ cx,cy: reg_pred + reference_point_xy │ ← 残差学习!
│ cz: reg_pred + reference_point_z │
│ sigmoid → 反归一化到真实坐标 │
└─────────────────────────────────────────┘
│
┌──────── 后处理(NMS-Free)─────────────┐
│ sigmoid(cls_score) → topK(300) │
│ → denormalize_bbox → range filter │
│ 无需NMS! │
└─────────────────────────────────────────┘
三、核心代码逐模块精讲
DETR3D核心流程图:
# ============================================================
# 整体流程总结
# ============================================================
print("""
【流程总结】
query_embed(可学习)
│
├─ query_pos → Linear → sigmoid → 3D参考点
│ │
│ lidar2img 投影
│ │
│ 图像像素坐标(u,v)
│ │
│ grid_sample 采样
│ │
└─ query ──→ Linear → 注意力权重 ──→ 加权聚合
│
BEV特征
│
┌────────────┴──────────┐
cls_head reg_head
│ │
类别得分 偏移量 + 参考点
│
最终3D框
关键设计:
① 参考点 从3D空间出发,不需要深度估计
② mask 不可见相机权重=0,自动过滤噪声
③ 残差 预测偏移而非绝对坐标,收敛更稳
""")
在 ONNX 中的表现
FastBEV 的 ONNX 图:
[图像特征] [BEV网格坐标(常量)]
└──────────┘
↓
grid_sample
↓
BEV 特征
DETR3D 的 ONNX 图:
[图像特征] [query_embed(常量)]
↓
ref_net(权重也是常量)
↓
参考点坐标
└──────────┘
↓
grid_sample
↓
采样特征 + query → cross_attention → 更新 query
↓
检测结果
---
唯一的区别
BEV网格坐标:
人工设计,均匀网格
含义固定:就是空间坐标
训练前后不变
query_embed:
训练前:随机初始化
训练后:每个 query 学会了"负责哪类目标/哪个区域"
推理时固定,但它的值是训练出来的,不是人工设计的
---
一句话总结
对,query_embed 就是一个可训练的常量输入。
FastBEV 的 BEV 网格坐标 = 人工设计的常量
DETR3D 的 query_embed = 训练出来的常量
推理阶段两者没有区别,都是固定数据送进网络。
差异只在于 query_embed 的值是通过训练数据学出来的。
DETR3D 核心思想完整 Demo:
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(42)
# ============================================================
# 基础参数
# ============================================================
B, N_cam, C = 1, 2, 8 # batch, 相机数, 特征维度
N_query = 3 # object query 数量
N_levels = 2 # FPN 层级数
pc_range = [-51.2, -51.2, -5.0, 51.2, 51.2, 3.0] # 感知范围
# ============================================================
# 准备工作(直接作为输入)
# ============================================================
# 1. 图像特征(模拟 ResNet+FPN 输出)
img_feats = [
torch.randn(B, N_cam, C, 4, 11), # FPN level0: [B, N_cam, C, H, W]
torch.randn(B, N_cam, C, 2, 6), # FPN level1
]
# 2. lidar2img 变换矩阵(标定文件读取,这里直接给)
# 含义:将 LiDAR 坐标系下的 3D 点投影到图像像素坐标
# = 相机内参 K × 外参 [R|t]
lidar2img = torch.tensor([[
# 前视相机:目标在正前方能看到
[[600, 0, 352, 0],
[ 0, 600, 128, 0],
[ 0, 0, 1, 0],
[ 0, 0, 0, 1]],
# 右视相机:目标在右前方能看到
[[600, 0, 352, 300],
[ 0, 600, 128, 0],
[ 0, 0, 1, 0],
[ 0, 0, 0, 1]],
]], dtype=torch.float32) # [B, N_cam, 4, 4]
IMG_H, IMG_W = 256.0, 704.0
print("输入准备完毕")
print(f" img_feats: {[f.shape for f in img_feats]}")
print(f" lidar2img: {lidar2img.shape}")
# ============================================================
# 核心1:Object Query 初始化
# ============================================================
print("\n【核心1】Object Query 初始化")
# query_embed 是唯一的可学习参数入口,分为两部分:
# query_pos → 生成 3D 参考点("我在哪里")
# query → 携带语义信息("我是什么")
query_embed = nn.Parameter(torch.randn(N_query, 2 * C))
query_pos, query = query_embed[:, :C], query_embed[:, C:]
# query_pos 通过线性层 + sigmoid 生成归一化 [0,1] 的 3D 参考点
ref_net = nn.Linear(C, 3)
reference_points = ref_net(query_pos).sigmoid() # [N_query, 3]
# 反归一化看真实坐标
def denorm(ref):
r = ref.clone()
r[..., 0] = r[..., 0] * (pc_range[3]-pc_range[0]) + pc_range[0]
r[..., 1] = r[..., 1] * (pc_range[4]-pc_range[1]) + pc_range[1]
r[..., 2] = r[..., 2] * (pc_range[5]-pc_range[2]) + pc_range[2]
return r
ref_real = denorm(reference_points.detach())
print(f" 3D 参考点(真实坐标/m):")
for i, (x, y, z) in enumerate(ref_real.numpy()):
print(f" Query {i}: x={x:.1f} y={y:.1f} z={z:.1f}")
# ============================================================
# 核心2:3D→2D 投影 + 特征采样
# ============================================================
print("\n【核心2】3D→2D 投影 + 特征采样")
def feature_sampling(img_feats, reference_points, lidar2img):
N_query = reference_points.shape[0]
# Step A: 归一化坐标 → 真实3D坐标 → 齐次坐标 [1, N_query, 4]
ref = denorm(reference_points.unsqueeze(0))
ref = torch.cat([ref, torch.ones(1, N_query, 1)], dim=-1)
# Step B: 投影到每个相机
# [B, N_cam, 4, 4] × [B, 1, N_query, 4] → [B, N_cam, N_query, 4]
ref_cam = (lidar2img.unsqueeze(2) @ ref.unsqueeze(1).unsqueeze(-1)).squeeze(-1)
print(f" 投影后图像坐标(除z前):")
for cam in range(N_cam):
for q in range(N_query):
v = ref_cam[0, cam, q].detach().numpy()
print(f" cam{cam} query{q}: u*z={v[0]:.0f} v*z={v[1]:.0f} z={v[2]:.2f}")
# Step C: 透视除法 → 归一化到 [-1,1]
eps = 1e-5
depth_valid = ref_cam[..., 2:3] > eps
ref_2d = ref_cam[..., :2] / (ref_cam[..., 2:3] + eps)
ref_norm = torch.stack([
ref_2d[..., 0] / IMG_W * 2 - 1,
ref_2d[..., 1] / IMG_H * 2 - 1,
], dim=-1) # [B, N_cam, N_query, 2]
# Step D: 有效性 mask(z>0 且在图像范围内)
mask = depth_valid & (ref_norm[..., 0:1].abs() < 1) \
& (ref_norm[..., 1:2].abs() < 1)
# [B, N_cam, N_query, 1]
print(f"\n 有效性 mask(该 query 在该相机是否可见):")
for cam in range(N_cam):
for q in range(N_query):
u = ref_2d[0, cam, q, 0].item()
v = ref_2d[0, cam, q, 1].item()
valid = mask[0, cam, q, 0].item()
print(f" cam{cam} query{q}: 像素({u:.0f},{v:.0f})"
f" {'✓' if valid else '✗ 不可见'}")
# Step E: grid_sample 在各 FPN 层级采样特征
# 这是 Cross Attention 中"取 K 和 V"的过程
sampled_list = []
for feat in img_feats: # feat: [B, N_cam, C, H, W]
B_, N_, C_, H_, W_ = feat.shape
grid = ref_norm.view(B_*N_, N_query, 1, 2) # grid_sample 要求 [B, H_out, W_out, 2]
s = F.grid_sample(feat.view(B_*N_, C_, H_, W_),
grid, mode='bilinear',
padding_mode='zeros', align_corners=False)
# s: [B*N_cam, C, N_query, 1]
sampled_list.append(s.view(B_, N_, C_, N_query))
# [B, N_cam, C, N_query, N_levels]
sampled = torch.stack(sampled_list, dim=-1)
# sampled 就是 K 和 V(从图像特征中取出的内容)
return sampled, mask
sampled_feats, mask = feature_sampling(
img_feats, reference_points.detach(), lidar2img
)
print(f"\n 采样特征 shape: {sampled_feats.shape}")
print(f" [B={B}, N_cam={N_cam}, C={C}, N_query={N_query}, N_levels={N_levels}]")
# ============================================================
# 核心3:注意力加权聚合
# ============================================================
print("\n【核心3】注意力加权聚合")
# 由 query 预测每个相机×每个层级的权重
# 这是 Cross Attention 中"Q 和 K 计算相似度"的过程
attn_net = nn.Linear(C, N_cam * N_levels)
nn.init.zeros_(attn_net.weight)
nn.init.zeros_(attn_net.bias) # 初始 sigmoid(0)=0.5,所有权重相等
attn_w = attn_net(query).view(N_query, N_cam, N_levels)
attn_w = torch.sigmoid(attn_w)
# query 就是 Q
# attn_net 替代了标准 attention 的 Q@K^T
# [N_query, N_cam, N_levels]
# 不可见相机的权重强制为 0(sigmoid × mask)
# mask: [B, N_cam, N_query, 1] → [N_query, N_cam, 1]
mask_w = mask[0].permute(1, 0, 2).expand_as(attn_w)
attn_w = attn_w * mask_w.float()
print(f" 注意力权重(×mask后):")
for q in range(N_query):
for cam in range(N_cam):
w = attn_w[q, cam].detach().numpy().round(3)
m = mask_w[q, cam, 0].item()
print(f" query{q} cam{cam}: {w}"
f"{' ← masked' if not m else ''}")
# sampled_feats: [B, N_cam, C, N_query, N_levels]
# 加权求和:对 N_cam 和 N_levels 维度求和
sf = sampled_feats[0].permute(2, 0, 3, 1)
# [N_query, N_cam, N_levels, C]
aw = attn_w.unsqueeze(-1)
# [N_query, N_cam, N_levels, 1]
# 这是 Cross Attention 中"用权重加权 V"的过程
bev_feat = (sf * aw).sum(dim=1).sum(dim=1)
# [N_query, C]
print(f"\n 聚合后 BEV 特征: {bev_feat.shape} [N_query={N_query}, C={C}]")
# ============================================================
# 核心4:检测头(残差回归)
# ============================================================
print("\n【核心4】检测头 —— 残差回归")
cls_head = nn.Linear(C, 3) # 3类
reg_head = nn.Linear(C, 3) # 预测 (dx, dy, dz) 偏移量
cls_pred = cls_head(bev_feat) # [N_query, 3]
reg_pred = reg_head(bev_feat) # [N_query, 3]
# 残差:pred = sigmoid(reg_pred + inverse_sigmoid(ref_point))
# 为什么这样:直接预测绝对坐标范围大难收敛
# 预测相对参考点的偏移,网络只需学"修正量"
def inv_sigmoid(x, eps=1e-5):
x = x.clamp(eps, 1-eps)
return torch.log(x / (1 - x))
final_norm = (reg_pred + inv_sigmoid(reference_points.detach())).sigmoid()
final_real = denorm(final_norm)
print(f" 检测结果:")
cls_names = ['car', 'ped', 'bg']
for q in range(N_query):
score = torch.softmax(cls_pred[q], dim=0).detach()
pred_cls = score.argmax().item()
ref = ref_real[q].numpy()
pred = final_real[q].detach().numpy()
delta = pred - ref
print(f" Query {q}:")
print(f" 参考点: ({ref[0]:.1f}, {ref[1]:.1f}, {ref[2]:.1f})m")
print(f" 预测偏移: ({delta[0]:.2f}, {delta[1]:.2f}, {delta[2]:.2f})m")
print(f" 最终位置: ({pred[0]:.1f}, {pred[1]:.1f}, {pred[2]:.1f})m")
print(f" 类别: {cls_names[pred_cls]} {score.numpy().round(2)}")
区别:
BEVDepth:先建立完整BEV地图,再检测
需要深度估计,特征稠密
DETR3D: 直接为每个目标找图像证据
不需要深度,特征稀疏(只采样N_query个点)
四、问题
Q1: DETR3D为什么只用Decoder没有Encoder?
标准DETR的Encoder用于图像特征的全局self-attention交互。DETR3D中:
- 图像特征已经通过ResNet+FPN充分提取
- 6路相机的特征交互通过cross-attention的多相机采样间接实现
- 去掉Encoder大幅降低计算量(6路高分辨率特征做self-attention代价极高)
Q2: 3D参考点投影到图像上可能遇到什么问题?
- 投影到相机后方(z≤0):用mask屏蔽
- 投影到图像外:用范围[-1,1]的mask屏蔽
- 一个3D点可能在多个相机上可见(重叠区域):attention权重自动学习选择最优相机
- 一个3D点可能在所有相机上都不可见(被遮挡/范围外):该query倾向于预测背景
- 投影精度依赖标定:内外参不准会导致采样位置偏移
Q3: attention_weights为什么初始化为0?
constant_init(self.attention_weights, val=0., bias=0.)
- sigmoid(0) = 0.5 → 初始时所有相机/层级的权重相等
- 训练过程中逐渐学会哪些相机/层级对每个query更重要
- 避免初始权重过大导致训练不稳定
Q4: 逐层监督(auxiliary loss)的作用?
每层Decoder都计算loss → 缓解深层Transformer的梯度消失问题,加速收敛。
- 浅层学粗略定位
- 深层学精细回归
- 类似FPN的多尺度监督思想
Q5: DETR3D收敛慢怎么解决?
DETR系列的通病(原始DETR需要500 epoch),DETR3D的缓解方法:
- Box Refinement:逐层更新参考点,每层从更好的位置出发
- 逐层辅助loss:每层都有监督信号
- CBGS(类别平衡采样):缓解类别不平衡
- GridMask增强:正则化防过拟合
- 后续改进:DN-DETR的去噪训练、DAB-DETR的直接anchor学习
Q6: normalize_bbox中为什么尺寸要取log?
w = bboxes[…, 3:4].log() # width
l = bboxes[…, 4:5].log() # length
h = bboxes[…, 5:6].log() # height
- 尺寸范围很大(0.5m~20m),直接回归数值范围大
- 取log后压缩到较小范围,L1 loss更均衡
- 小目标和大目标的尺寸误差在log空间中被等比例对待
Q7为什么用sigmoid而不是softmax?
- softmax会强制所有相机权重加和为1 → 当目标只在1个相机可见时,其他相机的噪声特征也被强制分配权重
- sigmoid让每个权重独立 → mask=0的相机自然被屏蔽,可见相机的权重也可以灵活调整
- 配合mask使用:sigmoid × mask,不可见相机直接置零
六、衔接
DETR3D和BEV检测是两种不同范式,对比着讲能展示系统性理解:
▎ “LSS-based的BEV方案(类似BEVDepth),而不是DETR3D这类query-based方案,核心考量是:
▎ 1. 量产部署:BEV方案主要是标准卷积算子,部署友好;DETR3D的动态attention和匈牙利匹配在芯片上难以高效实现
▎ 2. 多任务扩展:BEV特征图可以同时给检测、分割、OCC等任务使用,而DETR3D的稀疏query表示难以直接做密集任务
▎ 3. 精度:加了深度监督后BEV方案精度不输query-based
▎ 4. 但DETR3D思路的优势在于:不需要显式深度估计、NMS-free更优雅、理论上更易端到端训练”
更多推荐


所有评论(0)