
简介基于卷积神经网络的驾驶员疲劳检测与预警系统是一份面向计算机专业毕业生及项目实战学习者的完整毕业设计资源。项目围绕人脸识别与驾驶员状态监测展开包含模型训练、检测、摄像检测、视频检测等Python源码配套预训练权重、标注数据集及项目说明文档评审得分98分源码经调试可运行。压缩包共37个文件涵盖py脚本、pth权重、jpg测试图、txt说明与zip数据集等整体约500.41MB目录结构清晰。目前已有64人学习/下载适合需要完成类似课题设计、理解CNN目标检测与疲劳预警实现流程的读者可直接复用训练评估逻辑并在此基础上扩展。1. 疲劳检测的CNN链路从哪里开始疲劳驾驶导致的交通事故占比常年居高不下传统监管靠交警抽查拿不到连续驾驶状态数据。基于卷积神经网络的驾驶员疲劳检测与预警系统要解决的是用车内一枚普通USB摄像头持续分析驾驶员面部结合眼睛闭合比例、打哈欠频次等指标在疲劳发生前触发声光预警。这个课题属于嵌入式视觉落地不是实验室刷分项目模型结构不追求SOTA推理帧率、漏检率和误报率反而决定系统能不能用。适合计算机视觉方向的毕业设计也适合需要快速搭建预警原型的工程师。接下来按CNN数据准备、模型设计、训练与实时预警的顺序把可复现的流程和参数一次讲清。2. 为什么疲劳检测的主干网络选CNN而不是Transformer2.1 CNN对疲劳状态分类的三个核心优势疲劳检测通常把问题拆成人脸检测加状态分类。人脸检测出边界框后裁剪出的人脸块很小常见分辨率只有48×48或64×64要判断的其实是眼睛开闭和嘴巴开闭这种局部纹理差异。CNN在这种场景下的优势非常明确局部感受野天然适合捕捉眼睑边缘、瞳孔与巩膜的对比参数共享让模型不需要为每个位置独立学习特征平移等变性则保证了人脸在画面中稍微偏移时不改变分类结果。传统做法用HOG特征加SVM或者用EAR眼睛纵横比加阈值判断。这套方案的问题是EAR依赖人脸关键点检测精度而关键点在侧脸、遮挡、暗光下抖动剧烈阈值很容易被击穿。CNN直接对像素学习特征光线和角度的鲁棒性更好。相比之下Transformer的自注意力机制擅长建模长距离依赖但人脸块尺寸小、语义简单全局注意力带来的收益有限训练还需要更大规模数据推理延迟也不占优势。对这个任务来说CNN是性价比最优解。2.2 用PyTorch实现一个可复用的疲劳状态CNNLeNet5变体不套用ResNet这类大网络一个LeNet5风格的轻量CNN就能在CPU上跑实时。网络结构可以设计为共享底层特征、顶层分两路输出一路输出眼睛状态睁开/闭合另一路输出嘴巴状态张开/闭合。这里直接输出四类联合标签让单次前向同时拿到两个结果省一次推理。import torch.nn as nn class FatigueCNN(nn.Module): def __init__(self, num_classes4): super(FatigueCNN, self).__init__() # 输入为3通道48x48的人脸块 self.features nn.Sequential( nn.Conv2d(3, 16, 3, stride1, padding1), # 卷积核3x3填充1保持尺寸 nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 池化核2尺寸变24 nn.Conv2d(16, 32, 3, stride1, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 尺寸变12 nn.Conv2d(32, 64, 3, stride1, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 尺寸变6 ) self.classifier nn.Sequential( nn.Dropout(0.5), nn.Linear(64 * 6 * 6, 128), nn.ReLU(inplaceTrue), nn.Linear(128, num_classes), # 输出4类状态 ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) # 展平后送入全连接 return self.classifier(x)模型参数集中在最后的全连接层卷积层负责抽取眼睛和嘴巴共有的边缘纹理特征。stride全部取1配合padding1让特征图尺寸只受池化影响避免多次卷积后空间信息快速衰减。48×48输入是经验值低于32会丢眼睑细节高于64推理变慢实时检测场景不划算。四类联合标签的含义是[眼开, 眼闭, 嘴开, 嘴闭]实际训练时每个样本只落在一个类上比单独训练两个二分类网络省一次前向这使得普通笔记本摄像头场景下帧率能维持在25FPS以上。2.3 检测与分类的分工边界框加状态分类CNN分类器需要先知道人脸在哪里。常见做法是用MTCNN、OpenCV DNN或MediaPipe的人脸检测模块先拿到边界框把人脸裁剪出来缩放后送进FatigueCNN。人脸检测和状态分类各司其职检测器负责尺度鲁棒性分类器负责细节判断。不建议用YOLOv8直接回归疲劳状态。原因是闭眼是小目标且公开的疲劳闭眼检测数据集标注质量参差不齐端到端方案容易在远距离或侧脸时漏检。如果一定要用YOLOv8训练自己的数据集建议把目标定成人脸检测而不是状态分类闭眼判断仍交给专门的CNN分支处理。3. 疲劳检测数据集怎么准备公开数据源、自采数据与标签策略3.1 公开数据集怎么选训练状态分类器并不需要从零录制驾驶视频。学术圈公开的NTHU-DDD驾驶员困倦检测数据集包含不同人种、戴眼镜与不戴眼镜、不同光照条件下的驾驶片段YawDD专门针对打哈欠场景摄像头装在仪表盘上方CEW是闭眼人脸数据集适合补充闭眼正样本。这些数据集通常以压缩包形式提供申请后下载解压多为视频或图片序列。下载后需要自己抽帧并裁剪人脸原始视频不能直接丢进训练循环。如果做毕业设计建议自采一部分数据增强说服力。把手机用支架固定在挡风玻璃位置录10分钟正常驾驶状态和10分钟模拟疲劳状态按10秒一段切分再对每个片段抽帧标注。自采数据占比建议不低于30%答辩时展示自采样本的检测效果比纯公开数据集更有完成度。3.2 用OpenCV批量裁剪人脸并归档训练集拿到视频后第一步是把人脸区域批量裁剪成统一尺寸。下面这个脚本用OpenCV内置的Haar级联分类器从视频中每5帧抽一帧检测人脸输出48×48的JPG文件。Haar级联对正面人脸够用且不引入额外依赖。import cv2 import os from pathlib import Path data_root Path(raw_videos) out_root Path(face_samples) out_root.mkdir(parentsTrue, exist_okTrue) face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) for video_path in data_root.glob(*.mp4): cap cv2.VideoCapture(str(video_path)) frame_id 0 saved_id 0 while True: ok, frame cap.read() if not ok: break if frame_id % 5 ! 0: # 每5帧抽1帧避免相邻帧高度相似 frame_id 1 continue gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(64, 64) ) for (x, y, w, h) in faces: margin int(0.2 * w) # 外扩20%防止发型或下颌被裁掉 x1 max(0, x - margin) y1 max(0, y - margin) x2 min(frame.shape[1], x w margin) y2 min(frame.shape[0], y h margin) face frame[y1:y2, x1:x2] face cv2.resize(face, (48, 48), interpolationcv2.INTER_LINEAR) out_path out_root / f{video_path.stem}_{saved_id:05d}.jpg cv2.imwrite(str(out_path), face) saved_id 1 frame_id 1 cap.release()detectMultiScale的scaleFactor设为1.1每次缩放步长小检全率更高但速度稍慢minNeighbors设5过滤掉误检的孤立矩形。minSize设64是为了剔除画面中过小的模糊人脸这类样本即使裁剪出来也没有标注价值。外扩比例20%能保留完整额头和下巴避免眼睛或嘴巴区域被边界切掉这对后续关键点对齐和分类都有直接影响。3.3 数据增强参数与数据集划分方式裁剪完成后需要把图片按人员维度分组再做划分。疲劳检测数据集里同一个人的多段视频特征高度相似如果随机切分同一个人的帧会同时出现在训练集和验证集人脸身份信息被模型记住验证指标虚高换个人就失效。正确做法是先按人员ID分组再把组随机分配到训练、验证、测试集比例控制在8:1:1附近。增强策略用常见配置即可from torchvision import transforms train_transform transforms.Compose([ transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(10), transforms.ColorJitter(brightness0.1, contrast0.1), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])水平翻转不会改变眼睛开闭和嘴巴开闭的语义可以放心用。旋转角度限制在±10度角度过大会让眼睑位置产生真实情况下不存在的形变。亮度对比度抖动用0.1的小幅范围模拟车内明暗变化不建议加随机擦除眼睛和嘴巴区域是关键特征被遮罩后模型容易学到错误关联。3.4 标签策略半闭眼算不算闭这是最容易拖慢项目进度的地方。半闭眼是疲劳过程中最频发的状态但它和正常眨眼的下行阶段非常接近。建议在标注规范里写死一条规则眼睑遮住瞳孔面积超过50%该帧标记为闭。嘴巴连续张开超过1.5秒或纵向开口距离明显大于正常说话状态才标记为打哈欠短暂张嘴不标。不要做三分类区分半闭眼和全闭眼。类间距离太近小网络很难学出稳定边界而且半闭眼帧数少会带来严重的类别不平衡。把半闭眼归入闭眼类模型推理时对疲劳状态会更敏感这才是预警系统需要的行为。4. 模型训练、PERCLOS判据与预警触发逻辑4.1 训练超参与早停设置训练阶段不需要复杂的分布式配置。模型小、输入小单张消费级显卡几十秒就能跑完一个epoch。参数按下表设置即可超参数推荐值说明optimizerAdam(lr1e-3, weight_decay1e-4)小数据收敛稳weight_decay抑过拟合batch_size3248×48小图显存压力小epochs40配合早停不必跑满early stoppingpatience5, monitorval_loss防验证损失回升后继续空跑lr schedulerReduceLROnPlateau(factor0.1, patience2)验证损失平缓后降学习率lossCrossEntropyLoss四类联合输出直接用交叉熵训练循环保持基本结构重点在scheduler和早停的衔接顺序上验证损失先送入scheduler决定是否降学习率再判断是否更新最优模型。import torch import torch.nn as nn model FatigueCNN(num_classes4) device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, factor0.1, patience2 ) best_val_loss float(inf) bad_epochs 0 for epoch in range(40): model.train() train_loss 0.0 for x, y in train_loader: x, y x.to(device), y.to(device) optimizer.zero_grad() out model(x) loss criterion(out, y) loss.backward() optimizer.step() train_loss loss.item() val_loss validate(model, val_loader, device) # 验证函数返回平均loss scheduler.step(val_loss) if val_loss best_val_loss: best_val_loss val_loss bad_epochs 0 torch.save(model.state_dict(), checkpoints/best.pth) else: bad_epochs 1 if bad_epochs 5: print(fearly stop at epoch {epoch}) break逻辑上要区分scheduler.step和早停判断scheduler关心的是验证损失是否还在降降不动就降学习率早停关心的是连续多少个epoch没有刷新最优记录。两者的触发条件不同混在一起会把学习率调整次数和早停次数互相干扰。validate函数里记得关梯度计算用torch.no_grad()包住前向过程。4.2 PERCLOS与哈欠频率把CNN输出变成预警指标模型输出的还是单帧分类概率预警系统需要的是时间维度的统计数据。PERCLOS眼睛闭合时间占比是驾驶疲劳研究里最常用的指标之一公式是PERCLOS 闭眼帧数 / 时间窗口内总帧数通常窗口取3秒阈值取0.4。以30FPS摄像头为例一个窗口内有90帧如果其中超过36帧被判为闭眼就判定为疲劳状态。这个阈值来源于实验统计正常眨眼每次约100到200毫秒在90帧窗口中最多占用6帧不会误触发疲劳状态下的闭眼持续时间长连续多帧闭眼会让占比快速攀升。哈欠检测用频率辅助判断统计每分钟打哈欠次数超过3次判定为疲劳。更严格的做法是引入眼睛平均开度MATMean Aspect Ratio把眼睛开度的平均值和PERCLOS联合判断可以区分闭眼和频繁眨眼两种情况。4.3 实时检测主循环读取摄像头、推理、触发预警检测部分直接用MediaPipe的人脸检测模块省去自己处理光照和姿态的问题。主循环维护一个长度90的deque作为滑动窗口存储每一帧的闭眼状态和打哈欠状态每次新增帧后计算窗口内的闭眼比例超过阈值触发预警。import collections import cv2 import mediapipe as mp import torch import torchvision.transforms as transforms model FatigueCNN(num_classes4) model.load_state_dict(torch.load(best.pth, map_locationcpu)) model.eval() transform transforms.Compose([ transforms.ToPILImage(), transforms.Resize((48, 48)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) mp_face mp.solutions.face_detection cap cv2.VideoCapture(0) # 90帧窗口对应30FPS下3秒 eye_closed_history collections.deque(maxlen90) yawn_history collections.deque(maxlen90) with mp_face.FaceDetection(model_selection0, min_detection_confidence0.5) as det: while cap.isOpened(): ok, frame cap.read() if not ok: break rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results det.process(rgb) if results.detections: bbox results.detections[0].location_data.relative_bounding_box h, w, _ frame.shape x1 max(0, int(bbox.xmin * w)) y1 max(0, int(bbox.ymin * h)) x2 min(w, int((bbox.xmin bbox.width) * w)) y2 min(h, int((bbox.ymin bbox.height) * h)) face frame[y1:y2, x1:x2] if face.size 0: tensor transform(face).unsqueeze(0) with torch.no_grad(): logits model(tensor) pred torch.argmax(logits, dim1).item() # 0眼开,1眼闭,2嘴开,3嘴闭 eye_closed_history.append(1 if pred in [1] else 0) yawn_history.append(1 if pred in [2] else 0) if len(eye_closed_history) 90: perclos sum(eye_closed_history) / 90 if perclos 0.4: print(fatigue warning: perclos%.2f % perclos) # 在此接声光报警或串口指令 cv2.imshow(driver monitor, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这里有一个容易踩的坑MediaPipe返回的是相对坐标乘上宽高后必须做边界截断否则人脸贴近画面边缘时下标越界。deque的maxlen设成90窗口满后自动丢弃最旧帧不需要手动管理列表头尾。预警打印后还应加一个冷却时间比如触发后20秒内不再重复报警防止长时间闭眼状态下控制台被刷爆。5. 交付成可演示的项目验证指标、误报抑制与源码组织5.1 两个数字证明模型可用混淆矩阵和FPS答辩时演示效果再直观也不如拿出两张表靠谱。第一张表是测试集上的混淆矩阵和加权F1分数第二张表是实测FPS。FPS要用预处理加推理加后处理的完整链路时间计算不能只测模型前向的毫秒数。简单做法是用time.perf_counter包住主循环里的整段处理逻辑统计50帧的平均耗时。import time from sklearn.metrics import confusion_matrix, f1_score start time.perf_counter() # 主循环中完整处理一帧 fps 1.0 / (time.perf_counter() - start)如果FPS低于15帧需要回到人脸检测环节找原因。MediaPipe可以适当降低min_detection_confidence分类模型保持CPU推理即可不必上GPU。5.2 一个有效抑制误报的技巧多帧状态机单次PERCLOS超过阈值就报警的做法误报率偏高尤其在驾驶员低头看手机或转头和乘客说话时人脸检测框短暂丢失会清空窗口恢复后重新累计又不稳定。可以加一个三层状态机连续5个独立窗口都触发疲劳才进入报警态报警后冷却20秒。alert_state 0 threshold_count 0 cooldown_until 0 if perclos 0.4: threshold_count 1 else: threshold_count 0 if threshold_count 5: if time.time() cooldown_until: print(alarm triggered) cooldown_until time.time() 20 threshold_count 0这个状态机把单次波动的权重大幅降低代价是疲劳发生后报警延迟几秒对预警用途完全可以接受。5.3 源码、数据集和项目说明怎么组织一个好的毕设源码目录会让验收效率完全不同。建议按下面的结构整理driver-fatigue-detection/ ├── data/ │ ├── raw_videos/ # 原始视频 │ ├── face_samples/ # 裁剪后的人脸样本 │ └── labels/ # 标注文件 ├── models/ │ └── fatigue_cnn.py # 网络定义 ├── utils/ │ ├── preprocess.py # 抽帧与裁剪 │ └── alert.py # 阈值判断与报警逻辑 ├── checkpoints/ │ └── best.pth # 最优权重 ├── train.py ├── detect.py └── README.mdREADME里不要写从零开始的CNN科普要写清楚环境依赖、数据集来源与自采比例、训练脚本启动命令、每个超参数的含义以及复现演示的操作步骤。把错误归一到参数对应关系写进去比如“测试视频识别不准先查min_detection_confidence和PERCLOS阈值”这类排错提示比任何大段描述都更能说明你真正跑通了整套流程。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。