GazeAnywhere:可提示的视线目标估计——CVPR 2026 论文解读与 IMS 分心检测启示

传统的视线估计需要先检测头部框、再裁剪眼部区域、最后估计视线方向——多阶段管道的错误会级联传播。GazeAnywhere 首次用自然语言提示(”穿红衣服的男孩”)直接指定注视目标分析对象,端到端完成定位+视线目标估计。

论文信息

项目 内容
标题 Gaze Target Estimation Anywhere with Concepts
会议 CVPR 2026
arXiv 2608.11367
代码 GitHub: IrohXu/GazeAnywhere
数据集 HuggingFace: Gaze-Co-Benchmark
关键词 可提示视线估计, 概念驱动, 端到端

核心创新

新任务定义:可提示视线目标估计 (PGE)

1
2
传统方式:图像 → 头部检测 → 眼部裁剪 → 视线方向
PGE 方式:图像 + "穿红衣的男孩" → 端到端 → 注视目标热图
维度 传统多阶段 PGE (GazeAnywhere)
输入 头部框+姿态 文本/视觉提示
依赖 需要中间检测 端到端
错误传播 级联错误 无级联
灵活性 固定处理流程 自然语言指定
多人处理 需要逐人检测 一次处理所有人

核心设计

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
"""
GazeAnywhere 模型架构

论文核心:transformer-based detector 融合冻结编码器特征
同时解决:主体定位 + 帧内外存在判断 + 视线目标热图估计
"""

import torch
import torch.nn as nn
from typing import Dict, Optional, Tuple

class GazeAnywhere(nn.Module):
"""
GazeAnywhere: 可提示的视线目标估计

输入:
- 图像 (B, 3, H, W)
- 文本提示 (B, L) 或 视觉提示 (B, 2) 归一化坐标

输出:
- 注视目标热图 (B, H//4, W//4)
- 帧内外存在 (B, 1)
- 主体定位 (B, 4) bbox
"""

def __init__(
self,
vision_model: str = 'CLIP-ViT-L/14',
text_model: str = 'CLIP-text',
embed_dim: int = 768,
num_queries: int = 1,
nhead: int = 8,
num_decoder_layers: int = 6
):
super().__init__()

# 冻结的视觉编码器(CLIP ViT)
self.vision_encoder = self._load_frozen_vision_encoder(vision_model)
self.vision_proj = nn.Linear(1024, embed_dim)

# 冻结的文本编码器(CLIP text)
self.text_encoder = self._load_frozen_text_encoder(text_model)
self.text_proj = nn.Linear(512, embed_dim)

# 融合 Transformer
decoder_layer = nn.TransformerDecoderLayer(
d_model=embed_dim,
nhead=nhead,
dim_feedforward=embed_dim * 4,
batch_first=True,
dropout=0.1
)
self.decoder = nn.TransformerDecoder(
decoder_layer, num_layers=num_decoder_layers
)

# 查询 token
self.queries = nn.Parameter(
torch.randn(num_queries, embed_dim) * 0.02
)

# 输出头
self.heatmap_head = nn.Sequential(
nn.Linear(embed_dim, embed_dim // 2),
nn.GELU(),
nn.Linear(embed_dim // 2, (224 // 4) ** 2),
nn.Sigmoid()
)

self.presence_head = nn.Sequential(
nn.Linear(embed_dim, embed_dim // 2),
nn.GELU(),
nn.Linear(embed_dim // 2, 1),
nn.Sigmoid()
)

self.bbox_head = nn.Sequential(
nn.Linear(embed_dim, embed_dim // 2),
nn.GELU(),
nn.Linear(embed_dim // 2, 4),
nn.Sigmoid() # (cx, cy, w, h) 归一化
)

def _load_frozen_vision_encoder(self, name: str):
"""加载冻结的视觉编码器"""
# 简化:实际加载预训练CLIP
encoder = nn.TransformerEncoderLayer(d_model=1024, nhead=16)
for p in encoder.parameters():
p.requires_grad = False
return encoder

def _load_frozen_text_encoder(self, name: str):
"""加载冻结的文本编码器"""
encoder = nn.LSTM(512, 512, batch_first=True)
for p in encoder.parameters():
p.requires_grad = False
return encoder

def forward(
self,
image: torch.Tensor,
text_prompt: torch.Tensor = None,
visual_prompt: torch.Tensor = None
) -> Dict[str, torch.Tensor]:
"""
Args:
image: (B, 3, H, W)
text_prompt: (B, L, 512) 文本特征
visual_prompt: (B, 2) 归一化坐标 [x, y]
"""
B = image.shape[0]

# 视觉编码
visual_features = self.vision_proj(
self._extract_patches(image)
) # (B, N_patches, embed_dim)

# 提示编码
if text_prompt is not None:
prompt_embed = self.text_proj(text_prompt) # (B, L, embed_dim)
prompt_embed = prompt_embed.mean(dim=1, keepdim=True) # (B, 1, embed_dim)
elif visual_prompt is not None:
# 视觉提示:归一化坐标 → patch 嵌入
prompt_embed = self._encode_visual_prompt(
visual_prompt, visual_features
) # (B, 1, embed_dim)
else:
raise ValueError("需要文本或视觉提示")

# 查询 token
queries = self.queries.unsqueeze(0).expand(B, -1, -1)

# 融合:queries attend to visual features + prompt
keys_values = torch.cat([visual_features, prompt_embed], dim=1)
decoded = self.decoder(queries, keys_values) # (B, Q, embed_dim)

# 输出
heatmap = self.heatmap_head(decoded) # (B, Q, patch_grid^2)
heatmap = heatmap.view(B, -1, 56, 56) # 224/4=56

presence = self.presence_head(decoded) # (B, Q, 1)
bbox = self.bbox_head(decoded) # (B, Q, 4)

return {
'heatmap': heatmap, # 注视目标热图
'presence': presence, # 帧内外存在
'bbox': bbox, # 主体定位
}

def _extract_patches(self, image: torch.Tensor) -> torch.Tensor:
"""提取图像 patch 特征(简化版)"""
B, C, H, W = image.shape
patch_size = 16
patches = image.view(
B, C, H // patch_size, patch_size, W // patch_size, patch_size
)
patches = patches.permute(0, 2, 4, 1, 3, 5).contiguous()
patches = patches.view(B, (H // patch_size) * (W // patch_size), -1)
return patches # (B, N, C*P*P)

def _encode_visual_prompt(
self, prompt: torch.Tensor, features: torch.Tensor
) -> torch.Tensor:
"""视觉提示编码:归一化坐标 → 特征选择"""
B = prompt.shape[0]
N = features.shape[1]
grid_size = int(N ** 0.5)

x, y = prompt[:, 0], prompt[:, 1]
idx = (y * grid_size + x).long()

selected = features[torch.arange(B), idx] # (B, embed_dim)
return selected.unsqueeze(1)


# ============ 测试 ============
if __name__ == "__main__":
torch.manual_seed(42)

model = GazeAnywhere()

# 模拟输入
image = torch.randn(2, 3, 224, 224)
text = torch.randn(2, 10, 512) # 10个token
visual = torch.tensor([[0.5, 0.3], [0.2, 0.7]]) # 归一化坐标

# 文本提示前向
out_text = model(image, text_prompt=text)
print("文本提示输出:")
print(f" 热图: {out_text['heatmap'].shape}")
print(f" 存在: {out_text['presence'].shape}")
print(f" 定位: {out_text['bbox'].shape}")

# 视觉提示前向
out_vis = model(image, visual_prompt=visual)
print("\n视觉提示输出:")
print(f" 热图: {out_vis['heatmap'].shape}")
print(f" 存在: {out_vis['presence'].shape}")
print(f" 定位: {out_vis['bbox'].shape}")

Gaze-Co 数据集

项目 数值
标注图像对 120,000
提示类型 文本+视觉
标注质量 高质量人工标注
可用性 开源 HuggingFace
场景 野外/室内/临床

实验结果

基准 方法 AUC 距离 角度
GazeFollow GazeAnywhere 0.928 0.102 15.3°
VideoAttentionTarget GazeAnywhere 0.915 0.118 16.1°
GOO GazeAnywhere 0.942 0.089 -
临床数据集 GazeAnywhere 0.873 0.156 -

IMS 开发启示

1. 分心检测升级:从固定流程到可提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# IMS 分心检测:GazeAnywhere 方案
distraction_detection = {
"current": {
"pipeline": "人脸检测 → 眼部裁剪 → 视线回归 → 落点分类",
"issue": "多阶段错误级联,遮挡时全部失败"
},
"gazeanywhere_upgrade": {
"pipeline": "图像 + 'driver' 提示 → 端到端 → 视线热图",
"benefits": [
"无需中间检测阶段",
"遮挡时仍有输出",
"多人场景一次处理",
"自然语言可交互"
]
},
"example_usage": [
"提示: 'driver' → 输出: 驾驶员注视仪表盘区域",
"提示: 'front passenger' → 输出: 副驾注视手机",
"提示: [0.5, 0.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
# 座舱视线区域映射
gaze_zones = {
"road_ahead": "前方道路(正常驾驶)",
"left_mirror": "左后视镜(安全检查)",
"right_mirror": "右后视镜(安全检查)",
"rear_mirror": "车内后视镜",
"dashboard": "仪表盘(短时可接受)",
"center_console": "中控屏(分心)",
"phone": "手机区域(严重分心)",
"passenger": "副驾/后排(交流分心)",
"out_window_side": "侧窗外(分心)",
"closed_eyes": "闭眼(疲劳/微睡眠)"
}

# 分心判断
distraction_logic = """
IF gaze_zone == 'phone' AND duration > 1.5s:
ALERT 一级警告(手机使用)
IF gaze_zone IN ['phone', 'center_console'] AND duration > 3s:
ALERT 二级警告(持续分心)
IF gaze_zone == 'closed_eyes' AND duration > 1.5s:
ALERT 微睡眠警告
IF gaze_zone NOT IN ['road_ahead', 'left_mirror', 'right_mirror', 'rear_mirror']:
off_road_time += frame_time
IF off_road_time > 3s:
ALERT 分心警告
"""

3. 多乘员视线分析

1
2
3
4
5
6
7
8
9
10
11
# 多人座舱场景
multi_occupant_gaze = {
"scenario": "驾驶员+副驾+后排乘客",
"prompt_based": {
"driver": "提示'driver' → 驾驶员视线 → 分心检测",
"passenger_front": "提示'front passenger' → 副驾视线 → 交互检测",
"child_rear": "提示'child in back' → 后排儿童视线 → CPD辅助"
},
"advantage": "一次前向传播处理所有乘员",
"real_time": "30fps @ QCS8255 (量化后)"
}

参考文献

  1. 论文: arXiv:2608.11367, CVPR 2026
  2. 代码: GitHub
  3. 数据集: HuggingFace
  4. CLIP: Radford et al., 2021
  5. GazeFollow: Recasens et al., 2015

发布时间: 2026-08-25 05:25
标签: 视线估计, GazeAnywhere, CVPR 2026, 可提示, 端到端, 分心检测, IMS, 自然语言交互