铁路驾驶员疲劳检测:基于 YOLOv8 轻量 CNN 的实时视觉方案(Frontiers 2025 论文解读)

发布时间: 2026-09-05
分类: 论文解读 / 疲劳检测 / 铁路座舱
标签: YOLOv8, railway, drowsiness detection, CNN, lightweight, Lima Metro


论文信息

项目 内容
标题 A vision-based drowsiness detection system for railway operators using lightweight convolutional neural networks
期刊 Frontiers in Future Transportation, 2025
DOI 10.3389/ffutr.2025.1677442
发表日期 2025-11-11(期刊在线发布)
研究场景 Lima Metro(利马地铁)Linea Uno
模型 YOLOv8n-cls / YOLOv8s-cls / YOLOv8m-cls

1. 研究背景

1.1 铁路疲劳风险

  • 人工错误与疲劳是列车运营中的严重风险
  • 困倦导致:反应力下降、选择性注意降低、短期记忆错误
  • 轻则导致错误停车影响运营,重则导致安全事故

1.2 与汽车 DMS 的差异

维度 汽车DMS 铁路DMS
驾驶员位置 方向盘后 封闭驾驶室
光照条件 多变 隧道/日光交替
摄像头距离 60-80cm 60-65cm(本文)
运动特征 头部/眼动幅度小 幅度更小
部署环境 前装为主 后装为主
误报容忍 中 低(频繁误报影响运营)

1.3 本文目标

构建轻量级 CNN 模型(~2.7M参数),在列车驾驶室实现 >96% 准确率的实时疲劳检测


2. 方法详解

2.1 数据采集

参数 数值
采集设备 移动设备摄像头
视频格式 MP4
帧率 30 fps
分辨率 478 × 850 像素
摄像头距离 60-65 cm
采集场景 Linea Uno 列车驾驶室
时段 早上/下午/夜间

2.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
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
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
import cv2
import numpy as np
from typing import List, Tuple
import mediapipe as mp

class RailwayDrowsinessPreprocessor:
"""
铁路驾驶员疲劳检测数据预处理

论文方法复现:
1. 视频帧提取与降采样
2. 面部关键点检测(12个关键点)
3. EAR(眼睑开度比)计算
4. MAR(嘴巴开度比)计算
5. 数据增强
"""

def __init__(self):
# 使用 MediaPipe + cvzone 进行面部关键点检测
self.mp_face_mesh = mp.solutions.face_mesh
self.face_mesh = self.mp_face_mesh.FaceMesh(
max_num_faces=1,
refine_landmarks=True,
min_detection_confidence=0.5,
min_tracking_confidence=0.5
)

# 眼睛关键点索引(MediaPipe 468点模型)
self.LEFT_EYE = [33, 160, 158, 133, 153, 144] # 6点
self.RIGHT_EYE = [362, 385, 387, 263, 373, 380] # 6点
self.MOUTH = [13, 14, 78, 308] # 4点

def extract_frames(
self,
video_path: str,
target_fps: int = 30,
sample_ratio: int = 6
) -> List[np.ndarray]:
"""
帧提取与降采样

论文方法:每6帧取1帧(2,913/17,476)

Args:
video_path: 视频文件路径
target_fps: 目标帧率
sample_ratio: 采样比率

Returns:
frames: 采样后的帧列表
"""
cap = cv2.VideoCapture(video_path)
frames = []
frame_idx = 0

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

# 每6帧取1帧
if frame_idx % sample_ratio == 0:
frames.append(frame)

frame_idx += 1

cap.release()
return frames

def detect_facial_landmarks(
self,
frame: np.ndarray
) -> Tuple[np.ndarray, bool]:
"""
检测12个面部关键点

论文方法:使用 cvzone/MediaPipe 检测
- 左眼4点 + 右眼4点 + 嘴巴4点 = 12点
"""
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
results = self.face_mesh.process(rgb_frame)

if results.multi_face_landmarks:
landmarks = results.multi_face_landmarks[0].landmark
points = []
for idx in self.LEFT_EYE[:4] + self.RIGHT_EYE[:4] + self.MOUTH:
points.append([landmarks[idx].x, landmarks[idx].y])
return np.array(points), True

return np.array([]), False

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

EAR = (|p2-p6| + |p3-p5|) / (2 * |p1-p4|)

论文阈值:EAR < 18 判定为闭眼
注:论文使用的阈值与标准 EAR 公式不同,
可能是 cvzone 的 Ratio 实现

Args:
eye_points: 4个眼角点 (x, y)

Returns:
ear: 眼睑开度比值
"""
# 简化为4点版本的 EAR
# p1=左角, p2=上, p3=右角, p4=下
horizontal = np.linalg.norm(eye_points[0] - eye_points[2])
vertical = np.linalg.norm(eye_points[1] - eye_points[3])

if horizontal > 0:
ratio = vertical / horizontal
else:
ratio = 0

return ratio

def calculate_mar(self, mouth_points: np.ndarray) -> float:
"""
计算 Mouth Aspect Ratio (MAR)

论文阈值:MAR > 35 判定为打哈欠
"""
# 嘴角水平距离
horizontal = np.linalg.norm(mouth_points[0] - mouth_points[2])
# 嘴唇垂直距离
vertical = np.linalg.norm(mouth_points[1] - mouth_points[3])

if horizontal > 0:
ratio = vertical / horizontal * 100 # 论文使用放大系数
else:
ratio = 0

return ratio

def augment_frame(
self,
frame: np.ndarray,
rotation_range: Tuple[float, float] = (-15, 15),
brightness_range: Tuple[float, float] = (-0.2, 0.2)
) -> np.ndarray:
"""
数据增强

论文方法:
- 旋转:-15° 到 +15°
- 亮度:-20% 到 +20%

使用 Roboflow 进行数据增强
"""
h, w = frame.shape[:2]

# 随机旋转
angle = np.random.uniform(*rotation_range)
M = cv2.getRotationMatrix2D((w/2, h/2), angle, 1)
frame = cv2.warpAffine(frame, M, (w, h))

# 随机亮度调整
brightness = np.random.uniform(*brightness_range)
frame = np.clip(frame * (1 + brightness), 0, 255).astype(np.uint8)

return frame


# ============ YOLOv8 分类模型 ============

class RailwayDrowsinessClassifier:
"""
基于 YOLOv8 的铁路驾驶员疲劳分类器

论文对比三个变体:
| 模型 | 参数量 | 最佳Epoch | 损失值 |
|------|--------|----------|--------|
| YOLOv8n-cls | ~2.7M | 145 | 0.06787 |
| YOLOv8s-cls | ~11M | 100 | 0.04662 |
| YOLOv8m-cls | ~26M | 64 | 0.05495 |

最终选择 YOLOv8n-cls(轻量级最优)

训练配置:
- Image size: 224 × 224 × 3
- Optimizer: AdamW
- Learning rate: 0.01
- Batch size: 1024 (n) / 256 (s)
"""

def __init__(self, model_variant: str = 'n'):
"""
Args:
model_variant: 'n' (nano), 's' (small), 'm' (medium)
"""
self.variant = model_variant
self.model_name = f"yolov8{model_variant}-cls"

def get_config(self) -> dict:
"""返回论文训练配置"""
configs = {
'n': {
'epochs': 500,
'batch_size': 1024,
'learning_rate': 0.01,
'image_size': 224,
'optimizer': 'AdamW',
'best_epoch': 145,
'final_loss': 0.06787,
'params': 2_700_000 # ~2.7M
},
's': {
'epochs': 100,
'batch_size': 256,
'learning_rate': 0.01,
'image_size': 224,
'optimizer': 'AdamW',
'best_epoch': 100,
'final_loss': 0.04662,
'params': 11_000_000
},
'm': {
'epochs': 500,
'batch_size': 1024,
'learning_rate': 0.01,
'image_size': 224,
'optimizer': 'AdamW',
'best_epoch': 64,
'final_loss': 0.05495,
'params': 26_000_000
}
}
return configs[self.variant]

def get_training_script(self) -> str:
"""生成训练脚本"""
config = self.get_config()
return f"""
# 铁路驾驶员疲劳检测 - YOLOv8{self.variant}-cls 训练脚本
# 论文复现

from ultralytics import YOLO

# 加载预训练模型
model = YOLO('yolov8{self.variant}-cls.pt')

# 训练
results = model.train(
data='./railway_drowsiness_dataset/', # 数据集路径
epochs={config['epochs']},
batch={config['batch_size']},
lr0={config['learning_rate']},
imgsz={config['image_size']},
optimizer='{config['optimizer']}',
# 论文使用 Roboflow 进行数据增强
hsv_h=0.0, # 关闭色彩增强(铁路场景色彩固定)
hsv_s=0.0,
hsv_v=0.2, # 亮度 ±20%
degrees=15.0, # 旋转 ±15°
translate=0.1,
scale=0.0,
fliplr=0.0, # 水平翻转(人脸不翻转)
flipud=0.0,
patience=50, # 早停
device='cuda:0',
workers=8
)

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


# ============ 实时检测系统 ============

class RealTimeDrowsinessSystem:
"""
实时铁路驾驶员疲劳检测系统

部署硬件:NVIDIA A100 40GB (论文使用)
实际量产可用更轻量设备

检测逻辑:
1. 实时视频流 → 面部检测
2. EAR < 阈值 + 持续时间 > 833ms → 困倦
3. MAR > 阈值 → 打哈欠
4. 触发视觉/声音告警
"""

def __init__(self, config: dict = None):
config = config or {}
self.ear_threshold = config.get('ear_threshold', 0.18)
self.mar_threshold = config.get('mar_threshold', 0.35)
self.eye_closure_duration_threshold = 0.833 # 833ms

self.eye_closure_start = None
self.drowsy_state = False
self.alert_count = 0

def process_frame(
self,
frame: np.ndarray,
eye_landmarks: np.ndarray,
mouth_landmarks: np.ndarray,
timestamp: float
) -> dict:
"""
处理单帧

Args:
frame: 视频帧
eye_landmarks: 眼睛关键点
mouth_landmarks: 嘴巴关键点
timestamp: 时间戳(秒)

Returns:
result: 检测结果
"""
ear = self._calc_ear(eye_landmarks)
mar = self._calc_mar(mouth_landmarks)

result = {
'ear': ear,
'mar': mar,
'is_drowsy': False,
'is_yawning': False,
'alert_level': 0,
'timestamp': timestamp
}

# 检测闭眼
if ear < self.ear_threshold:
if self.eye_closure_start is None:
self.eye_closure_start = timestamp
else:
duration = timestamp - self.eye_closure_start
if duration >= self.eye_closure_duration_threshold:
result['is_drowsy'] = True
result['alert_level'] = 1
self.alert_count += 1
else:
self.eye_closure_start = None

# 检测打哈欠
if mar > self.mar_threshold:
result['is_yawning'] = True
if result['alert_level'] < 1:
result['alert_level'] = 1

# 连续困倦 → 升级告警
if self.alert_count >= 3:
result['alert_level'] = 2

return result

def _calc_ear(self, points: np.ndarray) -> float:
v1 = np.linalg.norm(points[1] - points[5])
v2 = np.linalg.norm(points[2] - points[4])
h = np.linalg.norm(points[0] - points[3])
return (v1 + v2) / (2.0 * h) if h > 0 else 0

def _calc_mar(self, points: np.ndarray) -> float:
v = np.linalg.norm(points[1] - points[3])
h = np.linalg.norm(points[0] - points[2])
return v / h if h > 0 else 0


if __name__ == "__main__":
# 预处理器测试
preprocessor = RailwayDrowsinessPreprocessor()
print("预处理模块初始化完成")

# 模型配置
classifier = RailwayDrowsinessClassifier('n')
config = classifier.get_config()
print(f"\n模型: YOLOv8n-cls")
print(f"参数量: {config['params']:,}")
print(f"训练轮数: {config['epochs']}")
print(f"最佳轮次: {config['best_epoch']}")
print(f"最终损失: {config['final_loss']}")

# 训练脚本
print("\n训练脚本:")
print(classifier.get_training_script())

# 实时系统
system = RealTimeDrowsinessSystem()
print("\n实时检测系统初始化完成")
print(f"闭眼阈值: EAR < {system.ear_threshold}")
print(f"闭眼时长阈值: {system.eye_closure_duration_threshold * 1000:.0f}ms")
print(f"打哈欠阈值: MAR > {system.mar_threshold}")

print("\n=== 论文报告结果 ===")
print("总准确率: 96.8%")
print("精确率: 97.28%")
print("召回率: 97.46%")
print("\n各时段准确率:")
print(" 早上: ~98.0%")
print(" 下午: ~95.4% (日光影响)")
print(" 夜间: ~97.0%")

3. 实验结果

3.1 数据集统计

项目 数值
总采集帧数 17,476
采样后帧数 2,913
增强后帧数 6,991
训练集 87% (6,082)
验证集 6.5% (454)
测试集 6.5% (454)
类别 2(Awake / Drowsy)

3.2 性能对比

模型 参数量 最佳Epoch 损失 准确率 推理速度
YOLOv8n-cls ~2.7M 145 0.06787 96.8% 最快
YOLOv8s-cls ~11M 100 0.04662 96.5% 中等
YOLOv8m-cls ~26M 64 0.05495 96.2% 较慢

关键发现:YOLOv8n-cls 以最少参数达到最高准确率,轻量化反而是最优选择。

3.3 各时段性能

时段 视频编号 帧数 准确率 精确率 召回率 影响
早上 V1 1,890 98.04% 97.79% 98.93% 光照最佳
早上 V2 2,250 96.22% 96.85% 96.33% 光照良好
下午 V3 2,100 94.81% 95.50% 95.00% 日光眩光影响
下午 V4 1,750 95.43% 96.00% 95.24% 光照变化
夜间 V5 1,950 97.50% 97.80% 97.50% 红外/低光

3.4 环境因素影响

环境因素 影响程度 原因 缓解方案
强日光眩光 🔴 高 过曝导致面部关键点丢失 HDR 摄像头 + 自动曝光
隧道交替 🟡 中 明暗快速切换致检测波动 自适应曝光 + 红外补光
夜间低光 🟢 低 红外摄像头效果好 940nm 红外补光
振动 🟡 中 帧间运动模糊 防抖算法 + 全局快门

4. 与汽车 DMS 的对比与启示

4.1 汽车可借鉴的技术

技术 铁路方案 汽车适配性
EAR < 18 阈值 简化4点EAR ✅ 可直接使用
833ms 闭眼判定 基于时长阈值 ✅ 标准方案
YOLOv8n-cls 2.7M参数轻量模型 ✅ 边缘部署理想选择
数据增强策略 旋转±15°+亮度±20% ✅ 通用策略
时段性能分析 早/午/夜分别评估 ✅ 应纳入测试规范

4.2 铁路特有挑战

挑战 说明 汽车是否存在
驾驶室封闭 光照完全依赖窗外 部分类似
隧道频繁切换 明暗交替剧烈 较少
操作员动作幅度小 铁路驾驶动作标准化 比汽车更小
误报容忍度极低 影响运营效率 汽车容忍度较高

5. 对 IMS 开发的落地建议

5.1 部署方案

模块 推荐方案 说明
检测模型 YOLOv8n-cls 2.7M 参数,边缘部署
面部关键点 MediaPipe FaceMesh 468点,CPU可运行
闭眼检测 EAR + 833ms 时长 成熟方案
打哈欠检测 MAR > 0.35 辅助特征
告警逻辑 三级告警 视觉→听觉→触觉

5.2 验证测试场景

场景ID 描述 前置条件 预期 判定标准
RW-01 正常驾驶(早晨) 光照>500 lux 无误报 准确率>97%
RW-02 闭眼3秒(模拟微睡眠) 正常光照 ≤1s检测 检出率>95%
RW-03 打哈欠+闭眼 正常光照 双重检测 检出率>90%
RW-04 强日光眩光 >2000 lux 降级提示 准确率>90%
RW-05 隧道交替行驶 明暗切换5次/分钟 平稳无频繁误报 误报率<5%
RW-06 夜间驾驶 <10 lux 红外检测正常 准确率>95%

6. 关键结论

6.1 论文核心贡献

  1. 轻量级方案验证:2.7M 参数 YOLOv8n 在铁路场景达到 96.8% 准确率
  2. 时段性能评估:下午日光是主要影响因素(94.8% vs 早上98%)
  3. 低成本部署:使用移动设备摄像头即可采集,适合后装

6.2 局限性

  1. 数据集规模小:2,913原始帧、5名参与者
  2. 单一场景:仅 Lima Metro Linea Uno
  3. 无认知分心检测:仅疲劳/打哈欠
  4. 强日光降级:未解决眩光问题
  5. 无多传感器融合:论文提到未来加入 EEG

6.3 对 IMS 的优先级建议

建议 优先级 理由
复用 YOLOv8n-cls 方案 🔴 高 轻量且高精度
加入时段测试规范 🟡 中 光照影响显著
增加隧道场景测试 🟡 中 铁路特有挑战
预留 EEG 接口 🟢 低 未来多模态融合

7. 参考资源


本文为论文详细解读与代码复现。 铁路驾驶室与汽车座舱在疲劳检测上有共性也有差异,YOLOv8n-cls 的轻量方案对 IMS 边缘部署有直接参考价值。