PERCLOS疲劳检测算法优化:从EAR计算到多模态融合

引言:疲劳检测的核心指标

PERCLOS(Percentage of Eyelid Closure Over Time)

眼睑闭合时间占比

计算公式

1
2
3
4
5
PERCLOS = (t_closed / t_total) × 100%

其中:
- t_closed:眼睑闭合时间
- t_total:总观测时间(通常60秒)

一、传统PERCLOS算法

1.1 关键特征

特征 说明 计算方法
EAR Eye Aspect Ratio (眼睑开合比)
MAR Mouth Aspect Ratio 嘴部开合比)
PERCLOS 眼睑闭合占比 时间累积
眨眼频率 眨眼次数/分钟 峰值检测

1.2 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
import cv2
import numpy as np

class EARCalculator:
"""
EAR计算器
"""
def __init__(self, landmarks_6points):
# 6点眼部关键点
self.left_eye = landmarks_6points[:6]
self.right_eye = landmarks_6points[6:]

def calculate_ear(self, landmarks):
"""
计算EAR
"""
# 左眼EAR
left_ear = self.calculate_single_ear(self.left_eye)

# 右眼EAR
right_ear = self.calculate_single_ear(self.right_eye)

# 平均EAR
ear = (left_ear + right_ear) / 2

return ear

def calculate_single_ear(self, eye_landmarks):
"""
计算单眼EAR
"""
# 纵向距离
vertical_1 = np.linalg.norm(eye_landmarks[1] - eye_landmarks[5])
vertical_2 = np.linalg.norm(eye_landmarks[2] - eye_landmarks[4])
vertical = (vertical_1 + vertical_2) / 2

# 横向距离
horizontal = np.linalg.norm(eye_landmarks[0] - eye_landmarks[3])

# EAR公式
ear = vertical / (2 * horizontal)

return ear

# 使用示例
ear_calculator = EARCalculator(landmarks_6points)
ear = ear_calculator.calculate_ear(frame_landmarks)

# 判断眨眼
EAR_THRESHOLD = 0.25
if ear < EAR_THRESHOLD:
print("眨眼检测")

1.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
class PERCLOSCalculator:
"""
PERCLOS计算器
"""
def __init__(self, window_size=60):
self.window_size = window_size # 秒
self.blink_states = [] # 保存眨眼状态
self.timestamp_offset = 0

def update(self, current_ear, timestamp):
"""
更新PERCLOS
"""
# 判断是否眨眼
is_blinking = current_ear < 0.25

self.blink_states.append({
'timestamp': timestamp,
'is_blinking': is_blinking
})

# 保持窗口大小
if len(self.blink_states) > self.window_size * 30: # 30fps
self.blink_states = self.blink_states[-self.window_size * 30:]

# 计算PERCLOS
perclose = self.compute_perclos()

return perclose

def compute_perclos(self):
"""
计算PERCLOS
"""
# 统计眨眼时长
total_blink_time = 0

for i in range(1, len(self.blink_states)):
prev = self.blink_states[i-1]
curr = self.blink_states[i]

if prev['is_blinking'] and not curr['is_blinking']:
# 眨眼结束
blink_duration = curr['timestamp'] - prev['timestamp']
total_blink_time += blink_duration

# 计算总时长
total_time = self.blink_states[-1]['timestamp'] - self.blink_states[0]['timestamp']

# PERCLOS
perclose = (total_blink_time / total_time) * 100

return perclose

# 使用示例
perclos_calculator = PERCLOSCalculator(window_size=60)

for frame in video_frames:
ear = calculate_ear(frame)
perclose = perclos_calculator.update(ear, frame.timestamp)

# 疲劳判断
if perclose > 80: # 80%眼睑闭合
print(f"⚠️ 疲劳检测:PERCLOS={perclose}%")

二、算法优化

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
class OptimizedPERCLOS:
"""
优化的PERCLOS计算器
"""
def __init__(self):
# 滑动窗口
self.window = collections.deque(maxlen=1800) # 60秒@30fps

# 峰值检测
self.peak_detector = PeakDetector(threshold=0.15, min_distance=10)

def update(self, ear, timestamp):
"""
更新窗口
"""
self.window.append((ear, timestamp))

# 检测眨眼峰值
blinks = self.peak_detector.detect([e[0] for e in self.window])

# 计算闭合时长
closed_duration = self.compute_closed_duration(blinks, self.window)

# 计算PERCLOS
perclose = closed_duration / len(self.window) * 100

return {
'perclos': perclose,
'blinks': len(blinks),
'blink_rate': len(blinks) / (len(self.window) / 30) # 每秒
}

def compute_closed_duration(self, blinks, window):
"""
计算闭合时长
"""
closed_frames = 0

for blink_start, blink_end in blinks:
# 统计峰值之间低于阈值的帧数
for i in range(blink_start, blink_end):
if i < len(window) and window[i][0] < 0.25:
closed_frames += 1

return closed_frames

2.2 MAR特征

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
class MARCalculator:
"""
MAR计算器(嘴部开合比)
"""
def __init__(self, mouth_landmarks):
self.landmarks = mouth_landmarks

def calculate_mar(self, landmarks):
"""
计算MAR
"""
# 嘴部关键点
left_mouth = landmarks[48]
right_mouth = landmarks[54]
upper_lip = landmarks[51]
lower_lip = landmarks[57]

# 横向距离
horizontal = np.linalg.norm(right_mouth - left_mouth)

# 纵向距离
vertical = np.linalg.norm(upper_lip - lower_lip)

# MAR
mar = vertical / (2 * horizontal)

return mar

# 使用场景
mar_calculator = MARCalculator(mouth_landmarks)
mar = mar_calculator.calculate_mar(frame_landmarks)

# 哈欠检测(MAR > 0.5且持续时间)
if mar > 0.5:
print("哈欠检测")

三、多模态融合

3.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
class MultiModalFatigueDetector:
"""
多模态疲劳检测器
"""
def __init__(self):
# 模态检测器
self.perclos_detector = PERCLOSCalculator()
self.mar_detector = MARCalculator()
self.head_nodding_detector = HeadNoddingDetector()

# 融合器
self.fusion = FusionNetwork()

def detect(self, landmarks, frame):
"""
多模态检测
"""
# 1. 特征提取
features = {
'perclos': self.perclos_detector.update(calculate_ear(landmarks), frame.timestamp),
'mar': self.mar_detector.calculate_mar(landmarks),
'head_nodding': self.head_nodding_detector.detect(landmarks)
}

# 2. 融合判断
fatigue_level = self.fusion.predict(features)

# 3. 分级报警
if fatigue_level > 0.8:
return 'critical'
elif fatigue_level > 0.6:
return 'warning'
elif fatigue_level > 0.4:
return 'mild'
else:
return 'normal'

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

class FusionNetwork(nn.Module):
"""
融合网络
"""
def __init__(self):
super().__init__()

# 特征编码
self.perclos_encoder = nn.Linear(1, 32)
self.mar_encoder = nn.Linear(1, 32)
self.head_encoder = nn.Linear(1, 32)

# 融合层
self.fusion = nn.Sequential(
nn.Linear(96, 64), # 32*3
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(64, 32),
nn.ReLU()
)

# 分类层
self.classifier = nn.Linear(32, 1) # 输出疲劳程度

def forward(self, perclos, mar, head_nodding):
"""
前向传播
"""
# 特征编码
f_perclos = self.perclos_encoder(perclos)
f_mar = self.mar_encoder(mar)
f_head = self.head_encoder(head_nodding)

# 融合
fused = torch.cat([f_perclos, f_mar, f_head], dim=1)
features = self.fusion(fused)

# 分类
fatigue_level = torch.sigmoid(self.classifier(features))

return fatigue_level

四、性能优化

4.1 实时性优化

优化方法 效果
关键点缓存 减少50%检测时间
滑动窗口 固定内存占用
向量化计算 提升推理速度3倍
模型轻量化 延迟降低60%

4.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
class EmbeddedFatigueDetector:
"""
嵌入式疲劳检测器
"""
def __init__(self, model_path):
# 加载轻量化模型
self.model = torch.jit.load(model_path)

# 输入预处理
self.preprocessor = Preprocessor()

def detect(self, frame):
"""
检测(<30ms)
"""
import time
start = time.time()

# 1. 预处理
normalized = self.preprocessor.normalize(frame)

# 2. 推理
with torch.no_grad():
landmarks = self.model(normalized)

# 3. 特征计算
ear = calculate_ear(landmarks)
mar = calculate_mar(landmarks)

# 4. PERCLOS
perclose = self.perclos_calculator.update(ear, time.time())

# 5. 判断
is_fatigued = perclose > 80 or mar > 0.5

elapsed = (time.time() - start) * 1000 # ms

return {
'fatigue_level': 'high' if is_fatigued else 'low',
'perclos': perclose,
'mar': mar,
'latency_ms': elapsed
}

五、Euro NCAP要求

5.1 测试标准

指标 要求
检测精度 >95%
误报率 <5%
响应时间 <2秒
PERCLOS阈值 80%
报警延迟 <1秒

5.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
class EuroNCAPValidator:
"""
Euro NCAP验证器
"""
def __init__(self):
self.test_cases = load_eurocap_cases()

def validate(self, detector):
"""
验证检测器
"""
results = {
'true_positive': 0,
'false_positive': 0,
'false_negative': 0,
'total': 0
}

for case in self.test_cases:
# 检测
prediction = detector.detect(case['frame'])

# 评估
if case['ground_truth'] == 'fatigued':
if prediction['fatigue_level'] != 'low':
results['true_positive'] += 1
else:
results['false_negative'] += 1
else:
if prediction['fatigue_level'] != 'low':
results['false_positive'] += 1

results['total'] += 1

# 计算指标
accuracy = (results['true_positive'] + results['true_negative']) / results['total']
precision = results['true_positive'] / (results['true_positive'] + results['false_positive'])
recall = results['true_positive'] / (results['true_positive'] + results['false_negative'])

return {
'accuracy': accuracy,
'precision': precision,
'recall': recall,
'f1': 2 * precision * recall / (precision + recall)
}

六、总结

6.1 优化策略

策略 说明
多特征融合 EAR+MAR+头位
时间累积 滑动窗口PERCLOS
轻量化部署 <30ms延迟
动态阈值 自适应调整

6.2 实施建议

  1. 短期:PERCLOS基础算法
  2. 中期:多模态融合
  3. 长期:端到端深度学习

参考文献

  1. Scientific Reports. “Optimized driver fatigue detection using multimodal neural networks.” 2025.
  2. Nature. “A dense multi-pooling convolutional network for fatigue driving detection.” 2025.
  3. Euro NCAP. “Safe Driving Assessment Protocol.” 2026.

本文是IMS疲劳检测系列文章之一,上一篇:3D姿态估计


PERCLOS疲劳检测算法优化:从EAR计算到多模态融合
https://dapalm.com/2026/03/13/2026-03-13-PERCLOS疲劳检测算法优化-从EAR计算到多模态融合/
作者
Mars
发布于
2026年3月13日
许可协议