简介:基于卷积神经网络CNN的疲劳驾驶识别检测系统完整毕业设计项目,主要面向计算机相关专业学生、正在进行毕业设计或课程设计的开发者,也适合需要实战CNN目标检测与图像分类的学习者。项目以驾驶员疲劳状态为检测目标,涵盖数据准备、模型训练、评估及实时摄像头检测全流程,可帮助理解深度学习在安全驾驶场景中的落地方式。资源包共37个文件,包括16个Python源码、9个编译后的pyc文件、3个模型权重文件、5张测试图片、2个文本说明及数据集压缩包,整体大小约500MB。源码覆盖配置、数据增强、损失函数、SSD网络结构、检测与视频检测等模块,预训练权重和数据集可直接用于迁移学习与复现实验。压缩包目录结构清晰,便于按功能模块检索。资源已经过导师指导并认可,评审分98分,适合作为高起点毕设参考或实战练习项目。目前已有128人学习使用,可作为项目完成度与实用性的参考。
1. 疲劳驾驶识别检测的课题拆解:先定边界再谈CNN
拿到这个毕设标题,第一步不是打开PyCharm敲代码,而是想清楚“疲劳驾驶识别检测”到底要检测什么。市面上70%的同类课题都把这它做成一个图像二分类问题,输入一张人脸的RGB图像,输出“正常”或“疲劳”,这个方向足够完成毕设,也方便在答辩时讲清楚。涉及CNN的原因也很直接:模型需要从眼睛闭合程度、嘴巴开合状态、头部姿态这些视觉特征里自动提取模式,传统方法靠人手工设计特征(例如EAR眼纵横比、嘴巴开合比),在光照变化和遮挡面前非常脆弱。CNN方案以数据驱动的方式替代手工特征,把“特征工程”变成了“网络结构设计”,这也是本课题最有讲解价值的地方。
我接下来不会给你一份假装存在的工程包逐一注解,而是把完整的实现路径拆成三块:如何准备与增强数据集、如何用PyTorch把CNN训练跑通、如何把模型从离线文件变成能对着摄像头实时出结果的检测系统。路径走完,你手里会是一个结构清楚、答辩能讲、代码能跑的高分毕设骨架。
2. CNN在疲劳识别里的模型选型与数据集组织
2.1 二分类还是三分类:先看摄像头能拍到什么
疲劳驾驶检测在算法层面通常有三种粒度:粗粒度只有“正常/疲劳”两个状态;中粒度把疲劳拆成“正常/打哈欠/闭眼”;细粒度直接输出PERCLOS(单位时间内眼睛闭合时间占比)等连续指标。我的建议是毕设做二分类或三分类,不要碰PERCLOS回归。
原因很实际:疲劳是一个过程状态,而“打哈欠”和“闭眼”是疲劳的两个强表征。如果做二分类,你只需要一个神经元的输出;如果做三分类,网络在特征层会更早地分离出“嘴部形态”和“眼部形态”,这会让模型的中间特征有可解释性,答辩时你可以把特征图拿出来当素材。但三分类要求数据集必须包含打哈欠和闭眼的独立标注,很多公开数据集的标注质量不足以支撑这个粒度,所以更稳妥的组合是:模型输出二分类,但损失函数基于两个语义通道的预测结果做融合。
2.1.1 主流的两个公开数据集怎么选
公开层面最常用的是NTHU Drowsy Driver Detection Dataset和YawDD(Yawning Detection Dataset),两者都提供视频片段而非单张图片,你需要自己抽帧。NTHU包含不同人种、戴眼镜/不戴眼镜、不同光照场景,样本量对于二分类足够;YawDD更偏嘴巴状态,打哈欠样本非常充足,但闭眼样本偏弱。我一般会把两个数据集的抽帧结果合并使用,而不是死磕某一个。
还有一个可行路线是自采数据:找同学对着摄像头录3到5分钟视频,每人分别录“正常说话”“低头闭眼”“张嘴打哈欠”三个动作,然后每3到5帧抽一帧保存。这样得到的自采集样本虽然数量少,但场景和答辩现场演示时的环境最接近,实测时不容易翻车。合并策略建议是公开数据7成、自采数据3成,避免模型在公开测试集上过拟合。
2.1.2 数据增强的边界条件
疲劳识别场景里,数据增强的策略要克制,不能像ImageNet分类那样激进。随机水平翻转是最安全的;随机亮度扰动放在光照差异大的时候有效;随机旋转只放±10度,再大会把眼睛和嘴巴的相对位置关系破坏掉。最不要用的是随机擦除和重度色彩抖动,前者会直接把眼睛区域擦掉,后者会让肤色失真是常见误用。增强的幅度原则上要保证人类肉眼仍然能够分辨出这个人的“清醒”还是“疲劳”,如果增强后连人都看不出状态,网络学到的一定是伪特征。
2.2 网络结构怎么定:ResNet18与MobileNetV2的取舍
CNN结构图是这个方向检索最多的内容,但对毕设来说,重点不是自己画一个新结构,而是懂得在成熟结构里选一个并讲清理由。基线我推荐ResNet18:它只有11M参数左右,在GPU上训练一轮很快,且残差连接的设计能让梯度顺畅回传,容错率高。MobileNetV2参数更少、推理更快,但精度在公开数据集上比ResNet18略低,适合放在“后期优化”章节里讲。
下表是我习惯使用的选型判断依据,你可以直接写进毕业设计的系统设计章节:
| 模型 | 参数量 | CPU实时推理表现 | 公开数据集聚类场景精度 | 毕设推荐度 | 理由 |
|---|---|---|---|---|---|
| ResNet18 | ~11M | 中等,约20-30ms/帧 | 较高 | 首选 | 训练稳定,结构经典,答辩容易讲 |
| MobileNetV2 | ~3.5M | 快,约10-15ms/帧 | 中 | 优化备选 | 适合做轻量化对比实验 |
| VGG16 | ~138M | 很慢,100ms以上 | 不占优 | 不推荐 | 参数量大,收益不值得 |
| ResNet50 | ~25M | 较慢 | 略高于ResNet18 | 可选 | 精度提升有限,训练时间翻倍 |
2.2.1 输入尺寸的决定:64x64为何够用
疲劳识别不是细粒度分类,不需要像人脸识别那样保留高分辨率纹理。把输入分辨率定为64x64是一个在特征充分性与计算量之间比较平衡的选择。理由有两点:第一,闭眼和打哈欠都是大尺度动作,眼睛区域哪怕只有12x12像素也足够卷积核捕捉到眼睑缝隙;第二,分辨率降下来之后,整个训练集可以整体放进显存,训练迭代速度显著加快,这对毕设周期是重要的。如果你用ResNet18的默认适配尺寸224x224,训练时间会变为4倍以上,收益几乎看不出来。
2.2.2 为什么不直接上YOLO做检测
YOLO系模型在做的是目标检测,输出的是框坐标加类别;本课题如果先做人脸检测再做人脸分类,那检测部分可以复用OpenCV自带的人脸级联器或者MTCNN,而不需要端到端训练检测模型。上来就调到YOLOv5训练自己的数据集,意味着你要额外准备几千张带框标注的人脸数据,这些标注工作量和论文创新点不匹配。常见的一线做法是:检测用OpenCV的Haar Cascade或DNN模块,分类用自己训练的小CNN,检测和分类解耦,这样每一块的错误定位都容易排查。
2.3 目录结构与数据划分的工程化约定
训练之前先把数据目录按ImageFolder格式组织好,这是PyTorch的默认数据组织方式,避免后面为数据加载写额外的解析逻辑:
dataset/ ├── train/ │ ├── normal/ # 正常状态图片 │ └── fatigue/ # 疲劳状态图片 ├── val/ │ ├── normal/ │ └── fatigue/ └── test/ ├── normal/ └── fatigue/验证集必须保证里面的人与训练集的人不重复。疲劳识别领域存在一个知名陷阱:模型记忆的是“张某的眼睛特征”而不是“闭合状态”,跨人验证时准确率会从95%跌到70%。所以划分数据集时,先按身份分组,再用sklearn.model_selection.GroupShuffleSplit进行划分,这是一个加分项,答辩时主动说出来会让老师觉得你懂评估方法。
3. 用PyTorch在本地把CNN训练全流程跑通
3.1 搭建数据管线:从文件夹到Tensor
确认安装好PyTorch、TorchVision、OpenCV、Matplotlib后,第一步是把数据管线写出来。下面这段代码是训练的起点,它从目录读图,做预处理和数据增强,并生成供模型训练用的batch。
import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练集:数据增强策略持克制,翻转+小角度旋转+亮度扰动 train_transform = transforms.Compose([ transforms.Resize((72, 72)), # 先稍放大,再随机裁剪回64x64,有轻微平移增强效果 transforms.RandomCrop(64), transforms.RandomHorizontalFlip(p=0.5), transforms.ColorJitter(brightness=0.15, contrast=0.15), # 不要动色相和饱和度 transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) # 验证集:只做Resize与Normalize,不做任何随机增强 val_transform = transforms.Compose([ transforms.Resize((64, 64)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) train_dataset = datasets.ImageFolder(root='dataset/train', transform=train_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) val_dataset = datasets.ImageFolder(root='dataset/val', transform=val_transform) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)关于参数选择:RandomCrop(64)前先把图片Resize到72,相当于在缩放图上随机取64x64的区域,这会带来最大约12%的平移扰动,对人脸轻微偏出画面中心的情况有正面作用。num_workers按本机CPU核心数设置,Windows上建议设为0或2,设太大可能出现重复数据加载的警告。该数据管线的错误排查重点是查看ImageFolder是否按字母顺序将两个类索引为0和1,打印train_dataset.classes确认即可。
3.2 搭建网络与训练循环:用主干替换最后一层
基于TorchVision加载预训练ResNet18(需联网下载权重),把最后的全连接层改为二分类输出。这里有一个很多教程不会提的点:训练时要把stem层和block层冻结还是解冻。完整项目里摘掉人脸的案例经验是解冻所有层效果更好,因为ImageNet预训练特征是面向通用物体的,而闭眼睁眼是细粒度纹理差异,低层特征也需要针对性微调。所以这里不冻结任何层,只是把学习率设低,初始阶段就能看到相对稳定的收敛。
import torch.nn as nn import torch.optim as optim from torchvision import models # 加载在ImageNet上预训练的ResNet18 model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 将最后的全连接层替换为适合二分类的结构 num_features = model.fc.in_features model.fc = nn.Sequential( nn.Dropout(0.3), nn.Linear(num_features, 2) ) model = model.cuda() criterion = nn.CrossEntropyLoss() # 学习率1e-4,配合余弦退火调度器,pytorch官方实现里这种配比在小数据集上最稳 optimizer = optim.Adam(model.parameters(), lr=1e-4, weight_decay=1e-4) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=20)这里有两个关键设计点。Dropout(0.3)加在全连接层前,能缓解小数据集上的过拟合,但不能设太高,0.5以上在特征已经比较稀疏时反而会让训练不充分。weight_decay=1e-4是L2正则,它配合Dropout共同控制模型容量。如果答辩时被问“为什么过拟合”,你会说出这两层正则的设计逻辑就已经合格。
训练主循环的写法要能把每个epoch的loss和val_acc记录到列表里,方便后面画曲线。额外建议是每个epoch结束时用当前模型跑一遍验证集,但也别每轮都打印全部指标,只保留loss和top1准确率足够。保存模型时有两个文件要保留:best_model.pth(按验证集准确率保存的最优权重)和last_model.pth(最终轮权重),前者用于测试与部署。
best_acc = 0.0 for epoch in range(20): model.train() running_loss = 0.0 for inputs, labels in train_loader: inputs, labels = inputs.cuda(), labels.cuda() optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * inputs.size(0) model.eval() correct = 0 total = 0 with torch.no_grad(): for inputs, labels in val_loader: inputs, labels = inputs.cuda(), labels.cuda() outputs = model(inputs) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() val_acc = correct / total if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), 'best_model.pth') print(f'Epoch {epoch+1}: loss {running_loss/len(train_loader.dataset):.4f}, ' f'val acc {val_acc:.4f}')训练中要盯住两个信号:训练loss下降但验证acc徘徊在50%附近,说明出现严重过拟合,应降低参数量或增大增强强度;训练loss和验证acc一起不动,说明学习率过低或者损失没有下降,可以用torch.autograd检查一下梯度是否非零——Optimizer.param_groups[0][‘lr’]是排查学习率时的第一检索对象。
3.3 训练时四个必调的参数与效果差异
这些参数不要在论文里给出唯一值,要呈现调参变化的对比表,这是毕设加分项:
| 参数 | 范围 | 过低时的表现 | 过高时的表现 |
|---|---|---|---|
| batch_size | 16-64 | 训练震荡,收敛慢 | 显存溢出或收敛过快但泛化差 |
| 初始学习率 | 1e-5-3e-4 | loss几乎不降 | 早期发散,loss变大 |
| weight_decay | 1e-5-1e-3 | 过拟合,val acc与训练acc差距拉大 | 欠拟合,训练acc上不去 |
| Dropout | 0.2-0.5 | 过拟合 | 欠拟合,训练慢 |
毕设场景里,我只建议你跑三组基线:batch_size 32 + lr 1e-4、batch_size 32 + lr 3e-4、batch_size 16 + lr 1e-4。其他组合不做实验,把时间留给后面的实时检测和界面。训练完成后输出loss曲线图和验证集准确率曲线图,这两张图在毕业论文里必须出现,它们是你训练过程可靠性的直接证据。
4. 从离线权重到摄像头实时检测的工程接线
4.1 人脸检测与关键区域裁剪的顺序
模型输出的输入尺寸是64x64的整张人脸,但实时视频流拿到的是一整帧1920x1080图像,中间必须有人脸检测环节。常见做法是先用OpenCV的Haar级联检测人脸框,再把人脸框区域裁剪出来缩放到64x64,送入CNN推理。Haar在正面人脸条件下速度很快,CPU上单帧检测约5ms,足以满足演示需求;缺点是侧脸和低头时可能漏检,所以演示时要略微正面对摄像头,属于物理层面的限定条件。
import cv2 import torch import numpy as np # 读取训练时保存的最优权重 model = models.resnet18(weights=None) model.fc = nn.Sequential(nn.Dropout(0.3), nn.Linear(model.fc.in_features, 2)) model.load_state_dict(torch.load('best_model.pth', map_location='cpu')) model.eval() face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_frontalface_default.xml' )加载时有一个容易踩的坑:因best_model.pth里保存的是state_dict,必须在构建相同网络结构后才能load_state_dict。如果不小心加载了完整模型,用torch.load直接得到的会是dict而非module。这段代码里map_location=‘cpu’保证了没有GPU的答辩机器上也能加载,实际部署时多用CPU只这一处设置就够。
4.2 帧级推理阈值与平滑策略:别让单帧抖动毁掉演示
神经网络对单帧图像的输出是一个二分类概率,在连续视频流中直接按“概率大于0.5即报警”会频繁出现误报,原因有三类:眨眼是正常生理动作、人脸检测框抖动导致输入变化、遮挡导致置信度降低。这里需要做时间维度的平滑,常见做法是维护一个有长度限制的队列,连续N帧都判定为疲劳后,再触发报警。N的取值一般为5到10,帧率25fps时0.2-0.4秒才能触发报警,既不会漏掉打哈欠这种持续动作,也不会被单独一帧眨眼误触发,这个参数要在论文实验里给出对比。
from collections import deque # 报警判定队列,连续6帧疲劳才触发报警 decision_buffer = deque(maxlen=6) alert_threshold = 0.75 def predict_frame(frame): faces = face_cascade.detectMultiScale( frame, scaleFactor=1.1, minNeighbors=5, minSize=(48, 48) ) for (x, y, w, h) in faces: # 裁剪人脸后保持宽高比向正方形填充,避免面部拉伸变形 face_roi = frame[y:y+h, x:x+w] face_resized = cv2.resize(face_roi, (64, 64)) face_rgb = cv2.cvtColor(face_resized, cv2.COLOR_BGR2RGB) face_tensor = torch.from_numpy(face_rgb).permute(2, 0, 1).float() / 255.0 face_tensor = (face_tensor - torch.tensor([0.485, 0.456, 0.406]).view(3,1,1)) / \ torch.tensor([0.229, 0.224, 0.225]).view(3,1,1) face_tensor = face_tensor.unsqueeze(0) with torch.no_grad(): logits = model(face_tensor) prob = torch.softmax(logits, dim=1)[0, 1].item() # 类别1是疲劳 decision_buffer.append(prob > alert_threshold) if sum(decision_buffer) == decision_buffer.maxlen: cv2.putText(frame, "FATIGUE ALERT", (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2) return framescaleFactor=1.1表示检测窗口每次缩放10%,该值越小检测越慢但越准,演示环境建议保持1.1。minNeighbors=5控制误检与漏检平衡,设为5能滤掉大部分非人脸区域,再调高会开始漏检戴眼镜的人。整段代码的处理流程是:先检测全部人脸,再对每张人脸裁剪、缩放、变化到与训练一致的均值和方差,最后取类别1的概率与阈值比较。注意decision_buffer的判空不要用len(decision_buffer)==6这种硬编码,要按maxlen动态取,避免后面对阈值做实验时改错。
4.3 离线评估:用精确率、召回率而不是准确率说明效果
疲劳识别正负样本通常不平衡,视频流中疲劳状态占比低,准确率会被绝大多数正常帧拉高到无意义水平。离线评估时,统计指标要同时打印三个值。精确率关心报警中有多少是真的疲劳,召回率关心真实疲劳被抓住的比例,F1是两者的调和平均。在模型评测代码中,用sklearn.metrics.classification_report和confusion_matrix即可拿到完整结果:将test目录下的所有图片跑一遍预测,收集预测标签和真实标签后直接打印。
训练集与验证集的准确率差异要控制在5个百分点以内,如果差距大于10,说明模型存在过拟合,必须先回去调数据增强和Dropout。接近毕业设计收尾时,所有实验跑完,把最终模型在test集上的结果、实时演示的of/video片段、三个状态(正常/眨眼/打哈欠)的样例帧截图整理到一起,这是写论文实验章的核心材料。
5. 把毕设从“能跑”做到“能答辩”的4个验收技巧
检测系统能跑只是及格线,下面四个技巧是毕业设计答辩前要补上的,每一处都不需要增加训练量,只需在已有结果上加工。
1. 画对系统结构图的粒度。系统结构图不要画成数据流加模块框的流水账,建议画三层:数据层放数据集采集与标注,算法层放人脸检测、CNN分类、时序平滑,应用层放报警与界面。CNN部分在图中单独标注“ResNet18骨干+全连接分类头”,老师一眼能看出你的技术选型是清晰的。架构图建议用Visio或draw.io画,不要直接贴代码注释截图。
2. 做一个单类别消融实验。训练两个模型,一个输入整脸,另一个只输入眼睛与嘴巴的区域裁剪图,对比在测试集上的差异。这个实验成本不高,但能证明你理解“疲劳线索集中在眼部和嘴部”这一领域知识,是拉开分数差距、展示分析能力的常用手法。实验结果可以做成柱状图,配上错误样本的可视化说明。
3. 用类激活图验证模型到底在看哪里。常见实现是Grad-CAM,它能够生成热力图可视化模型的注意力区域。如果疲劳样本的热力图主要集中在眼睛或嘴巴周围,说明模型学到了正确的语义特征;如果热力集中在背景或脸颊部位,说明模型学到了肤色或光照等伪特征,必须回去检查数据。这一张图放在论文里直观且专业。
4. 报一次误报率并解释原因。把所有测试视频按帧标注疲劳与正常,统计系统报警的正确次数和错误次数,计算误报率。误报率在5%以内是较好的结果,超过10%则优先调整平滑队列长度,而不是调分类阈值。这里建议画一条阈值从0.6到0.9的误报率曲线,看清阈值漂移的影响。
到最后几天,把训练命令、测试命令、演示命令分别整理成三个README片段,保证在无网络的教室里也能按步骤把demo跑起来,这比任何花哨功能都更能让你在答辩现场稳住局面。
本文还有配套的精品资源,点击获取