Euro NCAP 2026 疲劳检测深度解析:PERCLOS 算法实现与 KSS 映射完整指南

一、Euro NCAP 2026 疲劳检测要求

1.1 法规背景

Euro NCAP 2026 将疲劳检测从”推荐项”升级为”评分项”,纳入 Safe Driving 评估体系。根据 Euro NCAP Driver Engagement Protocol v1.0(2025年3月发布),疲劳检测必须满足以下要求:

检测能力 法规要求 检测时限
疲劳状态 KSS > 7(困倦) ≤60秒发出警告
微睡眠 闭眼 1-2 秒 ≤3秒触发一级警告
睡眠 闭眼 ≥3 秒 ≤3秒触发二级警告

1.2 KSS(Karolinska Sleepiness Scale)评分

KSS 是瑞典卡罗林斯卡学院开发的困倦度主观评分量表,Euro NCAP 采用 KSS 作为疲劳等级的参考标准:

KSS 等级 描述 系统响应 PERCLOS 参考
1-3 清醒,警觉 无需响应 <15%
4-6 轻度困倦 无需响应 15-30%
7 困倦,但努力保持清醒 一级警告 30-50%
8 极度困倦,需努力保持睁眼 二级警告 50-70%
9 非常困倦,即将入睡 紧急警告 >70%

1.3 PERCLOS 指标

PERCLOS(Percentage of Eyelid Closure) 是 NHTSA 认证的疲劳检测金标准,定义为:

在指定时间窗口内,眼睑遮盖瞳孔超过 80% 的帧数占总帧数的百分比。

计算公式:

1
PERCLOS = (闭眼帧数 / 总帧数) × 100%

二、PERCLOS 算法完整实现

2.1 依赖安装

1
2
3
4
5
6
# requirements.txt
numpy>=1.21.0
opencv-python>=4.5.0
mediapipe>=0.10.0
scipy>=1.7.0
matplotlib>=3.5.0
1
2
# 安装依赖
pip install numpy opencv-python mediapipe scipy matplotlib

2.2 眼睑开度计算

使用 MediaPipe Face Mesh 提取眼部关键点,计算 Eye Aspect Ratio (EAR):

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
import numpy as np
import mediapipe as mp
from typing import Tuple, Optional

class EyeOpennessDetector:
"""
基于 MediaPipe Face Mesh 的眼睑开度检测器

使用关键点:
- 左眼: 33 (内眼角), 133 (外眼角), 159 (上眼睑), 145 (下眼睑)
- 右眼: 362 (内眼角), 263 (外眼角), 386 (上眼睑), 374 (下眼睑)
"""

# MediaPipe Face Mesh 关键点索引
LEFT_EYE_INDICES = {
'inner_corner': 33,
'outer_corner': 133,
'upper_eyelid': 159,
'lower_eyelid': 145
}

RIGHT_EYE_INDICES = {
'inner_corner': 362,
'outer_corner': 263,
'upper_eyelid': 386,
'lower_eyelid': 374
}

def __init__(self,
static_image_mode: bool = False,
max_num_faces: int = 1,
refine_landmarks: bool = True,
min_detection_confidence: float = 0.5,
min_tracking_confidence: float = 0.5):
"""
初始化眼睑开度检测器

Args:
static_image_mode: 是否为静态图像模式
max_num_faces: 最大检测人脸数
refine_landmarks: 是否使用精细关键点
min_detection_confidence: 检测置信度阈值
min_tracking_confidence: 跟踪置信度阈值
"""
self.mp_face_mesh = mp.solutions.face_mesh
self.face_mesh = self.mp_face_mesh.FaceMesh(
static_image_mode=static_image_mode,
max_num_faces=max_num_faces,
refine_landmarks=refine_landmarks,
min_detection_confidence=min_detection_confidence,
min_tracking_confidence=min_tracking_confidence
)

def calculate_ear(self, landmarks: np.ndarray, eye_indices: dict) -> float:
"""
计算 Eye Aspect Ratio (EAR)

EAR = (垂直距离) / (水平距离)

Args:
landmarks: (468, 3) 关键点坐标
eye_indices: 眼睛关键点索引

Returns:
EAR 值,范围约 0-0.4,闭眼时接近 0
"""
# 提取关键点坐标
upper = landmarks[eye_indices['upper_eyelid']]
lower = landmarks[eye_indices['lower_eyelid']]
inner = landmarks[eye_indices['inner_corner']]
outer = landmarks[eye_indices['outer_corner']]

# 计算垂直距离(上眼睑到下眼睑)
vertical_dist = np.linalg.norm(upper - lower)

# 计算水平距离(内眼角到外眼角)
horizontal_dist = np.linalg.norm(inner - outer)

# 防止除零
if horizontal_dist < 1e-6:
return 0.0

ear = vertical_dist / horizontal_dist
return ear

def detect(self, frame: np.ndarray) -> Tuple[Optional[np.ndarray], float, float]:
"""
检测眼睑开度

Args:
frame: BGR 图像,shape=(H, W, 3)

Returns:
landmarks: 关键点坐标 (468, 3) 或 None
left_ear: 左眼 EAR
right_ear: 右眼 EAR
"""
# BGR 转 RGB
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

# 检测人脸关键点
results = self.face_mesh.process(rgb_frame)

if not results.multi_face_landmarks:
return None, 0.0, 0.0

# 提取第一个人脸的关键点
face_landmarks = results.multi_face_landmarks[0]

# 转换为 numpy 数组
h, w = frame.shape[:2]
landmarks = np.array([
[lm.x * w, lm.y * h, lm.z]
for lm in face_landmarks.landmark
])

# 计算左右眼 EAR
left_ear = self.calculate_ear(landmarks, self.LEFT_EYE_INDICES)
right_ear = self.calculate_ear(landmarks, self.RIGHT_EYE_INDICES)

return landmarks, left_ear, right_ear

def close(self):
"""释放资源"""
self.face_mesh.close()


# 测试代码
if __name__ == "__main__":
import cv2

detector = EyeOpennessDetector()
cap = cv2.VideoCapture(0)

print("按 'q' 退出")
while True:
ret, frame = cap.read()
if not ret:
break

landmarks, left_ear, right_ear = detector.detect(frame)
avg_ear = (left_ear + right_ear) / 2

if landmarks is not None:
# 显示 EAR 值
cv2.putText(frame, f"EAR: {avg_ear:.3f}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)

# 判断睁眼/闭眼
status = "CLOSED" if avg_ear < 0.15 else "OPEN"
cv2.putText(frame, f"Status: {status}", (10, 70),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255) if status == "CLOSED" else (0, 255, 0), 2)

cv2.imshow("Eye Openness", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break

cap.release()
detector.close()
cv2.destroyAllWindows()

2.3 PERCLOS 计算器

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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
import numpy as np
from collections import deque
from typing import List, Tuple
from dataclasses import dataclass
from enum import Enum

class FatigueLevel(Enum):
"""疲劳等级"""
ALERT = 1 # 清醒 (KSS 1-3)
SLIGHTLY_DROWSY = 2 # 轻度困倦 (KSS 4-6)
DROWSY = 3 # 困倦 (KSS 7)
VERY_DROWSY = 4 # 极度困倦 (KSS 8)
SLEEPY = 5 # 即将入睡 (KSS 9)


@dataclass
class FatigueEvent:
"""疲劳事件"""
timestamp: float # 时间戳
event_type: str # 事件类型: 'microsleep', 'sleep', 'perclos_warning'
duration: float # 持续时间(秒)
severity: int # 严重程度: 1=一级警告, 2=二级警告


class PERCLOSCalculator:
"""
PERCLOS 计算器

支持两种模式:
1. PERCLOS-80: 眼睑遮盖 >80% 视为闭眼
2. PERCLOS-70: 眼睑遮盖 >70% 视为闭眼(更敏感)
"""

def __init__(self,
fps: int = 30,
window_sec: int = 60,
ear_threshold: float = 0.15,
microsleep_threshold: float = 1.0,
sleep_threshold: float = 3.0,
perclos_warning_threshold: float = 30.0,
perclos_critical_threshold: float = 50.0):
"""
初始化 PERCLOS 计算器

Args:
fps: 视频帧率
window_sec: PERCLOS 计算窗口(秒)
ear_threshold: EAR 闭眼阈值,< 此值视为闭眼
microsleep_threshold: 微睡眠阈值(秒),1-2秒
sleep_threshold: 睡眠阈值(秒),>= 3秒
perclos_warning_threshold: PERCLOS 一级警告阈值(%)
perclos_critical_threshold: PERCLOS 二级警告阈值(%)
"""
self.fps = fps
self.window_frames = int(window_sec * fps)
self.ear_threshold = ear_threshold
self.microsleep_threshold = microsleep_threshold
self.sleep_threshold = sleep_threshold
self.perclos_warning_threshold = perclos_warning_threshold
self.perclos_critical_threshold = perclos_critical_threshold

# 滑动窗口存储 EAR 值
self.ear_buffer = deque(maxlen=self.window_frames)

# 闭眼状态跟踪
self.is_eyes_closed = False
self.eye_close_start_time = None
self.current_time = 0.0

# 事件记录
self.events: List[FatigueEvent] = []

def update(self, ear: float, timestamp: float = None) -> Tuple[FatigueLevel, List[FatigueEvent]]:
"""
更新 PERCLOS 计算并返回疲劳等级

Args:
ear: 当前 EAR 值(左右眼平均)
timestamp: 时间戳(秒),可选

Returns:
fatigue_level: 疲劳等级
new_events: 新触发的事件列表
"""
if timestamp is not None:
self.current_time = timestamp
else:
self.current_time += 1.0 / self.fps

# 添加到缓冲区
self.ear_buffer.append(ear)

# 判断睁眼/闭眼状态
is_closed = ear < self.ear_threshold

new_events = []

# 检测闭眼事件
if is_closed and not self.is_eyes_closed:
# 开始闭眼
self.is_eyes_closed = True
self.eye_close_start_time = self.current_time

elif not is_closed and self.is_eyes_closed:
# 闭眼结束,计算持续时间
duration = self.current_time - self.eye_close_start_time

# 判断是否为微睡眠或睡眠
if duration >= self.sleep_threshold:
event = FatigueEvent(
timestamp=self.eye_close_start_time,
event_type='sleep',
duration=duration,
severity=2
)
self.events.append(event)
new_events.append(event)

elif duration >= self.microsleep_threshold:
event = FatigueEvent(
timestamp=self.eye_close_start_time,
event_type='microsleep',
duration=duration,
severity=1
)
self.events.append(event)
new_events.append(event)

self.is_eyes_closed = False
self.eye_close_start_time = None

# 计算 PERCLOS
perclos = self.calculate_perclos()

# 根据闭眼持续时间和 PERCLOS 判断疲劳等级
if self.is_eyes_closed:
# 正在闭眼中,检查是否达到睡眠阈值
closed_duration = self.current_time - self.eye_close_start_time
if closed_duration >= self.sleep_threshold:
fatigue_level = FatigueLevel.VERY_DROWSY
elif closed_duration >= self.microsleep_threshold:
fatigue_level = FatigueLevel.DROWSY
else:
fatigue_level = self._perclos_to_fatigue_level(perclos)
else:
fatigue_level = self._perclos_to_fatigue_level(perclos)

# 检查 PERCLOS 阈值事件
if perclos >= self.perclos_critical_threshold:
if not any(e.event_type == 'perclos_critical' for e in self.events[-5:]):
event = FatigueEvent(
timestamp=self.current_time,
event_type='perclos_critical',
duration=0,
severity=2
)
self.events.append(event)
new_events.append(event)
elif perclos >= self.perclos_warning_threshold:
if not any(e.event_type == 'perclos_warning' for e in self.events[-5:]):
event = FatigueEvent(
timestamp=self.current_time,
event_type='perclos_warning',
duration=0,
severity=1
)
self.events.append(event)
new_events.append(event)

return fatigue_level, new_events

def calculate_perclos(self) -> float:
"""
计算当前 PERCLOS 值

Returns:
PERCLOS 百分比 (0-100)
"""
if len(self.ear_buffer) == 0:
return 0.0

closed_count = sum(1 for ear in self.ear_buffer if ear < self.ear_threshold)
perclos = (closed_count / len(self.ear_buffer)) * 100
return perclos

def _perclos_to_fatigue_level(self, perclos: float) -> FatigueLevel:
"""PERCLOS 映射到疲劳等级"""
if perclos < 15:
return FatigueLevel.ALERT
elif perclos < 30:
return FatigueLevel.SLIGHTLY_DROWSY
elif perclos < 50:
return FatigueLevel.DROWSY
elif perclos < 70:
return FatigueLevel.VERY_DROWSY
else:
return FatigueLevel.SLEEPY

def perclos_to_kss(self, perclos: float) -> int:
"""
PERCLOS 映射到 KSS 评分

Args:
perclos: PERCLOS 百分比

Returns:
KSS 评分 (1-9)
"""
if perclos < 15:
return 3
elif perclos < 25:
return 5
elif perclos < 35:
return 6
elif perclos < 50:
return 7
elif perclos < 65:
return 8
else:
return 9

def get_statistics(self) -> dict:
"""获取统计信息"""
return {
'current_perclos': self.calculate_perclos(),
'current_kss': self.perclos_to_kss(self.calculate_perclos()),
'total_events': len(self.events),
'microsleep_count': sum(1 for e in self.events if e.event_type == 'microsleep'),
'sleep_count': sum(1 for e in self.events if e.event_type == 'sleep'),
'buffer_size': len(self.ear_buffer)
}


# 完整测试
if __name__ == "__main__":
import cv2

# 初始化
eye_detector = EyeOpennessDetector()
perclos_calc = PERCLOSCalculator(fps=30, window_sec=60)

cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FPS, 30)

print("="*50)
print("Euro NCAP 2026 疲劳检测测试")
print("="*50)
print("按 'q' 退出")
print("按 's' 打印统计信息")
print()

frame_count = 0

while True:
ret, frame = cap.read()
if not ret:
break

frame_count += 1

# 检测眼睑开度
landmarks, left_ear, right_ear = eye_detector.detect(frame)

if landmarks is not None:
avg_ear = (left_ear + right_ear) / 2

# 更新 PERCLOS 计算
fatigue_level, events = perclos_calc.update(avg_ear)

# 处理事件
for event in events:
print(f"[{event.timestamp:.1f}s] {event.event_type.upper()}: "
f"持续 {event.duration:.1f}s, 严重程度 {event.severity}")

# 显示信息
stats = perclos_calc.get_statistics()

# EAR 值
cv2.putText(frame, f"EAR: {avg_ear:.3f}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)

# PERCLOS
perclos = stats['current_perclos']
color = (0, 255, 0) if perclos < 30 else (0, 255, 255) if perclos < 50 else (0, 0, 255)
cv2.putText(frame, f"PERCLOS: {perclos:.1f}%", (10, 60),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)

# KSS
kss = stats['current_kss']
cv2.putText(frame, f"KSS: {kss}", (10, 90),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)

# 疲劳等级
level_names = {
FatigueLevel.ALERT: "ALERT",
FatigueLevel.SLIGHTLY_DROWSY: "SLIGHTLY DROWSY",
FatigueLevel.DROWSY: "DROWSY",
FatigueLevel.VERY_DROWSY: "VERY DROWSY",
FatigueLevel.SLEEPY: "SLEEPY"
}
cv2.putText(frame, f"Status: {level_names[fatigue_level]}", (10, 120),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)

# 微睡眠/睡眠计数
cv2.putText(frame, f"Microsleeps: {stats['microsleep_count']}", (10, 150),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
cv2.putText(frame, f"Sleeps: {stats['sleep_count']}", (10, 180),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)

cv2.imshow("Fatigue Detection", frame)

key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
break
elif key == ord('s'):
print("\n" + "="*50)
print("统计信息:")
print(f" PERCLOS: {stats['current_perclos']:.1f}%")
print(f" KSS: {stats['current_kss']}")
print(f" 微睡眠次数: {stats['microsleep_count']}")
print(f" 睡眠次数: {stats['sleep_count']}")
print("="*50 + "\n")

# 最终统计
print("\n" + "="*50)
print("最终统计:")
final_stats = perclos_calc.get_statistics()
print(f" 总帧数: {frame_count}")
print(f" PERCLOS: {final_stats['current_perclos']:.1f}%")
print(f" KSS: {final_stats['current_kss']}")
print(f" 微睡眠次数: {final_stats['microsleep_count']}")
print(f" 睡眠次数: {final_stats['sleep_count']}")
print("="*50)

cap.release()
eye_detector.close()
cv2.destroyAllWindows()

三、Euro NCAP 测试场景详解

3.1 疲劳检测测试场景

Euro NCAP 定义了 5 个疲劳检测测试场景(F-01 至 F-05):

F-01: 轻度疲劳测试

前置条件:

  • 驾驶员正常坐姿,面部无遮挡
  • 光照条件:白天 500±100 lux 或夜间红外模式
  • 红外摄像头正常工作,帧率 ≥25fps
  • 车辆速度 ≥60 km/h

测试步骤:

  1. 驾驶员正常驾驶 2 分钟(建立基线)
  2. 驾驶员模拟轻度疲劳(PERCLOS 20-30%,持续 30 秒)
  3. 记录系统检测结果和警告时延

判定条件:

检测项 通过条件 失败条件
检测触发 无需警告 误报警告
基线稳定 PERCLOS < 15% 基线波动 >10%

预期输出:

1
2
3
4
5
[00:00] INFO: 开始测试 F-01
[02:00] INFO: 基线建立完成,PERCLOS: 8.5%
[02:30] INFO: 模拟轻度疲劳,PERCLOS: 25.3%
[03:00] INFO: 测试结束,无警告触发
[03:01] PASS: F-01 测试通过

F-02: 中度疲劳测试(一级警告)

前置条件:

  • 同 F-01
  • 驾驶员 KSS 评分目标:7

测试步骤:

  1. 驾驶员正常驾驶 2 分钟(建立基线)
  2. 驾驶员模拟中度疲劳(PERCLOS 35-50%,持续 30 秒)
  3. 驾驶员继续疲劳表现 30 秒
  4. 记录系统检测结果和警告时延

判定条件:

检测项 通过条件 失败条件
检测触发 检测到疲劳状态 未检测到
警告等级 一级警告 二级警告/无警告
检测时延 ≤60 秒 >60 秒

预期输出:

1
2
3
4
5
6
7
[00:00] INFO: 开始测试 F-02
[02:00] INFO: 基线建立完成,PERCLOS: 9.2%
[02:30] INFO: 模拟中度疲劳,PERCLOS: 42.1%
[02:45] WARN: 检测到疲劳,KSS 估计: 7
[02:46] WARN: 触发一级警告(疲劳驾驶)
[03:00] INFO: PERCLOS: 48.3%,KSS: 7
[03:30] PASS: F-02 测试通过,时延: 15s

F-03: 重度疲劳测试(二级警告)

前置条件:

  • 同 F-01
  • 驾驶员 KSS 评分目标:8-9

测试步骤:

  1. 驾驶员正常驾驶 2 分钟(建立基线)
  2. 驾驶员模拟重度疲劳(PERCLOS 55-70%,持续 60 秒)
  3. 记录系统检测结果和警告时延

判定条件:

检测项 通过条件 失败条件
检测触发 检测到重度疲劳 未检测到
警告等级 二级警告 一级警告/无警告
检测时延 ≤60 秒 >60 秒

F-04: 微睡眠测试(一级警告)

前置条件:

  • 同 F-01
  • 准备计时器精确控制闭眼时长

测试步骤:

  1. 驾驶员正常驾驶 2 分钟(建立基线)
  2. 驾驶员闭眼 1.5±0.2 秒(模拟微睡眠)
  3. 驾驶员睁眼继续驾驶 10 秒
  4. 重复步骤 2-3 共 5 次
  5. 记录每次检测结果和警告时延

判定条件:

检测项 通过条件 失败条件
检测触发 ≥4/5 次检测到 <4 次检测到
警告等级 一级警告 二级警告/无警告
检测时延 ≤3 秒 >3 秒

预期输出:

1
2
3
4
5
6
7
8
[00:00] INFO: 开始测试 F-04
[02:00] INFO: 基线建立完成
[02:30] WARN: 检测到微睡眠,时长: 1.6s
[02:30] WARN: 触发一级警告(微睡眠)
[02:45] WARN: 检测到微睡眠,时长: 1.4s
[02:45] WARN: 触发一级警告(微睡眠)
...(共 5 次)
[04:30] PASS: F-04 测试通过,检测率: 5/5

F-05: 睡眠测试(二级警告)

前置条件:

  • 同 F-01

测试步骤:

  1. 驾驶员正常驾驶 2 分钟(建立基线)
  2. 驾驶员闭眼 4±0.5 秒(模拟睡眠)
  3. 驾驶员睁眼继续驾驶 10 秒
  4. 重复步骤 2-3 共 3 次
  5. 记录每次检测结果和警告时延

判定条件:

检测项 通过条件 失败条件
检测触发 ≥2/3 次检测到 <2 次检测到
警告等级 二级警告 一级警告/无警告
检测时延 ≤3 秒 >3 秒

四、硬件选型指南

4.1 红外摄像头选型

型号 分辨率 帧率 特性 厂商 参考价格
OV2311 1600×1200 60fps 全局快门,RGB-IR OmniVision $15-20
OV9282 1280×800 60fps 单色,全局快门 OmniVision $12-15
AR0237IR 1920×1080 60fps RGB-IR,车规级 ON Semi $25-30
IMX390 1936×1096 60fps 车规级,HDR Sony $30-35

推荐配置:

1
2
3
4
5
6
摄像头: OV2311
分辨率: 1600×1200
帧率: 30fps(疲劳检测)/ 60fps(微睡眠检测)
波长: 940nm(不可见红外)
视场角: 60°(水平)
工作温度: -40°C ~ +85°C

4.2 红外补光选型

型号 波长 功率 特性 厂商 参考价格
SFH 4740 940nm 120mW/sr 高功率,车规级 ams OSRAM $2-3
VSLY5850 850nm 40mW/sr 中功率 Vishay $1-2
IR LED Array 940nm 500mW 阵列式 国产 $5-8

补光设计原则:

  • 光照均匀度 > 80%(眼部区域)
  • 避免眼镜反光(角度设计)
  • 不可见红外(940nm)减少驾驶员干扰

4.3 处理器平台选型

平台 NPU 算力 功耗 适用场景 厂商
QCS8255 26 TOPS 5-8W 高端车型,全功能 DMS Qualcomm
TDA4VM 8 TOPS 5-7W 中端车型,DMS + 前视 TI
RK3588 6 TOPS 5-8W 中端车型,性价比高 Rockchip
Orin NX 100 TOPS 10-25W 高端车型,多传感器融合 NVIDIA

部署性能参考:

平台 模型 推理时延 CPU 占用 功耗
QCS8255 Face Mesh + PERCLOS 15ms 25% 3W
TDA4VM Face Mesh + PERCLOS 25ms 30% 4W
RK3588 Face Mesh + PERCLOS 20ms 35% 4W

五、IMS 开发优先级排序

5.1 功能模块优先级

优先级 模块 功能 输入 输出 精度要求
P0 人脸检测 检测驾驶员人脸 图像帧 人脸框 召回率 > 99%
P0 眼睑开度估计 计算 EAR 人脸区域 EAR 值 MAE < 0.05
P0 PERCLOS 计算 滑动窗口统计 EAR 序列 PERCLOS% 实时性 < 100ms
P0 微睡眠检测 闭眼时长检测 EAR 值 事件触发 延迟 < 0.5s
P1 KSS 映射 PERCLOS 转 KSS PERCLOS% KSS 1-9 误差 < 1 级
P1 疲劳等级判定 多特征融合 PERCLOS + 微睡眠 疲劳等级 准确率 > 90%
P2 头部姿态 点头/摇头检测 人脸关键点 姿态角 精度 < 3°
P2 打哈欠检测 嘴部开度分析 人脸关键点 哈欠频率 准确率 > 85%

5.2 算法选型建议

任务 推荐算法 复杂度 精度 部署难度
人脸检测 MediaPipe Face Detection
关键点检测 MediaPipe Face Mesh
眼睑开度 EAR 计算(几何方法)
PERCLOS 滑动窗口统计
疲劳判定 规则引擎 + 阈值

5.3 开发路线图

gantt
    title IMS 疲劳检测开发路线图
    dateFormat  YYYY-MM-DD
    section 基础功能
    人脸检测模块     :done, 2026-01-01, 7d
    眼睑开度估计     :done, 2026-01-08, 10d
    PERCLOS 计算     :done, 2026-01-18, 5d
    微睡眠检测       :done, 2026-01-23, 7d
    section 法规合规
    Euro NCAP 场景验证 :active, 2026-02-01, 14d
    KSS 映射调优     :2026-02-15, 7d
    警告策略实现     :2026-02-22, 7d
    section 优化部署
    模型量化         :2026-03-01, 10d
    嵌入式部署       :2026-03-11, 14d
    性能优化         :2026-03-25, 7d

六、常见问题与解决方案

6.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
def detect_with_glasses(frame, detector):
"""
针对眼镜场景的检测优化

策略:
1. 使用 940nm 红外补光(穿透墨镜)
2. 多帧平滑(降低抖动)
3. 头部姿态补偿
"""
# 红外模式下,墨镜透明度提升
# 检测失败时,使用上一帧结果

landmarks, left_ear, right_ear = detector.detect(frame)

if landmarks is None:
# 使用历史数据
return None, None

# 检测置信度
if left_ear < 0.05 and right_ear < 0.05:
# 可能是眼镜反光,使用历史 EAR
return landmarks, historical_ear

return landmarks, (left_ear + right_ear) / 2

6.2 夜间低光问题

问题: 夜间光照不足,人脸检测失败

解决方案:

  • 使用 940nm 红外补光(主动照明)
  • 调整摄像头曝光时间和增益
  • 使用红外专用摄像头(去除 IR Cut Filter)

6.3 驾驶员遮挡问题

问题: 驾驶员佩戴口罩、帽子导致检测失败

解决方案:

  • Euro NCAP 允许在”极端遮挡”情况下不触发警告
  • 检测遮挡状态,记录但不误报
  • 提示驾驶员移除遮挡物
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def check_occlusion(landmarks):
"""
检查面部遮挡状态

遮挡判断:
- 口罩:下巴区域关键点缺失
- 帽子:额头区域关键点缺失
- 手部遮挡:突发性关键点抖动
"""
# 检查下巴区域(口罩检测)
chin_visible = check_chin_landmarks(landmarks)

# 检查额头区域(帽子检测)
forehead_visible = check_forehead_landmarks(landmarks)

return {
'mask': not chin_visible,
'hat': not forehead_visible,
'severe_occlusion': not chin_visible and not forehead_visible
}

七、参考资料

7.1 官方文档

7.2 开源项目

7.3 论文推荐

  1. Wierwille et al. (1994). “Research on Vehicle-Based Driver Status/Performance Monitoring” - PERCLOS 原始论文
  2. Bergasa et al. (2006). “Real-time system for monitoring driver vigilance” - 多特征融合疲劳检测
  3. Zhang et al. (2022). “Research on a Real-Time Driver Fatigue Detection Algorithm Based on Facial Video Sequences” - MDPI Applied Sciences

八、总结

关键要点

  1. PERCLOS 是疲劳检测的金标准:Euro NCAP 和 NHTSA 认可的量化指标
  2. 微睡眠检测是法规要求:1-2 秒闭眼触发一级警告,≥3 秒触发二级警告
  3. KSS 映射需要标定:PERCLOS 阈值因人而异,需要个性化标定
  4. 红外补光是必需品:夜间和墨镜场景必须使用主动红外照明
  5. 嵌入式部署需优化:模型量化、算子融合、NPU 加速

IMS 开发建议

阶段 重点任务 验证标准
MVP 人脸检测 + EAR + PERCLOS 内部测试通过
合规 Euro NCAP 场景验证 5/5 场景通过
量产 嵌入式部署 + 优化 推理时延 <50ms

发布日期: 2026-04-16
标签: Euro NCAP, DMS, PERCLOS, KSS, 疲劳检测, 眼动追踪, IMS
更新记录: v1.0 - 初始版本,完整实现 PERCLOS 算法与 Euro NCAP 合规指南


Euro NCAP 2026 疲劳检测深度解析:PERCLOS 算法实现与 KSS 映射完整指南
https://dapalm.com/2026/04/16/2026-04-16-euro-ncap-2026-fatigue-detection-perclos-kss/
作者
Mars
发布于
2026年4月16日
许可协议