面部动作单元时空检测:双循环自编码器在驾驶员情绪监测中的应用(论文解读)

论文信息

  • 标题: Spatiotemporal Facial Action Unit Detection using Twin Cycle Autoencoders for Driver Monitoring
  • 作者: Sai Sidharth D
  • 发表: arXiv(2026年7月18日)
  • 核心贡献: 提出双循环自编码器(Twin Cycle Autoencoders)架构,同时建模面部动作单元(AU)的空间分布和时间动态,用于驾驶员情绪/状态监测

1. 论文核心创新

1.1 研究背景

驾驶员情绪状态对驾驶安全有直接影响:

情绪状态 驾驶影响 检测难度
愤怒/路怒 攻击性驾驶、超速 ⭐⭐
焦虑/紧张 反应过度、误操作 ⭐⭐⭐
困倦/无聊 反应迟钝、警觉下降 ⭐⭐
分心/走神 注意力偏离道路 ⭐⭐⭐
正常/放松 安全驾驶 — 基线

面部动作单元(Action Unit, AU)是 FACS(面部动作编码系统)的基本组成单元,可客观描述面部肌肉运动。

1.2 现有方法局限

方法 局限
静态 AU 检测 忽略时间动态,误报率高
光流法 对光照敏感,计算量大
RNN/LSTM 序列建模能力有限,训练困难
Transformer 数据需求大,实时性挑战

1.3 Twin Cycle Autoencoders 核心思想

双循环一致性: 空间分支和时间分支通过循环重构互相约束,提升特征学习的有效性。

2. 方法详解

2.1 架构设计

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

class SpatialEncoder(nn.Module):
"""空间编码器:从单帧提取 AU 空间分布"""

def __init__(self, n_aus=12):
super().__init__()

# 面部特征提取(轻量级 CNN)
self.backbone = nn.Sequential(
nn.Conv2d(3, 32, 3, stride=2, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.Conv2d(32, 64, 3, stride=2, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Conv2d(64, 128, 3, stride=2, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.AdaptiveAvgPool2d((4, 4)),
)

# AU 回归头
self.au_head = nn.Sequential(
nn.Linear(128 * 16, 256),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(256, n_aus),
nn.Sigmoid() # AU 强度 0-1
)

def forward(self, x):
"""
Args:
x: (B, C, H, W) 单帧面部图像

Returns:
aus: (B, n_aus) AU 强度
feat: (B, 2048) 特征向量
"""
feat = self.backbone(x)
feat_flat = feat.flatten(1)
aus = self.au_head(feat_flat)
return aus, feat_flat


class TemporalEncoder(nn.Module):
"""时间编码器:建模 AU 序列动态"""

def __init__(self, n_aus=12, hidden_dim=128, n_layers=2):
super().__init__()

# GRU 建模时序
self.gru = nn.GRU(
input_size=n_aus,
hidden_dim=hidden_dim,
num_layers=n_layers,
batch_first=True,
bidirectional=True
)

# AU 时序预测头
self.temporal_head = nn.Sequential(
nn.Linear(hidden_dim * 2, n_aus),
nn.Sigmoid()
)

def forward(self, au_sequence):
"""
Args:
au_sequence: (B, T, n_aus) AU 时间序列

Returns:
au_pred: (B, T, n_aus) 预测的下一帧 AU
hidden: (B, hidden_dim*2) 时序特征
"""
out, hidden = self.gru(au_sequence)
au_pred = self.temporal_head(out)
return au_pred, hidden


class TwinCycleAutoencoder(nn.Module):
"""
双循环自编码器

空间循环:图像 → AU → 重建图像
时间循环:AU序列 → 预测下一帧 → 重建序列

两个循环互相约束:
- 空间编码的 AU 必须能重建面部图像
- 时间编码的 AU 必须能预测未来 AU
"""

def __init__(self, n_aus=12):
super().__init__()
self.spatial = SpatialEncoder(n_aus=n_aus)
self.temporal = TemporalEncoder(n_aus=n_aus)

# 图像解码器(空间循环重建)
self.image_decoder = nn.Sequential(
nn.Linear(n_aus, 256),
nn.ReLU(),
nn.Linear(256, 128 * 4 * 4),
nn.ReLU(),
)
self.deconv = nn.Sequential(
nn.ConvTranspose2d(128, 64, 3, stride=2, padding=1, output_padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.ConvTranspose2d(64, 32, 3, stride=2, padding=1, output_padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.ConvTranspose2d(32, 3, 3, stride=2, padding=1, output_padding=1),
nn.Sigmoid()
)

def forward(self, image_sequence):
"""
Args:
image_sequence: (B, T, C, H, W) 面部视频序列

Returns:
dict: 空间 AU、时序 AU、重建图像、预测 AU
"""
B, T, C, H, W = image_sequence.shape

# 1. 空间编码:逐帧提取 AU
frames = image_sequence.reshape(B*T, C, H, W)
au_spatial, _ = self.spatial(frames)
au_spatial = au_spatial.reshape(B, T, -1) # (B, T, n_aus)

# 2. 时间编码:建模 AU 动态
au_temporal, hidden = self.temporal(au_spatial)

# 3. 空间循环重建:AU → 图像
au_flat = au_spatial.reshape(B*T, -1)
recon_feat = self.image_decoder(au_flat)
recon_feat = recon_feat.reshape(B*T, 128, 4, 4)
recon_images = self.deconv(recon_feat)
recon_images = recon_images.reshape(B, T, C, H, W)

# 4. 时间循环预测:当前 AU → 预测下一帧 AU
# au_temporal 已经是预测序列

return {
'au_spatial': au_spatial, # (B, T, n_aus) 检测的 AU
'au_temporal': au_temporal, # (B, T, n_aus) 预测的 AU
'recon_images': recon_images, # (B, T, C, H, W) 重建图像
'hidden': hidden, # 时序特征
}

def compute_loss(self, outputs, targets):
"""
计算双循环一致性损失

Args:
outputs: forward() 返回值
targets: {
'images': (B, T, C, H, W) 原始图像,
'au_labels': (B, T, n_aus) AU 标签(可选)
}
"""
# 1. 空间循环损失:图像重建
recon_loss = F.mse_loss(
outputs['recon_images'],
targets['images']
)

# 2. 时间循环损失:AU 预测一致性
temporal_loss = F.l1_loss(
outputs['au_temporal'][:, :-1], # 预测 t
outputs['au_spatial'][:, 1:].detach() # 实际 t+1(stop gradient)
)

# 3. AU 检测损失(如果有标签)
if 'au_labels' in targets:
detection_loss = F.binary_cross_entropy(
outputs['au_spatial'],
targets['au_labels']
)
else:
detection_loss = torch.tensor(0.0)

# 总损失
total_loss = (
1.0 * recon_loss +
0.5 * temporal_loss +
1.0 * detection_loss
)

return {
'total': total_loss,
'recon': recon_loss,
'temporal': temporal_loss,
'detection': detection_loss
}


# 测试
if __name__ == "__main__":
model = TwinCycleAutoencoder(n_aus=12)

# 模拟输入:(batch=2, frames=16, channels=3, H=64, W=64)
x = torch.randn(2, 16, 3, 64, 64)
x = torch.sigmoid(x) # 归一化到 0-1

outputs = model(x)

print("Twin Cycle Autoencoder 输出:")
print(f" AU 空间: {outputs['au_spatial'].shape}")
print(f" AU 时序: {outputs['au_temporal'].shape}")
print(f" 重建图像: {outputs['recon_images'].shape}")
print(f" 隐藏特征: {outputs['hidden'].shape}")

# 损失计算
targets = {'images': x}
losses = model.compute_loss(outputs, targets)

print(f"\n损失:")
for k, v in losses.items():
print(f" {k}: {v.item():.4f}")

print(f"\n参数总数: {sum(p.numel() for p in model.parameters()):,}")

3. 关键 AU 与驾驶员状态映射

3.1 FACS AU 驾驶状态映射表

AU 编号 AU 名称 驾驶状态映射 检测难度
AU1 内眉提升 恐惧/惊讶 ⭐⭐
AU2 外眉提升 惊讶/关注 ⭐⭐
AU4 眉毛下压 愤怒/困惑/专注 ⭐⭐⭐
AU5 上眼睑提升 惊讶/警觉 ⭐⭐
AU6 面颊提升 微笑/愉悦 ⭐⭐
AU7 面颊收紧 厌恶/不悦 ⭐⭐⭐
AU9 鼻翼皱起 厌恶 ⭐⭐⭐
AU12 嘴角拉伸 微笑 ⭐⭐
AU15 嘴角下压 悲伤/不满 ⭐⭐⭐
AU17 下巴提升 怀疑/不信 ⭐⭐⭐
AU20 嘴唇拉伸 恐惧 ⭐⭐⭐
AU23 嘴唇紧绷 愤怒/紧张 ⭐⭐⭐

3.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
class DriverEmotionClassifier:
"""
基于 AU 检测结果的驾驶员情绪分类器

使用 FACS 规则+时序模式
"""

EMOTION_RULES = {
'anger': {
'required_aus': [('AU4', 0.5), ('AU23', 0.3)],
'optional_aus': ['AU7', 'AU17'],
'min_duration_frames': 5, # ~1s @ 5fps
},
'fear_surprise': {
'required_aus': [('AU1', 0.4), ('AU2', 0.4), ('AU5', 0.3)],
'optional_aus': ['AU20'],
'min_duration_frames': 3,
},
'happiness': {
'required_aus': [('AU6', 0.4), ('AU12', 0.4)],
'optional_aus': [],
'min_duration_frames': 5,
},
'sadness': {
'required_aus': [('AU15', 0.3), ('AU4', 0.2)],
'optional_aus': ['AU17'],
'min_duration_frames': 10,
},
'disgust': {
'required_aus': [('AU9', 0.3), ('AU7', 0.3)],
'optional_aus': ['AU15'],
'min_duration_frames': 5,
},
'concentration': {
'required_aus': [('AU4', 0.3)],
'optional_aus': ['AU7'],
'min_duration_frames': 15, # 需要持续 3s
},
}

# AU 索引映射(与模型输出对齐)
AU_INDICES = {
'AU1': 0, 'AU2': 1, 'AU4': 2, 'AU5': 3, 'AU6': 4,
'AU7': 5, 'AU9': 6, 'AU12': 7, 'AU15': 8, 'AU17': 9,
'AU20': 10, 'AU23': 11
}

def classify(self, au_sequence):
"""
分类情绪状态

Args:
au_sequence: (T, 12) AU 强度时间序列

Returns:
emotion: 情绪标签
confidence: 置信度
"""
T = len(au_sequence)
scores = {}

for emotion, rule in self.EMOTION_RULES.items():
# 检查必要 AU 是否满足阈值
required_met = 0
for au_name, threshold in rule['required_aus']:
idx = self.AU_INDICES[au_name]
# 检查最近 N 帧是否持续超过阈值
n_frames = min(T, rule['min_duration_frames'])
recent = au_sequence[-n_frames:, idx]
if np.mean(recent > threshold) > 0.6: # 60% 帧满足
required_met += 1

# 检查可选 AU
optional_met = 0
for au_name in rule['optional_aus']:
idx = self.AU_INDICES[au_name]
recent = au_sequence[-min(T, rule['min_duration_frames']):, idx]
if np.mean(recent > 0.3) > 0.5:
optional_met += 1

# 综合评分
total_required = len(rule['required_aus'])
total_optional = len(rule['optional_aus'])

score = (required_met / total_required) * 0.7
if total_optional > 0:
score += (optional_met / total_optional) * 0.3

scores[emotion] = score

# 选最高分
emotion = max(scores, key=scores.get)
confidence = scores[emotion]

if confidence < 0.3:
return 'neutral', confidence

return emotion, confidence


# 测试
if __name__ == "__main__":
import numpy as np

clf = DriverEmotionClassifier()

# 模拟愤怒序列(AU4 + AU23 持续高强度)
np.random.seed(42)
anger_seq = np.zeros((20, 12))
anger_seq[:, 2] = 0.6 # AU4
anger_seq[:, 11] = 0.5 # AU23
anger_seq[:, 5] = 0.4 # AU7

r = clf.classify(anger_seq)
print(f"模拟愤怒: {r[0]} (置信度: {r[1]:.2f})")

# 模拟愉悦序列
happy_seq = np.zeros((20, 12))
happy_seq[:, 4] = 0.6 # AU6
happy_seq[:, 7] = 0.7 # AU12

r = clf.classify(happy_seq)
print(f"模拟愉悦: {r[0]} (置信度: {r[1]:.2f})")

# 模拟中性
neutral_seq = np.random.rand(20, 12) * 0.1

r = clf.classify(neutral_seq)
print(f"模拟中性: {r[0]} (置信度: {r[1]:.2f})")

4. IMS 集成路线

4.1 DMS 摄像头复用

维度 现有 DMS AU 检测复用
摄像头 RGB-IR ✅ 直接使用
分辨率 2MP ✅ 足够
帧率 30fps ✅ 降采样到 5fps
计算资源 NPU ✅ 轻量级模型
新增硬件 无 零硬件增加

4.2 开发优先级

优先级 任务 时间 依赖
P0 AU 检测模型部署(轻量级) 2026 Q4 DMS 摄像头
P0 情绪分类规则引擎 2026 Q4 AU 检测
P1 情绪-驾驶行为关联分析 2027 Q1 CAN 信号
P1 路怒检测+ADAS 协同干预 2027 Q1 情绪分类
P2 个性化情绪基线 2027 Q2 历史数据
P2 多模态情绪融合(AU+语音+生理) 2027 Q2 麦克风

4.3 路怒检测场景

场景 AU 信号 行为信号 系统响应
被加塞后愤怒 AU4↑ + AU23↑ 急加速 + 方向盘猛打 警告+抑制油门
前车慢行不耐烦 AU4↑ + AU7↑ 跟车距离缩短 增加跟车提醒
正常驾驶微笑 AU6↑ + AU12↑ 平稳驾驶 无干预
困倦打哈欠 AU5+AU20 车道偏离 疲劳警告

5. 总结

核心洞察

  1. 双循环自编码器有效建模时空 AU:空间分支检测每帧 AU,时间分支捕捉动态变化,循环一致性约束提升无监督学习效果
  2. 12 个核心 AU 覆盖主要驾驶情绪:愤怒、恐惧、愉悦、悲伤、厌恶、专注
  3. 零硬件增加:完全复用 DMS 摄像头,模型轻量可在 NPU 上实时运行
  4. 路怒检测是直接应用:AU4(眉毛下压)+ AU23(嘴唇紧绷)持续 >1s 是愤怒的可靠信号

对 Euro NCAP 2026 的启示

  • Euro NCAP 2026 暂未要求情绪检测,但情绪监测是 DMS 高级功能
  • 路怒检测可作为主动安全增强项(抑制油门响应)
  • AU 检测可同时用于疲劳检测(AU5 上眼睑提升 = 瞌睡前兆)

参考来源:

  • D. Sai Sidharth, arXiv(2026-07),”Spatiotemporal Facial Action Unit Detection using Twin Cycle Autoencoders for Driver Monitoring”
  • FACS (Facial Action Coding System), Ekman & Friesen
  • Euro NCAP 2026 DSM Protocol