EyeCue:基于凝视增强的第一人称视频理解的驾驶员认知分心检测 | 论文解读与代码复现

论文信息

项目 内容
标题 EyeCue: Driver Cognitive Distraction Detection via Gaze-Empowered Egocentric Video Understanding
作者 Lang Zhang (Virginia Tech), JinYi Yoon (Inha University), Matthew Corbett (Army Cyber Institute at West Point), Abhijit Sarkar (Virginia Tech Transportation Institute), Bo Ji (Virginia Tech)
会议 IJCAI 2026 (International Joint Conference on Artificial Intelligence)
链接 arXiv:2605.07859
代码 GitHub: langzhang2000/EyeCue
数据集 CogDrive (3,662 标注样本,4个驾驶数据集融合)

核心创新

EyeCue 首次将眼动凝视信号与第一人称视频(egocentric video)进行跨模态融合来检测驾驶员认知分心。核心洞察:认知分心不体现在”看哪里”,而体现在”看到的东西如何被大脑处理”。同一凝视点在不同驾驶场景下可能代表完全不同的认知状态——红灯前注视交通灯是正常的,但直行时盯着路边停靠车辆则可能是认知分心。

三大贡献:

  1. 提出 EyeCue 框架,通过凝视引导的语义查询模块(GDSQ)建模凝视与视觉上下文的交互
  2. 构建 CogDrive 数据集,3,662 个带认知分心标注的第一人称视频片段
  3. 准确率达 74.38%,超越 11 个基线模型 7%+

方法详解

1. 问题定义

认知分心与视觉/手动分心不同:驾驶员目光在路面上、手在方向盘上,但思维已游离于驾驶之外。这种内部状态无法直接观测,传统 EEG/SRT 方法需要侵入式传感器,无法连续部署。

EyeCue 的思路:利用眼动凝视作为认知状态的行为线索,结合第一人称视频的视觉上下文,推断凝视与场景的交互模式是否异常。

2. 架构总览

3. 核心模块

3.1 Video Encoder(视频编码器)

基于 TimeSformer(facebook/timesformer-base-finetuned-k600),将第一人称视频片段编码为时空 token 序列。

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
# models/video_encoder.py 核心结构
import torch
import torch.nn as nn
from transformers import TimesformerModel

class VideoEncoder(nn.Module):
"""
TimeSformer 骨干网络,支持任意 clip 长度
通过时间嵌入插值适配不同帧数
"""
def __init__(self, clip_len=8):
super().__init__()
self.timesformer = TimesformerModel.from_pretrained(
"facebook/timesformer-base-finetuned-k600"
)
self.clip_len = clip_len
# 原始模型支持 16 帧,通过插值扩展

def forward(self, video_frames):
"""
Args:
video_frames: (B, C, T, H, W) 视频帧序列
Returns:
cls_token: (B, 1, D) 全局表示
video_tokens: (B, T*196, D) 空间 token
# 196 = 14*14 patches, 每帧的空间 patch 数
"""
outputs = self.timesformer(video_frames)
cls_token = outputs.last_hidden_state[:, :1, :]
video_tokens = outputs.last_hidden_state[:, 1:, :]
return cls_token, video_tokens

3.2 Gaze Encoder(凝视编码器)

轻量级 Transformer,将原始 (x, y) 凝视坐标编码为富语义表示。

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
# models/gaze_encoder.py 核心结构
class GazeEncoder(nn.Module):
"""
线性投影 + 可学习 CLS Token + 自注意力
输入: 原始凝视坐标序列 (B, T, 2)
输出: 凝视表示 (B, T, D) 和 CLS 表示 (B, 1, D)
"""
def __init__(self, embed_dim=768, num_heads=12, num_layers=4):
super().__init__()
# 线性投影: 2D 坐标 → 高维空间
self.proj = nn.Linear(2, embed_dim)
# 可学习 CLS Token
self.cls_token = nn.Parameter(torch.randn(1, 1, embed_dim))
# 多层自注意力
encoder_layer = nn.TransformerEncoderLayer(
d_model=embed_dim,
nhead=num_heads,
dim_feedforward=3072,
dropout=0.1,
batch_first=True
)
self.transformer = nn.TransformerEncoder(encoder_layer, num_layers)

def forward(self, gaze_sequence):
"""
Args:
gaze_sequence: (B, T, 2) 凝视点坐标
Returns:
cls_out: (B, 1, D) CLS 表示
gaze_tokens: (B, T, D) 时序凝视表示
"""
B = gaze_sequence.shape[0]
x = self.proj(gaze_sequence) # (B, T, D)

# 拼接 CLS Token
cls = self.cls_token.expand(B, -1, -1) # (B, 1, D)
x = torch.cat([cls, x], dim=1) # (B, T+1, D)

x = self.transformer(x)
cls_out = x[:, :1, :]
gaze_tokens = x[:, 1:, :]
return cls_out, gaze_tokens

3.3 凝视引导 Patch 选择 + 语义跨注意力融合

这是 EyeCue 的核心创新点。将每帧的凝视坐标映射到视频 token 空间的具体 patch,然后用凝视 token 作为 query,选中的 video patch 作为 key/value 进行跨注意力。

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
# models/semantic.py 核心逻辑
class GazeGuidedPatchSelection:
"""
将凝视坐标映射到 TimeSformer 的 patch 空间

视频帧被划分为 14×14 = 196 个 patch
凝视点 (x, y) → 对应的 patch 索引
"""
def __init__(self, img_size=224, patch_size=16):
self.grid_size = img_size // patch_size # 14
self.patch_size = patch_size

def select(self, gaze_points, video_tokens):
"""
Args:
gaze_points: (B, T, 2) 归一化凝视坐标 [0, 1]
video_tokens: (B, T*196, D) 视频空间 token
Returns:
selected_tokens: (B, T, D) 凝视对应的 patch token
"""
B, T, _ = gaze_points.shape
# 将归一化坐标映射到 patch 索引
grid_x = (gaze_points[..., 0] * self.grid_size).long()
grid_y = (gaze_points[..., 1] * self.grid_size).long()
grid_x = grid_x.clamp(0, self.grid_size - 1)
grid_y = grid_y.clamp(0, self.grid_size - 1)

# patch 索引 = y * grid_size + x
patch_idx = grid_y * self.grid_size + grid_x # (B, T)

selected = []
for b in range(B):
# video_tokens[b] shape: (T*196, D)
# 第 t 帧的 patch 索引: t*196 + patch_idx[b, t]
indices = patch_idx[b] + torch.arange(T, device=patch_idx.device) * 196
selected.append(video_tokens[b, indices])

return torch.stack(selected, dim=0) # (B, T, D)


class CrossAttentionBlock(nn.Module):
"""
凝视 token 作为 Query, 选中 video patch 作为 Key/Value
"""
def __init__(self, dim=768, num_heads=12):
super().__init__()
self.cross_attn = nn.MultiheadAttention(
embed_dim=dim,
num_heads=num_heads,
batch_first=True
)
self.norm_q = nn.LayerNorm(dim)
self.norm_kv = nn.LayerNorm(dim)
self.ffn = nn.Sequential(
nn.Linear(dim, dim * 4),
nn.GELU(),
nn.Linear(dim * 4, dim),
nn.Dropout(0.1)
)
self.norm_ffn = nn.LayerNorm(dim)

def forward(self, query, key_value):
"""
Args:
query: (B, T, D) 凝视 tokens
key_value: (B, T, D) 选中的 video tokens
Returns:
fused: (B, T, D) 融合后的表示
"""
# 跨注意力
attn_out, _ = self.cross_attn(
self.norm_q(query),
self.norm_kv(key_value),
self.norm_kv(key_value)
)
x = query + attn_out # 残差连接

# FFN
ffn_out = self.ffn(self.norm_ffn(x))
x = x + ffn_out
return x


class SemanticFusion(nn.Module):
"""
堆叠多层跨注意力块
"""
def __init__(self, dim=768, num_heads=12, num_layers=2):
super().__init__()
self.layers = nn.ModuleList([
CrossAttentionBlock(dim, num_heads)
for _ in range(num_layers)
])

def forward(self, gaze_tokens, selected_video_tokens):
x = gaze_tokens
for layer in self.layers:
x = layer(x, selected_video_tokens)
return x # (B, T, D)

4. 数据集 CogDrive

CogDrive 融合了 4 个现有驾驶数据集,按 DR(eye)VE 的标注流程添加认知分心标注:

来源数据集 类型 场景
DR(eye)VE 真实驾驶 多道路类型、天气
BDD-A 真实驾驶 多场景注意力标注
DADA-2000 真实驾驶 事故分析驾驶注意力
TrafficGaze 真实驾驶 交通场景凝视
  • 总样本数: 3,662 个视频片段
  • 标注: 二分类(0=专注,1=认知分心)
  • 输入: 视频片段(8帧采样)+ 凝视坐标序列

5. 实验结果

模型 类型 准确率
Gaze-only baseline 凝视序列 ~62%
TimeSformer (video only) 视频 ~65%
DCDD (single frame + gaze) 单帧+凝视 ~67%
EyeCue (full) 视频+凝视跨模态 74.38%

关键发现:

  • 凝视与视频的交互建模(跨注意力)比简单拼接提升 7%+
  • 跨场景泛化性:不同道路/天气/时间条件下均保持 >70% 准确率
  • 消融实验:去掉 GDSQ 模块后准确率下降 4.2%,证明凝视引导机制的关键性

代码复现

环境配置

1
2
3
4
5
6
7
8
9
10
11
# 基础环境
# Python >= 3.9, CUDA 11.8+, PyTorch 2.6.0
git clone https://github.com/langzhang2000/EyeCue.git
cd EyeCue

# 安装 PyTorch
pip install torch==2.6.0 torchvision==0.21.0 torchaudio==2.6.0 \
--index-url https://download.pytorch.org/whl/cu118

# 安装依赖
pip install -r requirements.txt

完整训练流程

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
"""
EyeCue 完整训练复现

论文:EyeCue: Driver Cognitive Distraction Detection via
Gaze-Empowered Egocentric Video Understanding
会议:IJCAI 2026
"""

import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from models.video_encoder import VideoEncoder
from models.gaze_encoder import GazeEncoder
from models.semantic import SemanticFusion, GazeGuidedPatchSelection
from models.head import ClassificationHead
from data.dataset import VideoGazeDataset
import argparse
import os
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix

class EyeCue(nn.Module):
"""
EyeCue 完整模型

组合: Video Encoder + Gaze Encoder + GDSQ + Semantic Fusion + Classification Head
"""
def __init__(self, clip_len=8, num_classes=2):
super().__init__()
self.clip_len = clip_len

# 视频编码器: TimeSformer
self.video_encoder = VideoEncoder(clip_len=clip_len)

# 凝视编码器: 轻量 Transformer
self.gaze_encoder = GazeEncoder(embed_dim=768, num_heads=12, num_layers=4)

# 凝视引导 Patch 选择
self.patch_selector = GazeGuidedPatchSelection(
img_size=224, patch_size=16
)

# 语义跨注意力融合
self.semantic = SemanticFusion(dim=768, num_heads=12, num_layers=2)

# 分类头
self.head = ClassificationHead(dim=768, num_classes=num_classes)

def forward(self, video_frames, gaze_sequence):
"""
Args:
video_frames: (B, C, T, H, W) 视频帧序列
gaze_sequence: (B, T, 2) 归一化凝视坐标 [0, 1]
Returns:
logits: (B, 2) 分类输出
"""
# 1. 视频编码
cls_token, video_tokens = self.video_encoder(video_frames)
# video_tokens: (B, T*196, D)

# 2. 凝视编码
gaze_cls, gaze_tokens = self.gaze_encoder(gaze_sequence)
# gaze_tokens: (B, T, D)

# 3. 凝视引导 Patch 选择
selected_tokens = self.patch_selector.select(
gaze_sequence, video_tokens
)
# selected_tokens: (B, T, D)

# 4. 语义跨注意力融合
fused = self.semantic(gaze_tokens, selected_tokens)
# fused: (B, T, D)

# 5. 分类
# 使用 CLS token 和融合特征
combined = torch.cat([gaze_cls.squeeze(1), fused.mean(dim=1)], dim=-1)
logits = self.head(combined)

return logits


def train_one_epoch(model, loader, optimizer, criterion, device):
model.train()
total_loss = 0
all_preds, all_labels = [], []

for video, gaze, labels in loader:
video = video.to(device)
gaze = gaze.to(device)
labels = labels.to(device)

optimizer.zero_grad()
logits = model(video, gaze)
loss = criterion(logits, labels)
loss.backward()
optimizer.step()

total_loss += loss.item()
preds = logits.argmax(dim=-1).cpu().numpy()
all_preds.extend(preds)
all_labels.extend(labels.cpu().numpy())

acc = accuracy_score(all_labels, all_preds)
return total_loss / len(loader), acc


def evaluate(model, loader, device):
model.eval()
all_preds, all_labels, all_probs = [], [], []

with torch.no_grad():
for video, gaze, labels in loader:
video = video.to(device)
gaze = gaze.to(device)
logits = model(video, gaze)
probs = torch.softmax(logits, dim=-1)
preds = logits.argmax(dim=-1)

all_preds.extend(preds.cpu().numpy())
all_labels.extend(labels.numpy())
all_probs.extend(probs[:, 1].cpu().numpy()) # 分心概率

acc = accuracy_score(all_labels, all_preds)
prec = precision_score(all_labels, all_preds)
rec = recall_score(all_labels, all_preds)
f1 = f1_score(all_labels, all_preds)
cm = confusion_matrix(all_labels, all_preds)

return {
'accuracy': acc,
'precision': prec,
'recall': rec,
'f1': f1,
'confusion_matrix': cm,
'probabilities': all_probs
}


if __name__ == "__main__":
# 参数配置(按论文设置)
config = {
'clip_len': 8, # 每个片段采样 8 帧
'batch_size': 4, # 论文设置
'epochs': 15,
'lr': 1e-5,
'device': 'cuda' if torch.cuda.is_available() else 'cpu',
'train_list': '/path/to/train.txt',
'val_list': '/path/to/val.txt',
'save_dir': 'checkpoints'
}

os.makedirs(config['save_dir'], exist_ok=True)

# 数据集
train_ds = VideoGazeDataset(
config['train_list'], clip_len=config['clip_len']
)
val_ds = VideoGazeDataset(
config['val_list'], clip_len=config['clip_len']
)

train_loader = DataLoader(
train_ds, batch_size=config['batch_size'],
shuffle=True, num_workers=1
)
val_loader = DataLoader(
val_ds, batch_size=config['batch_size'],
num_workers=1
)

# 模型
model = EyeCue(clip_len=config['clip_len']).to(config['device'])
optimizer = torch.optim.AdamW(model.parameters(), lr=config['lr'])
criterion = nn.CrossEntropyLoss()

# 训练
best_acc = 0
for epoch in range(config['epochs']):
train_loss, train_acc = train_one_epoch(
model, train_loader, optimizer, criterion, config['device']
)
val_metrics = evaluate(model, val_loader, config['device'])

print(f"Epoch {epoch+1}/{config['epochs']}")
print(f" Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.4f}")
print(f" Val Acc: {val_metrics['accuracy']:.4f}, "
f"Val F1: {val_metrics['f1']:.4f}")

if val_metrics['accuracy'] > best_acc:
best_acc = val_metrics['accuracy']
torch.save(model.state_dict(),
f"{config['save_dir']}/best_model.pth")
print(f" ✅ 新最佳模型已保存 (Acc: {best_acc:.4f})")

print(f"\n训练完成。最佳准确率: {best_acc:.4f}")

# 最终评估
model.load_state_dict(
torch.load(f"{config['save_dir']}/best_model.pth")
)
final_metrics = evaluate(model, val_loader, config['device'])

print("\n=== 最终评估结果 ===")
print(f"准确率: {final_metrics['accuracy']:.4f}")
print(f"精确率: {final_metrics['precision']:.4f}")
print(f"召回率: {final_metrics['recall']:.4f}")
print(f"F1: {final_metrics['f1']:.4f}")
print(f"混淆矩阵:\n{final_metrics['confusion_matrix']}")

推理脚本

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
"""
EyeCue 批量推理与评估

用法: python infer.py --test_list test.txt --checkpoint checkpoints/best_model.pth
"""

import csv
import torch
from torch.utils.data import DataLoader
from models.video_encoder import VideoEncoder
from models.gaze_encoder import GazeEncoder
from models.semantic import SemanticFusion, GazeGuidedPatchSelection
from models.head import ClassificationHead
from data.dataset import VideoGazeDataset
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix
import argparse

def main():
parser = argparse.ArgumentParser()
parser.add_argument('--test_list', required=True)
parser.add_argument('--checkpoint', default='checkpoints/best_model.pth')
parser.add_argument('--out_csv', default='predictions.csv')
parser.add_argument('--batch_size', type=int, default=4)
parser.add_argument('--clip_len', type=int, default=8)
parser.add_argument('--seed', type=int, default=42)
parser.add_argument('--device', default='auto')
args = parser.parse_args()

device = ('cuda' if args.device == 'auto' and torch.cuda.is_available()
else args.device)
if device == 'auto':
device = 'cpu'

torch.manual_seed(args.seed)

# 加载模型
model = EyeCue(clip_len=args.clip_len).to(device)
model.load_state_dict(torch.load(args.checkpoint, map_location=device))
model.eval()

# 数据
test_ds = VideoGazeDataset(args.test_list, clip_len=args.clip_len)
test_loader = DataLoader(test_ds, batch_size=args.batch_size, num_workers=1)

all_preds, all_labels, filenames = [], [], []

with torch.no_grad():
for video, gaze, labels in test_loader:
video = video.to(device)
gaze = gaze.to(device)
logits = model(video, gaze)
probs = torch.softmax(logits, dim=-1)
preds = logits.argmax(dim=-1)

all_preds.extend(preds.cpu().numpy())
all_labels.extend(labels.numpy())

# 指标
acc = accuracy_score(all_labels, all_preds)
prec = precision_score(all_labels, all_preds)
rec = recall_score(all_labels, all_preds)
f1 = f1_score(all_labels, all_preds)
cm = confusion_matrix(all_labels, all_preds)

print(f"Accuracy: {acc:.4f}")
print(f"Precision: {prec:.4f}")
print(f"Recall: {rec:.4f}")
print(f"F1: {f1:.4f}")
print(f"Confusion Matrix:\n{cm}")

# 保存预测
with open(args.out_csv, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['filename', 'label', 'pred', 'prob_distracted'])
for i, (fname, label, pred) in enumerate(zip(filenames, all_labels, all_preds)):
prob = all_probs[i] if 'all_probs' in dir() else 0.0
writer.writerow([fname, label, pred, f"{prob:.4f}"])

if __name__ == "__main__":
main()

IMS 开发启示

1. 认知分心检测的落地路线

EyeCue 提供了一条非侵入式认知分心检测的技术路线,对 IMS 开发有直接指导意义:

要素 EyeCue 方案 IMS 落地建议
视觉输入 第一人称 RGB 视频 DMS 摄像头(座舱内面向驾驶员)
凝视输入 外部眼动设备 DMS 已有视线估计模块,输出 (x, y) 坐标
计算平台 GPU (Tesla T4) 高通 QCS8255 Hexagon NPU,需量化
推理延迟 未报告实时指标 需 <100ms 满足 Euro NCAP 要求
准确率 74.38% 需 >90% 才能量产部署

2. 架构选型建议

3. 关键优化方向

优化点 问题 建议
视频编码器 TimeSformer 太重(110M参数) 换用 MobileViT-S (5.6M) 或 TinyViT
Patch 选择 需要精确凝视坐标 先确保视线估计精度 <2°
数据集 CogDrive 场景偏欧美 需补充中国道路场景数据
实时性 8帧推理延迟可能 >200ms 降低输入分辨率 224→160,或用 4帧
误报率 74% 准确率意味着 26% 误报 多帧投票 + 时序平滑,降低误报

4. 与 Euro NCAP 2026 的对接

Euro NCAP 2026 DSM 协议要求检测认知分心(D-01~D-05 场景),EyeCue 的技术路线可以直接对应:

NCAP 场景 描述 EyeCue 适用性
D-01 驾驶员长时间注视固定点 ✅ 直接适用
D-02 驾驶员目光在路面但思维游离 ✅ 核心场景
D-03 驾驶员跟车但反应迟钝 ⚠️ 需融合车辆动力学数据
D-04 驾驶员使用手机(认知分心) ⚠️ 需结合手动分心检测
D-05 驾驶员与乘客深度交谈 ⚠️ 需音频模态补充

5. 部署优先级

推荐分三阶段落地:

  1. 阶段一(概念验证): 用 EyeCue 开源代码在 GPU 上离线验证,确认在自有数据集上的准确率
  2. 阶段二(原型开发): 替换轻量骨干网络,在 QCS8255 上部署,目标推理 <100ms
  3. 阶段三(量产集成): 与疲劳检测、视觉分心检测融合,形成多级 DMS 策略

与现有方案的对比

方案 输入 认知分心检测 准确率 实时性 部署难度
EyeCue 视频+凝视 ✅ 原生支持 74.38% 需优化 中
PERCLOS 眼睑开度 ❌ 不支持 N/A ✅ <10ms 低
DCDD 单帧+凝视 ✅ 支持 ~67% ✅ <50ms 低
EEG-based 脑电信号 ✅ 高精度 ~90% ❌ 侵入式 极高
Gaze entropy 凝视序列 ✅ 支持 ~62% ✅ <20ms 低

总结

EyeCue 是认知分心检测领域的里程碑式工作,首次证明了凝视与视觉上下文的交互建模可以显著提升检测精度。虽然距离量产部署仍有差距(准确率、延迟、模型大小),但其技术路线为 IMS 团队提供了明确的研发方向。

关键收获:

  • 凝视坐标 × 视觉上下文 = 认知状态推断
  • 跨注意力机制是融合的有效手段
  • CogDrive 数据集可作为模型训练的起点
  • 量产化的核心挑战在轻量化和实时性

论文来源: arXiv:2605.07859 | 代码: GitHub | 会议: IJCAI 2026