航空塔台管制员多模态疲劳检测——眼动+ECG双流融合框架论文解读

论文信息

项目 内容
标题 Dual-Stream Fusion of Eye-Tracking and ECG Signals for Fatigue Detection in Remote Tower Air Traffic Controllers
期刊 Bioengineering (MDPI), Vol.13 Issue 7, Article 717
发表 2026年6月23日
链接 https://doi.org/10.3390/bioengineering13070717(OA,Europe PMC: PMC13405756)
相关论文 Remote Tower ATC Multimodal Fatigue Detection (Sensors, 2026年6月, DOI 10.3390/s26061856)
被试 34人(6名资深塔台管制教员 + 28名管制学员,中国民航飞行学院)

核心创新

  1. 眼动+ECG双流编码结构:模态特异性时序表征提取(CNN+BiLSTM+时间注意力),轻量级直接后期融合
  2. 航空座舱疲劳检测迁移:面向远程塔台管制员的非侵入式多模态疲劳检测框架(MFD-Net)
  3. 窗口级ECG专家特征:将ECG-derived RMSSD专家特征作为先验知识注入融合层,提升可解释性

问题定义

远程塔台管制员的特殊挑战

特征 传统塔台 远程塔台
视觉环境 直接观察窗外 摄像头+屏幕显示
注意力维持 3D全景感知 2D屏幕凝视
疲劳加速 较慢 更快(屏幕凝视+缺乏环境刺激)
检测难度 有行为线索 更难(仅看屏幕)

现有方案局限

方案 模态 局限
单PERCLOS 眼动 无法检测认知疲劳
单ECG/HRV 心电 延迟大(5min窗口)
单EEG 脑电 不可穿戴
摄像头面部表情 视觉 管制员面向屏幕,表情变化少

方法详解

1. 双流架构

注:论文实际架构为 CNN + Bi-LSTM + 时间注意力模块的双流并行编码与直接后期融合,输入为 5 秒窗口的眼动特征流与 ECG-derived 特征流(并非原始 ECG 波形 CNN 编码);以下代码为依据原文描述的示意实现,不可直接运行,非论文官方代码(论文未开源,数据因保密协议不公开)。

2. 眼动流:Transformer编码器

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
# 示意代码,不可直接运行(非论文官方实现;论文实际为 CNN+BiLSTM+时间注意力编码,非 Transformer)
import torch
import torch.nn as nn

class EyeTrackingStream(nn.Module):
"""
眼动追踪流:Transformer编码器

提取时序眼动特征:
- 凝视位置序列
- 眨眼频率/时长
- 扫视幅度/方向
- 瞳孔直径变化
"""

def __init__(self, d_model: int = 128, n_heads: int = 4,
n_layers: int = 3, input_dim: int = 8):
super().__init__()
# 输入投影:8维眼动特征→d_model
self.input_proj = nn.Linear(input_dim, d_model)

# 位置编码
self.pos_enc = PositionalEncoding(d_model)

# Transformer编码器
encoder_layer = nn.TransformerEncoderLayer(
d_model=d_model, nhead=n_heads,
dim_feedforward=d_model*4,
dropout=0.1, batch_first=True,
activation='gelu'
)
self.encoder = nn.TransformerEncoder(
encoder_layer, num_layers=n_layers
)

# 时序池化
self.temporal_pool = nn.AdaptiveAvgPool1d(1)

def forward(self, eye_data: torch.Tensor) -> torch.Tensor:
"""
Args:
eye_data: [B, T, 8] 眼动特征序列
特征维度: [gaze_x, gaze_y, pupil_diameter,
blink_flag, saccade_amp,
saccade_vel, fixation_dur,
gaze_dispersion]

Returns:
features: [B, d_model] 时序特征
"""
x = self.input_proj(eye_data) # [B, T, d_model]
x = self.pos_enc(x)
x = self.encoder(x)
# 时序平均池化
x = x.transpose(1, 2) # [B, d_model, T]
x = self.temporal_pool(x).squeeze(-1) # [B, d_model]
return x


class PositionalEncoding(nn.Module):
"""正弦位置编码"""
def __init__(self, d_model: int, max_len: int = 3600):
super().__init__()
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1).float()
div_term = torch.exp(
torch.arange(0, d_model, 2).float() *
(-torch.log(torch.tensor(10000.0)) / d_model)
)
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
self.register_buffer('pe', pe.unsqueeze(0))

def forward(self, x):
return x + self.pe[:, :x.size(1)]

3. 心电流:CNN编码器+专家特征

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
# 示意代码,不可直接运行(非论文官方实现;论文的ECG流输入为ECG-derived特征而非原始波形,
# 专家特征仅用RMSSD)
class ECGStream(nn.Module):
"""
ECG流:CNN编码器 + HRV专家特征

双路径:
1. CNN从原始ECG波形提取深度特征
2. 专家特征:从ECG计算的HRV指标
"""

def __init__(self, signal_length: int = 1800, # 60s@30Hz
d_model: int = 128):
super().__init__()
# CNN编码器
self.cnn = nn.Sequential(
nn.Conv1d(1, 32, 7, stride=2, padding=3),
nn.BatchNorm1d(32),
nn.ReLU(),

nn.Conv1d(32, 64, 5, stride=2, padding=2),
nn.BatchNorm1d(64),
nn.ReLU(),

nn.Conv1d(64, 128, 3, stride=2, padding=1),
nn.BatchNorm1d(128),
nn.ReLU(),

nn.AdaptiveAvgPool1d(1),
nn.Flatten(),
nn.Linear(128, d_model)
)

# 专家特征维度
self.expert_dim = 6
# 专家特征投影
self.expert_proj = nn.Sequential(
nn.Linear(self.expert_dim, 32),
nn.ReLU(),
nn.Linear(32, d_model)
)

def extract_hrv_features(
self, ecg: torch.Tensor, fs: int = 30
) -> torch.Tensor:
"""
提取HRV专家特征

Args:
ecg: [B, L] ECG信号

Returns:
hrv: [B, 6] HRV特征
"""
B = ecg.shape[0]
device = ecg.device

# R峰检测(简化版:使用阈值+找峰)
r_peaks = self._detect_r_peaks(ecg, fs)

features = []
for b in range(B):
peaks = r_peaks[b]
n_peaks = len(peaks)

if n_peaks < 3:
features.append(torch.zeros(self.expert_dim, device=device))
continue

# RR间期
rr = torch.diff(peaks.float()) / fs # 秒

# 1. 平均心率
mean_hr = 60.0 / rr.mean() if rr.mean() > 0 else 0

# 2. RMSSD
rmssd = torch.sqrt(torch.mean(rr[1:] - rr[:-1] ** 2))

# 3. SDNN
sdnn = rr.std()

# 4. LF功率 (0.04-0.15 Hz)
# 5. HF功率 (0.15-0.4 Hz)
# 6. LF/HF比值
if len(rr) > 10:
rr_interp = torch.nn.functional.interpolate(
rr.view(1, -1), size=256, mode='linear'
).squeeze(0)
freqs = torch.fft.rfftfreq(256, d=1/fs)
power = torch.abs(torch.fft.rfft(rr_interp)) ** 2

lf_mask = (freqs >= 0.04) & (freqs < 0.15)
hf_mask = (freqs >= 0.15) & (freqs <= 0.4)

lf_power = power[lf_mask].sum()
hf_power = power[hf_mask].sum()
lf_hf = (lf_power / (hf_power + 1e-8)).item()
else:
lf_power = hf_power = lf_hf = 0

features.append(torch.tensor([
mean_hr, rmssd.item(), sdnn.item(),
lf_power.item(), hf_power.item(), lf_hf
], device=device))

return torch.stack(features)

def _detect_r_peaks(self, ecg, fs):
"""简化版R峰检测"""
# 实际应使用Pan-Tompkins算法
peaks = []
for b in range(ecg.shape[0]):
signal = ecg[b].cpu().numpy()
threshold = np.percentile(signal[signal > 0], 95)
above = signal > threshold
# 找连续段的中点
from scipy.signal import find_peaks
pks, _ = find_peaks(signal, height=threshold,
distance=fs*0.3)
peaks.append(torch.tensor(pks))
return peaks

def forward(self, ecg: torch.Tensor) -> torch.Tensor:
"""
Args:
ecg: [B, L] ECG信号

Returns:
features: [B, d_model] ECG特征
"""
# CNN深度特征
ecg_input = ecg.unsqueeze(1) # [B, 1, L]
cnn_feat = self.cnn(ecg_input)

# HRV专家特征
hrv = self.extract_hrv_features(ecg)
expert_feat = self.expert_proj(hrv)

# 融合
return cnn_feat + expert_feat


class DualStreamFatigueDetector(nn.Module):
"""
双流疲劳检测模型

融合眼动+ECG进行疲劳分类
"""

def __init__(self, n_classes: int = 3):
super().__init__()
self.eye_stream = EyeTrackingStream(d_model=128)
self.ecg_stream = ECGStream(d_model=128)

# 后期融合
self.fusion = nn.Sequential(
nn.Linear(128 + 128, 64),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(64, n_classes)
)

def forward(self, eye_data: torch.Tensor,
ecg: torch.Tensor) -> torch.Tensor:
eye_feat = self.eye_stream(eye_data)
ecg_feat = self.ecg_stream(ecg)
fused = torch.cat([eye_feat, ecg_feat], dim=1)
return self.fusion(fused)


if __name__ == "__main__":
model = DualStreamFatigueDetector(n_classes=3)
# 5分钟@30Hz眼动 = 9000帧, 取30秒窗口
eye_input = torch.randn(4, 900, 8) # [B, T, 8]
ecg_input = torch.randn(4, 1800) # [B, 60s@30Hz]

output = model(eye_input, ecg_input)
print(f"眼动输入: {eye_input.shape}")
print(f"ECG输入: {ecg_input.shape}")
print(f"输出: {output.shape}")
print(f"参数量: {sum(p.numel() for p in model.parameters()):,}")

实验结果

性能对比(混合被试 5 折交叉验证协议,5 秒窗口)

⚠️ 以下为论文 Table 2-3 口径。注意:混合协议下同一被试/场景的重叠窗口可能同时出现在训练与测试集,论文明确提示该结果应解读为偏乐观的分布内估计,而非跨被试泛化能力。

方法 准确率 说明 模态
SVM 基线 0.7246 传统机器学习基线 眼+ECG特征
RF 基线 0.8230 传统机器学习基线 眼+ECG特征
Eye-only 73.45±2.01% 单模态变体 眼动
ECG-only 79.58±1.81% 单模态变体(Recall 最高) ECG
MFD-Net(双流后期融合) 85.20% Recall 83.33%,F1 79.09%,AUC 0.9337 眼+ECG+RMSSD专家特征

论文对融合贡献的解读:ECG 含有与心脏自主神经相关的疲劳敏感信息(ECG-only 的 Recall 最高),多模态融合主要改善的是整体指标平衡性(Accuracy/Precision/F1),而非单指标碾压。

零样本跨被试(LOSO)结果——论文的核心警示

指标 LOSO 零样本
准确率 70.95±21.59%
Recall 22.98±36.30%
AUC 0.6025±0.2984

低零样本 Recall 说明模型跨被试疲劳检出能力有限;轻量级目标被试校准(LOSO+30%微调)与序列概率聚合(3/5 窗口滑动平均)可部分缩小跨被试差距、改善时序稳定性,但论文同时提示校准结果也应谨慎解读(微调使用的是随机目标被试窗口)。

疲劳标签构建(原文口径)

论文未使用 PERCLOS 阈值或 RMSSD 阈值定义疲劳等级;疲劳标签为场景级弱监督标签——每个实验场景结束后按 SP-7 主观疲劳量表评分,场景内所有有效 5 秒窗口继承该场景标签。论文明确指出该弱标签策略可能引入标签噪声,且做了不同窗口时间位置的敏感性分析。

IMS开发启示

1. 从航空到汽车的迁移价值

技术点 航空塔台 汽车DMS 共用价值
眼动+ECG融合 ✅ 本文 可选配 ✅ 高
窗口级HRV专家特征 ✅ 本文 可复用 ✅ 高
Transformer时序编码 ✅ 本文 已有 ✅
非侵入式采集 ✅ 关键需求 ✅ ✅

2. 汽车DMS适配

航空场景 汽车场景 适配改动
管制员看屏幕 驾驶员看道路 眼动ROI不同
ECG胸贴 rPPG摄像头替代 采集方式不同
5s窗口(离线评估) 更短窗口 缩短检测延迟,需重验
3级疲劳 2级警告 映射ENCAP等级
固定座椅 移动车辆 增加运动补偿

3. 跨领域应用对照

领域 检测目标 眼动 ECG 可迁移性
汽车DMS 驾驶员疲劳 ✅ rPPG替代 基线
航空塔台 管制员疲劳 ✅ ✅ ✅ 高
铁路驾驶 列车员疲劳 ✅ ✅ ✅ 高
船舶驾驶 船员疲劳 ✅ ✅ ✅ 中
矿区运输 操作员疲劳 ✅ ✅ ✅ 中

总结

航空塔台管制员的双流疲劳检测框架对IMS有三重启示:

  1. 融合改善的是指标平衡性而非单指标碾压:混合协议下 MFD-Net 准确率 85.20%(SVM 0.7246 / RF 0.8230 / Eye-only 73.45% / ECG-only 79.58%),且 ECG-only 的 Recall 最高——ECG 含强疲劳敏感信息,融合的价值在于整体平衡
  2. 零样本跨被试是硬门槛:LOSO 下 Recall 仅 22.98%,论文自身明确提示不得将混合协议结果解读为泛化能力;车规量产必须按目标驾驶员校准设计
  3. 窗口级 RMSSD 专家特征注入:可解释先验 + 轻量级直接后期融合,架构可复用于车载 rPPG 替代 ECG 的场景(航空→汽车→铁路→船舶)