多任务视线估计:应对光照变化和眼镜遮挡的鲁棒方案

多任务视线估计:应对光照变化和眼镜遮挡的鲁棒方案

论文信息

  • 标题: Multi-task driver gaze estimation in real world driving scenes
  • 期刊: Engineering Applications of Artificial Intelligence, 2025
  • 发表时间: 2025年8月9日
  • DOI: 10.1016/j.engappai.2025.110894

核心问题

实际驾驶场景中的视线估计面临多重挑战:

挑战 影响 传统方法表现
光照变化 夜间、隧道、进出阴影区域 精度下降 30-50%
眼镜遮挡 反光、框架遮挡眼睛区域 检测失败率 > 20%
相邻区域混淆 相邻视线区域难以区分 准确率 < 80%
个人差异 不同驾驶员的视线习惯 泛化能力差

核心创新

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

class MultiTaskGazeEstimator(nn.Module):
"""多任务视线估计网络

任务:
1. 视线区域分类(Gaze Zone Classification)
2. 视线方向回归(Gaze Direction Regression)

优势:
- 区域分类提供粗粒度信息(左/右/仪表盘/后视镜等)
- 方向回归提供细粒度信息(俯仰角、偏航角)
- 联合训练提升泛化能力
"""

def __init__(self, num_zones=9):
super().__init__()

# 共享骨干网络(轻量化 MobileNetV3)
self.backbone = nn.Sequential(
nn.Conv2d(3, 32, 3, stride=2, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(inplace=True),

nn.Conv2d(32, 64, 3, stride=2, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),

nn.Conv2d(64, 128, 3, stride=2, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),

nn.Conv2d(128, 256, 3, stride=2, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
)

# 任务头1:视线区域分类
self.zone_classifier = nn.Sequential(
nn.AdaptiveAvgPool2d(1),
nn.Flatten(),
nn.Linear(256, 128),
nn.ReLU(inplace=True),
nn.Dropout(0.5),
nn.Linear(128, num_zones)
)

# 任务头2:视线方向回归
self.direction_regressor = nn.Sequential(
nn.AdaptiveAvgPool2d(1),
nn.Flatten(),
nn.Linear(256, 128),
nn.ReLU(inplace=True),
nn.Linear(128, 2) # (yaw, pitch) degrees
)

# 任务头3:眼睛状态检测(辅助任务)
self.eye_state_detector = nn.Sequential(
nn.AdaptiveAvgPool2d(1),
nn.Flatten(),
nn.Linear(256, 64),
nn.ReLU(inplace=True),
nn.Linear(64, 2) # (left_eye, right_eye) visibility
)

def forward(self, x):
"""
Args:
x: 输入图像, shape=(B, 3, H, W)

Returns:
zone_logits: 区域分类 logits, shape=(B, num_zones)
direction: 视线方向 (yaw, pitch), shape=(B, 2)
eye_state: 眼睛可见性, shape=(B, 2)
"""
# 共享特征提取
feat = self.backbone(x)

# 多任务输出
zone_logits = self.zone_classifier(feat)
direction = self.direction_regressor(feat)
eye_state = self.eye_state_detector(feat)

return zone_logits, direction, eye_state


class MultiTaskLoss(nn.Module):
"""多任务损失函数

L_total = L_zone + λ1 * L_direction + λ2 * L_eye_state

权重调整策略:
- 区域分类权重:1.0(主任务)
- 方向回归权重:0.5(辅助任务)
- 眼睛状态权重:0.3(辅助任务)
"""

def __init__(self, zone_weight=1.0, direction_weight=0.5, eye_weight=0.3):
super().__init__()
self.zone_weight = zone_weight
self.direction_weight = direction_weight
self.eye_weight = eye_weight

self.zone_loss = nn.CrossEntropyLoss()
self.direction_loss = nn.MSELoss()
self.eye_loss = nn.BCEWithLogitsLoss()

def forward(self, zone_pred, zone_target,
direction_pred, direction_target,
eye_pred, eye_target):

# 区域分类损失
loss_zone = self.zone_loss(zone_pred, zone_target)

# 方向回归损失(仅在有标注时计算)
valid_mask = (direction_target[:, 0] != -1) # 假设 -1 表示无效
if valid_mask.sum() > 0:
loss_direction = self.direction_loss(
direction_pred[valid_mask],
direction_target[valid_mask]
)
else:
loss_direction = 0.0

# 眼睛状态损失
loss_eye = self.eye_loss(eye_pred, eye_target)

# 总损失
total_loss = (
self.zone_weight * loss_zone +
self.direction_weight * loss_direction +
self.eye_weight * loss_eye
)

return total_loss, {
'zone': loss_zone.item(),
'direction': loss_direction.item() if isinstance(loss_direction, float) else loss_direction.item(),
'eye': loss_eye.item()
}


# 实际测试
if __name__ == "__main__":
# 创建模型
model = MultiTaskGazeEstimator(num_zones=9)

# 模拟输入
batch_size = 4
x = torch.randn(batch_size, 3, 224, 224)

# 推理
zone_logits, direction, eye_state = model(x)

print(f"区域分类输出: {zone_logits.shape}")
print(f"视线方向输出: {direction.shape}")
print(f"眼睛状态输出: {eye_state.shape}")

# 测试损失
criterion = MultiTaskLoss()
zone_target = torch.randint(0, 9, (batch_size,))
direction_target = torch.randn(batch_size, 2)
eye_target = torch.rand(batch_size, 2)

loss, loss_dict = criterion(zone_logits, zone_target,
direction, direction_target,
eye_state, eye_target)

print(f"\n总损失: {loss.item():.4f}")
print(f"区域损失: {loss_dict['zone']:.4f}")
print(f"方向损失: {loss_dict['direction']:.4f}")
print(f"眼睛损失: {loss_dict['eye']:.4f}")

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
class IlluminationAugmentation:
"""光照增强策略

模拟实际驾驶场景的光照变化:
1. 昼夜切换
2. 隧道进出
3. 阴影区域
4. 逆光场景
"""

def __init__(self):
self.augmentations = [
self.random_brightness,
self.random_contrast,
self.random_gamma,
self.random_shadow,
self.random_tunnel
]

def random_brightness(self, img, p=0.5):
"""随机亮度调整"""
if np.random.rand() < p:
factor = np.random.uniform(0.3, 1.5)
img = img * factor
img = np.clip(img, 0, 255)
return img

def random_contrast(self, img, p=0.5):
"""随机对比度调整"""
if np.random.rand() < p:
factor = np.random.uniform(0.5, 1.5)
mean = img.mean()
img = (img - mean) * factor + mean
img = np.clip(img, 0, 255)
return img

def random_gamma(self, img, p=0.3):
"""随机 Gamma 校正(模拟夜间场景)"""
if np.random.rand() < p:
gamma = np.random.uniform(0.5, 2.0)
img = np.power(img / 255.0, gamma) * 255
img = np.clip(img, 0, 255)
return img

def random_shadow(self, img, p=0.3):
"""随机阴影区域"""
if np.random.rand() < p:
h, w = img.shape[:2]
# 创建阴影区域
x1, y1 = np.random.randint(0, w//2), np.random.randint(0, h//2)
x2, y2 = np.random.randint(w//2, w), np.random.randint(h//2, h)

shadow = np.ones_like(img) * 0.7
shadow[y1:y2, x1:x2] = 1.0

img = img * shadow
img = np.clip(img, 0, 255)
return img

def random_tunnel(self, img, p=0.2):
"""模拟隧道场景(整体变暗)"""
if np.random.rand() < p:
# 突然变暗(进出隧道)
factor = np.random.uniform(0.3, 0.6)
img = img * factor
img = np.clip(img, 0, 255)
return img

def augment(self, img):
"""应用所有增强"""
for aug in self.augmentations:
img = aug(img)
return img.astype(np.uint8)

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
111
class GlassesOcclusionHandler:
"""眼镜遮挡处理

策略:
1. 眼镜检测(反射、框架)
2. 眼镜区域分割
3. 基于眼睛关键点的补偿
"""

def __init__(self):
# 眼镜反射特征
self.specular_threshold = 200

def detect_glasses(self, face_img, landmarks):
"""检测眼镜

Args:
face_img: 面部图像
landmarks: 面部关键点

Returns:
has_glasses: 是否戴眼镜
glasses_type: 眼镜类型(普通/墨镜)
"""
# 提取眼睛区域
left_eye = landmarks[36:42]
right_eye = landmarks[42:48]

# 检测反射
left_eye_region = self._extract_region(face_img, left_eye)
right_eye_region = self._extract_region(face_img, right_eye)

# 反射检测
left_specular = np.sum(left_eye_region > self.specular_threshold)
right_specular = np.sum(right_eye_region > self.specular_threshold)

# 判断
specular_ratio = (left_specular + right_specular) / (
left_eye_region.size + right_eye_region.size
)

has_glasses = specular_ratio > 0.1

# 判断墨镜
avg_brightness = (left_eye_region.mean() + right_eye_region.mean()) / 2
glasses_type = 'sunglasses' if avg_brightness < 50 else 'normal'

return has_glasses, glasses_type

def _extract_region(self, img, points):
"""提取多边形区域"""
from PIL import Image, ImageDraw

# 创建 mask
mask = Image.new('L', (img.shape[1], img.shape[0]), 0)
draw = ImageDraw.Draw(mask)

# 绘制多边形
pts = [(int(p[0]), int(p[1])) for p in points]
draw.polygon(pts, fill=255)

# 应用 mask
mask = np.array(mask)
region = img * (mask[:, :, np.newaxis] if len(img.shape) == 3 else mask)

return region

def estimate_gaze_with_glasses(self, face_img, landmarks, base_gaze):
"""戴眼镜时的视线估计

策略:
1. 使用眼睛外轮廓代替虹膜
2. 使用头部姿态补偿
3. 增加时间平滑
"""
has_glasses, glasses_type = self.detect_glasses(face_img, landmarks)

if not has_glasses:
return base_gaze

# 补偿策略
if glasses_type == 'sunglasses':
# 墨镜:主要依赖头部姿态
head_pose = self._estimate_head_pose(landmarks)
compensated_gaze = self._pose_to_gaze(head_pose)
else:
# 普通眼镜:结合眼睛外轮廓
eye_outer_corners = self._extract_eye_outer_corners(landmarks)
compensated_gaze = self._estimate_from_outer_corners(eye_outer_corners)

return compensated_gaze

def _estimate_head_pose(self, landmarks):
"""估计头部姿态(简化实现)"""
# 使用鼻尖、嘴角等关键点
return {'yaw': 0.0, 'pitch': 0.0, 'roll': 0.0}

def _pose_to_gaze(self, head_pose):
"""头部姿态转视线方向"""
# 简化:假设视线与头部方向一致
return {'yaw': head_pose['yaw'], 'pitch': head_pose['pitch']}

def _extract_eye_outer_corners(self, landmarks):
"""提取眼睛外角"""
left_outer = landmarks[36]
right_outer = landmarks[45]
return {'left': left_outer, 'right': right_outer}

def _estimate_from_outer_corners(self, corners):
"""基于外角估计视线(简化)"""
return {'yaw': 0.0, 'pitch': 0.0}

实验结果

1. 光照鲁棒性测试

场景 传统方法准确率 本文方法准确率 提升
白天正常光照 92.3% 93.1% +0.8%
夜间低光照 68.5% 89.7% +21.2%
隧道进出口 71.2% 91.3% +20.1%
阴影区域 74.6% 90.8% +16.2%
逆光场景 62.3% 88.5% +26.2%

2. 眼镜遮挡处理

眼镜类型 检测成功率 视线估计精度 RMSE (度)
无眼镜 - 93.1% 5.2
普通眼镜 96.5% 90.3% 6.8
墨镜 89.2% 82.7% 9.5
反光眼镜 91.8% 84.5% 8.7

3. 视线区域分类

区域 准确率 混淆主要区域
前方道路 96.2% 后视镜 (2.1%)
左后视镜 91.5% 左侧窗 (5.3%)
右后视镜 90.8% 右侧窗 (6.1%)
仪表盘 94.3% 中控屏 (3.8%)
中控屏 88.7% 仪表盘 (7.2%)
左侧窗 92.1% 左后视镜 (4.5%)
右侧窗 91.7% 右后视镜 (5.1%)
车顶 87.3% 后视镜 (8.9%)
腿部区域 85.6% 仪表盘 (10.2%)

IMS 开发启示

1. 视线估计模块架构

graph TB
    subgraph 输入处理
        A1[红外摄像头<br/>OV2311]
        A2[光照检测<br/>Histogram Analysis]
    end
    
    subgraph 预处理
        B1[光照归一化<br/>Retinex Algorithm]
        B2[眼镜检测<br/>Specular Analysis]
    end
    
    subgraph 多任务网络
        C1[骨干网络<br/>MobileNetV3]
        C2[区域分类<br/>9 zones]
        C3[方向回归<br/>yaw/pitch]
        C4[眼睛状态<br/>visibility]
    end
    
    subgraph 后处理
        D1[时间平滑<br/>Kalman Filter]
        D2[融合决策<br/>Zone + Direction]
    end
    
    A1 --> B1
    A2 --> B1
    B1 --> C1
    B2 --> C1
    C1 --> C2
    C1 --> C3
    C1 --> C4
    C2 --> D2
    C3 --> D2
    D2 --> D1

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
class OptimizedGazeEstimator:
"""优化版视线估计器(边缘部署)"""

def __init__(self, model_path, device='qcs8255'):
# 加载 INT8 量化模型
self.model = self._load_quantized_model(model_path, device)

# 时间平滑滤波器
self.kalman = self._init_kalman_filter()

# 历史记录
self.gaze_history = []
self.history_size = 5

def process_frame(self, frame):
"""处理单帧(完整流程)

Args:
frame: 红外图像, shape=(H, W, C)

Returns:
result: dict
- zone: int (0-8)
- direction: (yaw, pitch) in degrees
- confidence: float
"""
# 1. 光照检测
illumination = self._detect_illumination(frame)

# 2. 光照归一化
normalized_frame = self._normalize_illumination(frame, illumination)

# 3. 模型推理
zone_logits, direction, eye_state = self.model(normalized_frame)

# 4. 后处理
zone = zone_logits.argmax().item()
yaw, pitch = direction[0].item(), direction[1].item()

# 5. 时间平滑
smoothed_yaw, smoothed_pitch = self._apply_temporal_smoothing(yaw, pitch)

# 6. 置信度计算
confidence = self._calculate_confidence(zone_logits, eye_state)

return {
'zone': zone,
'zone_name': self.ZONE_NAMES[zone],
'direction': (smoothed_yaw, smoothed_pitch),
'confidence': confidence
}

ZONE_NAMES = [
'road_front', 'mirror_left', 'mirror_right',
'dashboard', 'center_screen', 'window_left',
'window_right', 'roof', 'lap'
]

def _detect_illumination(self, frame):
"""检测光照水平"""
# 简化:基于直方图
gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
hist = cv2.calcHist([gray], [0], None, [256], [0, 256])

# 平均亮度
mean_brightness = gray.mean()

# 分类
if mean_brightness < 50:
return 'night'
elif mean_brightness < 100:
return 'low'
elif mean_brightness > 200:
return 'bright'
else:
return 'normal'

def _normalize_illumination(self, frame, illumination):
"""光照归一化(简化版 Retinex)"""
if illumination == 'night':
# 增强
frame = cv2.convertScaleAbs(frame, alpha=2.0, beta=50)
elif illumination == 'bright':
# 压缩
frame = cv2.convertScaleAbs(frame, alpha=0.7, beta=0)

return frame

def _apply_temporal_smoothing(self, yaw, pitch):
"""时间平滑(简单移动平均)"""
self.gaze_history.append((yaw, pitch))

if len(self.gaze_history) > self.history_size:
self.gaze_history.pop(0)

# 平均
smoothed_yaw = np.mean([g[0] for g in self.gaze_history])
smoothed_pitch = np.mean([g[1] for g in self.gaze_history])

return smoothed_yaw, smoothed_pitch

def _calculate_confidence(self, zone_logits, eye_state):
"""计算置信度"""
zone_prob = torch.softmax(zone_logits, dim=1).max().item()
eye_visibility = torch.sigmoid(eye_state).mean().item()

confidence = zone_prob * 0.7 + eye_visibility * 0.3

return confidence

def _load_quantized_model(self, model_path, device):
"""加载量化模型(简化)"""
# 实际需要 QNN/SNPE 加载
return MultiTaskGazeEstimator()

def _init_kalman_filter(self):
"""初始化卡尔曼滤波器"""
# 简化实现
return None

参考文献

  1. Zhang et al. (2025). Multi-task driver gaze estimation in real world driving scenes. Engineering Applications of Artificial Intelligence.

  2. Hu et al. (2025). FIFA: Fine-grained Inter-frame Attention for Driver’s Video Gaze Estimation. CVPR 2025.


总结

多任务视线估计方法通过联合学习区域分类和方向回归,在光照变化、眼镜遮挡等挑战场景下取得了显著提升。

IMS 开发启示

  1. 多任务架构:区域分类 + 方向回归 + 眼睛状态,联合训练
  2. 光照鲁棒性:光照增强训练 + Retinex 归一化预处理
  3. 眼镜处理:反射检测 + 头部姿态补偿 + 外角估计
  4. 部署优化:INT8 量化 + 时间平滑 + 边缘推理

量化指标

  • 夜间准确率:89.7%(传统方法 68.5%)
  • 眼镜场景准确率:90.3%(普通眼镜)
  • 边缘推理延迟:< 30ms(QCS8255)

论文来源:Engineering Applications of Artificial Intelligence 2025 | IMS 研究笔记


多任务视线估计:应对光照变化和眼镜遮挡的鲁棒方案
https://dapalm.com/2026/08/16/2026-08-09-Multi-Task-Gaze-Estimation-Robust-Illumination/
作者
Mars
发布于
2026年8月16日
许可协议