ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python卷积神经网络驾驶员疲劳检测:从毕设到部署全流程

Python卷积神经网络驾驶员疲劳检测:从毕设到部署全流程 简介这份毕业设计资源面向计算机相关专业学生与Python初学者提供一套基于卷积神经网络的人脸识别驾驶员疲劳检测与预警系统完整源码可用于课程设计、毕设答辩或深度学习入门实践。压缩包共15个文件约2.8MB以py脚本、xml配置、txt说明、ui界面文件及whl依赖包为主涵盖主程序、界面逻辑、测试脚本与项目说明文档另附dlib安装包便于环境搭建。资源中已包含需求说明与README方便读者快速理解系统结构与运行流程并在此基础上进行功能扩展或算法调优。目前已有364人学习下载适合需要参考完整项目结构、掌握CNN人脸识别与疲劳预警实现思路的读者也可作为深度学习项目二次开发的起点。1. 从一张毕业设计封面说起Python卷积神经网络怎么做驾驶员疲劳检测每年毕业季实验室里总有几个学弟学妹拿着类似题目的开题报告来找我基于Python卷积神经网络的人脸识别驾驶员疲劳检测与预警系统。听起来每个词都认识拼在一起就不知道从哪下手。这个题目的本质其实很清晰——用摄像头采集驾驶员面部图像通过卷积神经网络判断眼睛闭合、打哈欠等疲劳特征一旦连续多帧判定为疲劳状态就触发预警。它解决的是长途驾驶、夜班货运场景下人为监控容易漏判的问题适合有一定Python基础、想找一个完整深度学习落地项目的本科生或初级工程师。很多人卡在第一步环境装了两天OpenCV和PyTorch版本打架数据集不知道怎么标模型训出来准确率虚高但实际跑起来全是误报。这篇笔记就按我实际带项目的顺序把这条链路拆开讲透让你能照着复现也能看清哪些地方容易翻车。2. 疲劳检测的技术选型为什么是CNN而不是传统特征2.1 从PERCLOS到CNN两条路线的取舍疲劳检测领域有一个经典指标叫PERCLOS即单位时间内眼睛闭合时间所占比例。传统做法是用Dlib或OpenCV的Haar级联检测人脸关键点算出眼睛纵横比EAR再统计闭眼帧占比。这条路在实验室光照稳定时能跑但上车就露馅驾驶员戴眼镜、侧脸、夜间红外补光不足、摄像头抖动都会让关键点漂移EAR阈值直接失效。我试过在同一个视频上把EAR阈值从0.2调到0.25误报率能从每小时3次跳到每小时11次完全是玄学调参。卷积神经网络走的是另一条路。它不依赖人工定义的眼睛开合几何规则而是直接从像素里学特征。浅层卷积核捕捉边缘和角点深层卷积核组合出眼睛、嘴巴的局部模式最后全连接层输出疲劳/清醒的概率。这个方案对光照变化、轻微遮挡的鲁棒性明显更好代价是需要标注数据和训练时间。对于毕业设计这个体量我一般建议用CNN做分类主干再配合人脸检测做预处理而不是端到端从整帧图直接回归疲劳分数——后者需要的数据量远超本科毕设能凑齐的规模。2.2 人脸检测与CNN分类的流水线设计整个系统的流水线分三段人脸检测、区域裁剪、CNN分类。人脸检测用OpenCV的DNN模块加载Caffe模型或者直接用MediaPipe前者依赖少、部署简单后者关键点更准但包体大。裁剪阶段把检测框按比例向外扩20%保证眼睛和嘴巴完整落在输入区域内然后统一缩放到模型需要的尺寸。CNN分类阶段有两种粒度一种是三分类清醒、闭眼、打哈欠另一种是二分类疲劳、非疲劳。三分类可解释性更强方便后续按闭眼时长和哈欠频率做融合判决二分类训练更简单但把闭眼和打哈欠混在一起调阈值时缺少抓手。我通常选三分类因为预警逻辑需要区分“闭眼持续2秒”和“连续打哈欠”这两种不同疲劳模式。模型结构上自己搭一个5层卷积加3层全连接的轻量网络就够用输入64×64灰度图参数量控制在50万以内在普通笔记本CPU上也能跑到15帧以上。如果追求更高精度可以换MobileNetV3或ShuffleNet但毕设答辩时老师更关心你是否理解卷积、池化、批归一化的作用而不是模型有多深。2.3 数据集从哪来公开集与自采视频的配比公开数据集里CEW和YawDD是比较常用的。CEW偏静态闭眼检测YawDD是车载场景下的打哈欠视频。但这两个集子标注格式不统一直接混用需要写转换脚本。我的做法是先用公开集做预训练让模型学到眼睛和嘴巴的基本纹理特征再用自己用手机拍的视频做微调。自采视频注意几点白天和夜间各拍一段戴眼镜和不戴眼镜各拍一段正脸和侧脸30度各拍一段。每个类别至少凑够800张裁剪后的人脸图三分类总共2400张起步。标注用LabelImg或者自己写个OpenCV窗口按键盘打标签的小工具别用Excel记文件名后期对不上。数据增强方面随机亮度调整±30%、小角度旋转±10度、水平翻转这三样必做。高斯噪声和模糊慎用因为疲劳检测本身就要区分“眼睛眯着”和“图像模糊”加模糊增强会让模型学到错误的边界。验证集划分按视频来源分不要按帧随机分否则同一段视频的相邻帧会同时出现在训练集和验证集里准确率虚高到99%但实际部署一塌糊涂——这个坑我见过至少三届学生踩。3. 用Python把CNN疲劳检测跑起来环境、代码与训练3.1 环境配置避开OpenCV与PyTorch的版本冲突环境是第一个拦路虎。Python版本选3.8到3.10之间太新了某些包没有预编译轮子。用conda建独立环境别在base里折腾。核心依赖就四个opencv-python、torch、torchvision、numpy。安装顺序有讲究先装numpy再装opencv-python最后装torch和torchvision。如果先装torch再装opencv有时候opencv会拉一个旧版numpy把torch的依赖覆盖掉导致import torch时报DLL加载失败。# 创建并激活环境 conda create -n fatigue python3.9 -y conda activate fatigue # 按顺序安装避免依赖覆盖 pip install numpy1.24.3 pip install opencv-python4.8.1.78 pip install torch2.0.1 torchvision0.15.2 --index-url https://download.pytorch.org/whl/cpu # 验证安装 python -c import cv2, torch; print(cv2.__version__, torch.__version__)上面命令里torch指定了CPU版本因为毕设环境不一定有NVIDIA显卡。如果有显卡且装好了CUDA把index-url换成对应cu118的地址即可。opencv-python选4.8.x是因为4.9之后某些DNN接口有变动网上教程大多基于4.8遇到问题好搜。numpy锁1.24是因为1.25和opencv 4.8在部分Windows机器上有兼容性告警。验证那行能同时打印出版本号说明环境基本通了。3.2 数据预处理脚本从视频到64×64灰度图数据准备阶段写一个脚本把视频按帧抽出来做人脸检测和裁剪存成模型能直接读的格式。下面这段代码用OpenCV的DNN人脸检测器遍历视频文件每隔5帧取一帧检测到人脸后扩边裁剪并缩放。import cv2 import os import numpy as np # 加载OpenCV自带的人脸检测模型 prototxt deploy.prototxt model res10_300x300_ssd_iter_140000.caffemodel net cv2.dnn.readNetFromCaffe(prototxt, model) def extract_faces(video_path, out_dir, label, frame_step5): cap cv2.VideoCapture(video_path) os.makedirs(os.path.join(out_dir, label), exist_okTrue) count 0 idx 0 while True: ret, frame cap.read() if not ret: break idx 1 if idx % frame_step ! 0: continue h, w frame.shape[:2] blob cv2.dnn.blobFromImage(frame, 1.0, (300, 300), (104.0, 177.0, 123.0)) net.setInput(blob) detections net.forward() for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence 0.6: continue box detections[0, 0, i, 3:7] * np.array([w, h, w, h]) x1, y1, x2, y2 box.astype(int) # 向外扩20%保证眼睛嘴巴完整 bw, bh x2 - x1, y2 - y1 x1 max(0, int(x1 - 0.2 * bw)) y1 max(0, int(y1 - 0.2 * bh)) x2 min(w, int(x2 0.2 * bw)) y2 min(h, int(y2 0.2 * bh)) face frame[y1:y2, x1:x2] if face.size 0: continue face cv2.cvtColor(face, cv2.COLOR_BGR2GRAY) face cv2.resize(face, (64, 64)) cv2.imwrite(os.path.join(out_dir, label, f{count:05d}.jpg), face) count 1 cap.release() print(f{label}: {count} faces saved) # 分别处理三类视频 extract_faces(alert.mp4, dataset, alert) extract_faces(closed.mp4, dataset, closed) extract_faces(yawn.mp4, dataset, yawn)这段代码的关键参数有三个。confidence阈值0.6是经验值调低到0.4会引入大量误检框调高到0.8在侧脸时会漏检。frame_step5表示每5帧取一帧30fps视频相当于每秒取6张既保证样本多样性又避免相邻帧冗余。扩边比例0.2是防止检测框刚好卡在眼睛边缘裁剪后丢失眼角信息。存图时用灰度图而不是彩色图因为疲劳特征主要靠纹理和形状颜色信息贡献很小灰度图还能把输入通道从3降到1减少计算量。3.3 CNN模型定义与训练循环模型结构不复杂五个卷积块每个块是卷积批归一化ReLU最大池化最后接全局平均池化和全连接分类头。输入64×64单通道输出3类。import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms class FatigueCNN(nn.Module): def __init__(self, num_classes3): super().__init__() def block(in_c, out_c): return nn.Sequential( nn.Conv2d(in_c, out_c, 3, padding1), nn.BatchNorm2d(out_c), nn.ReLU(inplaceTrue), nn.MaxPool2d(2) ) self.features nn.Sequential( block(1, 32), # 64 - 32 block(32, 64), # 32 - 16 block(64, 128), # 16 - 8 block(128, 128), # 8 - 4 block(128, 128) # 4 - 2 ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Dropout(0.5), nn.Linear(128, num_classes) ) def forward(self, x): return self.classifier(self.features(x)) # 数据加载与增强 train_tf transforms.Compose([ transforms.Grayscale(num_output_channels1), transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ColorJitter(brightness0.3), transforms.ToTensor(), transforms.Normalize([0.5], [0.5]) ]) val_tf transforms.Compose([ transforms.Grayscale(num_output_channels1), transforms.ToTensor(), transforms.Normalize([0.5], [0.5]) ]) train_ds datasets.ImageFolder(dataset/train, transformtrain_tf) val_ds datasets.ImageFolder(dataset/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers2) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers2) device torch.device(cuda if torch.cuda.is_available() else cpu) model FatigueCNN(num_classes3).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.5) for epoch in range(30): model.train() running_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() scheduler.step() # 验证 model.eval() correct, total 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) preds model(imgs).argmax(dim1) correct (preds labels).sum().item() total labels.size(0) print(fEpoch {epoch1}, Loss {running_loss/len(train_loader):.4f}, fVal Acc {correct/total:.4f})训练循环里有几个参数值得说。学习率1e-3配合Adam是常规起点如果loss震荡明显就降到5e-4。weight_decay设1e-4是给权重加L2正则防止全连接层过拟合。StepLR每10个epoch把学习率砍半让后期收敛更稳。Dropout 0.5加在全连接前因为全连接层参数量占了大头不dropout的话训练集准确率能到99%但验证集卡在85%上不去。批大小32是显存和梯度稳定性的折中显存不够就降到16但学习率也要相应降到5e-4。训练完成后把模型保存成torchscript格式部署时不需要原始模型定义文件加载更快。验证集准确率不是唯一指标还要看混淆矩阵闭眼和打哈欠两类之间的误判率如果超过10%说明模型没学好嘴巴和眼睛的区分特征需要检查裁剪区域是否把嘴巴裁掉了或者增加打哈欠样本的多样性。4. 预警逻辑与部署从分类结果到声光报警4.1 多帧融合判决别让单帧误判触发报警CNN每帧输出一个三分类概率但单帧结果抖动很大——驾驶员眨一下眼闭眼类概率瞬间冲到0.9如果直接触发报警一路下来报警器响个不停。必须做时序融合。我一般用滑动窗口加计数器维护一个长度为30的队列约1秒当闭眼类占比超过60%且持续超过2秒才判定为闭眼疲劳打哈欠类在10秒窗口内出现3次以上判定为哈欠疲劳。两个条件满足任意一个就触发预警。from collections import deque class FatigueJudge: def __init__(self, fps15): self.fps fps self.closed_win deque(maxlenint(fps * 1)) # 1秒窗口 self.yawn_win deque(maxlenint(fps * 10)) # 10秒窗口 self.closed_seconds 0 def update(self, cls_idx): # cls_idx: 0alert, 1closed, 2yawn self.closed_win.append(1 if cls_idx 1 else 0) self.yawn_win.append(1 if cls_idx 2 else 0) # 闭眼持续判定 if len(self.closed_win) self.closed_win.maxlen: ratio sum(self.closed_win) / len(self.closed_win) if ratio 0.6: self.closed_seconds 1.0 / self.fps else: self.closed_seconds 0 # 哈欠频率判定 yawn_count sum(self.yawn_win) if self.closed_seconds 2.0: return closed_fatigue if yawn_count 3: return yawn_fatigue return normal这个判决器的参数需要按实际帧率调整。fps15时1秒窗口存15帧闭眼占比阈值0.6意味着15帧里有9帧判为闭眼才计数。closed_seconds累加到2.0触发相当于连续2秒闭眼。哈欠窗口10秒内出现3次是因为正常人打哈欠间隔通常大于10秒短时间内连续打哈欠才是疲劳信号。如果摄像头帧率只有10fps要把fps参数改对否则时间计算全错。4.2 声光预警的硬件接口与软件触发预警执行层看硬件条件。最简单的方案是用电脑扬声器播放警报音Python里用pygame或simpleaudio加载一个wav文件循环播放。如果要做硬件联动常见的是通过串口给Arduino发指令控制蜂鸣器和LED灯。串口通信注意波特率匹配一般用9600或115200发送单字节指令比如b1表示报警、b0表示正常。import serial import time class AlertController: def __init__(self, portCOM3, baud9600): self.ser serial.Serial(port, baud, timeout1) time.sleep(2) # 等待串口初始化 def trigger(self, state): if state normal: self.ser.write(b0) else: self.ser.write(b1) def close(self): self.ser.close()串口初始化后必须延时2秒再发指令因为Arduino上电复位需要时间立刻发数据会丢。如果用的是树莓派做边缘端可以直接用GPIO控制蜂鸣器不需要额外单片机。软件层面还要加一个报警冷却时间触发后至少间隔5秒才能再次触发避免报警声连续不断引起驾驶员烦躁。4.3 实时视频流的性能优化实时跑的时候瓶颈往往不在CNN推理而在人脸检测和图像拷贝。优化手段有几个把人脸检测从每帧执行改成每3帧执行一次中间帧复用上一次的检测框因为驾驶员头部在短时间内不会大幅移动。图像预处理用cv2.resize的INTER_NEAREST插值比默认的双线性快一倍对64×64小图精度影响可忽略。模型推理前把numpy数组转torch tensor时用torch.from_numpy避免内存拷贝。如果帧率还是上不去考虑把模型量化成int8。PyTorch支持动态量化一行代码就能把卷积和全连接层的权重从float32压到int8模型体积缩小4倍CPU推理速度提升30%到50%精度损失通常在1%以内。# 动态量化示例 quantized_model torch.quantization.quantize_dynamic( model, {nn.Conv2d, nn.Linear}, dtypetorch.qint8 ) torch.jit.save(torch.jit.script(quantized_model), fatigue_quantized.pt)量化后的模型在树莓派4B上能跑到20fps以上满足实时性要求。注意量化只对CPU推理有效GPU上反而可能变慢。另外量化后的模型不能再训练所以要先训练收敛再量化。5. 避坑与排查疲劳检测项目里最容易翻车的五件事5.1 验证集准确率99%但实际部署全是误报现象训练日志里验证集准确率很快冲到99%但拿笔记本摄像头实时跑报警器每隔几秒就响一次。原因验证集划分时按帧随机分同一段视频的相邻帧同时进了训练集和验证集模型只是记住了训练样本没有真正学到泛化特征。解决按视频文件划分数据集同一段视频的所有帧只能出现在训练集或验证集其中一个里。如果自采视频不够至少保证验证集来自完全独立的拍摄场次。5.2 夜间红外画面下模型完全失效现象白天测试正常晚上车内光线暗模型把清醒判成闭眼。原因训练数据全是白天可见光图像模型没学过红外灰度图的纹理分布。解决自采数据时补拍夜间红外视频或者用亮度增强把白天图像模拟成夜间效果。更稳妥的做法是训练时加入随机伽马变换让模型对整体亮度变化不敏感。5.3 OpenCV读取视频返回空帧导致程序崩溃现象脚本跑了几百帧后突然报错提示frame为空。原因视频文件损坏或者摄像头被其他程序占用cap.read()返回False。解决每次read后判断ret为False时break并释放资源。摄像头场景下加一个重连机制连续10帧读取失败就重新初始化VideoCapture。5.4 模型文件太大塞不进树莓派内存现象在PC上训练好的模型保存为.pth有几十兆拷到树莓派上加载时报内存不足。原因保存时把优化器状态和完整模型结构都存进去了。解决只保存state_dict加载时先实例化模型再load_state_dict。如果还大就做动态量化或者把全连接层参数量砍掉一半。5.5 报警逻辑太灵敏导致驾驶员主动关掉系统现象驾驶员反馈报警太频繁干脆把摄像头遮住。原因单帧判决直接触发报警没有做时序平滑和冷却。解决按第4章的滑动窗口方案做多帧融合同时加报警冷却时间。另外把报警音量做成可调初期调低一点让驾驶员适应比一上来就最大音量更人性化。6. 把疲劳检测做扎实从毕设到可演示系统的最后一步6.1 用混淆矩阵定位模型短板训练完只看准确率是不够的。打印混淆矩阵看闭眼和打哈欠两类之间的误判。如果闭眼被大量判成打哈欠说明模型没学好眼睛区域的闭合特征可能是裁剪时嘴巴占比太大。反过来打哈欠被判成闭眼通常是嘴巴张开幅度不够大或者训练集里打哈欠样本太少。针对性地补拍对应场景的数据比盲目加层数有效得多。from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in val_loader: preds model(imgs.to(device)).argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annotTrue, fmtd, xticklabels[alert, closed, yawn], yticklabels[alert, closed, yawn]) plt.savefig(confusion_matrix.png)6.2 做一个能演示的GUI界面答辩时老师不会看你的训练日志他们要的是能跑起来的演示。用Tkinter或PyQt搭一个简单界面左边显示摄像头画面右边显示当前状态和疲劳计数。Tkinter自带不需要额外安装配合PIL的ImageTk把OpenCV帧显示到Label上。界面刷新用after方法定时调用不要用while循环阻塞主线程。import tkinter as tk from PIL import Image, ImageTk import cv2 class App: def __init__(self, root): self.root root self.label tk.Label(root) self.label.pack() self.status tk.Label(root, textStatus: Normal, font(Arial, 16)) self.status.pack() self.cap cv2.VideoCapture(0) self.judge FatigueJudge(fps15) self.update() def update(self): ret, frame self.cap.read() if ret: # 这里插入人脸检测和CNN推理得到cls_idx cls_idx 0 # 占位实际替换为模型输出 state self.judge.update(cls_idx) self.status.config(textfStatus: {state}) rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img Image.fromarray(rgb).resize((480, 360)) imgtk ImageTk.PhotoImage(img) self.label.imgtk imgtk self.label.config(imageimgtk) self.root.after(66, self.update) # 约15fps root tk.Tk() app App(root) root.mainloop()6.3 参数速查与调优顺序调参不要东一榔头西一棒子按固定顺序来能省很多时间。先调数据增强强度再调学习率和批大小最后调模型结构。数据增强不够时加层数只会加速过拟合。下面这张表是我常用的参数范围和调整方向。参数常用范围调大效果调小效果学习率1e-4 ~ 1e-3收敛快但不稳稳但慢批大小16 ~ 64梯度稳显存高显存低噪声大Dropout0.3 ~ 0.6抗过拟合强欠拟合风险闭眼窗口阈值0.5 ~ 0.7报警更敏感漏报增多闭眼持续秒数1.5 ~ 3.0更宽容更严格我自己的习惯是先把闭眼持续秒数设到2.5秒跑一周实际驾驶视频统计误报和漏报次数再微调窗口阈值。不要一上来就追求零误报那会导致漏报严重疲劳检测漏报比误报危险得多。最后说一句这个项目做完最大的收获不是模型准确率有多高而是学会了怎么把数据、模型、业务逻辑串成一条能跑的链路。希望帮到你。本文还有配套的精品资源点击获取
返回列表