资讯中心

基于SSD300的疲劳检测系统源码解析与实战

📅 2026/10/1 8:54:44
基于SSD300的疲劳检测系统源码解析与实战
简介这套Python毕业设计资源围绕卷积神经网络人脸识别驾驶员疲劳检测与预警系统面向计算机、人工智能等相关专业的学生用于课程设计、期末大作业或毕业设计。系统基于摄像头实时采集驾驶员面部图像经过预处理后利用CNN提取眼睛、嘴巴等关键特征再通过分类器判断疲劳状态并触发预警完整覆盖数据采集、预处理、特征提取、疲劳分类与报警等环节。资源共37个文件以Python源码为主包含16个.py脚本和9个.pyc编译文件涵盖模型训练、测试、实时检测、摄像头调用等模块同时提供3个.pth预训练权重、1个数据集压缩包以及5张测试图片另附说明文档和日志。压缩包整体约500MB结构清晰已有233人学习下载适合需要完整可运行方案、想快速复现或二次开发的人群通过该资源可以获得从模型训练到前端预警的整套代码与数据便于理解CNN在疲劳检测中的实际应用。1. 疲劳检测毕设的另一种打开方式让 SSD 直接回归“眼睛开合”大多数人在做基于 Python 的人脸识别驾驶员疲劳检测与预警系统时第一反应是“先检测人脸→裁剪眼部→用 CNN 分类睁眼还是闭眼”这套三段式流程本身没错但在这份源码里作者选了一条更省事的路线用 SSD300 检测器一次前向直接框出睁眼和闭眼把疲劳检测从“分类问题”变成了“目标检测问题”。拿到手的代码不需要自己搭分类头也不需要手动标眼部小图按照 VOC 格式准备好图像训练脚本跑完就是一个能连摄像头实时出框的预警系统。这个项目适合两类人一类是正在做毕业设计、需要完整源码和数据集兜底的学生另一类是想在车载场景里快速搭一个疲劳预警 Demo、但不想从零写检测器的从业者。接下来我会从网络结构、数据标注、训练复现到报警落地把这套代码的骨架和坑位逐一拆开。2. 源码结构拆解以 SSD300 为基座的检测链路拿到压缩包后第一感觉是文件铺得很满但真正决定系统行为的核心文件其实就几个ssd_net_vgg.py负责网络结构voc0712.py负责读 VOC 数据集Config.py管所有超参数loss_function.py算损失detection.py封装了预测逻辑余下的Train.py、Test.py、eval.py分别是训练、单图测试和评估入口。理解这条链路比直接跑通更重要因为你后面改类别、换数据集、调阈值都是在跟这几个文件打交道。2.1 VGG16 骨干与多层特征图SSD 为什么能兼顾速度和精度ssd_net_vgg.py里定义的是 SSD300 的标准结构。主干网络前半部分直接用 VGG16到conv4_3之后把全连接层替换成一系列卷积层构成conv7、conv8_2、conv9_2、conv10_2、conv11_2连同conv4_3一共取六个特征层分别做预测。大特征图感受野小负责小目标小特征图感受野大负责大目标。疲劳检测里最重要的“眼睛”属于小目标所以conv4_3这层对结果影响最大它的网格是 38×38每个位置生成 4 个先验框全图光是这一层就贡献了 5776 个框。# 先验框生成的核心逻辑对应 ssd_net_vgg.py 里的 prior_box 部分 def prior_box(feat_map_size, min_size, max_size, aspect_ratios): boxes [] for i in range(feat_map_size[0]): for j in range(feat_map_size[1]): cx (j 0.5) / feat_map_size[1] cy (i 0.5) / feat_map_size[0] for min_s in min_size: boxes.append([cx, cy, min_s, min_s]) # 正方形框 for ar in aspect_ratios: boxes.append([cx, cy, min_s * ar ** 0.5, min_s / ar ** 0.5]) # 长方形框 boxes.append([cx, cy, (min_size[0] * max_size[0]) ** 0.5, (min_size[0] * max_size[0]) ** 0.5]) # 跨层框 return boxes这段代码演示了单层特征图的先验框生成方式中心点落在网格中心尺寸由min_size和max_size决定宽高比由aspect_ratios控制。SSD 每个位置生成 4 到 6 个框全图累计 8732 个这就是模型一次前向的预测基数。之所以不用两阶段的 Faster R-CNN是因为它一张图要跑两次网络在车载场景的 CPU 或低端 GPU 上帧率撑不住而 YOLO 虽然快但早期版本对小目标召回率不如 SSD。毕设场景里有训练好的权重兜底SSD 是稳妥选择。2.2 数据集与标注格式fdd-dataset 的 VOC 结构fdd-dataset.zip解压后是标准的 VOC 目录结构Annotations下是每个训练样本的 XML 标注文件JPEGImages下是原始图像ImageSets/Main下是 trainval.txt、train.txt 等划分文件。这份资源里的数据集是驾驶员疲劳检测专用数据类别只有“睁眼”和“闭眼”这也解释了为什么训练入口的类别数要按 2 加 1 背景来配置。voc0712.py做的事情就是解析这些 XML把目标框坐标和类别标签转成训练用的张量。annotation folderJPEGImages/folder filenameframe_002314.jpg/filename object nameclosed_eye/name bndbox xmin168/xmin ymin142/ymin xmax218/xmax ymax172/ymax /bndbox /object /annotation这是一条简化后的标注样例。每个目标框必须落在图像范围内name字段严格对应你在Config.py里定义的VOC_CLASSES列表顺序否则训练时类别索引会错位。bus_dataset.log是作者贴出来的训练日志文件里面有每个迭代的 loss 值你可以拿它当参照来看自己的训练曲线是否正常。一般前 1000 步 loss 会从 8 以上掉到 3 以下后期稳定在 1 附近。2.3 训练与推理链路Train.py 到 detection.py 的数据流训练时voc0712.py每次迭代返回的是一批图像和对应的框坐标、类别标签ssd_net_vgg.py前向产出预测框的位置偏移和类别概率loss_function.py把预测值和真值对齐后计算损失反向传播更新权重。推理时detection.py把预测结果做一次阈值筛选和非极大值抑制输出置信度高于设定值的框再画到原图上。# detection.py 里的核心预测方法精简后 def predict(self, image, threshold0.5, nms_threshold0.45): x self.transform(image).unsqueeze(0) # 转为 300x300 张量 with torch.no_grad(): loc, conf self.net(x) # SSD 前向得到定位和置信度 boxes, labels, scores self.decode(loc, conf) # 解码成真实坐标 keep nms(boxes, scores, nms_threshold) # 非极大值抑制 return boxes[keep], labels[keep], scores[keep]这里有一个新手容易忽视的细节SSD 网络输出的loc是相对先验框的偏移量必须用先验框坐标解码还原成真实坐标不能直接画框。threshold0.5表示置信度低于 0.5 的预测会被丢弃在车载场景里如果检测目标较小可以适当降到 0.35 提高召回率但误报也会增多。nms_threshold0.45表示两个框的 IoU 超过 0.45 时只保留分数高的那个。3. 环境搭建与 Demo 复现从依赖安装到摄像头实时出框这个项目属于典型的老牌 PyTorch 工程环境匹配是第一道门槛。压缩包里的__pycache__目录显示编译产物是cpython-37.pyc说明作者开发和验证时用的是 Python 3.7。你非要用 3.10 也不是不能跑但voc0712.py、augmentations.py里的一些旧 API 有概率直接报错为了省时间直接按 3.7 来配最稳。3.1 依赖清单与版本对照基于项目里 import 到的库依赖集中在 torch、torchvision、opencv-python、numpy 这几个大件上。PyTorch 装 1.7 到 1.10 之间都行不建议装 2.x因为loss_function.py里可能用到旧版的Variable或smooth_l1_loss调用方式2.x 下部分 API 已经变更。OpenCV 用 4.5 或 4.8 均可过高版本要注意cv2.findContours的返回参数个数差异但本项目里影响不大。conda create -n fatigue python3.7 conda activate fatigue pip install torch1.8.1 torchvision0.9.1 pip install opencv-python4.5.5.64 numpy1.21.6torch1.8.1搭配torchvision0.9.1是这几个文件之间兼容性比较稳的组合。numpy不要装 1.24 以上部分旧代码里np.int的写法在新版已经移除虽然这个项目不一定踩到但统一锁版本能少很多折腾。装完后先跑一句python -c import torch, cv2, numpy; print(torch.__version__, cv2.__version__)确认没有静默导入失败。3.2 三个权重文件怎么选weights目录里提供了vgg16_reducedfc.pth、ssd_voc_5000_plus.pth、ssd300_VOC_100000.pth三个权重它们的分工完全不同。vgg16_reducedfc.pth是去掉全连接层的 VGG16 预训练权重只在从头训练时作为主干网络初始化用单独加载它跑检测是没用的。真正能直接出检测结果的是ssd_voc_5000_plus.pth从命名看是在作者自己的疲劳数据集上训练了 5000 步保存的权重。ssd300_VOC_100000.pth是在 VOC 通用目标检测数据集上训练十万步的结果类别是 20 类物体加载它只能识别通用目标和疲劳检测场景不匹配。# 加载权重并切换到评估模式 import torch from ssd_net_vgg import SSD_VGG net SSD_VGG(num_classes3) # 背景 睁眼 闭眼 state_dict torch.load(weights/ssd_voc_5000_plus.pth, map_locationcpu) net.load_state_dict(state_dict) net.eval()map_locationcpu的作用是当机器没有 GPU 或当前环境的 GPU 编号与训练时不一致时先把权重加载到 CPU 再搬到目标设备避免显式cuda()直接崩溃。net.eval()必须调用它会关闭 Dropout 和 BatchNorm 的训练行为否则同一张图两次前向结果会不一样。如果加载时报state_dict键名不匹配优先检查num_classes是否等于训练时的类别数。3.3 复现步骤先跑图片再跑摄像头推荐按“单张图片测试→视频文件→摄像头”三步走每一步都能独立验证环境是否正常。先跑test.py它内部会读入test.jpg或dnf_test.jpg这类测试图调用detection.py的预测函数画框并保存结果图。python test.py正常情况下会在当前目录生成带检测框的结果图控制台打印出检测到的类别和置信度。确认这一步通过后再跑video_detection.py处理视频文件最后才是camera.py打开摄像头。# camera.py 的核心结构 cap cv2.VideoCapture(0) # 0 表示默认摄像头外接摄像头可能是 1 while True: ret, frame cap.read() rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # OpenCV 默认 BGR boxes, labels, scores detector.predict(rgb, threshold0.5) # 遍历检测结果画框标注准确率 for box, label, score in zip(boxes, labels, scores): cv2.rectangle(frame, (int(box[0]), int(box[1])), (int(box[2]), int(box[3])), (0, 0, 255), 2) cv2.putText(frame, f{label}: {score:.2f}, (int(box[0]), int(box[1]) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imshow(Fatigue Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break这段代码有几个关键点。OpenCV 读出来的图像通道是 BGR而训练时用的是 RGB不转换的话检测效果会明显变差这是很多人跑完发现“模型不准”的第一原因。waitKey(1)的数值是等待毫秒数设成 1 表示每一帧都刷新显示设成 0 会卡在单帧等待按键视频流会冻住。按q退出循环后记得cap.release()和cv2.destroyAllWindows()否则摄像头资源不释放下次脚本会报设备占用。4. 训练自己的疲劳检测模型数据集构建与超参数调优项目自带的权重可以直接交差但如果你想在毕业设计里加一点自己的数据或者换一批更贴合自己场景的驾驶员样本就得走完整的训练流程。这个过程可以拆成四段整理数据集、修改配置文件、启动训练、评估效果。每一步都有对应的文件不需要改动网络结构。4.1 把自己拍的图像转成 VOC 格式假设你自己收集了 3000 张驾驶员面部图像睁眼闭眼各一半第一步是把每张图像对应的标注信息写成 XML 文件。常见做法是自己写一个标注脚本或者先用 LabelImg 人工标注前半部分再用程序批量生成后续数据。这里给一个批量生成 XML 的最小脚本它假设你已经用一个文本文件记录了每张图中眼睛框的坐标。import os import xml.etree.ElementTree as ET def write_annotation(img_name, img_w, img_h, objects, out_dir): annotation ET.Element(annotation) ET.SubElement(annotation, folder).text JPEGImages ET.SubElement(annotation, filename).text img_name size ET.SubElement(annotation, size) ET.SubElement(size, width).text str(img_w) ET.SubElement(size, height).text str(img_h) ET.SubElement(size, depth).text 3 for obj in objects: o ET.SubElement(annotation, object) ET.SubElement(o, name).text obj[name] b ET.SubElement(o, bndbox) ET.SubElement(b, xmin).text str(obj[xmin]) ET.SubElement(b, ymin).text str(obj[ymin]) ET.SubElement(b, xmax).text str(obj[xmax]) ET.SubElement(b, ymax).text str(obj[ymax]) tree ET.ElementTree(annotation) tree.write(os.path.join(out_dir, img_name.replace(.jpg, .xml)))这个脚本里objects是一个字典列表每个字典包含name、xmin、ymin、xmax、ymax五个字段。坐标必须是整数且xmax、ymax要大于xmin、ymin否则后面的数据增强会报非法框。写完后把 XML 放进Annotations图像放进JPEGImages再在ImageSets/Main下生成 train.txt、val.txt文本里每一行是不带后缀的图像文件名。4.2 改 Config.py类别、学习率、批量大小Config.py是整个训练流程的调度中心所有会影响模型行为的参数都集中在里面。核心的几个参数需要根据自己的数据规模调整我列一个典型配置供参考。参数名典型值说明VOC_CLASSES(background, open_eye, closed_eye)类别名列表顺序决定标签索引BATCH_SIZE8 或 16显存不够就调到 4梯度会震荡但能跑BASE_LR1e-3骨干网络微调用1e-4从头训练用1e-3MOMENTUM0.9SGD 冲量不要改WEIGHT_DECAY5e-4权重衰减防止过拟合MAX_ITER5000到20000数据量大就多跑看 loss 收敛情况NUM_CLASSES3背景 睁眼 闭眼和VOC_CLASSES长度一致# Config.py 中的典型配置片段 VOC_CLASSES (background, open_eye, closed_eye) BATCH_SIZE 8 BASE_LR 1e-3 MOMENTUM 0.9 WEIGHT_DECAY 5e-4 MAX_ITER 8000 NUM_CLASSES len(VOC_CLASSES)BASE_LR是最容易出问题的参数。如果你用ssd_voc_5000_plus.pth作为起点继续微调学习率一定要降到1e-4否则前几步 loss 就会冲到 NaN 或权重崩坏。如果是从头训练1e-3起步没问题但前提是vgg16_reducedfc.pth正确加载了。BATCH_SIZE受显存限制8G 显存跑 8 没问题4G 显存就设 4同时建议把输入尺寸从 300×300 换成 320×320 附近不会报错但这里保持默认最省事。4.3 loss_function.py 的损失构成定位和分类分开算loss_function.py实现的是 SSD 标准的多任务损失定位损失用 Smooth L1分类损失用交叉熵两者按权重相加。SSD 训练时有个关键细节——正负样本不均衡默认情况下大部分先验框都是背景所以代码里会按 1:3 的比例采样负样本只保留难分样本参与分类损失计算。# loss_function.py 的损失计算简化版 def forward(self, loc_pred, conf_pred, loc_target, conf_target): pos_mask conf_target 0 # 非背景的框为正样本 loc_loss smooth_l1_loss(loc_pred[pos_mask], loc_target[pos_mask]) # 负样本抽样按分类损失排序取难分样本参与计算 neg_mask hard_negative_mining(conf_pred, conf_target, pos_mask) conf_loss cross_entropy(conf_pred, conf_target, pos_mask | neg_mask) return loc_loss conf_lossloc_loss只在正样本框上计算因为背景框没有坐标回归目标。hard_negative_mining是 SSD 训练稳定性的关键它把所有负样本按分类损失从大到小排序取前若干难分样本参与梯度回传防止大量简单负样本淹没梯度。你训练时如果发现 loss 下降很慢大概率是正样本框太少说明数据集里眼睛区域标注过小或过少可以回头检查标注质量。4.4 启动训练与评估盯日志、存权重、看 mAP配置改好后直接运行训练脚本训练过程中会产生类似bus_dataset.log的日志输出记录每次迭代的 loss。训练结束后权重默认保存到weights/目录文件名会带上迭代数。python Train.py训练时建议每 100 步看一次日志如果 loss 在下降过程中出现突然跳高通常是某个 batch 的数据有问题比如 XML 坐标越界或图像读取失败如果 loss 一直在 10 以上不降优先检查类别名是否和标注文件一致。训练完成后用eval.py在验证集上评估 mAP数值在 0.75 以上基本够用0.85 以上属于不错的结果。# eval.py 的评估逻辑简写 from voc0712 import VOCDetection val_dataset VOCDetection(rootVOCdevkit, image_setval) average_precisions eval_ap(val_dataset, detector) mAP sum(average_precisions.values()) / len(average_precisions)评估时有一个容易忽略的问题验证集划分是否和训练集重叠。如果ImageSets/Main里的 val.txt 和 train.txt 有交集mAP 会虚高答辩时专家一深问就露馅。稳妥做法是训练前把数据集按 8:2 划分确保完全互斥。5. 避坑记录从环境装到模型预测七个翻车点复盘这套代码我复现过两次第一次是在同事机器上第二次是自己新换的笔记本每一次都踩了不同的坑。下面这几条是出现过频率最高的问题按现象、原因、解决写清楚你遇到同样的报错可以直接对着排查。5.1 FileNotFoundErrorVOC 数据集路径找不到现象运行Train.py或Test.py报FileNotFoundError: [Errno 2] No such file or directory: VOCdevkit/VOC2007/...。原因voc0712.py里默认从当前工作目录拼接VOCdevkit路径而你把数据集解压到了别的目录或者没有在项目根目录下启动脚本。解决把解压后的VOCdevkit文件夹放到项目根目录然后在项目根目录下执行python Train.py如果你把数据集改名了也要同步修改voc0712.py里的VOC_ROOT变量。5.2 权重加载报错state_dict 的键名对不上现象执行net.load_state_dict(state_dict)时报Missing key(s)或Unexpected key(s)。原因最常见的是num_classes没改加载的权重是在 3 类模型下训练的但你定义网络时传了 21 类最后一层卷积的输出通道数对不上。解决确认Config.py里NUM_CLASSES与权重训练时一致如果模型定义和权重来源不明也可以用strictFalse加载但这样部分层的权重不会生效效果会打折不建议用于正式使用。5.3 Python 版本不匹配__pycache__出卖了作者环境现象import voc0712直接报语法错误或者AttributeError: module torch has no attribute Tensor。原因压缩包里的.pyc文件是cpython-37.pyc说明代码是在 Python 3.7 下编译的在高版本下部分旧语法或旧 API 不兼容。解决直接用 conda 建一个 3.7 的虚拟环境把torch1.8.1装好通常能避免大部分兼容性问题。如果你非要用 3.10至少要把.pyc缓存清掉并逐个文件排查np.int、np.float这类旧写法。5.4 摄像头打不开或画面全黑现象camera.py运行后窗口弹出来但全黑或者直接报[ WARN] Cannot capture video. 原因cv2.VideoCapture(0)里的 0 是设备索引笔记本自带摄像头和外接 USB 摄像头并存时外接摄像头通常是 1 或 2你打开的是不存在的设备。解决先写一行python -c import cv2; print(cv2.VideoCapture(1).read())逐个索引试探找到能读到图像的索引填进去也可以改成cv2.VideoCapture(0, cv2.CAP_DSHOW)在 Windows 上强制用 DirectShow 驱动。5.5 训练时 loss 变成 NaN现象训练跑到几十步后日志里 loss 突然变成nan之后一直不恢复。原因学习率偏大导致梯度爆炸这是最常见的情况其次是某个 batch 的标注框坐标出现非法值比如xmax小于xmin让定位损失的 Smooth L1 算出了异常梯度。解决把BASE_LR从1e-3降到1e-4同时检查所有 XML 里xmax xmin、ymax ymin是否成立写一个小脚本遍历全部 XML 校验一遍非法的直接删除或重新标注。5.6 检测框大量重复目标被框好几遍现象一张图上同一个眼睛被框了三四次置信度都超过阈值。原因nms_threshold设得太大比如改到 0.6 以上时重叠度高的框不容易被抑制掉或者是detection.py里漏调了 NMS。解决把nms_threshold恢复到默认的 0.45如果还是重复检查代码里decode之后的keep索引是否正确应用到了原始boxes上常见错误是在 NMS 之后又用了未经筛选的完整框列表去画图。5.7 检测速度很慢摄像头画面明显卡顿现象camera.py跑起来只有每秒不到 10 帧画面有明显延迟。原因输入图像每次都走完整的 300×300 前向而摄像头分辨率较高时图像预处理和 OpenCV 的resize成了瓶颈另外如果你的环境用的是 CPU 推理SSD300 本身的 FLOPs 就不低。解决先用cv2.resize把摄像头采集的帧缩到 640×480 再送入检测器如果还卡把检测阈值从0.5提到0.6减少需要绘制的候选框数量实在不行改成隔帧检测即每两帧取一帧做预测上一帧的结果直接复用实测帧率能提升接近一倍。6. 从检测到预警PERCLOS 指标与报警逻辑的工程落地如果你的毕业设计到这里就交差其实已经具备了完整的技术链。但“疲劳检测”的完整闭环里“检测”只是眼睛“预警”才是最后那一下刹车。项目里的camera.py只做了实时画框并没有判断驾驶员是否真的疲劳很多人答辩时被问到“你如何定义疲劳”就卡住了。这里给一个工程上常用的落地方案PERCLOS即单位时间内眼睛闭合帧数占总帧数的比例。# 基于滑动窗口的疲劳判断逻辑 from collections import deque CLOSED_THRESHOLD 0.5 # 单帧闭眼置信度阈值 PERCLOS_THRESHOLD 0.4 # 滑动窗口内闭眼帧占比阈值 WINDOW_SIZE 150 # 滑动窗口大小约 5 秒 30fps eye_history deque(maxlenWINDOW_SIZE) while True: ret, frame cap.read() rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) boxes, labels, scores detector.predict(rgb, threshold0.4) # 统计闭眼取置信度最高的闭眼框 closed False for label, score in zip(labels, scores): if label closed_eye and score CLOSED_THRESHOLD: closed True break eye_history.append(1 if closed else 0) # 滑动窗口计算 PERCLOS perclos sum(eye_history) / len(eye_history) if perclos PERCLOS_THRESHOLD: # 触发报警这里可以接蜂鸣器、语音提示或写入日志 cv2.putText(frame, FATIGUE DETECTED!, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3)这段代码把单帧检测结果序列化成了一个滑动窗口。WINDOW_SIZE150在 30 帧率下对应 5 秒窗口PERCLOS_THRESHOLD0.4表示这段时间里闭眼帧超过 40% 就判定为疲劳。这两个参数是经验值你可以根据实际测试调整但要注意窗口太短容易误报窗口太长反应太慢一般建议在 3 到 8 秒之间选一个。报警动作可以接三种方式cv2.putText在画面上显示文字winsound.Beep()在 Windows 上发蜂鸣声或者把疲劳事件写入 CSV 文件方便事后分析。工程上我一般会把“检测”和“预警”拆成两个线程检测线程只负责把每一帧的闭眼状态写入队列预警线程独立消费队列做 PERCLOS 统计这样检测慢的时候报警逻辑不会被拖死。从那以后我每次拿到一套新的检测代码都会先跑一遍test.py确认环境再跑camera.py确认实时链路最后才改自己的业务逻辑。这套顺序看起来简单但它能帮你区分“环境问题”“模型问题”和“业务问题”省掉大量翻车的排查时间。希望这篇拆解对你有帮助也祝你一次跑通。本文还有配套的精品资源点击获取

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案