
简介本资源是一套基于深度学习的智能疲劳驾驶检测系统完整源码实现面向计算机视觉初学者、智能交通方向研究者及Python深度学习实践者旨在解决真实场景下驾驶员疲劳状态实时识别与预警这一关键安全问题。压缩包共84个文件总计105.27MB涵盖34个Python源码含数据预处理、MediaPipe面部关键点提取、眼动特征建模、YOLO/ONNX模型推理等核心模块、11个文本配置与说明文件含readme、参数配置、日志记录、7个JPG/JPEG图像样本、4个XML与4个PNG/WEBP界面资源以及.pt模型权重和.gitignore等工程必需文件。已有105人学习下载资源结构清晰按「基础知识→基础功能→疲劳检测→语音响应→系统界面」分层组织内置OpenCV、NumPy、MediaPipe等实战环境依赖说明并提供可直接运行的端到端检测脚本与可视化界面便于快速复现、调试与二次开发。 我盯着刚跑通的检测画面看了十分钟心里其实没底。程序里那个“疲劳预警”的红色框倒是跳得挺欢但仔细一看它只是在驾驶员打哈欠的时候弹了个提示遇到正常眯眼、眨眼、低头看导航这些动作要么漏报要么误报。这就是大多数疲劳驾驶检测项目的真实状态——人脸检测做得很热闹真正的“疲劳判定”却没接上。后来我重新梳理了这个“基于深度学习的智能疲劳驾驶检测系统设计源码”项目把整个链路拆开重做才把系统从“能识别脸”推进到“能判断疲劳”。这套系统做下来其实核心就三件事人脸在哪、眼睛和嘴巴什么状态、这些状态在时间上怎么累积成疲劳结论。本文会把这套系统的完整设计思路、源码模块、训练过程和踩坑记录都摊开讲适合正在做毕业设计、课程设计或者在研究DMS驾驶员监控系统的同学参考。1. 疲劳驾驶检测这件事技术选型为什么绕不开深度学习1.1 传统视觉方案的三个死穴很多人刚接触这个题目时第一反应是用传统图像处理来解肤色分割找脸、灰度投影找眼睛、模板匹配判断睁眼闭眼。这思路听起来直接实际放到驾驶舱里几乎不可用。先说肤色分割。车内光照是出了名的恶劣晴天侧光、隧道暗光、夜间仪表盘反光、中控屏幕补光肤色在RGB空间里的分布会剧烈漂移。你调一套HSV阈值早晚隧道里能把座椅皮套当成脸。第二个死穴是灰度投影找眼睛。它假设眼睛在面部图像里是一块明显暗区但驾驶员戴墨镜、戴普通眼镜反光、刘海遮住眉毛、口罩拉高遮住鼻梁这些情况都会让投影曲线完全失效。我在测试中遇到最多的情况就是眼镜框边缘的阴影被当成眼睛导致后期EAR眼睛纵横比计算偏差巨大。第三个问题更根本疲劳状态不是一个静态特征而是眼睛开合、嘴巴张合、头部姿态随时间变化的过程。传统方法每一帧都独立判断帧间抖动大、时序信息完全丢失根本撑不起“持续闭眼2秒判定微睡眠”这类逻辑。1.2 深度学习方案的设计边界用深度学习不是因为它“新”而是因为它把特征提取和状态分类这两件最不稳的事变成了数据驱动的问题。但深度学习也不是万能药设计这套系统时必须先定清楚边界。我画定的系统边界是单目摄像头采集驾驶员面部图像检测人脸区域、定位眼睛和嘴巴关键区域、分类眼睛开合状态、统计时间维度上的疲劳指标、触发分级告警。这个边界意味着系统不需要判断车辆是否偏移车道不需要检测方向盘握持状态只聚焦“驾驶员生理疲劳特征”。在这个边界下系统的技术栈分成三层感知层人脸检测和人脸关键点定位状态层基于关键点或ROI图像判断眼睛闭合、嘴巴张开、头部姿态决策层用PERCLOS、持续闭眼时长、打哈欠频率做时序累积输出疲劳等级这个三层划分非常重要它决定了源码的模块边界也让后期调试能快速定位问题。比如误报率高先看是状态层分类错了还是决策层阈值设置不合理而不是在几百行代码里大海捞针。2. 从驾驶室图像到疲劳判定核心算法链路拆解2.1 人脸检测与关键点定位为什么我放弃了dlib疲劳检测的第一步是找到人脸和面部关键区域。网上大量教程用dlib的68关键点模型我一开始也这么干但很快发现它在真实驾驶场景下有几个硬伤。dlib官方的shape_predictor_68_face_landmarks模型是基于老数据集训练的对正面、光照均匀、无遮挡的人脸表现尚可。但驾驶场景里驾驶员经常侧头看后视镜、低头看中控屏、抬手扶方向盘遮挡面部关键点会频繁抖动甚至丢失。而且dlib模型在ARM平台和部分低端CPU上推理速度一般没有GPU时很难保证实时性。我最终采用了两条腿走路的方案先用轻量级人脸检测器MediaPipe Face Detection / BlazeFace定位人脸框再用MediaPipe Face Mesh输出人脸关键点。Face Mesh会给出468个关键点其中眼睛轮廓有16个点嘴巴区域有20多个点比dlib的68点模型更适合计算精细的开合度。这里有个容易被忽略的坑Face Mesh本身是个全脸网格回归模型它的输出是归一化的坐标需要配合图像尺寸还原成像素坐标。很多人直接把归一化坐标当像素用导致ROI裁剪位置飘得厉害。2.2 眼睛开合度EAR的计算逻辑眼睛状态判断有两种主流做法一种是计算EAREye Aspect Ratio一种是把眼睛区域裁出来丢给CNN分类。我最终在系统里同时保留了这两种但主力用的是CNN分类EAR作为辅助校验。先讲EAR因为它背后的原理很有意思。EAR的计算公式是基于眼睛轮廓关键点的几何关系在一个眼睛的轮廓关键点中取左右眼角点P1、P4上眼睑两个点P2、P3下眼睑两个点P5、P6则EAR (||P2 - P6|| ||P3 - P5||) / (2 * ||P1 - P4||)这个比例在睁眼时相对稳定一般在0.25到0.35之间闭眼时因为垂直距离急剧缩小EAR会掉到0.1以下。它用比值代替绝对像素距离好处是对人脸尺度不敏感不管摄像头是靠近还是拉远睁眼时的EAR都落在差不多的区间。但EAR有个天生缺陷它极度依赖关键点的精度。只要眼镜框遮挡、极端侧脸、眼睛区域光线过暗导致关键点轻微偏移EAR就会出现周期性跳动。我测试中见过最离谱的情况是戴黑框眼镜的测试者正常睁眼时EAR被算到0.08直接触发闭眼判定。所以我在源码里把CNN眼睛状态分类器作为第一判定EAR作为兜底逻辑。这个设计在后面“踩坑”部分还会细说。2.3 疲劳判定指标PERCLOS、持续闭眼、打哈欠频率单帧的眼睛状态不足以判定疲劳因为正常眨眼本身就是一个闭合过程闭眼持续100到400毫秒完全正常。疲劳判定必须在时间轴上做统计。系统里用了三个指标第一个是PERCLOS这是疲劳驾驶研究领域最经典的指标。它的定义是单位时间内眼睛闭合程度超过80%的时间占比。P80标准下通常认为PERCLOS超过0.4就属于疲劳状态。实现时我用一个滑动窗口统计窗口内闭眼帧数占总帧数的比例。第二个是持续闭眼时长。这是微睡眠事件的核心特征。正常眨眼再怎么慢也不会超过半秒如果检测到连续闭眼超过1.5秒基本可以判定微睡眠。这里用连续帧计数实现假设摄像头帧率是30FPS那么45帧连续闭眼就应该触发告警。第三个是打哈欠频率。哈欠的检测逻辑和眼睛类似通过嘴巴开合度MARMouth Aspect Ratio判断张嘴状态然后统计单位时间内的哈欠次数。连续5分钟内出现3次以上哈欠配合PERCLOS指标疲劳概率就很高了。这三个指标不是简单叠加在源码里我用了分级告警状态机单指标超标给一级提醒两个指标同时超标给二级警告持续闭眼直接触发三级强制休息告警。分级处理的好处是降低误报对驾驶员的干扰——没人希望偶尔打个哈欠就被疯狂报警。3. 源码工程拆解模块划分与核心实现3.1 工程目录与推理引擎选择这套系统的源码目录结构如下fatigue_detection/ ├── config.yaml # 全局配置阈值、路径、参数 ├── requirements.txt ├── data/ │ ├── raw/ # 原始采集数据 │ ├── processed/ # 预处理后的ROI数据集 │ └── models/ # 权重文件存放 ├── src/ │ ├── detector/ │ │ ├── face_detector.py # 人脸检测封装 │ │ ├── eye_classifier.py # 眼睛状态分类 │ │ └── mouth_classifier.py # 嘴巴状态分类 │ ├── evaluator/ │ │ └── fatigue_evaluator.py # 时序疲劳判定 │ ├── utils/ │ │ ├── roi_extractor.py # 关键点转ROI │ │ └── visualization.py # 画面绘制 │ ├── train.py # 训练入口 │ ├── export_onnx.py # 模型导出 │ └── detect.py # 实时检测入口 └── tests/ └── test_fatigue_logic.py # 单元测试设计时有一个关键决策推理引擎选ONNX Runtime而不是直接调PyTorch。原因有三个。第一系统最终要跑在车内嵌入式设备上ONNX Runtime的部署生态更成熟可以无缝切到TensorRT或OpenVINO。第二ONNX模型是静态计算图推理速度快且更稳定。第三pyTorch推理需要依赖完整的torch库和模型定义代码部署时体积大、版本兼容麻烦而ONNX Runtime只是个轻量推理库。3.2 眼睛状态分类器的实现细节眼睛状态分类器是整个系统的核心。它接收一个裁剪后的眼睛ROI图像输出“睁眼”和“闭眼”两个类别的置信度。模型结构用的是MobileNetV3-Small输入尺寸是64x64的单通道灰度图。为什么用64x64这么小的输入因为眼睛ROI本身像素信息就不多输入太大只会增加计算量不会带来精度收益。我用MobileNetV3实测下来单张眼睛ROI的推理耗时在CPU上约5到8毫秒GPU上则可以忽略不计。关键实现细节在ROI提取而不是分类网络。眼睛区域的裁剪直接用Face Mesh的关键点坐标取上眼皮最高点和下眼皮最低点向外扩展20%的padding。这个padding非常重要因为直接紧贴眼睑裁剪会把上下眼皮之外的皮肤也框进去干扰分类器padding太小又可能因关键点抖动导致内容不全。ROI提取后进行两步预处理灰度化后用自适应直方图均衡化CLAHE解决光线不均的问题缩放到64x64并做归一化这里有个细节分类器训练时的数据分布和实时推理时的数据分布可能不一致。我在源码里加入了简单的在线校准逻辑——持续统计当前视频流中眼睛ROI的亮度直方图如果与训练集统计值偏移过大就调整CLAHE的clipLimit参数。这个机制在后来的夜间测试中帮了大忙。3.3 疲劳判定与告警模块状态机与滑动窗口疲劳判定模块我实现为一个有状态的类叫FatigueEvaluator。它内部维护一个环形缓冲区存储最近150帧的眼睛和嘴巴状态结果然后在这个滑动窗口上计算疲劳指标。核心逻辑如下class FatigueEvaluator: def __init__(self, config): self.window_size config[window_size] # 默认150帧 self.eye_buffer deque(maxlenself.window_size) self.mouth_buffer deque(maxlenself.window_size) self.alert_level 0 def update(self, eye_state, mouth_state): self.eye_buffer.append(eye_state) self.mouth_buffer.append(mouth_state) # 计算PERCLOS closed_frames self.eye_buffer.count(closed) perc_los closed_frames / len(self.eye_buffer) # 计算持续闭眼 continuous_closed self._get_continuous_closed_frames() # 计算哈欠频率 yawn_count self._count_yawns() self.alert_level self._decide_level(perc_los, continuous_closed, yawn_count) return self.alert_level判定逻辑最需要注意的是连续闭眼帧数的计算方式。如果简单地从头到尾遍历缓冲区的闭眼帧无法区分“一次持续闭眼”和“多次短暂闭眼”。正确做法是反向遍历数出从最新一帧开始连续闭眼的帧数一旦遇到睁眼帧就停止。这样得到的数字才是真正的持续闭眼时长。告警输出方面系统支持声音告警和界面告警。声音用winsound或蜂鸣器根据告警等级播放不同频率的提示音。界面告警是在视频画面上绘制人脸框和状态标签同时在侧边栏绘制PERCLOS的实时曲线方便调试时观察。4. 模型训练与数据准备多数人不会细抠的环节4.1 数据集选择公开数据集和自己采集怎么配合眼睛状态分类器的训练数据我用了三个来源的混合CEWClosed Eyes in the Wild数据集包含大量闭眼人脸图像MRL Eye Dataset包含不同光照和头部姿态下的眼睛图像自己用摄像头采集的驾驶员模拟场景数据混合数据集有个坑不同数据集的图像尺寸、光照条件、标注口径差异很大。MRL的眼睛图像是稳定的正视视角而自采数据包含大量侧脸和俯仰角变化。直接混合训练会导致模型在某个数据集上表现好在另一个上明显变差。我的处理方式是分阶段训练先用MRL和CEW做预训练让模型学到眼睛开合的基础特征再用自采数据做微调让模型适配驾驶舱的实际视角和光照。微调阶段把学习率降为预训练的十分之一避免破坏已经学到的通用特征。数据增强我们用了随机水平翻转、亮度扰动、高斯噪声、随机遮挡。其中随机遮挡特别有用它的思路是模拟眼镜框、方向盘、手指对眼睛部分的遮挡。实测发现加入遮挡增强后模型在戴眼镜人群上的准确率提升了约6个百分点。4.2 训练过程与调参记录眼睛分类器的训练参数如下表参数值说明输入尺寸64x64灰度图网络结构MobileNetV3-Small分类头改为2类优化器AdamW权重衰减1e-4初始学习率0.001余弦退火调度Batch Size128单卡Epochs40微调阶段只用15轮损失函数CrossEntropyLoss带类别权重训练时要注意类别不平衡。睁眼样本数量远多于闭眼样本如果不处理模型会把所有输入都判为睁眼准确率还能虚高到90%以上。我在损失函数里给闭眼类别加了1.5倍的权重同时训练时做了简单的过采样让每个batch中睁闭眼样本比例接近1:1。最终验证集准确率在98.7%左右但这里我要提醒一句验证集不能只从公开数据里抽。我用自采数据单独做了留出集这个留出集上的准确率只有95.4%。差出来的3个百分点就是数据分布差异造成的项目中如果只看公开数据验证集很容易高估模型真实表现。4.3 模型导出与量化从PyTorch到ONNX的坑训练完成后模型导出成ONNX格式。导出命令本身很简单python export_onnx.py --weights best.pt --output eye_classifier.onnx但有一个常规文档里不会提的坑PyTorch模型默认使用动态shape而ONNX Runtime在动态shape下推理可能触发额外的内存分配和性能损耗。我在导出时把输入尺寸固定为1x1x64x64用opset_version11并在导出参数里设置dynamic_axes为空确保ONNX模型是静态shape。量化部分我用ONNX Runtime的整数量化工具把模型从FP32压到INT8。量化结果让我有点意外眼睛分类器从FP32到INT8准确率只掉了0.8个百分点但推理速度提升了近3倍。原因是MobileNetV3里的激活函数和卷积对量化不敏感这个网络结构很适合低精度推理。但要注意量化校准数据集必须用真实场景的图像不能用训练集否则量化后的激活值分布会偏移。5. 实时检测、界面与告警联动从算法到可用的系统5.1 视频流接入与帧率控制检测系统的输入来自摄像头OpenCV的VideoCapture是常规选择。但直接循环读帧推理有个问题摄像头帧率可能不稳定某个瞬间帧率突然降低会导致疲劳判定窗口的时间跨度失真。我的处理方案是生产消费者模式。主线程用VideoCapture持续读帧写入一个大小为4的队列推理线程从队列取帧进行检测。这样摄像头读取和模型推理解耦队列满了就丢弃旧帧保证推理始终处理最新画面。这个方案下检测延迟大概增加一帧但帧率稳定性好了很多。帧率控制还有一个细节疲劳判定依赖时间窗口不同帧率下的窗口长度必须换算成帧数。配置文件里我设置了fps30滑动窗口150帧即5秒。如果实际检测帧率掉到20FPS同样150帧窗口的实际时间就变成了7.5秒PERCLOS指标的判定口径就变了。所以FatigueEvaluator会动态读取当前帧率自适应用帧数除以实际帧率换算成秒。5.2 告警输出声音、界面与日志的联动设计告警模块的架构比较直接状态输出接口返回当前告警等级界面层负责显示音频层负责发声日志层负责持久化。界面我用OpenCV的绘图函数实现不引入额外的GUI框架。画面左上角显示实时PERCLOS值和当前疲劳等级人脸框根据等级变色绿色正常、黄色一级提醒、橙色二级警告、红色三级强制休息。侧边栏画一个PERCLOS历史曲线方便观察趋势。音频告警的实现要考虑一个实际问题疲劳驾驶场景下告警声音如果太温和驾驶员根本没感觉。我用winsound.Beep生成了两种声音一级提醒用800Hz、持续0.3秒二级警告用1200Hz、持续0.6秒且重复三次三级强制休息用高低频交替的脉冲音频率和强度都明显区别于前两级。实测中三级告警音在车辆行驶噪音环境下仍然有辨识度。所有告警事件都会写入日志文件记录时间、告警等级、当前的PERCLOS值、持续闭眼时长。日志格式是JSON Lines方便后续做数据分析。5.3 一次完整的夜间低光实测记录系统完成后我做了多轮实车模拟测试最有参考价值的是夜间低光场景。测试环境是地下车库车内只有仪表盘灯光光照强度远低于正常白天驾驶。第一次测试结果很惨模型频繁把闭眼判成睁眼PERCLOS指标完全失真。排查后发现问题出在ROI预处理上地下车库的环境光偏黄且暗眼睛区域对比度过低CLAHE在低对比度下反而放大了噪声。我调整了两处参数CLAHE的clipLimit从2.0降低到1.5并把网格大小从8x8改成4x4同时抬高眼睛ROI的亮度下限像素值低于设定值的区域先做线性拉伸再做直方图均衡化。调整后夜间场景的检测准确率恢复到了白天测试的九成水平。这个测试还暴露了一个重要问题单摄像头方案在极端低光下的可靠性始终有限夜间的终极解决方案是近红外摄像头加红外补光。但如果项目阶段没有红外硬件图像预处理参数的本地自适应是退而求其次的有效手段。6. 实测中的三个大坑与完整排查链路6.1 戴眼镜的驾驶员被频繁误报这个坑出现过很多次值得单独立一节。现象的完整描述是测试者戴上黑框眼镜后系统在没有疲劳动作的情况下频繁触发一级提醒偶发二级警告。摘下眼镜后一切恢复正常。我的排查链路是这样展开的。第一步在界面上叠加显示眼睛关键点和EAR数值观察数值波动。结果发现EAR在0.15到0.08之间反复横跳明显低于裸眼时的0.3左右。这个现象表明要么是关线点定位出错要么是ROI内容本身被污染。第二步把每帧的ROI图像持久化到本地逐帧检查裁剪结果。发现一个规律当眼镜框下边缘恰好位于眼睛上方约10个像素的位置时关键点定位会把眼镜框下边缘当成上眼睑导致眼睛的ROI中混入大片镜框阴影。第三步检查CNN分类器的输出。用ROC分析发现模型在“带镜框阴影的睁眼图”上的置信度在0.4到0.6之间徘徊属于典型的分类边界样本。最终解决是组合拳一是ROI裁剪时把上眼睑区域向上扩展再缩小尽量规避镜框边缘二是在数据增强里加入“随机黑色横向条带”模拟镜框阴影三是决策层不再单独依赖分类器输出而是结合EAR做二次校验如果EAR异常偏低但分类器置信度也偏低则判定为“可疑但不告警”等待后续帧确认。6.2 驾驶员低头时人脸检测跟丢另一个高频问题测试者低头看手机或中控屏时人脸平面旋转角度超过40度MediaPipe的人脸检测器直接丢失目标导致所有指标中断。这个问题的本质是人脸检测器的召回率在极端姿态下不足。我的排查过程是先在离线视频上统计不同俯仰角下的人脸检测召回率。用Face Mesh的头部姿态估计输出pitch角从0度到60度均匀划分区间统计每个区间的检出率。结果发现pitch角超过35度后检出率从95%以上掉到70%以下。针对这个问题我做了三个改进。第一在感知层前面加一个运动检测器如果上一帧有脸、当前帧没脸且画面中心区域存在明显运动区域就判断为“可能低头了”此时不立即清空疲劳统计窗口而是保留最近30帧的数据。第二把关键点缓冲区改成tracking机制即使检测器丢掉目标也用光流法预测关键点位置最多延续15帧。第三如果持续跟丢超过2秒系统不做疲劳判定而是输出“检测中断”状态避免在目标已丢失的情况下继续累积PERCLOS造成误报。6.3 模型在真实车内光照下性能暴跌第三个大坑是模型在公开数据集上表现得很好一上车就崩。这个问题在深度学习项目里太常见了根源是训练数据和推理数据的分布不一致。具体到我的项目训练数据主要是MRL和CEW里的网络图片这些图片以实验室环境、正脸、均匀光照为主。而车内真实环境有复杂的色温变化、车窗外的动态背景、仪表盘和中控屏的补光甚至还有驾驶员佩戴帽子造成的头部阴影。解决思路分三层。第一层是数据层面扩大自采数据的比例覆盖清晨、中午、傍晚、夜间、地下车库五种光照场景。第二层是图像预处理层面引入多尺度的光照归一化——不只用CLAHE而是同时计算全局亮度统计量和局部对比度统计量做自适应调整。第三层是推理层面在输出端增加时间平滑滤波避免光照突变导致单帧结果剧烈抖动。我个人的体会是第三层往往被忽视但它对系统稳定性的提升非常明显。用一个带截止频率的时间低通滤波器处理分类置信度序列置信度的抖动幅度能从原来的一地鸡毛变得相对平稳误报率能下降一半以上。如果让我重新做一遍这个项目我最想调整的是数据采集这一步。现在回想起来当时硬啃公开数据集花费的时间如果花在真实驾驶舱环境的数据采集上收益会大得多。另外分享一个小技巧疲劳判定逻辑千万别用单帧硬判断把窗口拉长到3到5秒做统计投票系统稳定性会有一个质的提升。这套系统跑通之后再往工程化方向走无非是换更强的检测器、做模型剪枝、接实车CAN总线但核心的算法链路和判定逻辑基本不会变了。本文还有配套的精品资源点击获取