简介:面向计算机相关专业毕业设计、课程设计及期末大作业场景,这套基于卷积神经网络CNN的疲劳驾驶识别检测系统资源,提供完整源码与配套数据集,解决从模型训练到实时检测落地的关键问题。项目源自经导师指导并认可的高分毕业设计,评审98分,适合需要完整项目实战的学生与初级算法学习者。资源围绕驾驶员疲劳检测任务,包含Python训练/推理脚本、基于VGG16和SSD的模型权重(.pth)、数据集压缩包、配置与说明文档、训练日志以及测试图片等。压缩包共37个文件,以.py源码、.pyc编译文件、.pth权重、.jpg图片、.txt说明和.log日志为主,整体约500.41MB。目录中既有Train.py、Test.py、Config.py等入口文件,也有ssd_net_vgg.py、camera_detection.py等模型与检测模块,方便对照学习网络结构、训练流程和摄像头调用逻辑。目前已有128人学习下载,可作为计算机视觉方向高完成度参考,便于二次开发与功能扩展。
1. 疲劳驾驶识别为什么选这个 CNN 方案
疲劳驾驶导致的交通事故比例常年居高不下,但真正落地到车机或后装摄像头上的检测系统,第一瓶颈往往不是算法精度,而是你拿什么框架去承载「眼睛闭合」「打哈欠」「低头」这些细粒度状态。这个毕设项目给了一条很直接的路线:不先做人脸关键点检测、再算 EAR/MAR 阈值,而是直接用 SSD 目标检测框架,把睁眼、闭眼、嘴巴开合当成目标框一次回归出来。反直觉的地方在于,摄像头位置固定后,人脸尺度变化有限,SSD 这类单阶段检测器比「先裁人脸再分类」的两阶段流程更省算力,部署到普通笔记本摄像头也跟得上实时。整套源码包含完整的训练脚本、VOC 格式数据集、三种推理入口和多个预训练权重,适合正在做卷积神经网络方向课程设计或毕业设计的人直接解剖运行,也能给想把手头检测模型换成 SSD 管线的工程师做参考。
2. SSD-VGG16 检测管线与模型文件拆解
拿到压缩包先不要急着跑 Train.py,目录里ssd_net_vgg.py、l2norm.py、loss_function.py、voc0712.py这几个文件构成了 SSD 的核心闭环。疲劳驾驶场景下目标尺度小、形态多样,搞清楚这几个模块在管道里的位置,后面调参才有依据。
2.1 backbone 选型:VGG16 为什么够用
ssd_net_vgg.py里默认的 backbone 是 VGG16,这在今天看似乎不如 ResNet 或 CSPNet 新,但它的意义在于迁移非常顺滑。vgg16_reducedfc.pth是去掉了最后三个全连接层的 VGG16 预训练权重,加载之后 conv4_3 之前的所有卷积层都已经具备较强的边缘、纹理、五官结构响应,在这个基础上微调疲劳状态识别,只需要训练后面的新增卷积层和分类头。相比从零初始化,收敛速度肉眼可见地快,对只有一张普通显卡的环境也更友好。
常见做法是把 conv4_3 的 stride 从 2 改成 1,使其输出 38×38 的特征图来保留更细粒度的眼部、嘴部信息;但这样做会让感受野变小,所以原图被 resize 到 300×300 后输入。VGG16 前面的层参数量大,这个项目冻结前几层只训练后半部分是完全可行的,我是这么操作的:先把ssd_net_vgg.py里的base列表前 10 层requires_grad_(False),剩下卷积层和额外层用 1e-3 的学习率跑,比全程微调的 loss 曲线更稳定。
# 加载预训练 backbone 并局部冻结,示例来自 ssd_net_vgg.py 的初始化逻辑 import torch from ssd_net_vgg import SSD net = SSD(num_classes=2, backbone='vgg16_reducedfc') state_dict = torch.load('vgg16_reducedfc.pth', map_location='cpu') net.load_state_dict(state_dict, strict=False) for name, param in net.named_parameters(): layer_idx = name.split('.')[1] if layer_idx.isdigit() and int(layer_idx) < 10: param.requires_grad = False这段代码的关键是strict=False,因为预训练权重里没有 SSD 新增的extras、loc_layers、conf_layers这些结构,缺失的键会被自动跳过。layer_idx取的是基础网络层的序号,前 10 层基本覆盖 VGG16 的 conv1 到 conv3 段,冻结它们可以显著减少显存占用,把更多资源留给后面的多尺度预测层。如果你的数据集和疲劳检测差异很大,比如要检测遮挡状态,这 10 层的阈值可以下调到 6,让更多底层特征参与训练。
2.2 多尺度特征图与 default box 生成规则
SSD 的核心设计是「在不同深度的特征图上各自预测」,浅层特征图负责小目标,深层特征图负责大目标。ssd_net_vgg.py里通过extras模块在 VGG16 之后继续堆叠卷积,最终形成六层预测特征图,默认 8732 个候选框。疲劳驾驶场景下,闭眼和打哈欠的局部区域在整张图里的占比不大,这些框的分布直接决定召回率,必须先理解再动手。
| 预测层 | 输出尺寸 | 每位置默认框数 | 在该层的关注目标 |
|---|---|---|---|
| conv4_3 | 38×38 | 4 | 眼睛、嘴角等极小区域 |
| fc7(conv7) | 19×19 | 6 | 单眼、半张脸 |
| conv8_2 | 10×10 | 6 | 整张脸、方向盘区域 |
| conv9_2 | 5×5 | 6 | 头部、上半身 |
| conv10_2 | 3×3 | 4 | 大尺度遮挡目标 |
| conv11_2 | 1×1 | 4 | 全图级上下文 |
脸上的疲劳信号本质上是一个「局部细节优先」的任务,所以 conv4_3 层的 38×38 网格承担了最重的检测责任。由表可见,越靠前的层默认框越多,这与小目标在空间中出现的密度是匹配的。实际测试时如果发现闭眼状态常被漏检,优先调整 conv4_3 和 fc7 两层的min_size和max_size参数,而不是去动后面的层,这一点在Config.py里都有对应字段。若你之前是拿 YOLOv8 训练自己的数据集,对这种多尺度预测的感受可能不深,因为 ultralytics 封装把 anchor 生成全部隐藏了,而在这个项目里这些逻辑全在ssd_net_vgg.py里暴露着,反而更适合做机理研究。
2.3 l2norm.py 与 loss_function.py:两个容易被忽略的细节
l2norm.py的定位是给 conv4_3 的输出做通道维度归一化。VGG16 前几层特征值较大,不加归一化的话,深层特征图的梯度容易被浅层的大数值覆盖。这个模块虽然只有十几行,但去掉之后训练 loss 会明显波动,不要因为看起来像「附加件」就跳过。
# l2norm.py 的核心 forward 逻辑 import torch import torch.nn as nn class L2Norm(nn.Module): def __init__(self, n_channels, scale=20): super(L2Norm, self).__init__() self.n_channels = n_channels self.weight = nn.Parameter(torch.Tensor(n_channels)) self.eps = 1e-10 self.reset_parameters() def reset_parameters(self): nn.init.constant_(self.weight, self.scale) def forward(self, x): norm = x.pow(2).sum(dim=1, keepdim=True).sqrt() + self.eps x = x / norm out = self.weight.view(1, -1, 1, 1) * x return outnormalize之后再乘一个可学习的缩放向量,相当于让网络自己决定每个通道在归一化后应该放大多少。scale默认 20 是 SSD 作者从验证集上调出来的经验值,在疲劳状态这种类间差异很小的任务上,我一般会把 scale 初始化为 10 再训练,收敛后特征分布更集中。eps是防止除零,这个值过小会在 fp16 训练时导致数值不稳定,改成 1e-5 更保险。loss_function.py里的 MultiBoxLoss 则负责两件事:定位损失用 smooth L1 回归 default box 的偏移量,分类损失用交叉熵判断每个框属于哪类疲劳状态;同时通过 hard negative mining 控制正负样本比例在 1:3 左右,避免大量背景框淹没真实目标。
3. 从 fdd-dataset 到 Train.py:训练闭环的落地
原包里的fdd-dataset.zip就是为本项目定制的疲劳驾驶数据集,解压后是标准的 VOC 目录结构。这一步的实操价值在于:你要学会把一个陌生的 VOC 数据集灌进 SSD 管道,这是所有后续训练的前提。
3.1 VOC 格式数据集与 voc0712.py 加载器的改造
voc0712.py原本是为 PASCAL VOC 2007/2012 设计的加载器,它通过解析 XML 标注文件把目标类别和坐标读进来。疲劳驾驶数据集的 XML 结构与 VOC 一致,所以不需要重写,只需要修改路径和类别列表。
# voc0712.py 中类别定义的改造示例 # 原 VOC 有 20 类,疲劳驾驶数据集只有 2~3 个状态类 VOC_CLASSES = ( 'background', # 背景类,索引 0 'eye_closed', # 闭眼 'mouth_open', # 打哈欠/张嘴 ) # 数据集根目录指向解压后的 fdd-dataset dataset_root = 'data/fdd-dataset'类别表里background必须占第 0 位,SSD 的置信度输出维度是(num_classes+1) * num_priors,类别索引写错会导致训练时 loss 早停或者推理时全部预测成背景。如果你的数据集还定义了head_down这种低头状态,直接在VOC_CLASSES后面追加即可,但记得同步修改Config.py里的num_classes。加载器还会在__getitem__里根据 XML 的difficult标记决定是否忽略该目标,疲劳数据集没有专门标注这个字段,默认全部参与训练,如果发现某些图里有半张脸被错误框出,可以在预处理阶段把面积过小的框过滤掉。
3.2 augmentations.py:针对小目标的数据增强策略
augmentations.py实现的增强组合在 SSD 里叫「数据采样策略」,它不是简单随机翻转,而是通过随机裁剪改变目标在画面中的尺度分布,强迫模型适应不同距离下的眼睛和嘴部状态。这套增强策略专门解决摄像头安装位置不同、人脸时远时近的问题,比盲目加高斯噪声有效得多。
# augmentations.py 中随机裁剪的核心选择逻辑(伪代码示意) if random.random() < 0.6: # 随机选取一个与真实框 IoU 满足条件的裁剪区域 for _ in range(50): x1, y1, x2, y2 = random_crop_region(img.shape) ious = compute_iou(gt_boxes, (x1, y1, x2, y2)) if ious.min() >= 0.3: # 保证至少部分目标保留 img = img[y1:y2, x1:x2] gt_boxes = clip_and_adjust(gt_boxes, (x1, y1, x2, y2)) break这里0.3是一个关键阈值:如果裁剪区域与真实框的 IoU 太低,训练样本里都是残缺目标,模型会倾向把闭眼和打哈欠误判为背景;如果太高,又起不到尺度变化的效果。通常我会把阈值设在 0.3~0.4 之间。同时augmentations.py里还有色度、亮度、对比度的随机抖动,对车内光照变化是这个项目的刚需,尤其是傍晚和夜间仪表盘补光场景,建议保持不变。
3.3 Train.py 训练流程与三个权重的分工
训练脚本的任务是把预训练权重、数据集加载器、loss 函数串起来。项目里三个.pth文件分别扮演不同角色:vgg16_reducedfc.pth是 VGG16 预训练 backbone,ssd300_VOC_100000.pth是 SSD 在 PASCAL VOC 上训练 10 万步的完整初始化权重,ssd_voc_5000_plus.pth是短期训练的中间 checkpoint。在这个数据集上训练,优先加载ssd300_VOC_100000.pth比只加载 VGG16 效果更好,因为这部分权重已经见过大量的目标尺度变化,迁移到疲劳状态检测时框的定位会更稳。
python Train.py \ --dataset fdd \ --batch_size 8 \ --lr 1e-3 \ --num_epochs 50 \ --save_folder weights/ \ --pretrained ssd300_VOC_100000.pthbatch_size太小会导致 BN 统计量抖动,太大则显存溢出,8 是一个在 8GB 显卡上比较稳妥的值;lr采用多步衰减策略,每 20 个 epoch 降为原来的 0.1。Train.py里每隔固定迭代数会打印 loss 并保存一次模型,bus_dataset.log就是这类输出重定向后的记录文件,观察它时重点看Loss/Loc和Loss/Conf是否同步下降,如果 Conf 降了但 Loc 不动,通常是在线难例挖掘的负样本比例失调,检查loss_function.py里的neg_pos_ratio。
| 权重文件 | 角色定位 | 适用场景 |
|---|---|---|
| vgg16_reducedfc.pth | 只含 VGG16 卷积层 | 从零微调、显存极紧张 |
| ssd300_VOC_100000.pth | 完整 SSD-VOC 权重 | 常规迁移训练首选 |
| ssd_voc_5000_plus.pth | 训练中期的快照 | 恢复中断训练、对比效果 |
4. 图片、摄像头、视频三种推理方式的实现差异
项目里同时存在detection.py、camera_detection.py、camera_detection_1.py和video_detection.py,它们不是重复代码,而是分别覆盖静态检测、摄像头实时检测、视频文件检测三种部署场景。三者的模型加载和预处理逻辑完全一致,差异主要体现在数据源和帧率控制上。
4.1 detection.py:单张图片的完整推理链路
detection.py是理解整个推理流程的入口:读入图片、resize 到 300×300、归一化、经过 SSD 前向传播、对输出解码、最后做 NMS 去重。NMS 的 IoU 阈值在代码里通常设置为 0.45,低于这个值时相邻框不能合并,同一个眼睛会被框两次;高于这个值,两个相邻状态框可能被错误合并。疲劳检测场景中闭眼和打哈欠往往同时出现且位置接近,建议保持 0.45~0.5。
# detection.py 推理主流程关键代码 img = cv2.imread('test.jpg') img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (300, 300)).astype(np.float32) # SSD 默认使用 ImageNet 均值归一化 img -= (104.0, 117.0, 123.0) img = img.transpose(2, 0, 1) img = torch.from_numpy(img).unsqueeze(0) with torch.no_grad(): loc, conf = net(img) # loc 是框偏移,conf 是类别概率 boxes = decode(loc[0], priors) # 结合 default box 解码 result = nms(boxes, conf[0], 0.45) # 非极大值抑制decode这一步需要拿Config.py里的variance参数做缩放,不同版本的 SSD 实现方差值可能是 0.1 或 0.2,直接照搬其他仓库的解码函数会导致框整体偏移。使用过程中如果发现检测框比实际眼睛区域大一圈,优先怀疑是variance不匹配,而不是网络没收敛。
4.2 camera_detection.py:实时摄像头逐帧检测
camera_detection.py面向的是车载摄像头实时画面,用 OpenCV 的VideoCapture逐帧读取,每帧做一次前向推理,再把标注结果画回画面。这里对帧率的优化是最值得借鉴的地方。
# camera_detection.py 的核心循环结构 cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while True: ret, frame = cap.read() if not ret: break # 按 stride=2 抽帧推理,降低连续帧的计算开销 if frame_count % 2 == 0: dets = detect(frame) last_dets = dets draw_boxes(frame, last_dets) cv2.imshow('fatigue_detect', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break抽帧策略是实时摄像头检测的常用手段,frame_count % 2 == 0让每两帧执行一次推理,间隙帧直接复用上一帧的检测结果绘制边框。对 30FPS 的摄像头,相当于把推理频率降到 15FPS,CPU 也能跑得动。若只看推理结果不叠加连续帧信息,draw_boxes里的判断逻辑只依赖单帧概率,这时候闭眼概率波动会导致状态闪烁,解决方式放在第 5 章。camera_detection_1.py与camera_detection.py的差别需要 diff 对比,通常是多模型组合或者加入了一个简单的疲劳计数逻辑。
4.3 video_detection.py:视频文件离线批处理
video_detection.py的输入是录好的行车视频或监控录像,核心差异在于输出目标:它会把检测结果写回新的视频文件,并逐帧统计闭眼或打哈欠的帧数。
# video_detection.py 写回视频的配置 fourcc = cv2.VideoWriter_fourcc(*'mp4v') writer = cv2.VideoWriter( 'output_video.mp4', fourcc, fps, (frame_width, frame_height) )fourcc编码器要和输出文件后缀匹配,mp4v对应.mp4,XVID对应.avi,不改编码器直接改后缀会导致生成的视频播放器打不开。离线处理不追求实时性,可以关闭抽帧逻辑逐帧检测,并把检测类别、置信度打印到终端,方便后续把结果导入表格做统计。视频和摄像头两种入口封装的接口保持一致,换数据源只需改前三行,这是该项目代码结构上比较好的地方。
| 推理方式 | 输入 | 输出 | 核心差异 |
|---|---|---|---|
| detection.py | 单张 jpg | 标注后图片 | 无实时性要求,可跑完整 NMS |
| camera_detection.py | 摄像头流 | 实时画面 | 抽帧 + 复用上一帧结果 |
| video_detection.py | 视频文件 | 标注视频文件 | 逐帧写回,需要选对编码器 |
5. 部署调试:权重选型、pyc 缓存与 PERCLOS 判疲劳
5.1 三个 pth 与环境一致性检查
__pycache__目录里有大量cpython-37.pyc文件,说明原项目跑在 Python 3.7 环境。如果你的机器是更高版本,建议第一次运行前把缓存清理干净,避免 IDE 索引到旧字节码造成ImportError或函数签名对不上。
find . -name "*.pyc" -delete rm -rf __pycache__ # 校验当前环境关键依赖版本 python -c "import torch, cv2; print(torch.__version__, cv2.__version__)"运行项目前先确认 torch 版本与预训练权重的序列化方式兼容,PyTorch 1.6 之后保存的权重在 2.x 加载通常没问题,但反过来会报UnicodeDecodeError。如果 CPU 环境跑,camera_detection.py建议把输入分辨率降到 320×240,detect前向时间可以控制在 80ms 左右。
三个权重的选择逻辑很简单:最终部署优先用自己训练的权重,恢复实验用ssd_voc_5000_plus.pth,确定模型结构能跑通再切换成ssd300_VOC_100000.pth做迁移训练。直接把ssd300_VOC_100000.pth用于推理是常见误用,它的类别是 PASCAL VOC 的 20 类,不会输出你需要的闭眼、打哈欠结果。
5.2 从检测框到疲劳判定:PERCLOS 的实现
拿到每一帧的眼睛开合状态后,业界公认的疲劳指标是 PERCLOS,即单位时间内眼睛闭合帧数占总帧数的比例。这个项目没有直接封装该统计逻辑,需要自己补一段滑动窗口代码。
# 基于检测结果的 PERCLOS 疲劳判定 history = [] window_size = 300 # 按 30FPS 算,约 10 秒窗口 closed_threshold = 0.4 # 闭眼概率超过该值判定为闭眼 def push_status(p_eye_closed): history.append(1 if p_eye_closed > closed_threshold else 0) if len(history) > window_size: history.pop(0) perclos = sum(history) / len(history) return perclos > 0.4 # PERCLOS 超过 40% 触发疲劳报警 while cap.isOpened(): ret, frame = cap.read() eye_prob = detect_eye_state(frame) # 从 conf 向量里取闭眼类别概率 if push_status(eye_prob): cv2.putText(frame, "FATIGUE", (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 2)窗口大小和阈值要按摄像头安装位置调。仪表台上方的摄像头视角偏正,closed_threshold可以稍微降低;安装在 A 柱侧面时,眼睛本来就容易因角度被误判为闭合,阈值需要提高到 0.5。PERCLOS 的判定阈值 0.4 来自驾驶疲劳研究中的经典标准,但实际车内光线不足时闭眼概率普遍偏低,可以先观察正常驾驶 5 分钟的基线值再定。把窗口从 300 帧缩短到 120 帧能更快报警,但眨眼带来的瞬时闭合容易造成误报;反过来加长到 600 帧又会延迟报警,落地时建议把窗口和阈值作为参数暴露出来,让驾驶员测试后微调。将报警逻辑再加一层「连续 N 帧保持闭眼」的条件,误报率会更低。
本文还有配套的精品资源,点击获取