BEV:SparseBEV
一. 论文核心思想
完全不构建 BEV 特征图,900 个稀疏 query 在 3D 空间自适应生成采样点,直接从多视角多尺度图像特征中提取信息,用 Adaptive Mixing 聚合,实现高效端到端检测。最核心的亮点有两个:
- 自适应采样点(ASTS)
这是最核心的贡献,也是论文名字"Sparse"的来源。SparseBEV 的采样点:完全由 query 预测,自适应生成。 - Adaptive Mixing(AM)
128个采样点特征 → 如何聚合?普通做法:固定权重加权求和(信息损失大),SparseBEV:query 动态生成聚合矩阵。
二. 整体架构
6个摄像头图像 × T帧 [B, T6, C, H, W]
↓
Backbone (ResNet50/101/VoVNet/EVA02) + FPN
↓
多尺度特征 [B, T6, G*C, H, W] (4组 × 4层级)
↓
Query初始化: 30×30网格 → 900个query [B, 900, 10]
↓
┌────────────────────────────────────┐
│ 6层共享权重Transformer Decoder │
│ 每层: │
│ ① Scale-Adaptive Self-Attention │
│ ② Adaptive Spatio-Temporal Sampling│
│ ③ Adaptive Mixing │
│ ④ FFN │
│ ⑤ 分类+回归+Bbox Refine │
└────────────────────────────────────┘
↓
输出: 3D检测框 [B, 900, 10] + 类别 [B, 900, 10]
↓
NMS-Free Top-K选取 → 最终100个检测结果
三. 核心创新
# ============================================================
# 两大核心对比总结
# ============================================================
print("""
【两大核心原理总结】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
核心1:Adaptive Spatio-Temporal Sampling (ASTS)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
解决"去哪里采样"的问题
Sparse4D:几何先验(固定面中心)
SparseBEV:query 完全自适应预测
offset = Linear(query_feat) ← 数据驱动
pts = anchor_center + offset × size ← 物体尺寸缩放
+ 速度补偿:历史帧 pts -= vel × dt
一次采样顺带解决时序对齐
+ 只选最优视角(argmax valid)
view_idx 归一化为第3维
→ 3D grid_sample 一次完成多视角采样
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
核心2:Adaptive Mixing (AM)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
解决"怎么聚合"的问题
普通方法:固定权重求和(所有物体用同一套)
SparseBEV:query 动态生成聚合矩阵(每个物体定制)
M = Linear(query) → 通道混合矩阵 ← 定制通道组合
S = Linear(query) → 点压缩矩阵 ← 定制点选择
采样特征 → M(混通道)→ S(压点数)→ 256维query
大卡车的 M/S ≠ 行人的 M/S
→ 不同物体用不同的特征聚合策略
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
两者的关系
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ASTS 解决"采哪里" → 128个高质量采样点
AM 解决"怎么用" → 将128点压缩为1个256维特征
缺一不可:
采样好但聚合差 → 信息浪费
聚合好但采样差 → 巧妇难为无米之炊
""")
一个介绍核心原理的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
N_query = 3
N_groups = 2 # 采样组数(真实=4)
N_points = 2 # 每组采样点数(真实=4)
N_levels = 2 # FPN层级数
N_frames = 2 # 时序帧数(真实=8)
IMG_H, IMG_W = 256.0, 704.0
anchor_names = ['轿车', '卡车', '行人']
# 图像特征(直接作为输入)
# 多帧:[B, T*N_cam, C, H, W]
img_feats = [
torch.randn(B, N_frames * N_cam, C, 4, 8), # FPN level0
torch.randn(B, N_frames * N_cam, C, 2, 4), # FPN level1
]
# lidar2img [B, T*N_cam, 4, 4]
lidar2img = torch.tensor([[
# frame0 cam0
[[600, 0, 352, 0],
[ 0, 600, 128, 0],
[ 0, 0, 1, 0],
[ 0, 0, 0, 1]],
# frame0 cam1
[[600, 0, 352, 200],
[ 0, 600, 128, 0],
[ 0, 0, 1, 0],
[ 0, 0, 0, 1]],
# frame1 cam0(同上,简化)
[[600, 0, 352, 0],
[ 0, 600, 128, 0],
[ 0, 0, 1, 0],
[ 0, 0, 0, 1]],
# frame1 cam1
[[600, 0, 352, 200],
[ 0, 600, 128, 0],
[ 0, 0, 1, 0],
[ 0, 0, 0, 1]],
]], dtype=torch.float32) # [B, T*N_cam, 4, 4]
# 时间差(当前帧到各历史帧的时间间隔)
time_diff = torch.tensor([[0.0, 0.5]]) # [B, T],0.5秒间隔
print("=" * 60)
print("SparseBEV 两大核心原理 Demo")
print("=" * 60)
# ============================================================
# 初始化:anchor + query
# ============================================================
# anchor:3D位置+形状(x,y,z,w,l,h,sin,cos,vx,vy)
anchor = torch.tensor([
[15.0, 0.0, 0.5, 1.8, 4.5, 1.5, 0.0, 1.0, 5.0, 0.0], # 轿车,速度5m/s
[30.0, -5.0, 1.0, 2.5, 7.0, 3.0, 0.5, 0.9, 0.0, 0.0], # 卡车,静止
[ 8.0, 3.0, 0.9, 0.5, 0.5, 1.7, 0.0, 1.0, -1.0, 0.5], # 行人,慢速
]) # [N_query, 10]
# query feature:初始为0,将被图像特征填充
query_feat = torch.zeros(N_query, C)
# 网络模块
offset_net = nn.Linear(C, N_groups * N_points * 3) # 预测采样点偏移
scale_net = nn.Linear(C, N_groups * N_points * N_levels) # 预测尺度权重
param_gen = nn.Linear(C, N_groups * (C//N_groups * C//N_groups # M矩阵
+ N_points * N_frames * C//N_groups)) # S矩阵
out_proj = nn.Linear(N_groups * N_points * N_frames * C//N_groups, C)
# ============================================================
# 核心1:Adaptive Spatio-Temporal Sampling (ASTS)
# ============================================================
print("\n" + "=" * 60)
print("【核心1】Adaptive Spatio-Temporal Sampling (ASTS)")
print("=" * 60)
print("\n关键思想:")
print(" Sparse4D:固定几何关键点(面中心),几何先验强")
print(" SparseBEV:query 完全自适应预测采样点,数据驱动")
print(" 额外:速度补偿解决历史帧时序对齐问题")
def adaptive_sampling(anchor, query_feat, lidar2img, img_feats, time_diff):
"""
ASTS 核心:
1. query 自适应预测采样点偏移(不依赖固定几何)
2. 速度补偿历史帧采样点位置
3. 投影到图像,选最优视角采样
"""
N = anchor.shape[0]
# ── Step A:query 预测采样点偏移(自适应的关键)──────────
# 完全由 query 决定去哪里采样
# 不同组可以学到不同的采样策略
raw_offset = offset_net(query_feat) # [N, G*P*3]
raw_offset = raw_offset.view(N, N_groups, N_points, 3)
# sigmoid → [-0.5, 0.5],限制在物体范围内
offset = raw_offset.sigmoid() - 0.5 # [N, G, P, 3]
# 按物体尺寸缩放(和 Sparse4D 相同的几何约束)
size = anchor[:, 3:6].exp() # [N, 3] 真实尺寸
offset = offset * size.view(N, 1, 1, 3) # [N, G, P, 3]
# 平移到世界坐标
center = anchor[:, :3] # [N, 3]
pts = offset + center.view(N, 1, 1, 3) # [N, G, P, 3]
# 扩展到多帧 [N, G, P, T, 3]
pts = pts.unsqueeze(3).expand(N, N_groups, N_points, N_frames, 3).clone()
print(f"\n 【Step A】自适应采样点(当前帧,未补偿):")
print(f" 对比 Sparse4D 固定几何点 vs SparseBEV 自适应点:")
for q in range(N):
for g in range(N_groups):
for p in range(N_points):
pt = pts[q, g, p, 0].detach().numpy()
anc = anchor[q, :3].numpy()
print(f" {anchor_names[q]} 组{g}点{p}: "
f"({pt[0]:.2f},{pt[1]:.2f},{pt[2]:.2f})m "
f"偏离anchor({anc[0]:.1f},{anc[1]:.1f})m "
f"{'← 自适应,不固定' if g==0 and p==0 else ''}")
# ── Step B:速度补偿(时序核心)──────────────────────────
# 物体在历史帧位于不同位置
# 必须把采样点反向偏移到历史时刻的正确位置
vel = anchor[:, 8:10] # [N, 2] 预测速度 (vx, vy)
print(f"\n 【Step B】速度补偿(历史帧采样点偏移):")
print(f" 原理:物体以速度v运动,t秒前在 pos - v×t 处")
for t in range(N_frames):
dt = time_diff[0, t].item()
if dt == 0:
continue
# 反向偏移:历史帧采样点 = 当前位置 - 速度×时间差
displacement = vel * dt # [N, 2]
pts[:, :, :, t, 0] -= displacement[:, 0].view(N, 1, 1)
pts[:, :, :, t, 1] -= displacement[:, 1].view(N, 1, 1)
for q in range(N):
vx, vy = vel[q].detach().numpy()
before = anchor[q, :2].numpy()
after = pts[q, 0, 0, t].detach().numpy()
print(f" {anchor_names[q]} t-{dt:.1f}s: "
f"当前({before[0]:.1f},{before[1]:.1f})m "
f"→ 历史({after[0]:.2f},{after[1]:.2f})m "
f"[速度({vx:.1f},{vy:.1f})m/s × {dt:.1f}s]")
# ── Step C:投影到图像 + 选最优视角 ──────────────────────
# pts: [N, G, P, T, 3] → 展平投影
pts_flat = pts.reshape(N, -1, 3) # [N, G*P*T, 3]
N_total = pts_flat.shape[1]
# 齐次坐标
ones = torch.ones(N, N_total, 1)
pts_homo = torch.cat([pts_flat, ones], dim=-1) # [N, G*P*T, 4]
# 投影到所有相机(T帧 × N_cam个相机)
l2i = lidar2img[0] # [T*N_cam, 4, 4]
pts_cam = (l2i.unsqueeze(1) @
pts_homo.unsqueeze(0).unsqueeze(-1)).squeeze(-1)
# [T*N_cam, N, G*P*T, 4]
eps = 1e-5
u_norm = pts_cam[...,0] / (pts_cam[...,2]+eps) / IMG_W * 2 - 1
v_norm = pts_cam[...,1] / (pts_cam[...,2]+eps) / IMG_H * 2 - 1
valid = (pts_cam[...,2]>0.1) & (u_norm.abs()<1) & (v_norm.abs()<1)
# [T*N_cam, N, G*P*T]
print(f"\n 【Step C】投影到图像,选最优视角:")
print(f" SparseBEV 关键设计:每个点只选1个最优视角(argmax valid)")
print(f" 避免多视角重复采样,且可用3D grid_sample一次完成")
# ★ 只选最优视角(SparseBEV 关键设计)
# valid: [T*N_cam, N, G*P*T] → 在相机维度找最优
valid_count = valid.float().sum(dim=0) # [N, G*P*T]
best_cam = valid.float().argmax(dim=0) # [N, G*P*T] 最优相机索引
# 取最优相机的坐标
best_u = u_norm.gather(0, best_cam.unsqueeze(0)).squeeze(0) # [N, G*P*T]
best_v = v_norm.gather(0, best_cam.unsqueeze(0)).squeeze(0) # [N, G*P*T]
# view_idx 归一化为第3维(将多视角变成3D体积的深度方向)
view_idx_norm = best_cam.float() / (N_cam * N_frames - 1) # [N, G*P*T]
# 组织为采样坐标 [N, G*P*T, 3]
sample_coords = torch.stack([best_u, best_v, view_idx_norm], dim=-1)
print(f"\n 各采样点的最优相机选择(anchor 0 轿车):")
for i in range(min(4, N_total)):
cam = best_cam[0, i].item()
u = best_u[0, i].item()
v = best_v[0, i].item()
print(f" 采样点{i}: 最优相机={cam} "
f"归一化坐标({u:.3f},{v:.3f})")
# ── Step D:多尺度加权采样 ────────────────────────────────
# 预测各FPN层级的权重(softmax竞争)
scale_w = scale_net(query_feat) # [N, G*P*L]
scale_w = scale_w.view(N, N_groups, N_points, N_levels)
scale_w = scale_w.softmax(dim=-1) # [N, G, P, L]
# 在各FPN层级采样并加权
sampled_list = []
for lvl, feat in enumerate(img_feats):
feat_2d = feat[0] # [T*N_cam, C, H, W]
# 用3D grid_sample(第3维=view_idx)
# 这里简化为2D grid_sample
grid = sample_coords[:, :, :2].view(
N * N_total, 1, 1, 2)
feat_flat = feat_2d.mean(0, keepdim=True).expand(
N * N_total, -1, -1, -1) # 简化处理
s = F.grid_sample(feat_flat, grid,
mode='bilinear',
padding_mode='zeros',
align_corners=False)
sampled_list.append(s.view(N, N_total, C))
# 多尺度加权求和
sampled = torch.stack(sampled_list, dim=-1) # [N, G*P*T, C, L]
scale_w_exp = scale_w.view(N, N_groups*N_points, 1, N_levels)\
.expand_as(sampled[:, :N_groups*N_points])
# 简化:直接对层级取平均
sampled = sampled.mean(dim=-1) # [N, G*P*T, C]
# reshape为 [N, G, P*T, C]
sampled = sampled.view(N, N_groups, N_points * N_frames, C)
print(f"\n 采样结果 shape: {sampled.shape}")
print(f" [N_query={N_query}, N_groups={N_groups}, "
f"N_pts×T={N_points*N_frames}, C={C}]")
return sampled
sampled = adaptive_sampling(
anchor, query_feat, lidar2img, img_feats, time_diff
)
# ============================================================
# 核心2:Adaptive Mixing (AM)
# ============================================================
print("\n" + "=" * 60)
print("【核心2】Adaptive Mixing (AM)")
print("=" * 60)
print("\n关键思想:")
print(" 普通做法:固定权重加权求和 → 信息损失大,无法定制")
print(" AM:query 动态生成聚合矩阵,每个物体定制化混合策略")
print(f"\n 输入:{N_groups}组 × {N_points*N_frames}点 × {C}维特征")
print(f" 输出:{C}维 query 特征")
def adaptive_mixing(sampled, query_feat):
"""
Adaptive Mixing 核心:
query 动态生成 M(通道混合)和 S(点压缩)两个矩阵
为每个物体定制特征聚合策略
"""
N = query_feat.shape[0]
G = N_groups
P_T = N_points * N_frames # 点×帧
C_g = C // G # 每组的特征维度
# sampled: [N, G, P_T, C] → [N, G, P_T, C_g](按组分割)
x = sampled.view(N, G, P_T, C_g)
# ── Step A:query 生成自适应参数 ──────────────────────────
# M:通道混合矩阵 [C_g × C_g],混合特征通道
# S:点压缩矩阵 [P_T × out_pts],压缩采样点数量
out_pts = P_T * 2 # 输出点数(这里简化为不压缩)
M_size = C_g * C_g
S_size = P_T * C_g
params = param_gen(query_feat) # [N, G*(M_size+S_size)]
params = params.view(N, G, M_size + S_size)
M = params[:, :, :M_size].view(N, G, C_g, C_g) # [N, G, C_g, C_g]
S = params[:, :, M_size:].view(N, G, P_T, C_g) # [N, G, P_T, C_g]
print(f"\n 【Step A】query 生成自适应参数:")
print(f" M矩阵(通道混合)shape: {M.shape} "
f"[N, G, C_g={C_g}, C_g={C_g}]")
print(f" S矩阵(点压缩) shape: {S.shape} "
f"[N, G, P_T={P_T}, C_g={C_g}]")
print(f"\n 对比不同物体的 M 矩阵差异(体现定制化):")
for q in range(N):
m_norm = M[q, 0].detach().norm().item()
print(f" {anchor_names[q]} M矩阵范数: {m_norm:.4f} "
f"← 每个物体的混合策略不同")
# ── Step B:通道混合(M矩阵)────────────────────────────
# x: [N, G, P_T, C_g]
# M: [N, G, C_g, C_g]
# → 对每个采样点的特征做线性变换,混合通道信息
x_mixed = torch.matmul(x, M) # [N, G, P_T, C_g]
x_mixed = F.layer_norm(x_mixed, [C_g])
x_mixed = F.relu(x_mixed) + x_mixed # 残差
print(f"\n 【Step B】通道混合后 shape: {x_mixed.shape}")
print(f" 每个采样点的 {C_g} 维特征被重新混合")
print(f" 不同物体用不同的 M 矩阵 → 定制化通道组合")
# ── Step C:点压缩(S矩阵)──────────────────────────────
# S: [N, G, P_T, C_g]
# 用 S 对点维度做加权聚合(压缩 P_T 个点)
# 这里简化:用 S 做注意力加权
attn = (S * x_mixed).sum(dim=-1, keepdim=True) # [N, G, P_T, 1]
attn = attn.softmax(dim=2) # 在点维度归一化
x_agg = (x_mixed * attn).sum(dim=2) # [N, G, C_g]
print(f"\n 【Step C】点压缩后 shape: {x_agg.shape}")
print(f" {P_T} 个采样点被压缩为1个向量")
print(f" S 矩阵决定每个点的贡献权重 → 定制化点选择")
# ── Step D:输出投影 ─────────────────────────────────────
x_out = x_agg.reshape(N, G * C_g) # [N, C]
x_out = out_proj(x_out) # [N, C]
# 残差连接
output = query_feat + x_out
print(f"\n 【Step D】最终输出 shape: {output.shape} [N_query, C]")
return output
updated_query = adaptive_mixing(sampled, query_feat)
print(f"\n 更新后 query 范数(从0到有值):")
for q in range(N_query):
print(f" {anchor_names[q]}: "
f"{updated_query[q].norm().item():.4f}")
# ============================================================
# 检测头
# ============================================================
print("\n" + "=" * 60)
print("【检测头】query → 类别 + 3D框")
print("=" * 60)
cls_head = nn.Linear(C, 3)
reg_head = nn.Linear(C, 3)
cls_pred = cls_head(updated_query)
reg_delta = reg_head(updated_query)
final_pos = anchor[:, :3] + reg_delta.detach()
cls_names = ['car', 'truck', 'ped']
for q in range(N_query):
score = torch.softmax(cls_pred[q], dim=0).detach()
ref = anchor[q, :3].numpy()
pred = final_pos[q].detach().numpy()
print(f" {anchor_names[q]}:")
print(f" 参考点: ({ref[0]:.1f},{ref[1]:.1f},{ref[2]:.1f})m")
print(f" 预测位置: ({pred[0]:.2f},{pred[1]:.2f},{pred[2]:.2f})m")
print(f" 类别: {cls_names[score.argmax()]} "
f"{score.detach().numpy().round(2)}")
四. 问答
Q1: SparseBEV vs BEVFormer的核心区别?
答:BEVFormer构建密集的100×100 BEV特征图,每个BEV格点都attend图像特征,复杂度O(H_bev ×
W_bev)。SparseBEV完全不构建BEV图,每个query直接在3D空间生成采样点投影到图像,复杂度O(Q ×
P),Q=900远小于BEV的10000。
Q2: 为什么叫"SparseBEV"但没有BEV?
答: "Sparse"指的是稀疏采样策略 — 不在BEV空间密集采样,而是在query位置稀疏采样。虽然没有显式BEV特征图,但query的3D位置检测是在BEV坐标系下。
更多推荐


所有评论(0)