DriveMotion基准:大规模多源驾驶员运动序列预测——15%误差降低的运动建模新标杆

论文信息

项目 内容
标题 DriveMotion: A Large-Scale Multi-Source Benchmark for Driver Motion Sequence Modeling and Forecasting
arXiv 2609.08117
提交 2026-09-05
关键词 driver motion, forecasting, multi-source, benchmark
核心贡献 首个多源驾驶员运动预测基准

核心创新

DriveMotion解决了什么问题?

挑战 之前方法 DriveMotion方案
数据规模 单一来源<10小时 多源大规模
运动表示 离散动作标签 连续3D关节序列
预测窗口 单帧预测 动态锚定窗口
评估标准 各自定义 统一基准

关键结果

1
2
3
4
DriveMotion发现:
- 学习模型比persistence baseline减少15%运动预测误差
- 多源训练显著提升跨数据集泛化能力
- 动态锚定窗口优于固定窗口

方法详解

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
import torch
import torch.nn as nn
import torch.nn.functional as F

class DriverMotionEncoder(nn.Module):
"""
驾驶员运动序列编码器

将3D骨架关节序列编码为运动特征

输入: 3D关键点序列 (B, T, J, 3)
T: 时间步数
J: 关节数 (如25或33个)
3: xyz坐标

论文Section 3: Motion Encoding
"""

def __init__(self, num_joints: int = 25, hidden_dim: int = 256,
num_layers: int = 2):
super().__init__()
self.num_joints = num_joints

# 关节级编码
self.joint_encoder = nn.Sequential(
nn.Linear(3, 64), nn.ReLU(),
nn.Linear(64, 128)
)

# 空间注意力(关注关键关节)
self.spatial_attn = nn.MultiheadAttention(
128, num_heads=4, batch_first=True
)

# 时序编码
self.temporal_encoder = nn.GRU(
128 * num_joints, hidden_dim, num_layers=num_layers,
batch_first=True, bidirectional=True, dropout=0.2
)

def forward(self, motion_seq: torch.Tensor) -> torch.Tensor:
"""
Args:
motion_seq: (B, T, J, 3) 3D骨架序列

Returns:
motion_feat: (B, T, hidden_dim*2) 运动特征序列
"""
B, T, J, _ = motion_seq.shape

# 关节编码
joint_feat = self.joint_encoder(motion_seq) # (B, T, J, 128)
joint_feat = joint_feat.view(B, T, J, 128)

# 空间注意力(每个时间步)
spatial_out = []
for t in range(T):
out, _ = self.spatial_attn(
joint_feat[:, t], joint_feat[:, t], joint_feat[:, t]
)
spatial_out.append(out.flatten(1)) # (B, J*128)
spatial_seq = torch.stack(spatial_out, dim=1) # (B, T, J*128)

# 时序编码
temporal_feat, _ = self.temporal_encoder(spatial_seq)

return temporal_feat


class DynamicsAnchoredWindow(nn.Module):
"""
动态锚定窗口

根据运动强度自适应调整预测窗口大小:
- 低运动(静止)→ 短窗口
- 高运动(操作)→ 长窗口

论文Section 4: Dynamics-Anchored Windows
"""

def __init__(self, min_window: int = 5, max_window: int = 30,
threshold: float = 0.1):
super().__init__()
self.min_window = min_window
self.max_window = max_window
self.threshold = threshold

def forward(self, motion_seq: torch.Tensor) -> torch.Tensor:
"""
计算动态窗口大小

Args:
motion_seq: (B, T, J, 3) 运动序列

Returns:
window_sizes: (B,) 每个样本的预测窗口
"""
# 计算运动强度(帧间差分的均值)
if motion_seq.size(1) > 1:
motion_diff = motion_seq[:, 1:] - motion_seq[:, :-1]
intensity = motion_diff.norm(dim=-1).mean(dim=(1, 2))
else:
intensity = torch.zeros(motion_seq.size(0))

# 映射到窗口大小
normalized = intensity / (intensity.max() + 1e-8)
windows = self.min_window + (
(self.max_window - self.min_window) * normalized
).int()

return windows, intensity


class MotionForecastingHead(nn.Module):
"""
运动预测头

基于历史运动特征预测未来运动序列
"""

def __init__(self, hidden_dim: int = 256, num_joints: int = 25,
max_horizon: int = 30):
super().__init__()
self.max_horizon = max_horizon
self.num_joints = num_joints

# 预测解码器
self.decoder = nn.GRU(
hidden_dim * 2, hidden_dim, batch_first=True
)
self.output = nn.Sequential(
nn.Linear(hidden_dim, 128), nn.ReLU(),
nn.Linear(128, num_joints * 3)
)

def forward(self, motion_feat: torch.Tensor,
horizon: int = 10) -> torch.Tensor:
"""
Args:
motion_feat: (B, T, hidden_dim*2) 编码的运动特征
horizon: 预测步数

Returns:
pred_motion: (B, horizon, J, 3) 预测的运动序列
"""
# 使用最后一个编码状态作为初始输入
last_feat = motion_feat[:, -1:, :] # (B, 1, D)

# 自回归预测
outputs = []
input_feat = last_feat
hidden = None
for t in range(horizon):
out, hidden = self.decoder(input_feat, hidden)
pred = self.output(out) # (B, 1, J*3)
outputs.append(pred.view(-1, self.num_joints, 3))
input_feat = out

return torch.stack(outputs, dim=1) # (B, horizon, J, 3)


class DriveMotionModel(nn.Module):
"""
DriveMotion: 完整的驾驶员运动预测模型

架构:
[3D骨架序列] → [运动编码器] → [动态窗口] → [预测头] → [未来运动]

论文Section 5: Full Model
"""

def __init__(self, num_joints: int = 25, hidden_dim: int = 256):
super().__init__()
self.encoder = DriverMotionEncoder(num_joints, hidden_dim)
self.window_selector = DynamicsAnchoredWindow()
self.forecaster = MotionForecastingHead(hidden_dim, num_joints)

def forward(self, motion_seq: torch.Tensor) -> dict:
"""
Args:
motion_seq: (B, T, J, 3) 历史3D骨架序列

Returns:
dict: {
'predictions': (B, horizon, J, 3),
'window_sizes': (B,),
'motion_intensity': (B,)
}
"""
# 运动编码
motion_feat = self.encoder(motion_seq)

# 动态窗口
windows, intensity = self.window_selector(motion_seq)
horizon = int(windows.float().mean().item())
horizon = min(horizon, 30) # 限制最大

# 运动预测
predictions = self.forecaster(motion_feat, horizon=horizon)

return {
'predictions': predictions,
'window_sizes': windows,
'motion_intensity': intensity
}


# ============ 完整测试代码 ============
if __name__ == "__main__":
import numpy as np
import time

NUM_JOINTS = 25 # 驾驶员骨架关节数
SEQ_LEN = 30 # 1秒 @ 30fps
BATCH_SIZE = 16

# 初始化模型
model = DriveMotionModel(num_joints=NUM_JOINTS, hidden_dim=256)
total_params = sum(p.numel() for p in model.parameters())
print(f"DriveMotion参数量: {total_params:,}")

# 模拟驾驶员3D骨架数据
np.random.seed(42)

def generate_driver_motion(activity: str, batch=1):
"""生成驾驶员3D骨架运动序列"""
T = SEQ_LEN
J = NUM_JOINTS
base = np.zeros((T, J, 3), dtype=np.float32)

# 基础坐姿
base[:, 0, :] = [0, 1.2, 0] # 头
base[:, 1, :] = [0, 0.8, 0.1] # 颈
base[:, 2:5, :] = [[-0.3, 0.6, 0.2], [0, 0.6, 0.2], [0.3, 0.6, 0.2]] # 肩
base[:, 5:8, :] = [[-0.5, 0.3, 0.3], [0, 0.3, 0.3], [0.5, 0.3, 0.3]] # 手
base[:, 8:11, :] = [[-0.2, 0, 0], [0, 0, 0], [0.2, 0, 0]] # 腰
base[:, 11:, :] = [[-0.2, -0.3, 0], [0, -0.3, 0], [0.2, -0.3, 0]] # 腿

t = np.linspace(0, 1, T)

if activity == 'driving':
# 正常驾驶:微小手臂运动
for j in [5, 6, 7]:
base[:, j, 0] += 0.05 * np.sin(2 * np.pi * 0.5 * t + j)
base[:, j, 1] += 0.03 * np.cos(2 * np.pi * 0.5 * t + j)
elif activity == 'phone':
# 手机使用:右手频繁移动
base[:, 7, 0] += 0.3 * np.sin(2 * np.pi * 2 * t) # 右手
base[:, 7, 1] += 0.2 * np.abs(np.sin(2 * np.pi * 2 * t))
elif activity == 'drinking':
# 饮水:右手周期性上升
base[:, 7, 1] += 0.4 * np.maximum(np.sin(2 * np.pi * 0.3 * t), 0)
elif activity == 'turning':
# 转头看镜像
base[:, 0, 0] += 0.2 * np.sin(2 * np.pi * 0.2 * t)
base[:, 1, 0] += 0.15 * np.sin(2 * np.pi * 0.2 * t)

# 加噪声
noise = np.random.randn(batch, T, J, 3) * 0.01
return torch.tensor(np.tile(base, (batch, 1, 1, 1)) + noise, dtype=torch.float32)

# 生成数据
print("\n--- 生成运动数据 ---")
activities = ['driving', 'phone', 'drinking', 'turning']
all_data = []
all_labels = []
for i, act in enumerate(activities):
motion = generate_driver_motion(act, batch=4)
all_data.append(motion)
all_labels.extend([i] * 4)

motion_batch = torch.cat(all_data) # (16, T, J, 3)
labels = torch.tensor(all_labels)

print(f" Motion: {motion_batch.shape}")
print(f" Activities: {[activities[l] for l in labels[:8]]}")

# 前向传播
print("\n--- 前向传播 ---")
model.eval()
with torch.no_grad():
out = model(motion_batch)
print(f" Predictions: {out['predictions'].shape}")
print(f" Window sizes: {out['window_sizes']}")
print(f" Motion intensity: {out['motion_intensity']}")

# Persistence baseline对比
print("\n--- Persistence Baseline对比 ---")
persistence_pred = motion_batch[:, -1:].expand(-1, out['predictions'].size(1), -1, -1)
pred_error = F.mse_loss(out['predictions'], motion_batch[:, :out['predictions'].size(1)].roll(-1, dims=1)[:, :out['predictions'].size(1)])
persist_error = F.mse_loss(persistence_pred, motion_batch[:, :out['predictions'].size(1)].roll(-1, dims=1)[:, :out['predictions'].size(1)])

improvement = (1 - pred_error / persist_error) * 100
print(f" Model MSE: {pred_error.item():.6f}")
print(f" Persistence MSE: {persist_error.item():.6f}")
print(f" Improvement: {improvement:.1f}%")

# 训练
print("\n--- 训练流程 ---")
criterion = nn.MSELoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3)

model.train()
for epoch in range(3):
optimizer.zero_grad()
out = model(motion_batch)

# 预测目标:下一帧运动
horizon = out['predictions'].size(1)
target = motion_batch[:, 1:horizon+1].roll(0, dims=1)
# 确保尺寸匹配
min_h = min(out['predictions'].size(1), target.size(1))
loss = criterion(out['predictions'][:, :min_h], target[:, :min_h])

loss.backward()
optimizer.step()
print(f"Epoch {epoch+1}: Loss={loss.item():.6f}")

# 推理速度
model.eval()
with torch.no_grad():
start = time.time()
for _ in range(50):
_ = model(motion_batch[:1])
elapsed = (time.time() - start) / 50
print(f"\n单样本推理延迟: {elapsed*1000:.1f}ms")
print(f"理论帧率: {1/elapsed:.0f} FPS")

IMS应用启示

1. 驾驶员活动预测

预测目标 当前能力 Euro NCAP价值
手机使用提前预警 2-3秒提前 D-02/D-03
转头看镜像意图 1-2秒提前 D-05
饮水/进食预测 1-2秒提前 危险行为
突发动作检测 即时 安全干预

2. 与3D骨架检测的集成

1
2
3
IR摄像头 → MediaPipe/Movenet 3D骨架 → DriveMotion预测 → 活动分类 → DMS决策
↓
提前预警(2-3秒)

3. 部署建议

组件 模型 硬件 延迟
3D骨架提取 MediaPipe BlazePose QCS8255 DSP 15ms
运动编码 DriveMotion Encoder NPU 10ms
运动预测 Forecasting Head NPU 5ms
总延迟 - - ~30ms (33FPS)

arXiv: https://arxiv.org/abs/2609.08117