差分视线估计在长时间窄范围场景的验证——USC 2026论文解读

论文信息

项目 内容
标题 Adapting Appearance-Based Gaze Estimation to Narrow-Range, Long-Duration Screen Viewing
作者 Jordan Prescott
机构 Signal Analysis and Interpretation Lab, University of Southern California
arXiv 2610.11036
领域 Image and Video Processing (eess.IV)
数据集 PRECOG项目,140段长时面部视频+同步眼动追踪

核心发现

在长时间、窄范围视线场景(如长时间驾驶、屏幕阅读)中,差分视线估计(differential gaze estimation)是唯一显著优于常数预测器的方法。

方法 vs 常数预测器 视线重建质量
6个已发表模型 ❌ 不显著 低
差分gaze模型 ✅ 显著提升 高
差分+时序上下文 ✅✅ 最佳 最高

问题背景

窄范围长时间场景的特殊性

特征 通用视线估计 长时窄范围场景
视线范围 宽(±40°) 窄(±5°)
头部运动 大 小
持续时间 短(秒级) 长(分钟级)
标定漂移 不显著 严重累积
常数预测器 差 已经很好

常数预测器悖论

在窄范围场景中,简单预测”每帧都用该被试的平均视线”就能达到很低的角误差。这意味着低角误差不等于视线重建质量。

方法详解

差分视线估计

差分模型的核心思想:不直接预测绝对视线方向,而是预测两帧之间的视线变化量。

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

class DifferentialGazeModel(nn.Module):
"""
差分视线估计模型

论文: arXiv:2610.11036
核心: 预测两帧之间的视线位移而非绝对视线

架构:
1. 权重共享ResNet-18编码两帧眼部图像
2. MLP映射编码差到2D视线位移
3. 绝对视线 = 锚点视线 + 预测位移

优势:
- 消除个体化偏移(差分操作平移不变)
- 对标定漂移鲁棒(相对量不受绝对漂移影响)
- 在窄范围中区分微小变化更敏感
"""

def __init__(self, hidden_dim: int = 256):
super().__init__()

# 权重共享的眼部图像编码器
import torchvision.models as models
resnet = models.resnet18(weights='DEFAULT')
resnet.fc = nn.Identity()
self.encoder = resnet # 输出512维

# 差分映射头
self.diff_head = nn.Sequential(
nn.Linear(512 * 2, hidden_dim),
nn.BatchNorm1d(hidden_dim),
nn.ReLU(inplace=True),
nn.Dropout(0.2),
nn.Linear(hidden_dim, hidden_dim // 2),
nn.BatchNorm1d(hidden_dim // 2),
nn.ReLU(inplace=True),
nn.Dropout(0.2),
nn.Linear(hidden_dim // 2, 2) # (Δpitch, Δyaw) in radians
)

# 时序上下文模块(可选)
self.temporal_lstm = nn.LSTM(
input_size=2, # 差分序列
hidden_size=64,
num_layers=1,
batch_first=True
)
self.temporal_head = nn.Linear(64, 2)

def forward(
self,
anchor_eye: torch.Tensor, # (B, 3, 64, 64) 锚点帧眼部图像
current_eye: torch.Tensor, # (B, 3, 64, 64) 当前帧眼部图像
anchor_gaze: torch.Tensor, # (B, 2) 锚点视线 (pitch, yaw)
prev_diffs: torch.Tensor = None # (B, T, 2) 过去T帧差分序列
) -> Tuple[torch.Tensor, torch.Tensor]:
"""
前向传播

Args:
anchor_eye: 标定锚点帧的眼部裁剪
current_eye: 当前帧的眼部裁剪
anchor_gaze: 锚点的已知视线
prev_diffs: 过去T帧的差分序列(用于时序增强)

Returns:
predicted_gaze: (B, 2) 预测的绝对视线
predicted_diff: (B, 2) 预测的差分
"""
# 编码两帧
e_anchor = self.encoder(anchor_eye) # (B, 512)
e_current = self.encoder(current_eye) # (B, 512)

# 拼接并预测差分
concat = torch.cat([e_anchor, e_current], dim=-1) # (B, 1024)
diff = self.diff_head(concat) # (B, 2) 视线位移

# 绝对视线 = 锚点 + 差分
predicted_gaze = anchor_gaze + diff

# 时序增强(如果有历史差分)
if prev_diffs is not None:
temporal_out, _ = self.temporal_lstm(prev_diffs)
temporal_correction = self.temporal_head(temporal_out[:, -1, :])
predicted_gaze = predicted_gaze + temporal_correction

return predicted_gaze, diff

def calibrate(self, anchor_eye, anchor_gaze):
"""
被试标定:记录锚点帧和视线

在长时间场景中,需要周期性重新标定
因为标定漂移会累积
"""
self.register_buffer('anchor_eye', anchor_eye)
self.register_buffer('anchor_gaze', anchor_gaze)


class LongDurationGazeTracker:
"""
长时驾驶视线追踪系统

集成:
1. 差分视线估计
2. 周期性重标定(对抗漂移)
3. 时序平滑
4. 注视/扫视事件检测
"""

def __init__(self, model, recalibration_interval=300):
"""
Args:
model: DifferentialGazeModel
recalibration_interval: 重标定间隔(帧数,300帧=10秒@30fps)
"""
self.model = model
self.recal_interval = recalibration_interval
self.frame_count = 0
self.gaze_history = []
self.diff_history = []
self.anchor = None

def initialize(self, anchor_eye, anchor_gaze):
"""初始化锚点"""
self.model.calibrate(anchor_eye, anchor_gaze)
self.anchor = (anchor_eye, anchor_gaze)
self.frame_count = 0
self.gaze_history = [anchor_gaze]
self.diff_history = []

def step(self, current_eye):
"""
单帧处理

Returns:
gaze: (2,) 预测视线
is_fixation: bool 是否为注视
is_saccade: bool 是否为扫视
need_recalibration: bool 是否需要重标定
"""
if self.anchor is None:
return None, False, False, True

anchor_eye, anchor_gaze = self.anchor

# 准备历史差分
prev_diffs = None
if len(self.diff_history) > 5:
import numpy as np
prev_diffs = torch.stack([
torch.tensor(d) for d in self.diff_history[-10:]
]).unsqueeze(0)

# 预测
with torch.no_grad():
gaze, diff = self.model(
anchor_eye.unsqueeze(0),
current_eye.unsqueeze(0),
anchor_gaze.unsqueeze(0),
prev_diffs
)

gaze = gaze[0]
diff = diff[0]

# 更新历史
self.gaze_history.append(gaze)
self.diff_history.append(diff)
self.frame_count += 1

# 注视/扫射检测
diff_mag = diff.norm().item()
is_saccade = diff_mag > 0.05 # >~3° 突变
is_fixation = diff_mag < 0.01 # <~0.6° 微动

# 重标定检查
need_recal = self.frame_count >= self.recal_interval

return gaze.numpy(), is_fixation, is_saccade, need_recal


# 测试
if __name__ == "__main__":
print("=" * 60)
print("差分视线估计 - 长时窄范围场景测试")
print("=" * 60)

model = DifferentialGazeModel()
total_params = sum(p.numel() for p in model.parameters())
print(f"\n模型参数量: {total_params:,}")

# 模拟锚点和当前帧
B = 2
anchor_eye = torch.randn(B, 3, 64, 64)
current_eye = torch.randn(B, 3, 64, 64)
anchor_gaze = torch.tensor([[0.0, 0.0], [0.1, -0.05]]) # radians

# 预测
gaze, diff = model(anchor_eye, current_eye, anchor_gaze)

print(f"\n输入:")
print(f" 锚点视线: {anchor_gaze}")
print(f" 锚点眼部: {anchor_eye.shape}")
print(f" 当前眼部: {current_eye.shape}")
print(f"\n输出:")
print(f" 预测视线: {gaze}")
print(f" 视线差分: {diff}")
print(f" 差分幅度(rad): {diff.norm(dim=-1)}")
print(f" 差分幅度(°): {diff.norm(dim=-1) * 180 / 3.14159}")

# 长时追踪系统测试
print(f"\n{'='*60}")
print("长时追踪系统测试(10帧)")
print(f"{'='*60}")

tracker = LongDurationGazeTracker(model, recalibration_interval=5)

# 初始化
anchor_eye_single = torch.randn(3, 64, 64)
anchor_gaze_single = torch.tensor([0.0, 0.0])
tracker.initialize(anchor_eye_single, anchor_gaze_single)

print(f"\n{'帧':>4} | {'pitch(°)':>8} | {'yaw(°)':>8} | {'Δ(°)':>6} | {'状态':>8} | {'重标定':>6}")
print("-" * 55)

import numpy as np
for i in range(10):
current = torch.randn(3, 64, 64)
gaze, is_fix, is_sac, need_recal = tracker.step(current)

if gaze is not None:
pitch = gaze[0] * 180 / np.pi
yaw = gaze[1] * 180 / np.pi
diff_mag = np.sqrt(gaze[0]**2 + gaze[1]**2) * 180 / np.pi
status = "注视" if is_fix else ("扫视" if is_sac else "追踪")
recal = "是" if need_recal else "否"
print(f"{i:4d} | {pitch:8.2f} | {yaw:8.2f} | {diff_mag:6.2f} | {status:>8} | {recal:>6}")

print(f"\n历史长度: {len(tracker.gaze_history)}")

实验结果

数据集

项目 数值
来源 PRECOG项目
被试数 140人
评估方式 Subject-disjoint (leave-one-out)
标定帧数 统一预算
参考系统 高保真眼动追踪

模型对比

方法 类型 vs 常数预测器 注视/扫视一致性
6个已发表模型 绝对估计 ❌ 不显著 低
差分模型 差分估计 ✅ 显著提升 中
差分+时序 差分+LSTM ✅✅ 最佳 高

关键发现

  1. 低角误差≠有效重建:已发表模型角误差低,但无法正确检测注视和扫视事件
  2. 差分是唯一有效方案:差分模型是唯一在统计上显著优于常数预测器的静态方法
  3. 时序进一步增强:加入LSTM后,注视/扫视检测质量进一步提升

IMS开发启示

1. 长时间驾驶的特殊考量

场景 通用DMS 差分DMS
长途驾驶(>2小时) 标定漂移累积 差分不受绝对漂移影响
城市低速驾驶 视线范围窄 差分对微小变化更敏感
高速公路巡航 视线变化小 差分放大微小分心信号

2. 与EyeTAG的协同

EyeTAG和差分模型都使用差分思想,但角度不同:

方面 EyeTAG 差分模型
差分对象 自身预测的差分 两帧编码的差分
标定需求 无需锚点 需要锚点标定
适用场景 通用视线估计 长时窄范围
优势 消除扫视偏差 对标定漂移鲁棒
互补点 正常追踪 长时巡航

3. 周期性重标定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 重标定策略建议
recalibration_strategies = {
"时间触发": {
"间隔": "每10分钟",
"触发": "自动",
"代价": "1帧标定(驾驶员不知情)"
},
"事件触发": {
"条件": "追踪器置信度高+面部正面",
"优势": "不影响驾驶体验",
"实现": "质量评估器自动判断"
},
"混合策略": {
"默认": "时间触发10分钟",
"覆盖": "事件触发加速",
"推荐": True
}
}

4. 优先级排序

任务 优先级 预期收益
差分模型替代绝对估计 🟡 中 长时驾驶场景精度提升
注视/扫视事件检测 🔴 高 PERCLOS和分心检测基础
周期性重标定 🟡 中 对抗长时漂移
时序LSTM增强 🟢 低 进一步提升事件检测

总结

差分视线估计在长时间窄范围场景中的优越性有重要实际意义:在长途驾驶(高速公路巡航)中,差分模型比绝对估计更鲁棒,因为标定漂移是长时间驾驶的核心挑战。

关键数据:

  • 140人subject-disjoint评估
  • 差分是唯一显著优于常数预测器的方法
  • 差分+时序=最佳注视/扫视重建质量
  • 角误差低不等于有效视线重建

IMS落地建议: 在高速公路巡航等长时窄范围场景中,优先考虑差分视线估计方案,配合周期性重标定。与EyeTAG形成互补:EyeTAG用于通用场景,差分模型用于长时巡航。

信息来源: arXiv:2610.11036