Python+CNN人脸识别疲劳检测:从数据集到预警系统实战
发布时间:2026/10/1 3:05:14来源:尧图网络
简介本资源为基于卷积神经网络的人脸识别驾驶员疲劳检测与预警系统毕业设计完整项目包面向计算机相关专业正在做毕设的学生及需要项目实战练习的学习者也可作为课程设计或期末大作业参考。项目经导师指导并认可通过包含全部源码、数据集与项目介绍可直接作为毕设使用。压缩包共19个文件以11个Python脚本为核心辅以2个XML人脸检测器配置、2个TXT说明、1个HDF5模型权重、1个EXE可执行程序及MD文档等整体约78.33MB。项目围绕疲劳驾驶检测展开涵盖人脸检测、眼部状态识别、CNN模型训练与评估、Tkinter界面交互等模块并附有训练好的mini_XCEPTION模型文件便于快速复现与二次开发。目前已有244人学习下载适合希望掌握深度学习实战、人脸识别与疲劳预警系统搭建的学习者参考借鉴。1. 从一张打哈欠的抓拍说起这套疲劳检测系统到底在做什么凌晨两点跑长途的司机被车内摄像头抓拍到连续闭眼超过 1.5 秒中控屏弹出蜂鸣预警——这个场景背后是一套用 Python 加卷积神经网络搭起来的人脸识别疲劳检测与预警系统。它要解决的核心问题很具体把摄像头采集到的驾驶员面部图像经过人脸检测、眼睛和嘴巴区域裁剪、CNN 分类最终判断出「清醒 / 疲劳」两种状态并在疲劳时触发声音或界面预警。整套流程不依赖昂贵的红外设备一台普通 USB 摄像头加一块能跑 Python 的板子就能落地这也是它成为大量毕业设计和入门级工程实践首选方向的原因。适合谁看正在做 Python 毕业设计、想找一个「有数据集、有模型、有界面、能演示」的完整项目的同学也适合想把人脸识别从门禁打卡扩展到行为分析场景的工程师。卷积神经网络在这里不是噱头它承担的是把「眼睛睁闭」「嘴巴张合」这类局部纹理特征自动提取出来的活比传统 HOG 加 SVM 的方案在光照变化和头部轻微偏转时更稳。下面从数据、模型、训练、预警、避坑一路讲到能跑起来的程度。2. 数据集与标签体系疲劳检测的输入到底长什么样2.1 为什么不能直接拿整张人脸图训练很多人第一反应是把摄像头拍到的整张人脸丢进 CNN输出「疲劳 / 清醒」。我试过翻车得很彻底。整张脸里真正和疲劳相关的区域只有眼睛和嘴巴其余像素全是干扰模型会去学背景亮度、衣服颜色这些无关特征。常见做法是先用人脸检测器定位人脸框再按比例裁出左右眼和嘴巴三个 ROI分别送进网络。这样每个子网络只需要判断「这只眼睛是睁还是闭」「这张嘴是张还是合」任务简单数据需求也小。裁剪比例有讲究。以人脸框宽度 W、高度 H 为基准眼睛区域一般取上半部分横向 0.15W 到 0.45W、纵向 0.25H 到 0.45H 的矩形嘴巴区域取横向 0.3W 到 0.7W、纵向 0.6H 到 0.85H。这个比例不是拍脑袋是大量人脸关键点统计出来的经验值不同数据集可以微调但偏离太多会把眉毛或下巴裁进来。2.2 数据集的组织方式与标签文件标题里提到数据集实际工程中通常用两类来源拼起来一类是公开的闭眼/睁眼、张嘴/闭嘴分类图另一类是自己用摄像头录制的驾驶员视频抽帧。目录结构建议按下面的方式组织方便后续用ImageFolder直接读dataset/ ├── train/ │ ├── eye_open/ # 睁眼样本 │ ├── eye_closed/ # 闭眼样本 │ ├── mouth_open/ # 张嘴打哈欠样本 │ └── mouth_closed/ # 闭嘴样本 └── val/ ├── eye_open/ ├── eye_closed/ ├── mouth_open/ └── mouth_closed/每个类别建议不少于 800 张正负样本比例控制在 1:1 到 1:1.5 之间。闭眼样本天然比睁眼少可以用水平翻转、随机亮度扰动做增强但不要用大角度旋转因为眼睛旋转后纹理分布会变模型学到的特征反而失真。2.3 用 OpenCV 做批量裁剪的脚本拿到原始人脸图后需要批量裁出眼睛和嘴巴区域。下面这段脚本用 Haar 级联做人脸检测再按比例裁剪跑完直接生成上面那种目录结构import cv2 import os # 加载 OpenCV 自带的人脸检测器 face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) def crop_roi(img_path, save_root): img cv2.imread(img_path) if img is None: return gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.1, 5, minSize(80, 80)) for i, (x, y, w, h) in enumerate(faces): # 眼睛区域人脸框上半部分 eye img[y int(0.25*h): y int(0.45*h), x int(0.15*w): x int(0.45*w)] # 嘴巴区域人脸框下半部分 mouth img[y int(0.60*h): y int(0.85*h), x int(0.30*w): x int(0.70*w)] cv2.imwrite(os.path.join(save_root, eye, f{i}_eye.jpg), eye) cv2.imwrite(os.path.join(save_root, mouth, f{i}_mouth.jpg), mouth) for name in os.listdir(raw_faces): crop_roi(os.path.join(raw_faces, name), roi_output)detectMultiScale的scaleFactor设 1.1 表示每次图像尺寸缩小 10% 再检测值越小检测越细但越慢minNeighbors设 5 是过滤误检的阈值调高会减少误检但可能漏掉侧脸。裁剪出来的 ROI 统一缩放到 24×24 或 32×32 再存盘能省不少训练时的 IO 开销。3. 卷积神经网络结构设计两个小网络还是一个多任务网络3.1 眼睛和嘴巴分开建模的理由眼睛的睁闭是二分类嘴巴的张合也是二分类看起来可以合成一个四分类网络。但实际做下来分开建模更稳。原因是眼睛区域纹理细、对比度低嘴巴区域面积大、形变大两者对卷积核尺寸和感受野的需求不一样。眼睛用 3×3 小卷积核堆 3 层就够嘴巴需要更大的感受野来捕捉嘴角下拉的弧度。硬塞进一个网络要么眼睛欠拟合要么嘴巴过拟合。常见做法是训练两个独立的小 CNN推理时并行跑最后把结果做逻辑融合。融合规则很简单连续 N 帧内眼睛闭着的比例超过阈值或者嘴巴张开的比例超过阈值就判定疲劳。N 一般取 15 到 30对应摄像头 15fps 下 1 到 2 秒的时间窗。3.2 一个能跑通的轻量 CNN 定义下面这个网络结构参数量不到 10 万在 CPU 上单帧推理 5ms 以内适合部署到树莓派或旧笔记本import torch import torch.nn as nn class EyeNet(nn.Module): def __init__(self, num_classes2): super().__init__() self.features nn.Sequential( # 输入 1x24x24 nn.Conv2d(1, 16, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 16x12x12 nn.Conv2d(16, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 32x6x6 nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 64x3x3 ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64 * 3 * 3, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, num_classes) ) def forward(self, x): return self.classifier(self.features(x))三层卷积的感受野刚好覆盖 24×24 的眼睛区域再深就会过拟合。Dropout(0.3)是防止小数据集上全连接层记住样本如果训练集超过 5000 张可以降到 0.2。输入用单通道灰度图因为眼睛睁闭主要靠纹理和边缘颜色信息贡献很小单通道还能把第一层计算量砍掉三分之二。3.3 训练参数与早停策略训练时用交叉熵损失优化器选 Adam初始学习率 1e-3每 10 个 epoch 衰减到 0.5 倍。批大小设 64太小梯度噪声大太大在样本少时容易陷进尖锐极小值。早停看验证集准确率连续 8 个 epoch 不提升就停同时保存验证集上最好的权重。这里有个血泪经验不要只看训练准确率眼睛闭着的样本如果只占 20%模型全预测「睁眼」也能有 80% 准确率必须看每一类的召回率。from torch.utils.data import DataLoader from torchvision import datasets, transforms transform transforms.Compose([ transforms.Grayscale(), transforms.Resize((24, 24)), transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]) ]) train_set datasets.ImageFolder(dataset/train, transformtransform) val_set datasets.ImageFolder(dataset/val, transformtransform) train_loader DataLoader(train_set, batch_size64, shuffleTrue) val_loader DataLoader(val_set, batch_size64, shuffleFalse)Normalize的均值和方差都设 0.5是因为灰度图归一化到 [-1, 1] 后ReLU 的激活分布更对称收敛比 [0, 1] 快。如果换成彩色输入均值和方差要按 ImageNet 那套来但这里没必要。4. 预警逻辑与界面集成从模型输出到蜂鸣器响起来4.1 疲劳判定的时间窗与阈值模型每帧输出一个概率单帧结果抖动很大直接拿单帧判定会频繁误报。工程上用一个滑动窗口做平滑维护最近 30 帧的眼睛闭概率和嘴巴张概率分别求均值。眼睛闭概率均值超过 0.6 且持续 1.5 秒或者嘴巴张概率均值超过 0.5 且持续 2 秒触发预警。这两个时间阈值来自疲劳驾驶研究里的 PERCLOS 指标闭眼时间占比超过 0.4 就算疲劳换算到 30 帧窗口就是 12 帧以上闭眼。预警分两级一级是界面弹提示加黄色边框二级是蜂鸣器响加红色边框。一级触发后如果 3 秒内状态恢复就撤销二级触发后必须手动确认才能解除避免司机忽略。4.2 用 PyQt 搭一个能演示的界面毕业设计演示需要一个看得见的界面。用 PyQt5 加 OpenCV 的QImage转换把摄像头画面、检测框、状态文字和预警灯画在一起from PyQt5.QtWidgets import QLabel, QVBoxLayout, QWidget from PyQt5.QtGui import QImage, QPixmap import cv2 class MonitorWindow(QWidget): def __init__(self): super().__init__() self.label QLabel() layout QVBoxLayout() layout.addWidget(self.label) self.setLayout(layout) self.cap cv2.VideoCapture(0) def update_frame(self): ret, frame self.cap.read() if not ret: return # 这里插入人脸检测和 CNN 推理得到 fatigue_flag fatigue_flag self.infer(frame) if fatigue_flag: cv2.rectangle(frame, (0, 0), (frame.shape[1], frame.shape[0]), (0, 0, 255), 8) rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.label.setPixmap(QPixmap.fromImage(qimg))QImage构造时第四个参数ch * w是每行字节数传错会导致图像错位这是 PyQt 显示 OpenCV 图像最常见的坑。infer方法里把 ROI 裁剪、归一化、模型前向、滑动窗口平滑串起来返回布尔值。4.3 模型导出与推理加速训练完的 PyTorch 模型直接推理在 CPU 上也能跑但如果要部署到算力更弱的板子可以导出成 ONNX 再用 ONNX Runtime 跑速度能提升 30% 到 50%。导出时注意固定输入尺寸动态轴只留 batch 维import torch.onnx model EyeNet() model.load_state_dict(torch.load(eye_best.pth, map_locationcpu)) model.eval() dummy torch.randn(1, 1, 24, 24) torch.onnx.export(model, dummy, eye_net.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}})导出后先用onnxruntime跑一遍和 PyTorch 结果对比误差在 1e-4 以内才算成功。如果差得多多半是某层不支持或算子版本不匹配把opset_version调到 11 再试。5. 避坑与排查那些让系统在答辩现场翻车的细节5.1 摄像头曝光突变导致误报现象白天演示正常晚上开灯瞬间预警狂响。原因摄像头自动曝光调整时画面整体变亮眼睛区域对比度骤降CNN 把低对比度的睁眼误判成闭眼。解决在 ROI 送入网络前做一次直方图均衡化或者把摄像头曝光锁定不让它自动调。我一般会在代码里加一句cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25)关掉自动曝光。5.2 戴眼镜时眼睛区域反光现象戴眼镜的测试者闭眼时模型判睁眼。原因镜片反光在眼睛区域形成高亮斑块CNN 学到的闭眼特征是暗色横条反光把特征盖住了。解决训练集里加入戴眼镜的样本至少占 20%推理时如果检测到人脸框内有强反光先做一次局部对比度拉伸。这个坑没有一劳永逸的算法解只能靠数据覆盖。5.3 滑动窗口初始状态误触发现象程序刚启动就报警。原因滑动窗口初始化为全零前几帧还没填满时均值计算把零当成闭眼概率。解决窗口用队列实现长度不足 30 帧时不判定或者初始值填 0.5 的中性值。这个 bug 在答辩现场特别致命因为评委刚坐下就看到报警。5.4 多线程下界面卡死现象摄像头画面卡住预警不响应。原因CNN 推理和界面刷新在同一个线程推理耗时 30ms 时界面就掉到 30fps 以下。解决把推理放到QThread里通过信号槽把结果传回主线程刷新界面。注意 OpenCV 的VideoCapture不要跨线程读容易崩。5.5 模型文件路径写死导致换机器跑不起来现象在自己电脑上好好的拷到答辩教室的电脑就报FileNotFoundError。原因代码里写了绝对路径。解决所有路径用os.path.join(os.path.dirname(__file__), weights, eye_best.pth)拼或者用pathlib.Path。这个坑每年都有人踩提前改成相对路径能省很多事。6. 把准确率再往上推一档数据增强与模型集成的具体技巧前面跑通的是基线版本如果答辩要求准确率指标或者你想让系统在真实车内光照下更稳可以在这几个方向上继续压榨。第一个技巧是针对性数据增强。通用增强里的随机旋转对眼睛区域有害但随机调整 gamma 值特别有用因为车内光照变化主要体现为整体亮度偏移。用transforms.Lambda挂一个 gamma 变换gamma 在 0.6 到 1.4 之间随机取能让模型对明暗变化的鲁棒性明显提升。第二个技巧是模型集成。训练三个结构相同但初始化不同的 EyeNet推理时把三个网络的 softmax 输出求平均。单模型准确率 94% 时三模型集成通常能到 96% 以上代价是推理时间翻三倍。如果部署平台算力够这个投入产出比很高。集成时注意三个模型要用不同的随机种子训练否则学到的特征太像集成收益会打折。第三个技巧是阈值后处理。模型输出的概率不要直接卡 0.5而是根据验证集上的 ROC 曲线找最佳工作点。眼睛闭的判定阈值可以设到 0.55 甚至 0.6宁可漏报一点也不要误报因为误报会让司机对预警麻木。这个阈值调整不需要重新训练改一行代码就能验证效果。优化手段预期准确率提升推理耗时变化适用场景gamma 增强1% 到 2%无光照变化大的车内三模型集成2% 到 3%约 3 倍算力充足的演示机阈值后处理0.5% 到 1%无所有场景优先做增加戴眼镜样本2% 到 4%无测试者戴眼镜最后说一个验证方法不要只用自己录的视频测。找三五个同学每人录一段包含正常驾驶、打哈欠、低头看手机、戴眼镜、逆光五个场景的 30 秒视频跑一遍统计误报和漏报。这个测试集比任何公开数据集都更能暴露问题因为公开数据集的采集条件和你的实际部署环境差得远。我自己做的时候就是靠这个土办法发现逆光场景下漏报率高达 40%后来加了直方图均衡化才压到 8%。这套方案从数据到模型到界面代码量不大但每个环节都有能深挖的细节。我习惯在每次改完参数后把验证集上的混淆矩阵打出来看一眼比只看准确率数字踏实得多。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网