铁路驾驶员视觉疲劳检测:YOLOv8轻量级CNN实现与实车验证

论文信息

  • 标题: A vision-based drowsiness detection system for railway operators using lightweight convolutional neural networks
  • 期刊: Frontiers in Future Transportation, Volume 6, 2025
  • 链接: https://doi.org/10.3389/ffutr.2025.1677442
  • 发表日期: 2025年11月11日
  • 核心方法: YOLOv8-cls + 面部关键点 + 833ms闭眼阈值

核心创新

首次在真实铁路运营环境(Lima地铁Linea Uno)中部署YOLOv8轻量级CNN(~2.7M参数),实现96.8%疲劳检测准确率。系统在早/午/夜三个时段的实车测试中验证了鲁棒性,明确了太阳眩光是视觉疲劳检测的主要降质因素。

关键差异点(vs 汽车DMS):

  • 铁路驾驶舱光照更极端:隧道频繁切换、阳光直射仪表盘反射
  • 驾驶员坐姿更固定 → 面部检测ROI更稳定
  • 驾驶员操作频率更低 → 疲劳累积更缓慢但更隐蔽

方法详解

1. 整体架构

2. 数据采集

参数 值
摄像头位置 驾驶员正前方 60-65cm
视频格式 MP4, 30fps, 478×850px
原始帧数 17,476
采样策略 每6帧取1帧 → 2,913帧
数据增强后 6,991帧
划分比例 训练87% / 验证6.5% / 测试6.5%

3. 面部关键点与疲劳指标

使用 cvzone 库(基于MediaPipe)检测12个面部关键点:

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
import cv2
from cvzone.FaceMeshModule import FaceMeshDetector

class RailwayFatigueDetector:
"""
铁路驾驶员疲劳检测器
基于 YOLOv8n-cls + 面部关键点

论文: Front. Future Transp., 2025
"""

def __init__(self):
self.detector = FaceMeshDetector(maxFaces=1)
# 闭眼阈值:关键点比值 < 18 判定为闭眼
self.EYE_CLOSED_THRESHOLD = 18
# 打哈欠阈值:嘴部关键点比值 > 35
self.YAWN_THRESHOLD = 35
# 闭眼持续时间阈值(ms):833ms = 约25帧@30fps
self.EYE_CLOSED_DURATION_MS = 833
self.eye_closed_start = None
self.fatigue_detected = False

def calculate_ear(self, eye_landmarks):
"""
计算眼睛纵横比(Eye Aspect Ratio的变体)

Args:
eye_landmarks: 4个眼睛关键点 [(x1,y1), (x2,y2), (x3,y3), (x4,y4)]

Returns:
ratio: 眼睛开度比值
"""
# 水平距离
h_dist = ((eye_landmarks[0][0] - eye_landmarks[3][0])**2 +
(eye_landmarks[0][1] - eye_landmarks[3][1])**2) ** 0.5
# 垂直距离
v_dist = ((eye_landmarks[1][1] - eye_landmarks[2][1]))

if v_dist == 0:
return 999 # 避免除零

ratio = h_dist / v_dist
return ratio

def process_frame(self, frame):
"""
处理单帧视频

Args:
frame: BGR图像

Returns:
dict: {
'eye_state': 'open' | 'closed',
'fatigue': bool,
'ear': float
}
"""
img, faces = self.detector.findFaceMesh(frame, draw=False)

if not faces:
return {'eye_state': 'no_face', 'fatigue': False, 'ear': 0}

face = faces[0]

# 左眼关键点(MediaPipe索引)
left_eye = [face[33], face[160], face[158], face[133]]
right_eye = [face[362], face[385], face[387], face[263]]

left_ear = self.calculate_ear(left_eye)
right_ear = self.calculate_ear(right_eye)
avg_ear = (left_ear + right_ear) / 2

eye_closed = avg_ear > self.EYE_CLOSED_THRESHOLD

import time
current_time = time.time() * 1000 # ms

if eye_closed:
if self.eye_closed_start is None:
self.eye_closed_start = current_time
duration = current_time - self.eye_closed_start
if duration >= self.EYE_CLOSED_DURATION_MS:
self.fatigue_detected = True
return {
'eye_state': 'closed',
'fatigue': True,
'ear': avg_ear,
'closed_duration_ms': duration
}
else:
self.eye_closed_start = None
self.fatigue_detected = False

return {
'eye_state': 'closed' if eye_closed else 'open',
'fatigue': self.fatigue_detected,
'ear': avg_ear
}


# 实际测试代码
if __name__ == "__main__":
import numpy as np

detector = RailwayFatigueDetector()

# 模拟铁路驾驶视频处理
cap = cv2.VideoCapture("railway_driver_test.mp4")

fatigue_events = []
frame_count = 0
fps = 30

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

result = detector.process_frame(frame)
frame_count += 1

if result.get('fatigue'):
timestamp = frame_count / fps
fatigue_events.append({
'time_s': timestamp,
'ear': result['ear'],
'closed_ms': result.get('closed_duration_ms', 0)
})
print(f"[{timestamp:.1f}s] WARN: 疲劳检测 - 闭眼{result.get('closed_duration_ms', 0):.0f}ms")

cap.release()

print(f"\n=== 检测结果 ===")
print(f"总帧数: {frame_count}")
print(f"疲劳事件: {len(fatigue_events)} 次")
print(f"平均EAR: {np.mean([e['ear'] for e in fatigue_events]):.2f}" if fatigue_events else "无疲劳事件")

4. YOLOv8模型训练

三个变体对比:

模型 参数量 Epochs Batch 学习率 优化器 最优Epoch Loss
YOLOv8n-cls ~2.7M 500 1024 0.01 AdamW 145 0.068
YOLOv8s-cls ~11M 100 256 0.01 AdamW 100 0.047
YOLOv8m-cls ~26M 500 1024 0.01 AdamW 64 0.055

选型结论: YOLOv8n-cls最优——2.7M参数足够,更大模型过拟合。

5. YOLOv8训练代码

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
from ultralytics import YOLO
import torch

def train_railway_drowsiness_model():
"""
YOLOv8 铁路驾驶员疲劳分类模型训练

数据集: 6,991张图片 (Awake/Drowsy二分类)
训练环境: Python 3.8, PyTorch, NVIDIA A100 40GB
"""
# 加载预训练模型
model = YOLO('yolov8n-cls.pt') # nano版本,2.7M参数

# 训练参数(按论文Table 1)
results = model.train(
data='railway_drowsiness_dataset/',
epochs=500,
batch=1024,
imgsz=224,
lr0=0.01,
optimizer='AdamW',
# 正则化
weight_decay=0.01,
label_smoothing=0.1,
# 数据增强
hsv_h=0.015, # 色调
hsv_s=0.7, # 饱和度
hsv_v=0.4, # 明度
degrees=0, # 旋转(论文用±15°,YOLO内置)
translate=0.1,
scale=0.5,
fliplr=0.5,
# 早停
patience=50,
# 保存
save=True,
project='railway_drowsiness',
name='yolov8n_cls_v1'
)

# 验证
metrics = model.val()
print(f"Accuracy: {metrics.accuracy:.4f}")
print(f"Precision: {metrics.precision:.4f}")
print(f"Recall: {metrics.recall:.4f}")

return model

# 导出ONNX用于部署
def export_for_deployment(model_path):
model = YOLO(model_path)
# 导出ONNX
model.export(format='onnx', imgsz=224)
# 导出INT8量化模型
model.export(format='engine', imgsz=224, int8=True)
print("导出完成: ONNX + TensorRT INT8")

实验结果

实车测试5名驾驶员

视频 时段 总帧数 疲劳帧 准确率 精确率 召回率
V1 早班 1,890 1,110 98.04% 97.79% 98.93%
V2 早班 2,250 1,230 96.22% 97.62% 95.72%
V3 午班 1,740 1,050 94.83% 95.89% 95.89%
V4 午班 1,620 1,020 95.99% 97.33% 96.50%
V5 夜班 2,250 1,290 97.87% 97.51% 98.85%
平均 - - - 96.59% 97.23% 97.30%

关键发现

指标 结果 分析
整体准确率 96.8% 超过96%目标
早班准确率 97.13% 光照稳定,检测最佳
夜班准确率 97.87% 红外/低光环境稳定
午班准确率 95.41% 太阳眩光降质6%
误报率 ~6%(午班) 阳光反射导致面部识别失败
模型参数量 ~2.7M 可实时部署
闭眼阈值 833ms 对应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
import matplotlib.pyplot as plt
import numpy as np

# 按时段的准确率对比
shifts = ['Morning', 'Afternoon', 'Night']
accuracy = [97.13, 95.41, 97.87]
precision = [97.71, 96.61, 97.51]
recall = [97.33, 96.20, 98.85]

fig, ax = plt.subplots(figsize=(10, 6))
x = np.arange(len(shifts))
width = 0.25

ax.bar(x - width, accuracy, width, label='Accuracy', color='#2196F3')
ax.bar(x, precision, width, label='Precision', color='#4CAF50')
ax.bar(x + width, recall, width, label='Recall', color='#FF9800')

ax.set_ylabel('Percentage (%)')
ax.set_title('Railway Driver Drowsiness Detection Performance by Shift')
ax.set_xticks(x)
ax.set_xticklabels(shifts)
ax.legend()
ax.set_ylim(94, 100)

# 标注午班下降原因
ax.annotate('Solar glare\n−2.5% accuracy',
xy=(1, 95.41), xytext=(1.3, 94.5),
arrowprops=dict(arrowstyle='->', color='red'),
fontsize=9, color='red')
plt.tight_layout()
plt.savefig('railway_drowsiness_by_shift.png', dpi=150)
plt.show()

IMS开发启示

1. 汽车DMS→铁路DMS的技术迁移

技术要素 汽车DMS 铁路DMS 迁移难度
闭眼检测 EAR + PERCLOS EAR + 833ms阈值 ✅ 直接复用
面部检测 MediaPipe/RetinaFace MediaPipe + cvzone ✅ 相同
模型架构 YOLOv8n-cls YOLOv8n-cls ✅ 相同
光照鲁棒性 红外补光 隧道频繁切换 ⚠️ 需HDR
振动环境 中等 更强(钢轨) ⚠️ 需防抖
部署平台 QCS8255/NVIDIA 车载GPU 差异较大

2. 833ms闭眼阈值的合理性

论文依据: 833ms ≈ 25帧@30fps,对应PERCLOS的KSS评分≥7(明显困倦)。

IMS验证标准:

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
# IMS疲劳检测阈值验证
THRESHOLDS = {
'eye_closed_ms': 833, # 论文验证有效
'perclos_window_s': 60, # 60秒滑动窗口
'perclos_warn_pct': 30, # 30%闭眼→一级警告
'perclos_danger_pct': 50, # 50%闭眼→二级警告
'yawn_count_threshold': 3, # 3次/分钟→疲劳
'blink_rate_threshold': 25 # >25次/分钟→疲劳
}

def validate_fatigue_threshold(eye_openness_series, fps=30):
"""
按论文标准验证疲劳阈值

Args:
eye_openness_series: 眼睑开度序列 (0-1)
fps: 帧率

Returns:
fatigue_level: 0(清醒) | 1(轻度) | 2(中度) | 3(重度)
"""
window_frames = 60 * fps # 60秒窗口
threshold_ms = 833
threshold_frames = int(threshold_ms / 1000 * fps)

# 统计闭眼事件
closed_events = []
current_closed = 0

for val in eye_openness_series:
if val < 0.2: # 闭眼
current_closed += 1
else:
if current_closed >= threshold_frames:
closed_events.append(current_closed / fps * 1000)
current_closed = 0

# 计算PERCLOS
window = eye_openness_series[-window_frames:]
perclos = np.sum(window < 0.2) / window_frames * 100

# 判定等级
if perclos >= 50 or len(closed_events) >= 3:
return 3 # 重度疲劳
elif perclos >= 30 or len(closed_events) >= 1:
return 2 # 中度疲劳
elif perclos >= 15:
return 1 # 轻度疲劳
else:
return 0 # 清醒

3. 铁路DMS对IMS的优先级建议

优先级 功能 技术方案 周期
P0 闭眼+PERCLOS YOLOv8n + MediaPipe 2个月
P1 打哈欠检测 嘴部关键点比值>35 1个月
P1 光照鲁棒性 RGB-IR双模摄像头 3个月
P2 多模态融合 视觉+HRV+行为 6个月
P3 铁路信号关联 联列车控数据 12个月

4. 铁路vs汽车部署差异

部署环境对比:

维度 汽车 铁路 影响
光照变化 日夜+隧道 频繁隧道+眩光 需HDR+WDR
振动 中等 强(钢轨接头) 摄像头防抖
驾驶员距离 50-70cm 60-65cm 相似
驾驶时长 2-4小时 6-8小时 疲劳累积更长
操作频率 高(方向盘) 低(监控为主) 被动疲劳更突出
网络连接 4G/5G GSM-R/4G 边缘部署需求

5. 论文的局限性分析

局限 影响 IMS改进方案
仅5名测试者 统计显著性不足 扩展到≥50人验证
午班眩光降质6% 误报率偏高 加红外补光+HDR摄像头
未做PERCLOS 缺少时序累积 增加60s滑窗PERCLOS
无个体基线 统一阈值不精准 建立个体校准基线
无多模态 单一视觉 融合HRV+行为信号
无持续学习 模型不适应新用户 在线学习框架

性能对比

系统 平台 参数量 准确率 实时性 环境
本文(YOLOv8n) NVIDIA A100 2.7M 96.8% ✅ 实时 真实铁路
汽车DMS(典型) QCS8255 ~5M 95-97% ✅ 实时 真实道路
EEG穿戴式 专用芯片 N/A 92% ❌ 非实时 模拟器
传统CNN(VGG) GPU ~138M 93% ❌ 非实时 实验室

总结

这篇论文的核心价值在于真实铁路环境验证——不是模拟器,不是实验室,而是真实运营的地铁。96.8%准确率虽不算突破性,但在铁路场景下首次证明了轻量级CNN的可行性。

对IMS的3条核心建议:

  1. 833ms闭眼阈值可直接采用,已在真实运营环境验证
  2. 午班眩光问题是视觉DMS通病,必须用RGB-IR双模摄像头解决
  3. 铁路DMS是IMS技术栈的自然扩展,核心算法可复用,需适配光照和振动环境

论文链接: https://doi.org/10.3389/ffutr.2025.1677442
数据集: 6,991帧(可联系UNTELS申请)