尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于SSD与VGG16的疲劳驾驶检测系统:Python+PyTorch实战解析

基于SSD与VGG16的疲劳驾驶检测系统:Python+PyTorch实战解析 简介这是一套基于Python卷积神经网络的人脸识别驾驶员疲劳检测与预警系统毕业设计项目面向计算机专业正在完成课程设计、期末大作业或毕业设计的本科生。项目采用SSD/VGG网络框架覆盖人脸检测、疲劳状态判断如闭眼、打哈欠等核心环节包含模型训练、评估、摄像头实时检测与视频检测模块经导师指导并获98分高分。资源压缩包共37个文件总大小约500MB主要包含16个Python源码文件网络定义、损失函数、数据增强、推理测试等、3个预训练权重文件、1个数据集压缩包及多张测试图片可支持从数据准备到训练推理的完整流程复现。已有182人学习下载适合希望快速上手完整项目、积累实战经验的深度学习学习者。借助该资源读者可直接加载权重验证检测效果也可根据数据集与脚本二次开发为论文或报告提供有力支撑。1. 从疲劳驾驶到实时预警这套PythonCNN毕业设计到底做了什么夜间跑高速最怕的不是车况是你自己先扛不住。方向盘开始无意识地修正、视线偏离车道线超过两秒事故往往就发生在这一瞬间。与其事后靠行车记录仪复盘不如在视觉层面直接把“人”盯住摄像头对着驾驶员面部用卷积神经网络框出人脸再根据眼睛的张开与闭合状态算出疲劳指标一旦连续多帧闭眼就触发报警。这就是这套基于Python与PyTorch的疲劳驾驶检测系统的核心链路也是它作为毕业设计拿到98分评审分的底气所在。整套系统的技术路线并不复杂SSD作为人脸检测器取VGG16作为backbone配合FDDB公开数据集训练出两个不同步数的权重检测到人脸后结合眼睛关键点计算PERCLOS单位时间内眼睛闭合时间占比超过阈值就认为驾驶员处于疲劳状态。项目里既包含完整的训练代码Train.py、voc0712.py、augmentations.py也包含可以直接跑通的实时推理脚本camera_detection.py、video_detection.py还提供了权重和数据集不用再满互联网找资源。对于正在做课程设计、期末大作业的在校生或者想快速上手SSD与人脸检测实战的开发者这套源码的参考价值都相当高。下面从模型原理、训练流程、疲劳判定逻辑和模型选型四个层面把它彻底拆开。2. SSD与VGG16 backbonevgg16_reducedfc.pth在人脸检测里的真实作用2.1 为什么选SSD而不是YOLO或Faster R-CNN疲劳检测场景对目标检测模型有两个硬性要求一是延迟要低摄像头画面必须实时处理每帧推算时间超过100毫秒预警就失去了意义二是人脸尺度变化大驾驶员稍微前倾或后仰面部在画面中的大小差异非常明显。Faster R-CNN精度高但两阶段结构在CPU上跑不动而YOLO虽然快早期版本的密集小目标召回率并不理想。SSDSingle Shot MultiBox Detector走的是单阶段检测路线在VGG16的基础上额外接了多层卷积特征图每一层负责预测不同尺度的先验框。这种多尺度特征金字塔设计让它在保持检测速度的同时对人脸这类尺度变化较大的目标有不错的鲁棒性。项目里所有检测逻辑都封装在ssd_net_vgg.py中backbone直接复用VGG16在ImageNet上的预训练权重也就是文件列表里的vgg16_reducedfc.pth——注意“reducedfc”这个后缀它意味着原始VGG16的全连接层被替换成了卷积层只保留到conv5_3之前的特征提取部分后面的层全部交给SSD新增的检测分支。2.2 vgg16_reducedfc.pth与模型组装权重是如何接上的vgg16_reducedfc.pth并不包含SSD检测头的参数它只负责初始化主干网络。在ssd_net_vgg.py中模型分两段加载权重前半段是把VGG16的预训练参数拷贝到backbone对应的卷积层后半段是SSD特有的extra layers和prediction layers这些层从头训练。如果直接对整个模型调用load_state_dict会因为键名不匹配而报错这也是很多刚拿到这套源码的人容易卡住的地方。# ssd_net_vgg.py 中加载权重的常见写法 import torch from collections import OrderedDict def load_vgg16_reduced(pretrained_pathvgg16_reducedfc.pth): state_dict torch.load(pretrained_path, map_locationcpu) new_state OrderedDict() for key, value in state_dict.items(): # 去掉 module. 前缀适配单卡训练 new_key key.replace(module., ) new_state[new_key] value return new_state # 只把 backbone 部分复制进去SSD 的 extra_layers 不做初始化 def init_ssd_backbone(model, pretrained_path): vgg_weights load_vgg16_reduced(pretrained_path) model_dict model.state_dict() matched {k: v for k, v in vgg_weights.items() if k in model_dict and model_dict[k].shape v.shape} model_dict.update(matched) model.load_state_dict(model_dict) return model这段代码的关键在于shape比对。VGG16的classifier层是4096维的全连接而SSD中的conv6是1024通道的3×3卷积两者虽然都叫fc6或classifier.0但形状完全不同直接拷贝会报size mismatch错误。用条件过滤把形状不一致的键剔除掉只加载匹配部分这是最稳妥的做法。2.3 先验框设计与六层特征图的分工SSD300在这套项目中采用的先验框配置横跨六层特征图每一层对应的grid size和先验框数量各不相同。下面这张表是ssd_net_vgg.py中Config.py里的典型配置特征图层尺寸先验框数量/格感受野覆盖目标conv4_338×384近距离人脸大目标conv719×196中近距离人脸conv8_210×106中等距离人脸conv9_25×56远距离小目标conv10_23×34小目标补充conv11_21×14全局语义信息训练阶段loss_function.py计算的是SSD标准的多任务损失定位损失用Smooth L1分类损失用交叉熵正负样本按1:3的比例做hard negative mining。如果训练时发现loss收敛但检测框乱跳优先检查Config.py里的min_scale和max_scale是否与数据集目标尺度匹配。FDDB数据集中的人脸占比普遍较大min_scale设为0.2即可如果换成手机拍摄的小人脸数据集需要下调到0.1以下。3. 从FDDB数据集到Train.py训练一个能用在驾驶场景的人脸检测器3.1 voc0712.py把FDDB转换成VOC格式的加载器FDDB数据集在项目压缩包中对应fdd-dataset.zip它原本的标注格式是椭圆坐标和人脸矩形框混合而SSD训练通常依赖VOC格式的XML标注。voc0712.py这个名字沿用了VOC0712数据集的加载接口它的职责就是把FDDB的原始标注读取到PyTorch的Dataset类中同时提供图像路径映射。# voc0712.py 中 Dataset 构建的核心逻辑 import os import cv2 import torch from torch.utils.data import Dataset class FDDBDataset(Dataset): def __init__(self, root, image_settrain, transformNone): self.root root self.transform transform self.ids [] # 读取 train.txt每行是图片相对于 root 的路径 with open(os.path.join(root, ImageSets, image_set .txt)) as f: lines f.readlines() for line in lines: self.ids.append(line.strip()) def __getitem__(self, index): img_id self.ids[index] image cv2.imread(os.path.join(self.root, JPEGImages, img_id .jpg)) height, width, _ image.shape # 读取 VOC 格式的 XML 标注 annotation self._load_annotation(img_id, width, height) # 归一化坐标到 [0, 1] boxes torch.FloatTensor(annotation[boxes]) labels torch.LongTensor(annotation[labels]) if self.transform: image, boxes, labels self.transform(image, boxes, labels) return image, boxes, labels def _load_annotation(self, img_id, width, height): # 解析 XML 中的 bndbox转成 [xmin, ymin, xmax, ymax] 并归一化 passDataset的__getitem__返回三个值归一化后的图像张量、边界框坐标和类别标签。这里有一个很容易踩的坑FDDB的标注坐标是椭圆参数形式很多同学直接从标注文件拷贝到XML导致中心点和半径被当成矩形框来解析训练时loss直接发散。正确做法是把椭圆转成外接矩形以中心点(cx, cy)为中心长短轴(a, b)为半宽高得到xmin cx - aymax cy b。3.2 augmentations.py为小样本数据集增加鲁棒性FDDB数据集规模在训练脚本里往往只用到几千张直接硬训SSD容易过拟合。augmentations.py提供了一组针对目标检测的数据增强策略核心是保持图像与标注框同步变换。常见的组合包括随机水平翻转、随机裁剪、颜色抖动和亮度对比度调整。特别注意这里的随机裁剪不能像分类网络那样随意截取如果裁剪区域与某个标注框的交并比小于0.5需要直接丢弃该框或重新裁剪否则会给训练带来大量错误的正样本。# augmentations.py 中随机翻转和裁剪的同步逻辑 import random import cv2 import numpy as np class RandomFlip(object): def __call__(self, image, boxes, labels): if random.random() 0.5: h, w, _ image.shape image image[:, ::-1].copy() # 水平翻转后 xmin w - xmax, xmax w - xmin xmin boxes[:, 0].copy() boxes[:, 0] w - boxes[:, 2] boxes[:, 2] w - xmin return image, boxes, labels这里最容易被忽视的是copy()调用。OpenCV的切片操作返回的是视图修改视图会影响原数组在翻转坐标时如果直接对boxes[:, 0]赋值下一步计算xmin时读到的是已经改动的值导致坐标错乱。这个bug极其隐蔽表现是训练Loss能降但验证集完全无法检测到人脸。3.3 Train.py的完整训练循环与两个权重文件来源Train.py是入口脚本它把上面所有模块串起来。模型结构基于SSD300优化器采用SGDmomentum取0.9weight_decay设为5e-4。学习率采用warmup策略前500步从1e-4线性升到1e-3之后在80000步和100000步各衰减一次。项目压缩包中的ssd_voc_5000_plus.pth和ssd300_VOC_100000.pth对应两个训练阶段前者只训练了5000步用于快速验证网络能否收敛后者完整训练100000步是最终部署用的权重。# Train.py 核心训练循环精简版 for iteration, (images, boxes, labels) in enumerate(train_loader): images images.to(device) # 把真实框编码成先验框的偏移量和类别 target_locs, target_confs encoder.encode(boxes, labels) pred_locs, pred_confs model(images) # 计算多任务损失 loss criterion(pred_locs, pred_confs, target_locs, target_confs) optimizer.zero_grad() loss.backward() optimizer.step() if iteration % 5000 0: torch.save(model.state_dict(), fssd_voc_{iteration}_plus.pth)训练时一个实用的监控手段是开启tensorboard记录每一类loss的曲线。定位loss和分类loss的收敛速度不同如果分类loss降得快而定位loss始终震荡通常是先验框的scale参数设置不合理反过来定位正常、分类loss不降则是正负样本比例失调。用ssd_voc_5000_plus.pth做快速验证可以大幅缩短调试周期确认参数正常后再跑全量100000步。4. camera_detection.py与疲劳判定从人脸框到PERCLOS预警4.1 推理代码的封装detection.py中的detect方法训练完成后detection.py负责把模型封装成便于调用的接口。它内部完成了图像预处理、SSD前向推理、置信度过滤和NMS去重。这个类的输入是BGR格式的numpy.ndarray输出是检测到的人脸框列表每个框包含(xmin, ymin, xmax, ymax, score)。NMS的阈值设置为0.45置信度过滤阈值设置为0.5这两个参数直接影响误检率。在驾驶室这种背景相对单一的场景建议把置信度阈值提升到0.6以减少车窗反光、后视镜倒影造成的误报。4.2 眼睛闭合程度的量化EAR算法的实际实现疲劳检测的难点在于如何用数字描述“眼睛闭了多少”。目前应用最广的方法是计算眼睛纵横比EAR它利用眼睛轮廓的六个关键点计算纵向距离与横向距离的比值。正常人睁眼时EAR在0.25到0.35之间闭眼时趋近于0.1以下。项目中没有单独打包关键点模型常见做法是复用OpenCV的Facemark或者Dlib的68点检测器截取SSD检测出的人脸区域后送入关键点模型再提取左右眼的六个关键点坐标。# 基于 68 点关键点的 EAR 计算 import numpy as np def eye_aspect_ratio(eye_points): # eye_points 是 6 个 (x, y) 坐标按顺序对应眼角和上下眼睑 p2_p6 np.linalg.norm(eye_points[1] - eye_points[5]) p3_p5 np.linalg.norm(eye_points[2] - eye_points[4]) p1_p4 np.linalg.norm(eye_points[0] - eye_points[3]) ear (p2_p6 p3_p5) / (2.0 * p1_p4) return ear def calculate_total_ear(landmarks): left_eye landmarks[42:48] right_eye landmarks[36:42] left_ear eye_aspect_ratio(left_eye) right_ear eye_aspect_ratio(right_eye) return (left_ear right_ear) / 2.0EAR的计算看似简单实际工程化时有一个细节眼睛在某个瞬间会快速眨动导致EAR瞬间跌到0.1以下又被抬回来这并不代表疲劳。必须带入时间维度看的是单位时间内EAR低于阈值的累计时长占比也就是PERCLOS指标。Dlib文档里的landmarks[36:42]对应右眼轮廓点[42:48]对应左眼轮廓点注意Dlib的Landmark坐标是(x, y)用np.linalg.norm计算欧氏距离即可。4.3 基于连续帧的疲劳预警触发逻辑camera_detection.py中的预警逻辑并不复杂设定EAR阈值为0.22当连续20帧中EAR低于阈值的帧数超过12帧就判定为一次闭眼事件闭眼事件在60秒内累计出现8次触发疲劳报警。这里用“比例”而不是“绝对次数”是因为不同人眼睛大小差异明显亚洲人和欧美人同一EAR值对应的实际闭合程度不同更稳妥的做法是先让驾驶员正常睁眼状态下测量基线EAR用基线值的75%作为动态阈值。# camera_detection.py 中的疲劳判定循环核心逻辑 EAR_THRESH 0.22 CLOSED_FRAMES 20 MIN_CLOSED_RATIO 0.6 frame_count 0 closed_count 0 alarm_triggered False while cap.isOpened(): ret, frame cap.read() faces detector.detect(frame) if len(faces) 0: xmin, ymin, xmax, ymax, _ faces[0] # 取最大的人脸 face_roi frame[ymin:ymax, xmin:xmax] landmarks get_landmarks(face_roi) ear calculate_total_ear(landmarks) if ear EAR_THRESH: closed_count 1 else: closed_count max(0, closed_count - 1) # 睁眼帧递减 if frame_count CLOSED_FRAMES: if closed_count / CLOSED_FRAMES MIN_CLOSED_RATIO: alarm_triggered True cv2.putText(frame, FATIGUE DETECTED, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 2) frame_count 0 closed_count 0 frame_count 1闭眼判定采用“累计窗口滑动”而不是“连续帧达标”是为了避免驾驶员频繁眨眼造成的单次误报。窗口长度取20帧在30fps的摄像头下大约是0.67秒正常人一次眨眼在0.1到0.15秒正常眨眼最多影响3到4帧窗口比例不会超标。如果不想引入关键点模型退一步的替代方案是根据SSD输出框的高宽比来粗判闭眼但这个方案受驾驶员头部姿态影响极大侧脸时高宽比失真严重不建议在需要交付评分的项目里这么干。5. 模型选型与验证让Test.py和eval.py的结果说话5.1 Test.py单图推理与置信度调参Test.py跑的是单张图片的推理作用不是展示效果而是快速验证模型在特定场景下的表现。dnf_test.jpg和test.jpg这两张测试图都包含驾驶室场景运行python Test.py --image test.jpg --weights ssd300_VOC_100000.pth --threshold 0.5输出的test_done.jpg保存了画好边界框的结果图。如果发现漏检优先降低threshold而不是重新训练threshold从0.5降到0.3一般能多召回10%到15%的远距离小人脸同时也会引入少量误检需要结合NMS阈值联动调整。5.2 两个pth权重如何选择项目压缩包里同时提供了两个权重文件很多第一次用这套源码的人会困惑该用哪个。这两者的训练步数不同性能和速度也完全不同适用场景也不一样权重文件训练步数特点建议使用场景ssd_voc_5000_plus.pth5000收敛不完全检测框抖动明显调试代码、验证流程、跑通Demossd300_VOC_100000.pth100000完整收敛精度高但参数量大最终部署、性能对比、毕业答辩演示换用ssd_voc_5000_plus.pth跑同一张test.jpg能看到人脸框在边缘附近明显抖动置信度比分也偏低这是正常现象。5000步只训了大约6个epochSSD的先验框偏移量还没学准不建议在正式演示中使用。5.3 eval.py批量评测如何量化模型效果eval.py提供的是批量评估能力它在验证集上计算每个类别的平均精度APAverage Precision和mAP。评估前需要确认voc0712.py中实例化的Dataset指向test集而不是train集否则评估结果虚高。SSD300在FDDB的验证集上100000步权重一般能达到0.88左右的AP5000步权重只能到0.7左右。如果评估结果比这个值低很多优先检查测试图片的预处理是否与训练一致——包括归一化方式、图像尺寸是否Resize到300×300、通道顺序是RGB还是BGR。# Test.py 单图推理入口 import torch import cv2 from ssd_net_vgg import build_ssd checkpoint torch.load(ssd300_VOC_100000.pth, map_locationcpu) model build_ssd(num_classes2) model.load_state_dict(checkpoint) model.eval() image cv2.imread(test.jpg) image_resized cv2.resize(image, (300, 300)) # RGB 转 BGR 再归一化SSD 训练时用的是 ImageNet 的 mean 和 std rgb_image cv2.cvtColor(image_resized, cv2.COLOR_BGR2RGB).astype(float32) rgb_image (rgb_image - (123.0, 117.0, 104.0)) / 1.0 tensor torch.from_numpy(rgb_image.transpose(2, 0, 1)).unsqueeze(0) with torch.no_grad(): preds model(tensor) # preds 包含 [batch, num_prior, 4num_classes]后面接 decode 模块最后一层model(tensor)输出的原始张量是相对先验框的偏移量不能直接当作坐标使用必须先经过detection.py中的decode逻辑还原成真实坐标再做NMS。很多人在这一步直接拿输出画框画出来全是歪的就是这个原因。训练和部署之间的预处理差异是导致检测失效的最大隐患务必保持augmentations.py中测试分支与训练分支的归一化参数完全一致。本文还有配套的精品资源点击获取
返回列表