可解释DMS面部关键点监控:个体化标定+MediaPipe实现方案

论文信息

项目 内容
标题 Interpretable Driver Behavioral State Monitoring using Facial Landmarks for Human-Autonomous Vehicle Interaction
期刊 Frontiers in Robotics and AI (2026)
DOI 10.3389/frobt.2026.1896859
机构 Tecnologico de Monterrey, Mexico; Houston Methodist Hospital
测试车辆 Polaris GEM e6 自动驾驶车
接受时间 2026-09-15

核心发现

个体化标定+面部关键点的可解释DMS方案,SVM分类器达到91% F1,优于CNN/RNN(~80%)。

关键指标

指标 数值 说明
人脸检测成功率 98.5% MediaPipe
SVM F1-score(室内) ~91% 最佳分类器
RF F1-score(室外) ~70% 泛化性差
CNN/RNN F1-score ~80% 与启发式基线相当
行为状态分类 5类 neutral/闭眼/偏离/哈欠/频繁眨眼

五种行为状态

状态 检测指标 统计显著性
正常 基线EAR/MAR/头部角度 -
闭眼 EAR < 阈值 p < 0.001 ✅
视线偏离 头部旋转角 > 阈值 p > 0.05 ❌
哈欠 MAR > 阈值 p < 0.001 ✅
频繁眨眼 眨眼频率 > 阈值 p < 0.01 ✅

关键发现: “视线偏离”状态(头部旋转角)未能达到统计显著性——说明单纯头部旋转不是可靠的分心指标,需要配合视线估计。

方法详解

系统架构

核心代码实现

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
import cv2
import numpy as np
import mediapipe as mp
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import classification_report, f1_score

class InterpretableDMS:
"""可解释DMS系统 - 基于面部关键点"""

def __init__(self):
self.mp_face = mp.solutions.face_mesh
self.face_mesh = self.mp_face.FaceMesh(
max_num_faces=1,
refine_landmarks=True,
min_detection_confidence=0.5
)

# 个体化基线
self.baseline_ear = None
self.baseline_mar = None
self.baseline_head_rot = None
self.is_calibrated = False

# MediaPipe关键点索引
self.LEFT_EYE = [33, 160, 158, 133, 153, 144]
self.RIGHT_EYE = [362, 385, 387, 263, 373, 380]
self.MOUTH = [61, 291, 13, 14, 17, 0]

# 状态定义
self.STATES = {
0: 'neutral',
1: 'eye_closure',
2: 'out_of_safe_zone',
3: 'yawning',
4: 'excessive_blinking'
}

def extract_features(self, landmarks):
"""提取可解释特征"""

# EAR (Eye Aspect Ratio)
left_ear = self._compute_ear(landmarks, self.LEFT_EYE)
right_ear = self._compute_ear(landmarks, self.RIGHT_EYE)
ear = (left_ear + right_ear) / 2

# MAR (Mouth Aspect Ratio)
mar = self._compute_mar(landmarks, self.MOUTH)

# 头部旋转角
head_rot = self._compute_head_rotation(landmarks)

return np.array([ear, mar, head_rot[0], head_rot[1], head_rot[2]])

def _compute_ear(self, landmarks, eye_indices):
"""计算眼睛纵横比"""
p = [np.array([landmarks[i].x, landmarks[i].y]) for i in eye_indices]
# EAR = (||p2-p6|| + ||p3-p5||) / (2 * ||p1-p4||)
vertical1 = np.linalg.norm(p[1] - p[5])
vertical2 = np.linalg.norm(p[2] - p[4])
horizontal = np.linalg.norm(p[0] - p[3])
if horizontal == 0:
return 0
return (vertical1 + vertical2) / (2 * horizontal)

def _compute_mar(self, landmarks, mouth_indices):
"""计算嘴部纵横比"""
p = [np.array([landmarks[i].x, landmarks[i].y]) for i in mouth_indices]
vertical = np.linalg.norm(p[2] - p[3]) # 上唇到下唇
horizontal = np.linalg.norm(p[0] - p[1]) # 左嘴角到右嘴角
if horizontal == 0:
return 0
return vertical / horizontal

def _compute_head_rotation(self, landmarks):
"""简化版头部旋转角估计"""
# 使用关键点3D坐标估计Pitch/Yaw/Roll
nose_tip = np.array([landmarks[1].x, landmarks[1].y, landmarks[1].z])
left_eye = np.array([landmarks[33].x, landmarks[33].y, landmarks[33].z])
right_eye = np.array([landmarks[263].x, landmarks[263].y, landmarks[263].z])

# Yaw: 左右眼到鼻尖的水平距离比
eye_mid = (left_eye + right_eye) / 2
yaw = (nose_tip[0] - eye_mid[0]) * 2 # 简化

# Pitch: 鼻尖z坐标
pitch = nose_tip[2]

# Roll: 两眼连线的角度
dy = right_eye[1] - left_eye[1]
dx = right_eye[0] - left_eye[0]
roll = np.arctan2(dy, dx)

return [pitch, yaw, roll]

def calibrate(self, frame, n_samples=30):
"""个体化标定"""
ears, mars, rotations = [], [], []

for _ in range(n_samples):
results = self.face_mesh.process(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
if results.multi_face_landmarks:
lm = results.multi_face_landmarks[0].landmark
features = self.extract_features(lm)
ears.append(features[0])
mars.append(features[1])
rotations.append(features[2:])

self.baseline_ear = np.mean(ears)
self.baseline_mar = np.mean(mars)
self.baseline_head_rot = np.mean(rotations, axis=0)
self.is_calibrated = True

return {
'baseline_ear': self.baseline_ear,
'baseline_mar': self.baseline_mar,
'baseline_head_rot': self.baseline_head_rot.tolist()
}

def classify_state(self, features, model='svm'):
"""分类行为状态"""
if not self.is_calibrated:
return 'uncalibrated'

# 偏离基线的程度
ear_dev = (self.baseline_ear - features[0]) / max(self.baseline_ear, 1e-6)
mar_dev = (features[1] - self.baseline_mar) / max(self.baseline_mar, 1e-6)

# 阈值判断(启发式状态机)
if ear_dev > 0.4: # EAR下降40%以上
return 'eye_closure'
elif mar_dev > 1.5: # MAR增大150%以上
return 'yawning'
else:
return 'neutral'


# === 训练分类器 ===
def train_classifiers(features, labels):
"""训练并评估多种分类器"""

classifiers = {
'SVM': SVC(kernel='rbf', C=1.0, gamma='scale'),
'Random Forest': RandomForestClassifier(n_estimators=100, random_state=42),
}

# 10-fold 交叉验证
cv = StratifiedKFold(n_splits=10, shuffle=True, random_state=42)

results = {}
for name, clf in classifiers.items():
f1_scores = []
for train_idx, test_idx in cv.split(features, labels):
X_train, X_test = features[train_idx], features[test_idx]
y_train, y_test = labels[train_idx], labels[test_idx]

clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
f1 = f1_score(y_test, y_pred, average='weighted')
f1_scores.append(f1)

results[name] = {
'mean_f1': np.mean(f1_scores),
'std_f1': np.std(f1_scores),
}
print(f"{name}: F1 = {np.mean(f1_scores):.3f} ± {np.std(f1_scores):.3f}")

return results


if __name__ == "__main__":
# 模拟数据
np.random.seed(42)
n_samples = 500
features = np.random.randn(n_samples, 5) # EAR, MAR, Pitch, Yaw, Roll
labels = np.random.randint(0, 5, n_samples) # 5种状态

print("=== 可解释DMS分类器对比 ===")
print(f"数据量: {n_samples} 样本, 5特征, 5类")
print()
train_classifiers(features, labels)

print("\n关键发现:")
print("1. SVM在室内测试中F1=91%,最稳定")
print("2. RF在室外测试中F1降至70%(泛化性差)")
print("3. CNN/RNN F1≈80%,未超越启发式基线(81%)")
print("4. 视线偏离(head rotation)不显著(p>0.05)")
print("5. 个体化标定是关键——统一阈值不可靠")

IMS 开发启示

1. 可解释性的价值

方面 黑盒深度学习 可解释方案 (本文)
调试 难以定位错误 可追溯到EAR/MAR值
认证 难以通过法规 可提供决策依据
用户信任 “AI说你疲劳” “你的闭眼比例超过30%”
误报分析 无法定位 可查看特征值偏差

2. 个体化标定的必要性

论文明确指出:统一阈值在不同人之间不可靠。

驾驶员特征 影响 标定方式
眼睛大小 EAR基线差异大 30秒正常驾驶标定
嘴巴大小 MAR基线差异大 同上
驾驶姿势 头部角度基线不同 同上
眨眼频率 个体差异显著 统计正常眨眼频率

3. SVM > 深度学习的场景

条件 SVM优势 深度学习劣势
小数据集 有效 过拟合
可解释性 决策边界清晰 黑盒
边缘部署 模型小(<1MB) 模型大(>10MB)
室内跨session 稳定 不一定更好
室外跨session 稳定 泛化性不确定

Euro NCAP 关联

Euro NCAP 场景 本论文的启示
F-01~F-03 疲劳检测 EAR+MAR个体化标定可落地
D-01~D-03 分心检测 头部旋转不够——需视线估计
D-04/D-05 手机使用 需要更多行为特征

硬件配置

组件 论文使用 车载替代 参数
摄像头 Intel RealSense L515 OV2311 2MP, 全局快门
处理器 PC QCS8255 26TOPS NPU
框架 MediaPipe + OpenCV 同 轻量化
HMI ROS + Foxglove 车机UI 实时状态显示

局限性

  1. 室外泛化差 — RF从91%降到70%
  2. 视线偏离不显著 — 单纯头部旋转不可靠
  3. 无视线估计 — 缺少gaze direction
  4. 5类状态有限 — 缺少手机使用、吸烟等场景
  5. 无实时性报告 — 帧率未明确

总结

这篇论文验证了一个重要观点:在DMS中,可解释的个体化标定方案可以优于黑盒深度学习。对 IMS 团队:

  1. 个体化标定是必须的 — 30秒正常驾驶建立基线
  2. SVM在小数据场景下最优 — 不需要大数据,模型<1MB
  3. 头部旋转不等于分心 — 需要配合视线估计
  4. 可解释性有法规价值 — 能向审核方说明决策依据
  5. MediaPipe+OpenCV足够轻量 — 可在边缘设备实时运行

参考链接: