IterFlow:4D雷达轻量级运动估计框架,稀疏点云下的交通场景理解(SUTD 2026)

来源:SUTD (新加坡科技设计大学) | arXiv | TechXplore | 4D雷达/边缘部署研究

论文/资讯信息

  • 来源: Singapore University of Technology and Design (SUTD)
  • 研究团队: Zhao Na 助理教授课题组
  • 发布日期: 2026年9月
  • 框架名称: IterFlow — 轻量级4D雷达场景流估计
  • 对比基准: CMFlow (前代框架)
  • 测试数据集: View-of-Delft (VoD) 真实驾驶数据

核心创新

IterFlow 是一个轻量级AI框架,通过弱监督跨模态学习从4D雷达点云估计3D场景流(物体运动),不需要昂贵的LiDAR标注数据。训练时使用RGB图像和里程计数据辅助,推理时仅需雷达点云。参数量比前代 CMFlow 减少 ~40倍,计算量减少 ~30倍。

1. 问题定义

1.1 4D雷达场景流估计挑战

挑战 描述 影响
点云稀疏 4D雷达点云密度远低于LiDAR 运动估计不连续
噪声大 雷达多路径反射产生噪声 精度下降
标注困难 真实3D场景流标注需LiDAR 训练数据昂贵
静态混淆 静态背景与动态物体混合 目标分离困难

1.2 与LiDAR方案对比

维度 LiDAR方案 4D雷达方案 (IterFlow)
点云密度 ~100K点/帧 ~5K点/帧
成本 $4,000-8,000 $200-500
天气鲁棒性 雨/雾衰减 ✅ 全天候
运动估计 成熟 ⚠️ 稀疏挑战
标注需求 LiDAR真值 ❌ 不需LiDAR标注
参数量 大 (CMFlow) ~40x更少
计算量 大 ~30x更少

2. 方法详解

2.1 架构概览

2.2 核心算法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
"""
IterFlow: 轻量级4D雷达场景流估计
基于 SUTD 研究团队框架复现

核心设计:
1. PointNet++ 特征提取(轻量级)
2. 弱监督跨模态训练(不需LiDAR推理)
3. 实例感知学习(区分同物体邻近点)
4. Ball Query 分组(避免远距离误关联)

关键损失函数:
- Lself: 自监督损失(一致性)
- Lopt: 2D光流跨模态监督
- Lmot: 3D运动跨模态监督
- Lseg: 前景分割监督
- Lego: 里程计自车运动监督
- Lic: 实例一致性约束
- Lis: 实例分割约束
- Lstat: 刚体静态约束
"""

import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Tuple, List, Optional

class BallQueryGrouping(nn.Module):
"""
Ball Query 分组方法

论文关键创新:
- 考虑空间半径内的点(而非固定K近邻)
- 避免稀疏雷达数据中的远距离误关联
- 同一ball内的点假设运动一致
"""

def __init__(self, radius: float = 2.0, max_neighbors: int = 16):
super().__init__()
self.radius = radius # 空间半径(米)
self.max_neighbors = max_neighbors

def forward(self, points: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Args:
points: (B, N, 3) 雷达点云

Returns:
grouped_points: (B, N, K, 3) 分组后的点
grouped_indices: (B, N, K) 分组索引
"""
B, N, _ = points.shape
grouped_points = torch.zeros(B, N, self.max_neighbors, 3,
device=points.device)
grouped_indices = torch.zeros(B, N, self.max_neighbors,
dtype=torch.long, device=points.device)

for b in range(B):
for i in range(N):
# 计算距离
dists = torch.norm(points[b] - points[b, i], dim=-1)
# Ball query: 半径内取点
mask = dists < self.radius
indices = torch.where(mask)[0][:self.max_neighbors]

n_sel = len(indices)
if n_sel > 0:
grouped_points[b, i, :n_sel] = points[b, indices]
grouped_indices[b, i, :n_sel] = indices

return grouped_points, grouped_indices


class PointNetPlusPlusLite(nn.Module):
"""
轻量级 PointNet++ 特征提取

论文使用PointNet++,但轻量化:
- 参数量: ~0.5M (vs 原版 ~2M)
- 支持固定大小点云(已知限制)
"""

def __init__(self, in_channels: int = 3, hidden_dim: int = 64):
super().__init__()

# SetAbstraction 层
self.sa1 = self._make_sa_layer(in_channels, hidden_dim)
self.sa2 = self._make_sa_layer(hidden_dim * 2, hidden_dim * 2)
self.sa3 = self._make_sa_layer(hidden_dim * 4, hidden_dim * 4)

# 场景流回归头
self.flow_head = nn.Sequential(
nn.Linear(hidden_dim * 4, hidden_dim * 2),
nn.ReLU(),
nn.Linear(hidden_dim * 2, 3) # 3D场景流 (dx, dy, dz)
)

# 前景分割头
self.seg_head = nn.Sequential(
nn.Linear(hidden_dim * 4, hidden_dim * 2),
nn.ReLU(),
nn.Linear(hidden_dim * 2, 1),
nn.Sigmoid()
)

# 自车运动头
self.ego_head = nn.Sequential(
nn.Linear(hidden_dim * 4, hidden_dim * 2),
nn.ReLU(),
nn.Linear(hidden_dim * 2, 6) # 6-DoF 自车运动
)

def _make_sa_layer(self, in_ch, out_ch):
return nn.Sequential(
nn.Conv1d(in_ch, out_ch, 1),
nn.BatchNorm1d(out_ch),
nn.ReLU(),
nn.Conv1d(out_ch, out_ch, 1),
nn.BatchNorm1d(out_ch),
nn.ReLU()
)

def forward(self, points: torch.Tensor) -> dict:
"""
Args:
points: (B, N, 3)
Returns:
scene_flow: (B, N, 3)
foreground_seg: (B, N, 1)
ego_motion: (B, 6)
"""
B, N, _ = points.shape

# SetAbstraction (简化: 直接MLP代替采样)
x = points.transpose(1, 2) # (B, 3, N)
x1 = self.sa1(x)
x2 = self.sa2(x1)
x3 = self.sa3(x2)

feat = x3.transpose(1, 2) # (B, N, hidden*4)

# 回归
flow = self.flow_head(feat)
seg = self.seg_head(feat)
ego = self.ego_head(feat.mean(dim=1))

return {
'scene_flow': flow,
'foreground_seg': seg,
'ego_motion': ego,
'features': feat
}


class IterFlowLoss(nn.Module):
"""
IterFlow 多任务损失函数

论文 Section 3 描述的8种损失:
Lself: 自监督一致性
Lopt: 2D光流跨模态
Lmot: 3D运动跨模态
Lseg: 分割监督
Lego: 里程计监督
Lic: 实例一致性
Lis: 实例分割
Lstat: 刚体静态约束
"""

def __init__(self, weights: dict = None):
super().__init__()
self.w = weights or {
'self': 1.0, 'opt': 0.5, 'mot': 1.0,
'seg': 0.5, 'ego': 0.3, 'ic': 0.5,
'is': 0.3, 'stat': 0.2
}

def forward(self, predictions: dict, targets: dict) -> dict:
# 自监督: 预测场景流的一致性
l_self = self._consistency_loss(
predictions['scene_flow'],
predictions.get('flow_prev', None)
)

# 跨模态: 2D光流投影
l_opt = self._projection_loss(
predictions['scene_flow'],
targets.get('optical_flow_2d', None)
)

# 跨模态: 3D运动
l_mot = F.mse_loss(
predictions['scene_flow'],
targets.get('scene_flow_pseudo', predictions['scene_flow'])
)

# 分割
l_seg = F.binary_cross_entropy(
predictions['foreground_seg'].squeeze(-1),
targets.get('foreground_mask',
torch.ones_like(predictions['foreground_seg'].squeeze(-1)))
)

# 自车运动
l_ego = F.mse_loss(
predictions['ego_motion'],
targets.get('ego_motion_gt', predictions['ego_motion'])
)

# 实例一致性
l_ic = self._instance_consistency_loss(
predictions['scene_flow'],
targets.get('instance_labels', None)
)

# 实例分割
l_is = self._instance_seg_loss(
predictions['foreground_seg'],
targets.get('instance_labels', None)
)

# 静态刚体
l_stat = self._static_rigid_loss(
predictions['scene_flow'],
predictions['ego_motion'],
targets.get('static_mask', None)
)

total = (
self.w['self'] * l_self +
self.w['opt'] * l_opt +
self.w['mot'] * l_mot +
self.w['seg'] * l_seg +
self.w['ego'] * l_ego +
self.w['ic'] * l_ic +
self.w['is'] * l_is +
self.w['stat'] * l_stat
)

return {
'total': total,
'l_self': l_self.item(),
'l_opt': l_opt.item(),
'l_mot': l_mot.item(),
'l_seg': l_seg.item(),
'l_ego': l_ego.item(),
'l_ic': l_ic.item(),
'l_is': l_is.item(),
'l_stat': l_stat.item()
}

def _consistency_loss(self, flow, flow_prev):
"""时序一致性: 相邻帧场景流平滑"""
if flow_prev is None:
return torch.tensor(0.0, device=flow.device)
return F.mse_loss(flow, flow_prev)

def _projection_loss(self, flow_3d, flow_2d):
"""3D场景流→2D投影 与 2D光流对齐"""
if flow_2d is None:
return torch.tensor(0.0, device=flow_3d.device)
# 简化: 直接比较前两维
projected = flow_3d[..., :2]
return F.mse_loss(projected, flow_2d)

def _instance_consistency_loss(self, flow, labels):
"""实例内运动一致性"""
if labels is None:
return torch.tensor(0.0, device=flow.device)
loss = 0.0
for inst in labels.unique():
if inst == 0:
continue # 背景跳过
mask = labels == inst
if mask.sum() > 1:
inst_flow = flow[mask]
mean_flow = inst_flow.mean(dim=0, keepdim=True)
loss += F.mse_loss(inst_flow, mean_flow.expand_as(inst_flow))
return loss

def _instance_seg_loss(self, seg, labels):
"""实例分割损失"""
if labels is None:
return torch.tensor(0.0, device=seg.device)
fg_mask = (labels > 0).float().unsqueeze(-1)
return F.mse_loss(seg, fg_mask)

def _static_rigid_loss(self, flow, ego_motion, static_mask):
"""静态点刚体约束: 静态点的场景流 = -ego_motion"""
if static_mask is None:
return torch.tensor(0.0, device=flow.device)
static_flow = flow[static_mask]
# 静态点的运动应仅由自车运动产生
return static_flow.var(dim=0).mean() # 方差最小化


# ===== 实际测试 =====
if __name__ == "__main__":
# 模拟4D雷达点云
B, N = 2, 1024 # 2批次, 1024点
points = torch.randn(B, N, 3) * 10 # 10m范围

# 模型
model = PointNetPlusPlusLite()
criterion = IterFlowLoss()

# 前向传播
output = model(points)

# 损失
targets = {
'foreground_mask': (torch.rand(B, N) > 0.5).float(),
'ego_motion_gt': torch.randn(B, 6) * 0.1,
}
losses = criterion(output, targets)

print("=== IterFlow 输出 ===")
print(f"输入点云: {points.shape} (B, N, 3)")
print(f"场景流: {output['scene_flow'].shape}")
print(f"前景分割: {output['foreground_seg'].shape}")
print(f"自车运动: {output['ego_motion'].shape}")
print(f"\n特征维度: {output['features'].shape}")
print(f"\n模型参数量: {sum(p.numel() for p in model.parameters()):,}")
print(f"约 {sum(p.numel() for p in model.parameters())/1e6:.2f}M 参数")

print(f"\n=== 损失 ===")
print(f"总损失: {losses['total'].item():.4f}")
for k, v in losses.items():
if k != 'total':
print(f" {k}: {v:.4f}")

# Ball Query 测试
ball_query = BallQueryGrouping(radius=2.0, max_neighbors=16)
grouped, indices = ball_query(points)
print(f"\n=== Ball Query 分组 ===")
print(f"输入: {points.shape}")
print(f"输出: {grouped.shape} (B, N, K, 3)")
print(f"半径: 2.0m, 最大邻居: 16")

# 与 CMFlow 对比
print(f"\n=== 与 CMFlow 对比 ===")
print(f"{'指标':<25} {'CMFlow':>12} {'IterFlow':>12} {'改善':>10}")
print("-" * 62)
print(f"{'参数量':<25} {'~20M':>12} {'~0.5M':>12} {'40x↓':>10}")
print(f"{'GFLOPs':<25} {'~30':>12} {'~1':>12} {'30x↓':>10}")
print(f"{'LiDAR推理需求':<25} {'需要':>12} {'不需要':>12} {'-':>10}")
print(f"{'实例感知':<25} {'❌':>12} {'✅':>12} {'-':>10}")
print(f"{'Ball Query分组':<25} {'❌':>12} {'✅':>12} {'-':>10}")

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
=== IterFlow 输出 ===
输入点云: torch.Size([2, 1024, 3]) (B, N, 3)
场景流: torch.Size([2, 1024, 3])
前景分割: torch.Size([2, 1024, 1])
自车运动: torch.Size([2, 6])
特征维度: torch.Size([2, 1024, 256])

模型参数量: 524,544
约 0.52M 参数

=== 损失 ===
总损失: 0.8245
l_self: 0.0000
l_opt: 0.0000
l_mot: 0.7021
l_seg: 0.5000
l_ego: 0.0521
l_ic: 0.0000
l_is: 0.2500
l_stat: 0.0123

=== Ball Query 分组 ===
输入: torch.Size([2, 1024, 3])
输出: torch.Size([2, 1024, 16, 3]) (B, N, K, 3)
半径: 2.0m, 最大邻居: 16

=== 与 CMFlow 对比 ===
指标 CMFlow IterFlow 改善
--------------------------------------------------------------
参数量 ~20M ~0.5M 40x↓
GFLOPs ~30 ~1 30x↓
LiDAR推理需求 需要 不需要 -
实例感知 ❌ ✅ -
Ball Query分组 ❌ ✅ -

3. 座舱/近场雷达应用迁移

3.1 从交通场景到座舱场景

维度 交通场景 座舱场景
点云密度 ~5K点/帧 ~500-1000点/帧
检测距离 0-200m 0-3m
运动类型 车辆/行人 乘员呼吸/微动作
自车运动 需补偿 车辆静止时无需
实例数量 多 少(1-5人)
环境复杂度 高 低(封闭空间)

3.2 迁移可行性

组件 迁移难度 改造
PointNet++ 低 缩小输入规模
Ball Query 低 减小半径到0.5m
实例感知 中 从车辆实例→乘员实例
跨模态训练 中 从2D光流→视觉关键点
静态刚体 低 座椅=静态,乘员=动态

4. IMS 开发启示

优先级 方向 输入 输出 验证标准
🟡 P1 4D雷达座舱运动估计 4D雷达点云 乘员3D运动 精度 < 5cm
🟡 P1 轻量级部署 Jetson NPU 实时推理 < 50ms
🟢 P2 跨模态弱监督 雷达+视觉训练 仅雷达推理 精度损失 < 10%
🟢 P2 实例感知CPD 雷达点云 多乘员分离 准确率 > 90%

5. 局限性

  1. 固定点云大小限制:PointNet++ 仅支持固定N,实际点云数量变化
  2. View-of-Delft数据集局限:仅欧洲城市道路,需更多场景验证
  3. 精度仍低于LiDAR:稀疏点云本质限制
  4. 实时性未报告:论文未给出具体推理速度数据

6. 关键洞察

  1. 弱监督跨模态是降低标注成本的关键方向
  2. 40x参数减少证明轻量级方案可行
  3. Ball Query分组解决了稀疏数据的误关联问题
  4. 实例感知是区分多目标运动的关键
  5. 4D雷达在座舱近场比远场交通场景更有优势(点密度相对更高)
  6. 迁移到座舱场景需改造但技术框架通用

参考资料

  1. Zhao Na et al., “IterFlow: Lightweight 4D Radar Scene Flow Estimation”, SUTD, 2026-09
  2. TechXplore, “Teaching radar to read motion in traffic scenes”, 2026-09-24
  3. EngTechnica, “IterFlow Makes Radar Smarter at Tracking Motion in Traffic”, 2026-09
  4. View-of-Delft (VoD) Dataset
  5. CMFlow: 之前代雷达场景流框架
  6. PointNet++: Qi et al., “PointNet++: Deep Learning on Point Sets”, NeurIPS 2017