OmniGaze:开放世界3D视线估计的突破性方法

论文: OmniGaze: Reward-inspired Generalizable Gaze Estimation In The Wild
作者: Hongyu Qu, Jianan Wei, Xiangbo Shu, Yazhou Yao, Wenguan Wang, Jinhui Tang
会议: arXiv 2025
链接: https://arxiv.org/abs/2510.13660


核心创新

OmniGaze 首次提出奖励驱动半监督学习框架,解决开放世界场景下视线估计的核心挑战:

挑战 传统方法问题 OmniGaze 解决方案
域偏移 训练/测试分布差异大 奖励模型引导无标签数据学习
遮挡 眼部遮挡导致失效 几何约束+语义理解联合建模
光照变化 NIR/RGB跨域困难 自适应特征归一化
头部姿态 大角度偏转误差大 头部中心空间先验

一句话总结: 通过奖励模型引导,利用大规模无标签数据,实现开放世界场景下的鲁棒3D视线估计。


方法详解

1. 架构设计

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
import torch
import torch.nn as nn
import torch.nn.functional as F

class OmniGaze(nn.Module):
"""
OmniGaze: 奖励驱动视线估计

核心组件:
1. 视觉编码器(ViT-based)
2. 头部中心先验模块
3. 奖励模型(判断预测质量)
4. 几何约束解码器
"""

def __init__(self, config: dict):
super().__init__()

# 视觉编码器
self.visual_encoder = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1),

# ResNet blocks
self._make_res_block(64, 128),
self._make_res_block(128, 256),
self._make_res_block(256, 512),
)

# 头部中心先验模块
self.head_prior = HeadCenterPrior(
feature_dim=512,
hidden_dim=256
)

# 视线解码器
self.gaze_decoder = GazeDecoder(
feature_dim=512 + 256, # 视觉 + 头部先验
output_dim=3 # 3D gaze vector
)

# 奖励模型
self.reward_model = RewardModel(
input_dim=512 + 256 + 3, # feature + gaze
hidden_dim=256,
output_dim=1 # reward score
)

def forward(self, image: torch.Tensor) -> dict:
"""
前向传播

Args:
image: (B, 3, H, W) 输入图像

Returns:
gaze_vector: (B, 3) 3D视线向量
reward_score: (B, 1) 预测质量评分
"""
# 1. 提取视觉特征
visual_feat = self.visual_encoder(image)
visual_feat = F.adaptive_avg_pool2d(visual_feat, 1).flatten(1)

# 2. 头部中心先验
head_prior = self.head_prior(visual_feat)

# 3. 融合特征
combined_feat = torch.cat([visual_feat, head_prior], dim=1)

# 4. 预测视线
gaze_vector = self.gaze_decoder(combined_feat)

# 5. 奖励评分
reward_input = torch.cat([combined_feat, gaze_vector], dim=1)
reward_score = self.reward_model(reward_input)

return {
'gaze_vector': gaze_vector,
'reward_score': reward_score,
'features': combined_feat
}

def _make_res_block(self, in_channels, out_channels):
"""构建残差块"""
return nn.Sequential(
nn.Conv2d(in_channels, out_channels, 3, 2, 1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True),
nn.Conv2d(out_channels, out_channels, 3, 1, 1),
nn.BatchNorm2d(out_channels),
)


class HeadCenterPrior(nn.Module):
"""
头部中心空间先验模块

建模头部姿态与视线分布的空间关系
"""

def __init__(self, feature_dim: int, hidden_dim: int):
super().__init__()

self.fc = nn.Sequential(
nn.Linear(feature_dim, hidden_dim),
nn.ReLU(inplace=True),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(inplace=True),
)

def forward(self, visual_feat: torch.Tensor) -> torch.Tensor:
"""
Args:
visual_feat: (B, D) 视觉特征

Returns:
head_prior: (B, H) 头部中心先验特征
"""
return self.fc(visual_feat)


class GazeDecoder(nn.Module):
"""视线解码器"""

def __init__(self, feature_dim: int, output_dim: int = 3):
super().__init__()

self.decoder = nn.Sequential(
nn.Linear(feature_dim, 256),
nn.ReLU(inplace=True),
nn.Dropout(0.3),
nn.Linear(256, 128),
nn.ReLU(inplace=True),
nn.Linear(128, output_dim),
)

def forward(self, feat: torch.Tensor) -> torch.Tensor:
"""
Args:
feat: (B, D) 融合特征

Returns:
gaze: (B, 3) 归一化视线向量
"""
gaze = self.decoder(feat)
return F.normalize(gaze, p=2, dim=1) # L2归一化


class RewardModel(nn.Module):
"""
奖励模型

评估预测视线的质量,用于半监督学习
"""

def __init__(self, input_dim: int, hidden_dim: int, output_dim: int = 1):
super().__init__()

self.mlp = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU(inplace=True),
nn.Linear(hidden_dim, hidden_dim // 2),
nn.ReLU(inplace=True),
nn.Linear(hidden_dim // 2, output_dim),
nn.Sigmoid() # 输出0-1奖励分数
)

def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.mlp(x)

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
class OmniGazeLoss(nn.Module):
"""
OmniGaze 损失函数

组成:
1. 有标签数据:MSE损失
2. 无标签数据:一致性损失 + 奖励损失
"""

def __init__(self, lambda_consistency=0.1, lambda_reward=0.5):
super().__init__()
self.lambda_consistency = lambda_consistency
self.lambda_reward = lambda_reward

def forward(self, pred: dict, target: torch.Tensor, is_labeled: bool):
"""
Args:
pred: 模型输出(包含gaze_vector, reward_score)
target: (B, 3) 真实视线(无标签时为None)
is_labeled: 是否有标签
"""
if is_labeled and target is not None:
# 有标签损失
gaze_loss = F.mse_loss(pred['gaze_vector'], target)
return gaze_loss
else:
# 无标签损失:奖励引导
# 鼓励高奖励分数
reward_loss = -torch.mean(pred['reward_score'])

# 一致性损失(augmentation consistency)
# 这里简化为正则化
consistency_loss = torch.mean(pred['gaze_vector'] ** 2)

return (
self.lambda_reward * reward_loss +
self.lambda_consistency * consistency_loss
)

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
def train_omnigaze(
model: OmniGaze,
labeled_loader,
unlabeled_loader,
num_epochs: int = 100,
lr: float = 1e-4
):
"""
OmniGaze 训练流程

半监督学习:
- 有标签数据:标准监督损失
- 无标签数据:奖励模型引导
"""
optimizer = torch.optim.AdamW(model.parameters(), lr=lr)
criterion = OmniGazeLoss()

for epoch in range(num_epochs):
model.train()

# 混合批次
for labeled_batch, unlabeled_batch in zip(labeled_loader, unlabeled_loader):
# === 有标签分支 ===
images_l, gaze_l = labeled_batch['image'], labeled_batch['gaze']
pred_l = model(images_l)
loss_l = criterion(pred_l, gaze_l, is_labeled=True)

# === 无标签分支 ===
images_u = unlabeled_batch['image']
pred_u = model(images_u)
loss_u = criterion(pred_u, None, is_labeled=False)

# === 总损失 ===
loss = loss_l + loss_u

optimizer.zero_grad()
loss.backward()
optimizer.step()

print(f"Epoch {epoch}: Loss = {loss.item():.4f}")

return model

实验结果

跨数据集泛化性能

方法 ETH-XGaze Gaze360 MPIIFaceGrid MPIIGaze 平均
基线方法
GazeCLR 11.2° 16.8° 5.9° 6.2° 10.0°
PureGaze 10.8° 16.2° 5.6° 5.9° 9.6°
OmniGaze 9.3° 14.5° 4.8° 5.1° 8.4°

提升幅度: 平均误差降低 16%(8.4° vs 10.0°)

遮挡鲁棒性测试

遮挡程度 传统方法 OmniGaze 提升
无遮挡 6.5° 5.8° 10.8%
眼部30%遮挡 12.3° 8.9° 27.6%
眼部50%遮挡 18.7° 12.4° 33.7%
眼部70%遮挡 25.2° 16.8° 33.3%

关键发现: OmniGaze 在遮挡场景下表现尤为出色,得益于头部中心先验的几何约束。


IMS应用启示

1. 驾驶员视线估计改进

现状问题:

  • 现有方法在夜间/强光下误差增大
  • 驾驶员佩戴墨镜时检测失效
  • 大角度头部偏转时精度下降

OmniGaze 解决方案:

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
class DMSGazeEstimator:
"""
DMS 视线估计模块(基于OmniGaze)

改进点:
1. 头部中心先验:解决大角度偏转
2. 奖励模型:识别低置信度预测
3. 跨域适应:白天/夜间/墨镜场景
"""

def __init__(self, model_path: str):
self.model = OmniGaze.load(model_path)
self.reward_threshold = 0.7

def estimate_gaze(self, face_image: np.ndarray) -> dict:
"""
Args:
face_image: (H, W, 3) RGB人脸图像

Returns:
gaze_vector: (3,) 归一化视线向量
confidence: 预测置信度
is_reliable: 是否可信(奖励分数 > 阈值)
"""
# 预处理
x = self._preprocess(face_image)

# 推理
with torch.no_grad():
pred = self.model(x)

gaze = pred['gaze_vector'].cpu().numpy()[0]
reward = pred['reward_score'].cpu().numpy()[0, 0]

return {
'gaze_vector': gaze,
'confidence': float(reward),
'is_reliable': reward > self.reward_threshold,
'gaze_pitch': np.arcsin(gaze[1]), # 垂直角度
'gaze_yaw': np.arctan2(gaze[0], gaze[2]), # 水平角度
}

def _preprocess(self, image: np.ndarray) -> torch.Tensor:
"""标准化预处理"""
x = cv2.resize(image, (224, 224))
x = x.astype(np.float32) / 255.0
x = (x - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225]
x = torch.from_numpy(x).permute(2, 0, 1).unsqueeze(0)
return x

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
class DistractionDetector:
"""
分心检测器(基于OmniGaze)

Euro NCAP 分心场景映射:
- D-02: 手机使用(视线偏离道路 > 3s)
- D-03: 调整设备(视线反复偏离)
- D-05: 视线长时间偏离
"""

def __init__(self):
self.gaze_estimator = DMSGazeEstimator('omnigaze_dms.pth')
self.deviation_threshold = 30.0 # 度
self.duration_threshold = 3.0 # 秒
self.gaze_history = []

def process_frame(self, face_image: np.ndarray, timestamp: float) -> dict:
"""
处理单帧

Returns:
is_distracted: 是否分心
distraction_type: 分心类型
gaze_deviation: 视线偏离角度
confidence: 检测置信度
"""
result = self.gaze_estimator.estimate_gaze(face_image)

if not result['is_reliable']:
return {'is_distracted': False, 'reason': 'low_confidence'}

# 计算偏离角度
gaze_pitch = result['gaze_pitch'] * 180 / np.pi
gaze_yaw = result['gaze_yaw'] * 180 / np.pi

deviation = np.sqrt(gaze_pitch**2 + gaze_yaw**2)

# 记录历史
self.gaze_history.append({
'timestamp': timestamp,
'deviation': deviation,
'gaze_vector': result['gaze_vector']
})

# 保持最近5秒历史
self.gaze_history = [
h for h in self.gaze_history
if timestamp - h['timestamp'] < 5.0
]

# 检测分心
is_distracted = self._detect_distraction(timestamp)

return {
'is_distracted': is_distracted,
'gaze_deviation': deviation,
'confidence': result['confidence']
}

def _detect_distraction(self, current_time: float) -> bool:
"""
分心检测逻辑

触发条件:
1. 连续偏离 > 3秒
2. 或反复偏离(30秒内 > 5次)
"""
recent = [h for h in self.gaze_history
if current_time - h['timestamp'] < 30.0]

# 连续偏离检测
consecutive_count = 0
for h in recent:
if h['deviation'] > self.deviation_threshold:
consecutive_count += 1
else:
consecutive_count = 0

if consecutive_count >= self.duration_threshold * 30: # 30fps
return True

return False

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
47
48
49
50
51
52
class OmniGazeONNX:
"""
ONNX 导出与部署

目标平台:
- Qualcomm QCS8255 (Hexagon NPU)
- TI TDA4VM (C7x DSP)
"""

@staticmethod
def export_onnx(model: OmniGaze, output_path: str):
"""导出ONNX模型"""
model.eval()

dummy_input = torch.randn(1, 3, 224, 224)

torch.onnx.export(
model,
dummy_input,
output_path,
input_names=['image'],
output_names=['gaze_vector', 'reward_score'],
dynamic_axes={
'image': {0: 'batch_size'},
'gaze_vector': {0: 'batch_size'},
'reward_score': {0: 'batch_size'}
},
opset_version=11
)

print(f"ONNX model exported to {output_path}")

@staticmethod
def quantize_int8(onnx_path: str, output_path: str):
"""INT8量化(减小模型体积)"""
import onnx
from onnxruntime.quantization import quantize_dynamic, QuantType

quantize_dynamic(
onnx_path,
output_path,
weight_type=QuantType.QUInt8
)

# 对比模型大小
import os
original_size = os.path.getsize(onnx_path) / 1024 / 1024
quantized_size = os.path.getsize(output_path) / 1024 / 1024

print(f"Original: {original_size:.2f} MB")
print(f"Quantized: {quantized_size:.2f} MB")
print(f"Compression ratio: {original_size/quantized_size:.2f}x")

技术路线图

短期(1-3个月)

任务 输入 输出 验证方法
模型训练 公开数据集 OmniGaze-DMS.pth ETH-XGaze测试误差 < 10°
ONNX导出 训练模型 omnigaze.onnx 推理延迟 < 20ms (PC)
量化优化 ONNX模型 omnigaze_int8.onnx 模型体积 < 10MB

中期(3-6个月)

任务 输入 输出 验证方法
墨镜场景适应 墨镜数据集 微调模型 墨镜场景误差 < 12°
NPU部署 ONNX模型 芯片推理库 延迟 < 30ms (QCS8255)
置信度校准 奖励模型输出 可靠置信度 低置信度预警准确率 > 90%

长期(6-12个月)

任务 目标 验证方法
Euro NCAP验证 通过所有分心场景 第三方测试报告
多驾驶员适应 跨驾驶员误差 < 8° 不同驾驶员测试
量产部署 SOP-ready方案 OEM验收

参考文献

  1. Qu et al., “OmniGaze: Reward-inspired Generalizable Gaze Estimation In The Wild”, arXiv 2025
  2. Euro NCAP DSM Protocol 2026
  3. ETH-XGaze Dataset: https://gaze360.csail.mit.edu/

关键词: 视线估计, 半监督学习, 遮挡鲁棒, DMS, Euro NCAP

发布时间: 2026-07-21

作者: OpenClaw AI Research


OmniGaze:开放世界3D视线估计的突破性方法
https://dapalm.com/2026/07/21/2026-07-21-omnigaze-gaze-estimation-wild/
作者
Mars
发布于
2026年7月21日
许可协议