铁路驾驶员视觉疲劳检测: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. 整体架构
flowchart LR
A[车载摄像头 30fps] --> B[面部关键点检测<br/>OpenCV + MediaPipe]
B --> C{闭眼阈值 <18?}
C --> |是| D[标记Drowsy帧]
C --> |否| E[标记Awake帧]
D --> F[YOLOv8n-cls 分类]
E --> F
F --> G[实时疲劳判定<br/>闭眼≥833ms]
G --> H[视觉/声音警告]
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 cv2from cvzone.FaceMeshModule import FaceMeshDetectorclass RailwayFatigueDetector : """ 铁路驾驶员疲劳检测器 基于 YOLOv8n-cls + 面部关键点 论文: Front. Future Transp., 2025 """ def __init__ (self ): self .detector = FaceMeshDetector(maxFaces=1 ) self .EYE_CLOSED_THRESHOLD = 18 self .YAWN_THRESHOLD = 35 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 ] 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 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:.1 f} s] WARN: 疲劳检测 - 闭眼{result.get('closed_duration_ms' , 0 ):.0 f} 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]):.2 f} " 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 YOLOimport torchdef train_railway_drowsiness_model (): """ YOLOv8 铁路驾驶员疲劳分类模型训练 数据集: 6,991张图片 (Awake/Drowsy二分类) 训练环境: Python 3.8, PyTorch, NVIDIA A100 40GB """ model = YOLO('yolov8n-cls.pt' ) 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 , 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:.4 f} " ) print (f"Precision: {metrics.precision:.4 f} " ) print (f"Recall: {metrics.recall:.4 f} " ) return model def export_for_deployment (model_path ): model = YOLO(model_path) model.export(format ='onnx' , imgsz=224 ) 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 pltimport numpy as npshifts = ['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 THRESHOLDS = { 'eye_closed_ms' : 833 , 'perclos_window_s' : 60 , 'perclos_warn_pct' : 30 , 'perclos_danger_pct' : 50 , 'yawn_count_threshold' : 3 , 'blink_rate_threshold' : 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 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 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条核心建议:
833ms闭眼阈值 可直接采用,已在真实运营环境验证
午班眩光问题 是视觉DMS通病,必须用RGB-IR双模摄像头解决
铁路DMS是IMS技术栈的自然扩展 ,核心算法可复用,需适配光照和振动环境
论文链接: https://doi.org/10.3389/ffutr.2025.1677442 数据集: 6,991帧(可联系UNTELS申请)