AttentiveGaze:不确定性感知的多模态视线估计框架(2026 论文解读+代码复现)

AttentiveGaze:不确定性感知的多模态视线估计框架

论文信息

项目 内容
标题 AttentiveGaze: an uncertainty-aware multimodal feature fusion for robust gaze estimation
作者 Choksy, P. J., Patel, H., Chowdhury, A., Pachade, S. P., & Puar, A.
机构 Akeso Eyecare (北京), EyelignAI (印度)
期刊 Multimedia Tools and Applications
年份 2026
链接 BioEngineer 报道

1. 核心创新

AttentiveGaze 解决了视线估计在 DMS 部署中的两个核心痛点:

  1. 自适应多模态融合:不再用固定权重融合眼睛/面部/头部姿态特征,而是让网络逐样本动态决定各模态权重
  2. 不确定性感知:每个视线预测同时输出置信度(方差),让 DMS 知道”什么时候不该信自己”

1.1 为什么不确定性很重要

“A driver monitoring system that silently misreads a drowsy driver’s gaze as attentive is worse than useless; it manufactures false reassurance.”

场景 传统系统 AttentiveGaze
强光反射遮挡眼部 静默误判 ⚠️ 高方差 → 降级处理
极端头部旋转 静默误判 ⚠️ 高方差 → 保守策略
正常光照正面 正常预测 ✅ 低方差 → 可信
佩戴墨镜 静默误判 ⚠️ 高方差 → 升级告警

2. 方法详解

2.1 系统架构

graph TD
    A[输入图像] --> B[眼部区域裁剪]
    A --> C[全脸图像]
    A --> D[头部姿态估计]
    
    B --> E[注意力增强眼部特征提取器<br/>空间+通道注意力]
    C --> F[全脸特征提取]
    D --> G[头部姿态特征]
    
    E --> H[跨模态注意力融合模块]
    F --> H
    G --> H
    
    H --> I[可学习门控机制<br/>动态权重分配]
    
    I --> J[多头嵌入层<br/>并行子空间投影]
    
    J --> K[回归头]
    
    K --> K1[视线方向预测<br/>pitch, yaw]
    K --> K2[不确定性/方差预测<br/>σ²]

2.2 关键组件

2.2.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
"""
AttentiveGaze 注意力增强眼部特征提取器
空间注意力 + 通道注意力,学习眼部最诊断性的区域
"""

import torch
import torch.nn as nn
import torch.nn.functional as F

class SpatialAttention(nn.Module):
"""空间注意力:学习眼部图像中哪些空间位置最重要"""

def __init__(self, kernel_size: int = 7):
super().__init__()
self.conv = nn.Conv2d(2, 1, kernel_size, padding=kernel_size//2)

def forward(self, x):
# 沿通道维度聚合
avg_pool = torch.mean(x, dim=1, keepdim=True) # (B, 1, H, W)
max_pool, _ = torch.max(x, dim=1, keepdim=True) # (B, 1, H, W)

# 拼接后卷积
combined = torch.cat([avg_pool, max_pool], dim=1)
attention = torch.sigmoid(self.conv(combined))

return x * attention


class ChannelAttention(nn.Module):
"""通道注意力:学习哪些特征通道最重要"""

def __init__(self, channels: int, reduction: int = 16):
super().__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.max_pool = nn.AdaptiveMaxPool2d(1)

self.mlp = nn.Sequential(
nn.Linear(channels, channels // reduction),
nn.ReLU(),
nn.Linear(channels // reduction, channels)
)

def forward(self, x):
b, c, _, _ = x.shape
avg_out = self.mlp(self.avg_pool(x).view(b, c))
max_out = self.mlp(self.max_pool(x).view(b, c))
attention = torch.sigmoid(avg_out + max_out)
return x * attention.view(b, c, 1, 1)


class AttentionEnhancedEyeEncoder(nn.Module):
"""
AttentiveGaze 眼部特征提取器

结合空间注意力和通道注意力,
自适应聚焦虹膜边界、角膜反射等诊断性区域
"""

def __init__(self, in_channels: int = 3, feat_channels: int = 64):
super().__init__()

# 基础卷积特征提取
self.conv1 = nn.Conv2d(in_channels, feat_channels, 3, padding=1)
self.bn1 = nn.BatchNorm2d(feat_channels)

self.conv2 = nn.Conv2d(feat_channels, feat_channels*2, 3, stride=2, padding=1)
self.bn2 = nn.BatchNorm2d(feat_channels*2)

self.conv3 = nn.Conv2d(feat_channels*2, feat_channels*4, 3, stride=2, padding=1)
self.bn3 = nn.BatchNorm2d(feat_channels*4)

# 注意力模块
self.ca1 = ChannelAttention(feat_channels)
self.sa1 = SpatialAttention()
self.ca2 = ChannelAttention(feat_channels*2)
self.sa2 = SpatialAttention()
self.ca3 = ChannelAttention(feat_channels*4)
self.sa3 = SpatialAttention()

def forward(self, x):
# Stage 1
x = F.relu(self.bn1(self.conv1(x)))
x = self.ca1(x)
x = self.sa1(x)

# Stage 2
x = F.relu(self.bn2(self.conv2(x)))
x = self.ca2(x)
x = self.sa2(x)

# Stage 3
x = F.relu(self.bn3(self.conv3(x)))
x = self.ca3(x)
x = self.sa3(x)

# 全局平均池化
x = F.adaptive_avg_pool2d(x, 1).flatten(1)
return x


# 测试
if __name__ == "__main__":
encoder = AttentionEnhancedEyeEncoder(in_channels=3, feat_channels=32)

# 模拟眼部裁剪图像 (B, C, H, W)
eye_img = torch.randn(4, 3, 64, 64)
features = encoder(eye_img)
print(f"输入: {eye_img.shape}")
print(f"输出特征: {features.shape}") # (4, 128)
print(f"参数量: {sum(p.numel() for p in encoder.parameters()):,}")

2.2.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
"""
跨模态注意力融合 + 可学习门控
让网络动态决定眼部/面部/头部特征的权重
"""

class CrossModalAttention(nn.Module):
"""跨模态注意力:每个模态查询其他模态的互补信息"""

def __init__(self, dim: int, num_modalities: int = 3):
super().__init__()
self.num_modalities = num_modalities

# 每对模态一个注意力
self.cross_attn = nn.ModuleList([
nn.MultiheadAttention(dim, num_heads=4, batch_first=True)
for _ in range(num_modalities * (num_modalities - 1) // 2)
])

def forward(self, *modalities):
"""前向传播"""
# 简化实现:两两跨模态注意力
idx = 0
enhanced = list(modalities)

for i in range(self.num_modalities):
for j in range(i+1, self.num_modalities):
# i 查询 j
qi = enhanced[i].unsqueeze(1) # (B, 1, D)
kj = vj = enhanced[j].unsqueeze(1)
attn_out, _ = self.cross_attn[idx](qi, kj, vj)
enhanced[i] = enhanced[i] + attn_out.squeeze(1)
idx += 1

return torch.cat(enhanced, dim=-1)


class LearnableGate(nn.Module):
"""可学习门控:动态分配各模态权重"""

def __init__(self, input_dim: int, num_modalities: int = 3):
super().__init__()
self.gate = nn.Sequential(
nn.Linear(input_dim, input_dim // 2),
nn.ReLU(),
nn.Linear(input_dim // 2, num_modalities),
nn.Softmax(dim=-1)
)

def forward(self, *modalities):
"""返回加权融合后的特征"""
stacked = torch.stack(modalities, dim=1) # (B, M, D)
merged = stacked.flatten(1) # (B, M*D)

weights = self.gate(merged) # (B, M)

# 加权求和
weighted = (stacked * weights.unsqueeze(-1)).sum(dim=1)
return weighted, weights


# 测试
if __name__ == "__main__":
eye_feat = torch.randn(4, 128)
face_feat = torch.randn(4, 128)
head_feat = torch.randn(4, 128)

cross_attn = CrossModalAttention(128, num_modalities=3)
fused = cross_attn(eye_feat, face_feat, head_feat)
print(f"跨模态融合输出: {fused.shape}")

gate = LearnableGate(128*3, num_modalities=3)
weighted, weights = gate(eye_feat, face_feat, head_feat)
print(f"门控权重: {weights[0].detach()}")
print(f"加权输出: {weighted.shape}")

2.2.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
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
"""
不确定性感知回归头
基于 heteroscedastic regression (Nix & Weigend, 1994)
预测视线方向 + 方差
"""

class UncertaintyGazeRegressor(nn.Module):
"""
不确定性感知视线回归器

输出: (pitch, yaw, sigma_pitch, sigma_yaw)
其中 sigma 是预测的不确定性(标准差)

损失函数: NLL Loss
L = 0.5 * exp(-log_var) * ||pred - target||^2 + 0.5 * log_var
"""

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

# 多头嵌入
self.heads = nn.ModuleList([
nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim // 2)
)
for _ in range(num_heads)
])

# 回归头:方向 + log方差
self.direction_head = nn.Linear(hidden_dim // 2 * num_heads, 2) # pitch, yaw
self.log_var_head = nn.Linear(hidden_dim // 2 * num_heads, 2) # log(sigma^2)

def forward(self, x):
# 多头嵌入
head_outputs = [head(x) for head in self.heads]
multi_head_feat = torch.cat(head_outputs, dim=-1)

# 预测方向和方差
direction = self.direction_head(multi_head_feat) # (B, 2)
log_var = self.log_var_head(multi_head_feat) # (B, 2)

# 确保方差为正
var = torch.exp(log_var.clamp(-10, 10)) # 防止数值溢出

return direction, var

def loss(self, pred_direction, pred_var, target_direction):
"""
Heteroscedastic NLL Loss

L = 0.5 * (pred - target)^2 / var + 0.5 * log(var)
"""
nll = 0.5 * (pred_direction - target_direction)**2 / pred_var \
+ 0.5 * torch.log(pred_var)
return nll.sum(dim=-1).mean()

def predict_with_confidence(self, x, confidence_threshold: float = 0.05):
"""
带置信度的预测

Args:
x: 输入特征
confidence_threshold: 最大可接受标准差(度)

Returns:
direction, is_reliable
"""
direction, var = self.forward(x)
std = torch.sqrt(var)

# 如果标准差超过阈值,标记为不可靠
is_reliable = (std <= confidence_threshold).all(dim=-1)

return direction, is_reliable, std


# 完整 pipeline 测试
if __name__ == "__main__":
# 模拟输入
batch_size = 8
eye_feat = torch.randn(batch_size, 128)
face_feat = torch.randn(batch_size, 128)
head_feat = torch.randn(batch_size, 128)

# 融合
cross_attn = CrossModalAttention(128, 3)
fused = cross_attn(eye_feat, face_feat, head_feat)

# 门控
gate = LearnableGate(128*3, 3)
weighted, weights = gate(eye_feat, face_feat, head_feat)

# 不确定性回归
regressor = UncertaintyGazeRegressor(128, num_heads=4)
direction, var = regressor(weighted)

print(f"视线方向 (pitch, yaw): {direction[0].detach()}")
print(f"不确定性 (σ²): {var[0].detach()}")

# 带置信度的预测
pred, reliable, std = regressor.predict_with_confidence(weighted)
print(f"预测可靠: {reliable[0].item()}")
print(f"标准差: {std[0].detach()}")

# 训练 loss
target = torch.randn(batch_size, 2) # 真实视线方向
loss = regressor.loss(direction, var, target)
print(f"NLL Loss: {loss.item():.4f}")

3. 实验结果

3.1 数据集

数据集 场景 规模
MPIIFaceGaze 笔记本摄像头日常场景 15人
EyeDiap Idiap 研究所,RGB+深度 16人
GazeCapture 移动设备大规模采集 1474人

3.2 性能对比

方法 MPIIFaceGaze EyeDiap GazeCapture 不确定性输出
Full-Face (2017) 11.5° ~10° ~13° ❌
GazeML (2020) ~9° ~8° ~11° ❌
AttentiveGaze competitive competitive competitive ✅

作者强调:目标不是在平均误差上超越 SOTA,而是在实时性 + 不确定性方面提供实用价值

3.3 不确定性的价值

场景 传统系统行为 AttentiveGaze 行为
眼镜反光 误判但无提示 σ↑ → 标记不可靠
极端头部旋转 误判但无提示 σ↑ → 降级到保守策略
墨镜遮挡 误判但无提示 σ↑ → 升级到二级检测
正常条件 正常预测 σ↓ → 高置信度预测

4. 对 IMS 开发的启示

4.1 可落地的开发建议

优先级 建议 具体行动
🔴 P0 DMS 输出增加不确定性指标 视线预测模型输出 (pitch, yaw, σ) 三元组
🔴 P0 基于不确定性的降级策略 σ > 阈值时:跳过视线分析,改用头部姿态代理
🟡 P1 多模态自适应融合 用门控替代固定权重融合眼部/面部/头部特征
🟡 P1 注意力增强眼部特征 在眼部裁剪图上使用空间+通道注意力
🟢 P2 OOD 检测 利用不确定性识别分布外样本(新用户/新环境)

4.2 DMS 降级策略框架

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
"""
基于不确定性的 DMS 降级策略
"""

class GazeBasedDMS:
def __init__(self, gaze_model, threshold_deg=3.0):
self.model = gaze_model
self.threshold = threshold_deg # 3度标准差阈值

def assess_attention(self, eye_img, face_img, head_pose):
"""评估驾驶员注意力状态"""
# 获取特征
eye_feat = self.eye_encoder(eye_img)
face_feat = self.face_encoder(face_img)
head_feat = self.head_encoder(head_pose)

# 融合 + 预测
fused = self.fuse(eye_feat, face_feat, head_feat)
direction, reliable, std = self.model.predict_with_confidence(
fused, self.threshold
)

if not reliable:
# 降级策略
return self._fallback_strategy(head_pose, std)

# 正常分析
return self._analyze_gaze(direction, std)

def _fallback_strategy(self, head_pose, std):
"""不确定性过高时的降级策略"""
# 1. 使用头部姿态作为视线代理
gaze_proxy = head_pose[:2] # pitch, yaw

# 2. 缩短分析窗口(更多依赖时序信息)
# 3. 提高告警灵敏度(宁可误报不漏报)

return {
'gaze_direction': gaze_proxy,
'confidence': 'low',
'strategy': 'head_pose_proxy',
'uncertainty_std': std,
'recommendation': 'increase_alert_sensitivity'
}

5. 技术路线判断

5.1 核心洞察

AttentiveGaze 揭示了 DMS 视线估计从”追求平均精度”到”知道何时不可信”的范式转变:

  1. 不确定性是安全关键系统的刚需:不是锦上添花,而是底线要求
  2. 自适应融合优于固定融合:不同场景下各模态的诊断价值不同
  3. 紧凑实时是部署前提:在车内不能依赖云端大模型

5.2 对 Euro NCAP 的间接影响

虽然 Euro NCAP 不直接评估不确定性,但不确定性感知系统在以下方面有优势:

  • 减少误报:不可靠预测不触发告警
  • 提高召回率:可靠预测的可信度更高
  • 可审计性:系统决策可追溯(”为什么告警/未告警”)

6. 参考


AttentiveGaze:不确定性感知的多模态视线估计框架(2026 论文解读+代码复现)
https://dapalm.com/2026/10/06/2026-10-06-001-attentivegaze-uncertainty-aware-gaze-estimation/
作者
Mars
发布于
2026年10月6日
许可协议