基于CNN的驾驶员疲劳检测与预警系统:从模型到部署
2026/9/23 4:16:33 网站建设 项目流程

简介:这份资源是面向高校计算机相关专业学生的Python毕业设计完整项目,主题为基于卷积神经网络的人脸识别驾驶员疲劳检测与预警系统,适合用作毕业设计、期末大作业或课程设计,也适合想入门深度学习与计算机视觉实战的初学者。压缩包共37个文件,约500.41MB,包含16个py源码文件、9个pyc编译文件、3个pth模型权重、5张jpg效果图以及txt说明与log日志等,覆盖SSD网络结构、VGG主干、损失函数、数据增强、摄像头与视频检测等模块,并附带数据集与训练权重,下载后可直接运行调试。项目已获导师指导并通过,代码完整、结构清晰,读者可据此掌握人脸检测、疲劳状态判别与预警逻辑的完整实现流程,理解模型训练、评估与推理各环节的衔接方式,并参考效果图与日志快速定位问题。目前已有905人学习,适合需要一份可落地高分项目参考的读者。

1. 从一张打哈欠的截图说起:这套疲劳检测系统到底在做什么

凌晨两点跑长途的司机,眼皮开始打架,方向盘轻微跑偏,等反应过来已经压线——这类事故的根因不是技术不行,而是疲劳没有在临界点之前被拦住。基于卷积神经网络的人脸识别驾驶员疲劳检测与预警系统,要解决的就是这件事:用摄像头抓人脸,用 CNN 判断眼睛睁闭和嘴巴张合,再换算成疲劳指标,超阈值就报警。它适合做 Python 毕业设计的同学,也适合想入门计算机视觉落地的工程师。整套链路的核心不是"识别出这是谁",而是"识别出这个人现在困不困",所以人脸识别在这里更多是定位与对齐,真正的判断交给卷积神经网络。热词里常出现的 opencv人脸识别、cnn卷积神经网络代码、python下载cv2,基本都绕不开这条主线。下面我按自己搭过的一版方案,把选型、代码、参数和踩过的坑讲清楚。

2. 为什么用 CNN 而不是传统特征:疲劳检测的技术选型

2.1 眼睛和嘴巴的状态判断,传统方法为什么容易翻车

早期做法是用 Haar 级联定位人脸,再在眼睛区域算像素灰度均值,或者用 EAR(Eye Aspect Ratio,眼睛纵横比)判断睁闭。这套方法在实验室灯光下能用,一到真实驾驶舱就崩:逆光时眼睛区域一片死黑,戴眼镜时镜片反光,侧脸时关键点漂移。我最早那版就是纯 EAR,白天准确率还行,傍晚迎着夕阳直接误报率飙到三成以上,属于典型的玄学现场。

卷积神经网络的价值在于它不依赖手工设计的阈值。它从像素里自己学"半睁眼""眯眼""正常睁眼"的纹理差异,对光照和角度的鲁棒性明显更好。代价是需要标注数据、需要训练、需要算力,但对毕业设计这个体量,用迁移学习完全扛得住。

2.2 人脸识别与疲劳检测的分工:别把两件事混成一件

很多人一上来就想训一个"疲劳/清醒"二分类网络,输入整张图。这样做的坑是:背景、座椅、方向盘全被卷进特征,模型学到的可能是"这个人穿蓝衣服所以清醒"。正确分工是两段式:

  • 第一段:人脸检测与对齐,把脸抠出来、摆正,去掉背景干扰。常用 MTCNN 或 OpenCV 的 DNN 人脸检测器。
  • 第二段:在裁剪后的人脸(或眼睛、嘴巴子区域)上跑 CNN 分类,输出睁闭眼、张闭嘴的概率。

这样每一段都简单、可调试、可替换。人脸识别在这里的作用是"把人脸框稳定住",不是做身份识别,别被名字带偏。

2.3 数据集与标签怎么定:三类状态比二分类更实用

只分"疲劳/清醒"太粗。我一般会标三类:正常、疑似疲劳(打哈欠或频繁眨眼)、疲劳(闭眼持续超过阈值)。数据来源可以是公开的驾驶员疲劳数据集,也可以自己用手机录几十段视频抽帧。每类至少几百张,注意要覆盖戴眼镜、夜间、侧脸几种情况,否则模型上线就露馅。

提示:抽帧时同一段视频的相邻帧高度相似,训练集和验证集要按视频划分,不能按帧随机划分,否则验证准确率虚高,这是最常见的翻车点之一。

2.4 最小可跑通的环境搭建

先把环境弄干净,别在系统 Python 里乱装。用 conda 建虚拟环境,装这几个核心包:

# 创建并激活虚拟环境 conda create -n fatigue python=3.9 -y conda activate fatigue # 核心依赖:opencv 做人脸检测,torch 跑 CNN,numpy 做数值 pip install opencv-python numpy torch torchvision pillow -i https://pypi.tuna.tsinghua.edu.cn/simple

逻辑说明:opencv-python提供cv2,负责读摄像头和做人脸检测;torch/torchvision负责 CNN 的构建与推理;-i指定国内源,避免下载超时。参数上 Python 选 3.9 是因为 torch 各版本对 3.9 支持最稳,别追最新版。装完用python -c "import cv2, torch; print(cv2.__version__, torch.__version__)"验证,能打印版本号就说明环境通了。这一步对应热词里的 vscode配置python、pycharm配置python环境,IDE 用哪个都行,关键是解释器指向这个 conda 环境。

3. 把 CNN 模型搭起来并训练:从网络结构到训练脚本

3.1 一个够用的轻量 CNN 结构

毕业设计不需要 ResNet 那么重,自己搭一个 4 层卷积的小网络就够,推理快、好解释。结构是:卷积-池化-卷积-池化-卷积-池化-全连接-输出。热词里提到的 lenet5卷积神经网络 就是这个思路的经典版本,我在此基础上加了 BatchNorm 和 Dropout 提升稳定性。

import torch import torch.nn as nn class FatigueCNN(nn.Module): def __init__(self, num_classes=3): super().__init__() # 输入假设为 64x64 灰度图,通道数 1 self.features = nn.Sequential( nn.Conv2d(1, 32, 3, padding=1), # 卷积层1,提取边缘纹理 nn.BatchNorm2d(32), # 归一化,加速收敛 nn.ReLU(), nn.MaxPool2d(2), # 64->32 nn.Conv2d(32, 64, 3, padding=1), # 卷积层2,提取局部形状 nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), # 32->16 nn.Conv2d(64, 128, 3, padding=1), # 卷积层3,提取眼睛嘴巴语义 nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2), # 16->8 ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128 * 8 * 8, 128), nn.ReLU(), nn.Dropout(0.5), # 防过拟合 nn.Linear(128, num_classes) # 输出3类 ) def forward(self, x): return self.classifier(self.features(x))

逻辑说明:features负责逐层提取特征,通道数从 32 到 64 到 128 递增,空间尺寸从 64 逐次降到 8;classifier把特征图拉平后接全连接。参数上padding=1保证卷积不缩尺寸,MaxPool2d(2)每次减半,Dropout(0.5)是防过拟合的关键。num_classes=3对应正常、疑似疲劳、疲劳三类,如果你只做二分类改成 2 即可。输入统一成 64x64 灰度,是为了降低计算量,实测对睁闭眼判断精度损失很小。

3.2 数据加载与训练循环

数据按文件夹分好类,用ImageFolder直接读,省得自己写 Dataset。

from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练集做增强,验证集只做归一化 train_tf = transforms.Compose([ transforms.Grayscale(), # 转灰度 transforms.Resize((64, 64)), # 统一尺寸 transforms.RandomHorizontalFlip(), # 随机翻转,增强泛化 transforms.RandomRotation(10), # 小角度旋转,模拟头部晃动 transforms.ToTensor(), transforms.Normalize([0.5], [0.5]) # 单通道归一化 ]) val_tf = transforms.Compose([ transforms.Grayscale(), transforms.Resize((64, 64)), transforms.ToTensor(), transforms.Normalize([0.5], [0.5]) ]) train_ds = datasets.ImageFolder('data/train', transform=train_tf) val_ds = datasets.ImageFolder('data/val', transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False)

逻辑说明:训练集加RandomHorizontalFlipRandomRotation是为了让模型见过更多姿态,验证集绝不能加增强,否则评估失真。Normalize([0.5],[0.5])把像素从 [0,1] 映射到 [-0.5,0.5],是单通道的常用做法。batch_size=32是显存和收敛速度的折中,显存小就降到 16。

import torch.optim as optim device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = FatigueCNN(num_classes=3).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-3) for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() out = model(imgs) loss = criterion(out, labels) loss.backward() optimizer.step() # 每个 epoch 后在验证集上评估 model.eval() correct = total = 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) pred = model(imgs).argmax(1) correct += (pred == labels).sum().item() total += labels.size(0) print(f'epoch {epoch}, val_acc={correct/total:.4f}')

逻辑说明:Adam学习率1e-3是分类任务的稳妥起点,30 个 epoch 对这个小网络足够。model.train()model.eval()的切换不能省,因为 Dropout 和 BatchNorm 在两种模式下行为不同,忘了切会导致验证结果乱跳。训练完把权重存下来:torch.save(model.state_dict(), 'fatigue_cnn.pth')

3.3 关键参数怎么调:三个必看指标

参数建议值调整方向
学习率 lr1e-3验证 loss 震荡就降到 1e-4
batch_size32显存不足降到 16 或 8
Dropout0.5训练准确率高但验证低就加大

判断过拟合看训练准确率和验证准确率的差距,差距超过 10 个百分点就说明模型在背训练集,加大 Dropout 或补数据。判断欠拟合看两个都低,那就加层数或加 epoch。

4. 实时推理与预警逻辑:把模型接到摄像头上

4.1 摄像头读取与人脸裁剪

训练好的模型要跑在实时视频流上,流程是:读帧 → 检测人脸 → 裁剪 → 送模型 → 拿分类结果。

import cv2 import numpy as np import torch # 加载人脸检测器(OpenCV 自带 DNN 模型) face_net = cv2.dnn.readNetFromCaffe( 'deploy.prototxt', 'res10_300x300_ssd_iter_140000.caffemodel') model = FatigueCNN(num_classes=3) model.load_state_dict(torch.load('fatigue_cnn.pth', map_location='cpu')) model.eval() cap = cv2.VideoCapture(0) # 0 表示默认摄像头 while True: ret, frame = cap.read() if not ret: break h, w = frame.shape[:2] # 构造 blob 送人脸检测网络 blob = cv2.dnn.blobFromImage(frame, 1.0, (300, 300), (104, 177, 123)) face_net.setInput(blob) detections = face_net.forward() for i in range(detections.shape[2]): conf = detections[0, 0, i, 2] if conf < 0.5: # 置信度阈值,低于 0.5 丢弃 continue box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) x1, y1, x2, y2 = box.astype('int') face = frame[y1:y2, x1:x2] if face.size == 0: continue # 预处理成模型输入 gray = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY) gray = cv2.resize(gray, (64, 64)) tensor = torch.tensor(gray, dtype=torch.float32).unsqueeze(0).unsqueeze(0) / 255.0 tensor = (tensor - 0.5) / 0.5 with torch.no_grad(): pred = model(tensor).argmax(1).item() cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, ['normal', 'drowsy', 'fatigue'][pred], (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow('fatigue', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

逻辑说明:blobFromImage的均值(104,177,123)是 OpenCV 人脸检测模型的固定参数,不能改。置信度阈值0.5是漏检和误检的平衡点,光线差可以降到 0.4。预处理必须和训练时一致:灰度、64x64、除以 255 再减 0.5 除 0.5,任何一步不一致,推理结果就是错的,这是血泪经验。

4.2 疲劳判定不能只看单帧:加时间窗口

单帧判断会抖,闭眼一帧就报警太敏感。正确做法是维护一个滑动窗口,统计最近 N 帧里疲劳类占比。

from collections import deque window = deque(maxlen=30) # 最近30帧 fatigue_count = 0 # 在每帧拿到 pred 后 window.append(pred) if len(window) == 30: fatigue_ratio = sum(1 for p in window if p == 2) / 30 if fatigue_ratio > 0.6: # 60% 以上判为疲劳 trigger_alarm()

逻辑说明:maxlen=30对应约 1 秒(按 30fps),fatigue_ratio > 0.6是报警阈值。这两个参数要按实际帧率调,帧率低就减小窗口。报警可以是播放声音、弹窗或串口触发硬件,毕业设计里用playsound播一段提示音最简单。

4.3 预警系统的三种触发方式

  • 声音报警:playsound('alarm.wav'),最直接。
  • 界面弹窗:用 Tkinter 或 PyQt 弹红色警告框。
  • 硬件联动:通过串口给单片机发指令,点亮 LED 或震动马达,对应热词里的基于stm32的人脸识别门禁系统设计思路。

选哪种看你的硬件条件,纯软件方案用前两种就够,答辩时演示效果好。

5. 避坑与排查:这套系统最容易翻车的五个地方

5.1 摄像头能开但画面全黑

现象:cap.read()返回 True,但 frame 全黑。原因:摄像头被其他程序占用,或索引不对。解决:关掉其他调用摄像头的软件,把VideoCapture(0)换成VideoCapture(1)试,Linux 下用ls /dev/video*确认设备号。

5.2 训练准确率 99% 但实际用起来乱报

现象:验证集准确率很高,接摄像头却频繁误判。原因:训练数据太干净,和真实光照差异大,或者验证集按帧划分导致泄漏。解决:补录真实场景数据,验证集按视频划分,训练时加亮度、对比度增强。

5.3 推理速度慢,画面卡成幻灯片

现象:每帧处理超过 100ms,视频不流畅。原因:模型在 CPU 上跑,或每帧都重新加载模型。解决:模型只加载一次放在循环外;有 GPU 就.to('cuda');把输入从 64x64 降到 48x48;人脸检测可以隔帧做,不必每帧都检测。

5.4 戴眼镜就判成闭眼

现象:不戴眼镜正常,一戴眼镜就报疲劳。原因:训练集里戴眼镜样本太少,镜片反光被当成闭眼特征。解决:专门补戴眼镜的数据,训练时加随机亮度扰动模拟反光,必要时在检测前做一次直方图均衡化。

5.5 报警太频繁或从不报警

现象:要么一直响,要么困了也不响。原因:滑动窗口长度和比例阈值没调好。解决:先打印每帧的 pred 和 fatigue_ratio,观察真实数据分布,再定阈值。别拍脑袋设 0.5,我一般从 0.6 起调,配合 30 帧窗口。

6. 让这套系统更抗造:几个能加分的进阶技巧

把基础版跑通之后,想拿高分或者真正能用,可以在这几个方向上加码。第一是多特征融合:现在只用眼睛嘴巴,可以再加头部姿态——低头、点头频率也是疲劳信号,用cv2.solvePnP估个粗略的头部角度,和 CNN 结果加权。第二是模型轻量化:把训练好的网络用torch.quantization做动态量化,模型体积能压到原来的四分之一,CPU 推理速度翻倍,嵌入式部署时特别有用,对应热词里基于树莓派的人脸识别那类场景。

第三是数据增强的针对性设计:与其无脑翻转旋转,不如模拟真实干扰——随机加高斯噪声模拟夜间噪点,随机调 gamma 模拟隧道进出光变化,随机遮挡一小块模拟手挡脸。这些增强比通用增强更能提升实际鲁棒性。第四是预警分级:别只有"报警/不报警"两档,可以设三级——轻度疲劳弹提示、中度播声音、重度持续报警并记录时间戳,答辩时这个分级逻辑很能体现工程思维。

验证方法上,别只看准确率。做一个混淆矩阵,重点看"疲劳"类被误判成"正常"的比例,这个漏报比误报危险得多。再录几段真实驾驶或模拟驾驶视频,统计从真正疲劳到系统报警的延迟,控制在 2 秒内算合格。

我自己踩过最深的坑是:一开始追求模型精度,把网络堆到十几层,结果树莓派上跑不动,答辩现场卡成 PPT。后来砍回四层卷积加量化,精度只掉了一个点,速度却上来了。能跑起来的简单模型,永远比跑不动的复杂模型值钱,这个习惯我保持到现在。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询