基于OpenCV与CNN的驾驶员疲劳检测预警系统设计与实现

发布时间:2026/10/11 19:39:50
基于OpenCV与CNN的驾驶员疲劳检测预警系统设计与实现
简介一套基于Python卷积神经网络的人脸识别驾驶员疲劳检测与预警系统完整项目适用于毕业设计、课程设计与实际项目开发。系统通过摄像头实时采集人脸综合打哈欠、眨眼、点头三种行为特征并分析人脸朝向、眼睛开合度、眨眼频率、瞳孔收缩率等数据实现对驾驶员注意力状态的有效评估与疲劳预警。压缩包共20个文件主要包括11个py源码文件覆盖数据预处理、CNN模型训练、检测评估与Tkinter图形界面2个XML文件用于人脸与眼睛特征检测1个HDF5模型权重文件及可直接运行的EXE程序附带系统说明、运行说明与README文档便于部署和二次开发整体包体约78.33MB。目前已有723人学习下载。项目架构清晰、注释完整适合学习CNN落地应用可直接在此基础上扩展检测指标或优化模型。1. 疲劳检测系统到底是什么一个能跑通的CNN毕业设计项目深夜跑长途最危险的往往不是路况而是驾驶员自己先撑不住。市面上很多疲劳检测方案都在做“闭眼超过几秒就报警”的规则可一旦遇到戴墨镜、眼睛小、光照剧烈变化的场景误报率高到没人敢用。这份基于Python卷积神经网络的人脸识别驾驶员疲劳检测与预警系统资源换了个思路——OpenCV的haarcascade先把人脸和眼睛定位出来轻量CNN再对裁剪区域做疲劳/正常的分类最后结合眨眼、打哈欠、点头三路信号和瞳孔、人脸朝向数据给出综合预警。整套源码在Pycharm Python3.6环境下测试通过附预训练模型、文档和可直接运行的exe适合毕业设计、课程设计拿来复现也适合在此基础上换数据继续拓展。2. 检测逻辑与模型选型三路疲劳信号怎么被CNN识别先说结论这个系统不是那种“喂一帧图像、吐一个疲劳分数”的黑匣子端到端方案。它实际上分两层——前一层是传统计算机视觉负责找脸、定位眼睛、算几何特征后一层是卷积神经网络负责把裁剪出来的人脸区域映射成“疲劳/正常”的概率。这么拆的好处很直接传统方法擅长快速定位CNN擅长处理光照变化和个体差异两者各干各擅长的活。2.1 三路物理信号眼睛开合度、嘴部张合与点头频率疲劳在视觉上有三个最直观的表现眨眼变频繁、打哈欠、点头。系统把这三路信号分开提取最后再汇总到预警模块。眼睛部分依赖haarcascade_eye.xml给出的眼部包围盒代码里实时计算眼睛框的高度与宽度之比睁眼时这个比值在0.25到0.35之间闭眼瞬间趋近于0。系统统计连续N帧内的比值变化把一次快速闭合计为一次眨眼如果一分钟内眨眼次数明显高于正常基线通常正常是每分钟15到20次就记为一个疲劳特征。嘴巴的张合度用类似思路检测到嘴巴开度突然变大且持续超过一定时长就计为一次哈欠。点头则是看人脸框在画面中的垂直位移和面积变化——坐标往下掉、框面积变大代表头部在低垂连续多次低头就计入疲劳评分。瞳孔朝向和瞳孔收缩率在系统里作为辅助特征参与最终判定不单独触发报警。这套“几何特征提取 行为统计 状态分类”的组合方式比我最早接触的单一阈值方案稳健得多。单纯阈值方案把眼睛宽度当判断依据换个眼型的人误报率直接翻倍而这里的CNN作用是“兜底”把几何特征拿不准的边界情况通通交给模型去判断。baojin.py作为报警模块负责在综合评分越线时发出声音和视觉提示它与detect_class.py通过一个触发条件衔接后面第5章会细说这个衔接处最容易踩的坑。2.2 为什么是mini_XCEPTION轻量CNN与人脸状态分类的匹配模型文件命名为_mini_XCEPTION.102-0.66.hdf5按命名习惯看102是训练轮数0.66是验证准确率。XCEPTION是Chollet在2017年提出的网络结构核心是深度可分离卷积SeparableConv2D把普通卷积拆成逐通道卷积加逐点卷积两步计算量大幅下降。mini_XCEPTION是它的裁剪版把通道数和层数都砍了一截参数量控制在适合CPU实时推理的量级。回顾同类项目VGG16参数量过亿一个权重文件动辄几百MBResNet系列虽然精度高但推理延迟大在笔记本CPU上做实时检测很吃力。mini_XCEPTION在这类“人脸状态分类”任务里是性价比很高的选择因为疲劳/正常分类本身是粗粒度任务不需要ImageNet级别的判别能力。可能有同学会问用它和直接用MobileNet比哪个好我在类似场景里两种都试过MobileNet的提速空间更大但mini_XCEPTION的模型文件更小、迁移学习更容易收敛。疲劳检测本质上和人脸表情识别是同一类问题输入尺寸通常统一到48x48或64x64的灰度图模型结构差异带来的精度差距远小于数据质量和阈值标定带来的差距。如果你只是想先跑通流程完全不需要重新训练直接加载这个hdf5文件做推理就行想提高精度时再用自己的数据在cnn.py里做微调。2.3 完整数据流从人脸定位到报警触发把整个链路摊开看数据是这样流动的摄像头或视频文件逐帧读取先经过haarcascade_frontalface_default.xml检测人脸区域拿到人脸框坐标后裁剪ROI统一resize到模型输入尺寸做灰度化和归一化然后送入mini_XCEPTION前向推理输出“正常/疲劳”的类别概率。与此同时前面提到的眼睛开合度、嘴部张合、点头频率等几何信号也在并行计算最终这两路信息在决策层汇合超过阈值就触发报警。这个项目里几个脚本的分工很清晰extract_face.py负责从原始图片或视频里批量抽取人脸图用于构建训练集data_provider.py和load_and_process.py负责数据加载和预处理split_train_test.py划分训练集和测试集convert.py负责格式转换detect_class.py是做实时检测的主入口evaluate.py用于评估模型在测试集上的表现tkinter_UI.py是图形界面。check.py从命名和调用关系看是一个自检脚本建议在正式跑摄像头之前先运行一次确认hdf5模型能加载、摄像头能打开省得在UI里一卡半天查不出原因。第一次拿到项目时沿着这个顺序把脚本读一遍基本能摸清整套系统的数据流比直接看UI代码高效得多。3. 环境搭建与完整运行从解压到预警界面弹出的完整步骤拿到压缩包后解压出来是一个Python_FatigueDrivingDetection-master目录里面系统说明.txt和运行说明.txt两个文档还是值得先花十分钟读一遍的。很多坑在文档里其实写了但大多数人习惯跳过文档直接跑源码结果在环境上卡一晚上。下面按我实际跑通的顺序把环境、数据准备和两条运行路线都过一遍。3.1 环境版本对齐Python 3.6与TensorFlow/Keras版本组合开发环境明确写的是Pycharm Python3.6搭配卷积神经网络。这里版本不是随便选的Python 3.6对应TensorFlow 1.x时代而hdf5模型文件是那个版本下保存的换到新版环境容易出现反序列化错误。我推荐的版本组合如下表组件推荐版本说明Python3.6项目基准版本3.7跑通概率也高但没必要冒险TensorFlow1.15.0与Keras 2.2.4搭配最稳加载hdf5不出错Keras2.2.4项目源码里的模型定义和加载API基于这个版本OpenCV4.5.5安装时用opencv-python包里包含haarcascade所需文件NumPy1.19.5版本过高会与TensorFlow 1.x出现兼容警告用conda建独立环境是最省心的做法命令如下conda create -n fatigue python3.6 -y conda activate fatigue pip install numpy1.19.5 opencv-python4.5.5.64 pip install tensorflow1.15.0 keras2.2.4 pillow注意这里tensorflow装的是CPU版正常pip默认就是CPU版不需要额外指定。很多新手在这里翻车是把tensorflow装成了tensorflow-gpu结果没有对应CUDA环境import直接报错。另外千万别单独执行pip install cv2PyPI上根本没有叫cv2的包正确做法是装opencv-python。装完后在Python交互环境里执行import cv2和import tensorflow确认没有红字再继续。3.2 数据准备两步走split_train_test与convert进入项目根目录后依次执行数据划分和格式转换脚本。这里要注意顺序不能反先划分再转换否则数据泄漏的风险会比较高。cd Python_FatigueDrivingDetection-master python split_train_test.py python convert.pysplit_train_test.py做的事是把数据集按比例随机切分成训练集和测试集常见的切分比例是8比2。这个脚本会读取按类别组织的图片目录输出两个文件列表供后续训练和评估使用。convert.py则是把图片统一处理成CNN模型能接受的形态包括灰度化、缩放到48x48、像素值归一化到0到1范围最终输出为npy数组文件。为什么要单独拆一个convet步骤而不是在训练时实时处理因为重复的resize和归一化在每轮训练中都会执行提前转换一次能省下大量训练时间。如果你打算换自己的数据集把图片按normal、fatigue两个文件夹放好重新跑这两步就行。3.3 两条运行路线detect_class命令行与tkinter_UI界面项目提供了两套运行入口一套是命令行实时检测另一套是带按钮的图形界面。先试命令行版本启动后摄像头画面里会画出人脸框旁边显示当前状态和疲劳概率python detect_class.py按Q键退出。如果命令行版本能正常出画面说明模型加载、摄像头调用、预测链路都是通的。接着再启动图形界面python tkinter_UI.py界面里通常会有开始检测、停止、退出这类按钮以及实时状态显示区域。免Python环境的同学可以直接双击解压目录里的tkinter_UI.exe但注意exe运行时读取的是当前目录下的模型文件所以不要单独把exe拷走除非连models文件夹一起带上。整个流程走通之后你会看到某个疲劳动作触发后baojin.py响起报警声提示界面状态从“正常”切到“疲劳”。这里我建议一开始先把报警阈值调高一点避免演示时频繁误报具体怎么标定阈值放在最后一章讲。4. 数据管道与核心代码解读data_provider、模型加载与检测分类跑通之后就该看核心代码了。这个项目的关键是三块数据怎么供进去、模型怎么加载、预测结果怎么变成报警决策。4.1 data_provider自定义数据生成器怎么写data_provider.py的作用是给训练脚本提供数据核心是一个生成器每次产出一个小批量的图片数组和对应的标签。我按项目的结构重写了一个等价实现逻辑可以对照参考import os import cv2 import numpy as np from tensorflow.keras.utils import to_categorical class DataProvider: def __init__(self, data_root, image_size(48, 48), batch_size32, class_names(normal, fatigue)): self.data_root data_root self.image_size image_size self.batch_size batch_size self.class_names class_names def build_index(self): paths, labels [], [] for cls_id, cls in enumerate(self.class_names): cls_dir os.path.join(self.data_root, cls) if not os.path.isdir(cls_dir): continue for fn in os.listdir(cls_dir): paths.append(os.path.join(cls_dir, fn)) labels.append(cls_id) return paths, np.array(labels) def generator(self, paths, labels, shuffleTrue): while True: if shuffle: perm np.random.permutation(len(paths)) paths_shuf [paths[i] for i in perm] labels_shuf labels[perm] else: paths_shuf, labels_shuf paths, labels for i in range(0, len(paths_shuf), self.batch_size): batch_paths paths_shuf[i:i self.batch_size] batch_labels labels_shuf[i:i self.batch_size] images [] for p in batch_paths: img cv2.imread(p, cv2.IMREAD_GRAYSCALE) if img is None: continue img cv2.resize(img, self.image_size) images.append(img.astype(np.float32) / 255.0) if not images: continue x np.expand_dims(np.array(images), axis-1) y to_categorical(batch_labels[:len(images)], num_classeslen(self.class_names)) yield x, y这段代码里几个关键点说一下。build_index把normal和fatigue两个目录下所有图片路径和类别ID对应起来类别ID就是目录的枚举顺序。generator返回的是一个无限循环的迭代器这是Keras训练时fit_generator的要求每个epoch自动取完一轮数据后重新洗牌。图片读取用IMREAD_GRAYSCALE原因是模型输入是单通道灰度图。resize到48x48后立刻除以255做归一化这一步如果漏了模型输出的概率会整体漂移训练时loss也降不下去。np.expand_dims在末尾加通道维度灰度图本身是H×W加了之后变成H×W×1和模型输入对齐。最后to_categorical把整数标签变成one-hot向量两个类别就是形如[1, 0]或[0, 1]的编码。这个生成器对应到训练脚本里通常会配合steps_per_epoch使用值等于样本总数除以batch_size。4.2 加载预训练模型load_model与hdf5文件对接模型文件在models目录下是一个hdf5格式文件。加载方式很简单但如果中间步骤没对齐就会报错这是最常见的坑之一。from tensorflow.keras.models import load_model model_path models/_mini_XCEPTION.102-0.66.hdf5 model load_model(model_path) model.summary() # 推理单帧与detect_class.py内部逻辑一致 import cv2 import numpy as np frame cv2.imread(sample.jpg) gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) face cv2.resize(gray, (48, 48)) x np.expand_dims(np.expand_dims(face, axis-1), axis0).astype(np.float32) / 255.0 prob model.predict(x)[0] print(class probs:, prob)load_model读取hdf5文件时会同时恢复网络结构和权重所以推理场景下不需要先手写cnn.py里的结构定义。但如果你打算在现有权重基础上继续训练就要用cnn.py先创建模型结构再调用load_weights加载权重两者不要混用。predict返回的是一个二维数组第一维是batch第二维是类别概率这里只有一个样本所以取[0]拿第一个样本的输出。注意输入x的维度是(1, 48, 48, 1)四维少了任何一个维度模型都会直接抛错。预处理必须和训练时完全一致灰度、48x48、除以255。4.3 detect_class预测概率如何转成疲劳判定模型输出的概率本身不能直接拿来报警因为摄像头的单帧画面受抖动影响很大单帧误判率比想象中高。detect_class.py里用的是连续帧计数策略我把它简化成核心逻辑fatigue_frame_count 0 ALARM_THRESHOLD 30 # 连续疲劳帧数达到该值才触发报警 while True: ret, frame cap.read() # haarcascade定位人脸并裁剪得到roi过程略 pred model.predict(roi)[0] fatigue_prob pred[1] if len(pred) 1 else pred[0] if fatigue_prob 0.5: fatigue_frame_count 1 if fatigue_frame_count ALARM_THRESHOLD: baojing.trigger() # 连接baojin.py报警模块 else: fatigue_frame_count 0这里ALARM_THRESHOLD是工程上最有调参空间的一个数值。设得太大人都点头半天了才报警设得太小路面颠簸一下就可能误报。常见做法是先设30跑一段时间真实验证后再根据误报率和漏报率的平衡去调整。疲劳概率阈值0.5也不是硬性规定正常状态和疲劳状态的预测分数分布如果存在明显分隔可以打印出来看一眼这个后面第6章专门讲标定方法。5. 避坑指南路径、版本与摄像头三类常见问题排查这个项目我在不同机器上反复跑过翻车点高度集中在路径、版本、摄像头这三类。下面五条踩坑记录都是我实际遇到过的每一条都按现象、原因、解决三步写清楚。5.1 读图返回None中文路径与OpenCV的编码限制现象训练脚本跑起来后日志一直提示加载不到图片cv2.imread返回None但打开文件管理器看图片明明就在那儿。原因OpenCV的imread底层用的是fopen读文件不处理Windows下中文路径的Unicode编码。只要项目路径里出现“毕业设计”“新建文件夹”这类中文就会静默失败返回None而不是抛异常所以排查起来特别隐蔽。解决最省事的是把整个项目移到纯英文路径下。不想移动项目的用二进制读取再解码的方案import numpy as np import cv2 def cv_imread(path): data np.fromfile(path, dtypenp.uint8) return cv2.imdecode(data, cv2.IMREAD_GRAYSCALE)把源码里所有cv2.imread替换成这个函数就行。我个人习惯在替换后加一层断言读出来是None就直接抛异常避免图片缺失导致模型在残缺数据上静默跑偏那种问题出得很晚、查起来成本极高。5.2 hdf5模型加载报错str object has no attribute decode现象执行load_model在反序列化阶段抛AttributeError错误信息指向decode属性。原因这个hdf5文件是TensorFlow 1.x Keras 2.x年代保存的权重里包含以bytes类型存储的字符串。TensorFlow 2.x把序列化格式换了默认反序列化器遇到旧格式就报错而这种错误信息对新手很不友好完全看不出来是版本问题。解决最稳的是按前面版本表建一个TF1.15 Keras2.2.4的环境五分钟搞定不要纠结。如果坚持用TF2可以改tf.keras方式加载并传入custom_objects或者用h5py手动把权重读出来后重新赋值但后者工作量大且容易出错。我的经验是这种毕业设计项目不值得在版本兼容性上耗时间直接降级环境最快。跑之前顺手打印一下tensorflow.version确认环境没串。5.3 摄像头黑屏VideoCapture索引和权限现象tkinter_UI点开始检测画面黑屏程序不报错但frame一直是空有时直接卡死无响应。原因cv2.VideoCapture(0)里的“0”在设备多的时候不一定对应内置摄像头。另外Linux环境下摄像头设备节点权限不足也会导致黑屏。解决先写三行代码确认索引和连接状态import cv2 for idx in range(3): cap cv2.VideoCapture(idx) print(idx, cap.isOpened()) ret, frame cap.read() print(ret, frame.shape if ret else None) cap.release()哪个索引能读出画面就改代码里对应的数字。Linux权限问题用sudo usermod -a -G video $USER解决重启后生效。还有一种是内置摄像头被其他程序抢占比如开了OBS或者会议软件没退出把占用程序关了再跑UI。5.4 报警不触发阈值与连续帧逻辑没走到现象检测画面一切正常人脸框也画出来了但状态一直显示normal怎么疲劳都触发不了报警。原因一类是阈值设太高模型预测概率普遍落在0.4到0.6区间0.5的硬阈值把真实疲劳帧全部压掉了。另一类是detect_class里判断疲劳的阈值和baojin.py报警模块的触发条件各调各的中间没衔接上疲劳计数到了但报警函数没被调用。还有一类隐蔽原因模型输入尺寸和训练时不一致推理概率整体被拉低。解决在detect_class.py里临时加一行print把所有帧的预测概率打出来跑10分钟正常状态、10分钟模拟疲劳状态看两组分布。如果正常段还有不少帧超过0.5说明分类边界不够清晰需要回头检查预处理是否一致而不是急着调阈值。确认预处理没问题后把连续帧数从30降到20试几轮找到漏报和误报的平衡点。这个排查流程是通用的换上任何阈值模型都适用。5.5 训练不收敛归一化与类别均衡现象用自己的数据重训loss在0.7附近震荡准确率始终在50%左右相当于模型什么都没学到。原因最常见的是数据没归一化直接把0到255的像素值喂进网络梯度更新不稳定。其次是normal和fatigue两个类别的图片数量差距悬殊模型偏向多数类输出始终是多数类。再有就是学习率用了默认的0.001以上mini_XCEPTION这种小网络对学习率很敏感。解决确认训练脚本里的预处理器和data_provider保持一致灰度、48x48、除以255.0缺一不可。统计两个类别的样本数数量少的做上采样比如复制粘贴几轮或者做随机旋转、水平翻转的数据增强。最后把学习率调到1e-4这个量级重新训练。我在重训这种小模型时还会加早停patience设为10到15轮避免过拟合还把最优权重覆盖掉。6. 进阶调优用录制视频标定阈值与换数据重训的实用技巧默认的0.5阈值只是让系统“能跑”要让它在真实场景下少误报还得自己做标定。我的做法是先录两段视频normal.mp4拍正常驾驶状态fatigue.mp4模拟连续打哈欠、频繁眨眼和点头。然后用模型把所有帧的概率都打出来看两组分布的重叠情况。import cv2 import numpy as np from tensorflow.keras.models import load_model model load_model(models/_mini_XCEPTION.102-0.66.hdf5) def collect_probs(video_path): cap cv2.VideoCapture(video_path) probs [] while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 这里还需要对人脸做裁剪和预处理省略与detect_class保持一致 face cv2.resize(gray, (48, 48)) x np.expand_dims(np.expand_dims(face, axis-1), axis0) x x.astype(np.float32) / 255.0 probs.append(model.predict(x)[0][1]) return np.array(probs) normal_p collect_probs(normal.mp4) fatigue_p collect_probs(fatigue.mp4) print(normal : mean %.3f max %.3f % (normal_p.mean(), normal_p.max())) print(fatigue: mean %.3f min %.3f % (fatigue_p.mean(), fatigue_p.min()))如果fatigue_p的最小值远大于normal_p的最大值说明模型区分度很好阈值取两者中点的整数就行如果两组分布有明显重叠说明预处理不一致或者这两段视频本身没拍出明显的疲劳特征。这个标定脚本每次跑完都留一份分布输出到文本文件里方便换数据后对比。标定完阈值再回头改detect_class里的判定代码重新跑一遍验证。如果满足于现有模型用预训练hdf5就够了。想真正拥有自己的检测模型就按3.2节的方法组织normal和fatigue两个文件夹跑split_train_test、convert再在cnn.py里建结构、加载预训练权重、把全连接层换掉重新微调。训练完后用evaluate.py评估精度选验证集上表现最好的权重导出。推理速度方面如果打算部署到嵌入式设备可以考虑把hdf5转成ONNX再用OpenVINO加速CPU推理速度能再提几倍不过毕业设计在笔记本上演示的话原地跑就挺流畅了。前年我用默认阈值直接跑了一段夜间模拟驾驶视频误报率接近三成当时还以为是模型不行后来录了十分钟正常、十分钟疲劳的真实素材做标定阈值一改误报立即降下来。从那以后我每次拿到这种带状态阈值的项目第一件事就是先把阈值用自己录的视频标一遍再谈上线或者演示。这个习惯救了我好几次希望帮到你。本文还有配套的精品资源点击获取