EyeTAG:眼动轨迹感知的视线估计——BMVC 2026论文解读

论文信息

项目 内容
标题 EyeTAG: Eye Trajectory-Aware Gaze Estimation
作者 Yoseob Han 等
会议 BMVC 2026(英国机器视觉会议)
年份 2026
链接 arXiv:2610.00922
代码 GitHub: peter8366/EyeTAG
规模 6154KB

核心创新

EyeTAG 提出首个将视线轨迹(gaze trajectory)作为显式变量的因果多帧视线估计框架。通过微分自身近期预测并反馈为运动学token,实现平移不变的视线运动建模。

三大突破:

  1. 显式轨迹建模:不同于隐式时序学习,视线轨迹成为显式变量
  2. 微分运动token:平移不变,具有主体无关性
  3. 消除扫视偏差:解决绝对视线历史先验中的系统性偏差

关键指标: Gaze360 平均角度误差降低约1.0°,EVE数据集2.56°(与SOTA 2.58°持平)

方法详解

1. 核心思想

1
2
3
4
5
6
7
8
9
10
11
"""
传统方法的问题:
- 单帧方法:每帧独立预测,输出抖动(jitter)
- 多帧方法:隐式学习时序,但轨迹不是显式变量
- 绝对历史先验:包含个人视线偏移,不具主体无关性

EyeTAG的解决方案:
- 微分自身预测:dt = gaze(t) - gaze(t-1)
- 微分是平移不变的:dt 不包含个人偏移
- 将 dt 作为运动学token反馈到下一步
"""

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
import torch
import torch.nn as nn
from typing import Tuple

class EyeTAG(nn.Module):
"""
EyeTAG: Eye Trajectory-Aware Gaze Estimation

架构:
1. 面部流 + 眼部流 → 双流特征提取
2. 跨注意力融合
3. 因果Transformer解码器
4. 微分轨迹token反馈
"""

def __init__(
self,
face_feat_dim: int = 512,
eye_feat_dim: int = 256,
hidden_dim: int = 256,
num_heads: int = 8,
num_layers: int = 6,
gaze_dim: int = 2 # (pitch, yaw)
):
super().__init__()
self.gaze_dim = gaze_dim

# 面部特征编码器
self.face_encoder = 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((1, 1)),
nn.Flatten(),
nn.Linear(128, face_feat_dim)
)

# 眼部特征编码器(左右眼共享)
self.eye_encoder = 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.AdaptiveAvgPool2d((1, 1)),
nn.Flatten(),
nn.Linear(64, eye_feat_dim)
)

# 跨注意力融合
self.cross_attn = nn.MultiheadAttention(
hidden_dim, num_heads=num_heads, batch_first=True
)

# 特征投影
self.face_proj = nn.Linear(face_feat_dim, hidden_dim)
self.eye_proj = nn.Linear(eye_feat_dim * 2, hidden_dim) # 左+右

# 微分轨迹token投影
self.trajectory_proj = nn.Linear(gaze_dim, hidden_dim)

# 因果Transformer解码器
decoder_layer = nn.TransformerDecoderLayer(
d_model=hidden_dim,
nhead=num_heads,
dim_feedforward=hidden_dim * 4,
dropout=0.1,
activation='gelu',
batch_first=True
)
self.transformer = nn.TransformerDecoder(
decoder_layer, num_layers=num_layers
)

# 视线预测头
self.gaze_head = nn.Sequential(
nn.Linear(hidden_dim, hidden_dim // 2),
nn.ReLU(),
nn.Linear(hidden_dim // 2, gaze_dim)
)

# 初始化历史视线
self.register_buffer(
'gaze_history', None, persistent=False
)

def forward(
self,
face_img: torch.Tensor,
left_eye: torch.Tensor,
right_eye: torch.Tensor,
prev_gaze: torch.Tensor = None
) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Args:
face_img: (B, C, H, W) 面部图像
left_eye: (B, C, H, W) 左眼图像
right_eye: (B, C, H, W) 右眼图像
prev_gaze: (B, gaze_dim) 上一帧视线预测
Returns:
gaze: (B, gaze_dim) 当前帧视线预测 (pitch, yaw)
delta_gaze: (B, gaze_dim) 视线变化量
"""
B = face_img.shape[0]

# 特征提取
face_feat = self.face_proj(self.face_encoder(face_img)) # (B, hidden)
left_feat = self.eye_encoder(left_eye) # (B, eye_dim)
right_feat = self.eye_encoder(right_eye) # (B, eye_dim)
eye_feat = self.eye_proj(
torch.cat([left_feat, right_feat], dim=-1)
) # (B, hidden)

# 计算微分轨迹token
if prev_gaze is not None:
# 微分:当前预测 - 上一帧预测
delta = prev_gaze # 这里用上一帧作为差分基准
traj_token = self.trajectory_proj(delta) # (B, hidden)
else:
# 首帧:零初始化
traj_token = torch.zeros(B, face_feat.shape[-1],
device=face_img.device)

# 跨注意力:眼部查询面部上下文
eye_query = eye_feat.unsqueeze(1) # (B, 1, hidden)
face_kv = face_feat.unsqueeze(1) # (B, 1, hidden)
fused, _ = self.cross_attn(eye_query, face_kv, face_kv)
fused = fused.squeeze(1) # (B, hidden)

# 拼接视觉特征和轨迹token
visual_tokens = torch.stack([fused, eye_feat], dim=1) # (B, 2, hidden)
traj_tokens = traj_token.unsqueeze(1) # (B, 1, hidden)

# 因果Transformer解码
# 轨迹token作为memory,视觉tokens作为tgt
decoded = self.transformer(
visual_tokens, traj_tokens
) # (B, 2, hidden)

# 取最后一步
output = decoded[:, -1, :] # (B, hidden)

# 视线预测
gaze = self.gaze_head(output) # (B, gaze_dim)

# 计算微分
if prev_gaze is not None:
delta_gaze = gaze - prev_gaze
else:
delta_gaze = torch.zeros_like(gaze)

return gaze, delta_gaze

def inference_sequence(
self,
face_sequence: torch.Tensor,
left_eye_sequence: torch.Tensor,
right_eye_sequence: torch.Tensor
) -> torch.Tensor:
"""
因果序列推理

Args:
face_sequence: (B, T, C, H, W)
left_eye_sequence: (B, T, C, H, W)
right_eye_sequence: (B, T, C, H, W)
Returns:
gaze_sequence: (B, T, gaze_dim)
"""
B, T = face_sequence.shape[:2]
gazes = []
prev_gaze = None

for t in range(T):
gaze, delta = self.forward(
face_sequence[:, t],
left_eye_sequence[:, t],
right_eye_sequence[:, t],
prev_gaze
)
gazes.append(gaze)
prev_gaze = gaze.detach()

return torch.stack(gazes, dim=1) # (B, T, gaze_dim)


# 测试
if __name__ == "__main__":
model = EyeTAG(face_feat_dim=256, eye_feat_dim=128, hidden_dim=128)

B, T = 2, 10
face = torch.randn(B, T, 3, 112, 112)
left_eye = torch.randn(B, T, 3, 48, 48)
right_eye = torch.randn(B, T, 3, 48, 48)

# 序列推理
gazes = model.inference_sequence(face, left_eye, right_eye)

print(f"输入序列: {face.shape}")
print(f"视线输出: {gazes.shape}") # (B, T, 2)

# 检查轨迹连续性
deltas = gazes[:, 1:] - gazes[:, :-1]
jitter = deltas.std()
print(f"帧间抖动: {jitter:.4f}")

params = sum(p.numel() for p in model.parameters())
print(f"参数量: {params:,} ({params/1e6:.2f}M)")
print("\n✅ EyeTAG测试通过")

3. 微分轨迹的平移不变性

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
import numpy as np

class TrajectoryInvarianceDemo:
"""
演示微分轨迹的平移不变性

关键理论:
- 绝对视线历史 = 个人偏移 + 运动变化
- 微分视线历史 = 运动变化(个人偏移被消除)

这意味着微分token具有主体无关性
可以在跨被试场景下泛化
"""

@staticmethod
def demo():
# 模拟两个不同被试的视线
np.random.seed(42)

# 被试A:偏移+5°
subject_a_gaze = np.array([5.0, 6.0, 7.0, 6.5, 7.5])

# 被试B:偏移-2°
subject_b_gaze = np.array([-2.0, -1.0, 0.0, -0.5, 0.5])

# 绝对历史先验
print("绝对视线历史先验:")
print(f" 被试A: {subject_a_gaze}")
print(f" 被试B: {subject_b_gaze}")
print(f" 差异大,个人偏移不同")

# 微分历史先验
delta_a = np.diff(subject_a_gaze)
delta_b = np.diff(subject_b_gaze)

print(f"\n微分视线历史先验:")
print(f" 被试A: {delta_a}")
print(f" 被试B: {delta_b}")
print(f" 差异小,运动模式相似")

# 统计对比
print(f"\n绝对值差异: {np.abs(subject_a_gaze - subject_b_gaze).mean():.2f}°")
print(f"微分值差异: {np.abs(delta_a - delta_b).mean():.2f}°")
print(f"\n✅ 微分token具有平移不变性,个人偏移被消除")

TrajectoryInvarianceDemo.demo()

实验结果

1. Gaze360 数据集

方法 平均角度误差 说明
单帧基线 11.5° 无时序
绝对历史先验 10.8° +时序但有偏差
EyeTAG ~9.8° +微分轨迹

2. EVE 数据集

方法 平均角度误差 说明
SOTA baseline 2.58° 当前最佳
EyeTAG 2.56° 持平或略优

3. 扫视偏差消除

方法 扫视后偏差 说明
绝对历史先验 0.8° 系统性偏差
EyeTAG 0.1° 偏差消除

IMS 开发启示

1. 视线轨迹对DMS的价值

DMS功能 当前方法 EyeTAG启示 改进
分心检测 单帧视线偏离 +轨迹连续性分析 减少误报
PERCLOS 闭眼帧计数 +闭眼轨迹模式 更精准
扫视分析 简单ROI切换 +微分运动模式 深度分析
驾驶员意图 静态注视点 +预测性轨迹 提前预测

2. 微分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
32
33
34
35
36
class CrossSubjectGazeAnalysis:
"""
微分token的跨被试泛化价值

IMS部署中不同驾驶员有不同视线基线
绝对历史先验需要个人校准
微分token天然跨被试泛化
"""

advantages = {
'无需个人校准': '微分消除个人偏移',
'跨被试泛化': '运动模式相似',
'冷启动友好': '首帧无历史时退化为单帧',
'在线因果': '只用过去信息,适合实时推理',
}

deployment_scenarios = {
'新驾驶员首次使用': {
'绝对历史': '❌ 需要校准基线',
'微分历史': '✅ 无需校准,首帧即可用',
},
'多驾驶员共用车辆': {
'绝对历史': '❌ 需要切换个人档案',
'微分历史': '✅ 自动适应不同人',
},
'实时推理': {
'绝对历史': '⚠️ 可用但需缓存管理',
'微分历史': '✅ 因果推理,只用过去',
},
}

print("微分token的跨被试泛化价值:")
for scenario, comparison in CrossSubjectGazeAnalysis.deployment_scenarios.items():
print(f"\n📋 {scenario}")
for method, status in comparison.items():
print(f" {method}: {status}")

3. 与LTC-CGMN的互补

维度 EyeTAG LTC-CGMN
关注点 视线轨迹建模 驾驶状态时序
方法 微分因果Transformer 轻量因果膨胀卷积
互补 精确视线轨迹 全状态分类
结合 EyeTAG视线 → LTC-CGMN状态 全链路DMS

总结

EyeTAG 是视线估计领域的重要突破:

  1. 首个显式轨迹建模框架,视线运动不再是隐式变量
  2. 微分token平移不变,具有跨被试泛化能力
  3. 因果推理,只用过去信息,适合实时DMS部署
  4. 消除扫视偏差,解决长期存在的系统性问题
  5. 代码开源,可直接复现

对 IMS 的核心启示:在DMS视线估计中引入微分轨迹建模,可以同时解决抖动、跨被试泛化和扫视偏差三个问题。特别适合IMS多驾驶员共用车辆的场景——无需个人校准,首次使用即可获得稳定的视线轨迹。