
简介一份面向计算机相关专业毕业设计与深度学习实战的完整工程基于卷积神经网络实现人脸识别和驾驶员疲劳检测预警使用Python编写经导师指导并评审通过评分为98分源码本地编译可运行。系统可实时读取摄像头画面通过检测眨眼频率、眼睛闭合时间等特征判断疲劳状态并触发预警。资源共20个文件压缩包约78MB11个Python脚本覆盖数据加载、CNN模型构建、训练评估与Tkinter界面检测等环节另有2个XML级联分类器用于人脸/眼部检测1个hdf5预训练权重、1个可直接运行的exe程序以及运行说明、系统说明等文档结构分明。目前已有51人学习下载。配套数据集包含标记为疲劳与非疲劳的人脸图像学习者可循着数据预处理、模型训练、实时检测预警的完整流程掌握CNN在图像识别中的应用并借助说明文档快速复现和调整参数适合需要完整毕设方案或想深入实践深度学习项目的人群。1. 这套基于 CNN 的驾驶员疲劳检测系统拆开到底能学到什么做过毕业设计的同学都有这种体验系统能跑但答辩老师一句“检测原理是什么闭眼阈值为什么设 0.4”就直接卡壳。这套基于卷积神经网络的驾驶员疲劳检测与预警系统恰恰是把整条链路完整打通的项目——摄像头或视频帧进来Haar 级联先定位人脸与眼部区域CNNmini-XCEPTION对眼睛做睁眼/闭眼二分类再按闭眼帧率和持续时长双阈值触发预警。源码、预训练权重、数据集、Tkinter 界面和 Windows 可执行文件都齐了Python 环境下装好依赖就能直接复现。适合正在做计算机类毕业设计、或者想完整走一遍 CNN 图像分类落地流程的同学。别把它当黑匣子跑通就算完后面我把每一层拆开讲。2. 系统链路与模型选型Haar 前端、CNN 分类、双阈值预警2.1 端到端检测链路一帧图像怎么变成报警信号整个系统的数据流是这样的摄像头或视频文件读取一帧图像 → 转成灰度图 → 用haarcascade_frontalface_default.xml检测人脸 → 在人脸区域内用haarcascade_eye.xml裁剪出眼部 ROI → resize 到模型输入尺寸常见做法是 64×64 灰度单通道→ 像素归一化到 [0,1] → 喂给 CNN 推理输出睁眼/闭眼概率 → 把当前帧的判定结果推入滑动窗口 → 统计窗口内闭眼帧比例以及连续闭眼帧数 → 超过阈值就触发报警提示。这套链路里几个 Python 文件的分工非常清晰文件在链路中的角色extract_face.pyHaar 级联人脸检测与眼部 ROI 提取detect_class.py对眼部 ROI 做 CNN 推理输出睁/闭状态baojin.py滑动窗口统计与报警判定tkinter_UI.py视频画面、检测状态与报警信息的界面展示cnn.pymini-XCEPTION 模型定义与训练入口evaluate.py在测试集上评估模型并输出指标建议第一次复现时先跑extract_face.pydetect_class.py把单帧输出打出来确认 ROI 和分类正常再进 UI不要一上来就开整个tkinter_UI.py否则出了问题不好定位是前端还是推理链路。2.2 mini-XCEPTION轻量 CNN 为什么够用XCEPTION 是 Inception 思想的一个极端变体核心是用深度可分离卷积Depthwise Separable Convolution把标准卷积拆成“深度卷积 逐点卷积”两步参数量和计算量都大幅下降。_mini_XCEPTION是它的缩小版专门为 CPU 实时推理设计。文件_mini_XCEPTION.102-0.66.hdf5的命名格式按这类权重常见的命名习惯102 大概率对应训练轮次0.66 是验证集准确率。对睁/闭眼二分类来说这个精度不算高但作为毕业设计演示完整链路完全够用后面你可以用evaluate.py复测再决定要不要用自带的数据集重训。模型定义的核心结构在cnn.py里大致长这样from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, DepthwiseSeparableConv2D, GlobalAveragePooling2D, Dense, Dropout def build_mini_xception(input_shape(64, 64, 1), num_classes2): model Sequential([ Conv2D(8, (3, 3), activationrelu, paddingsame, input_shapeinput_shape), DepthwiseSeparableConv2D(16, (3, 3), activationrelu, paddingsame), Dropout(0.2), DepthwiseSeparableConv2D(32, (3, 3), activationrelu, paddingsame), GlobalAveragePooling2D(), Dropout(0.3), Dense(num_classes, activationsoftmax) ]) return model关键在于DepthwiseSeparableConv2D替代了普通卷积层参数量少了一个量级笔记本 CPU 上跑一帧也就是几十毫秒。最后用GlobalAveragePooling2D替代 Flatten 全连接进一步压缩参数降低过拟合风险。输出层用 softmax 得到两个类别的概率detect_class.py里取概率更大的类别作为当前帧的判定结果。2.3 疲劳判定闭眼帧率与持续时长双阈值如果只用单帧结果判断疲劳光线变化、头部转动、检测框抖动都会造成误报。所以baojin.py用的是两个维度叠加闭眼帧率窗口内闭眼帧占比和连续闭眼时长。这个思路和交通领域常用的 PERCLOS 疲劳判定是一致的但实现上更直观。我在复现时会把判定逻辑简化成下面这段这也是这类报警模块最常见的写法from collections import deque WINDOW_SIZE 60 # 滑动窗口约2秒 30fps CLOSE_RATIO 0.4 # 窗口内闭眼帧占比阈值 CONTINUOUS_FRAMES 15 # 连续闭眼帧数阈值约0.5秒 state_queue deque(maxlenWINDOW_SIZE) # 只存0和11表示闭眼 continuous_close 0 alarm False def update_state(is_close): global continuous_close, alarm state_queue.append(1 if is_close else 0) if is_close: continuous_close 1 else: continuous_close 0 close_ratio sum(state_queue) / len(state_queue) alarm (close_ratio CLOSE_RATIO) or (continuous_close CONTINUOUS_FRAMES) return alarm参数含义WINDOW_SIZE60对应 2 秒窗口避免单帧闪断CLOSE_RATIO0.4表示窗口内有四成帧是闭眼就报警CONTINUOUS_FRAMES15兜底处理“眨眼但碰巧窗口内占比高”的情况防止正常眨眼被误判。这两个阈值不是拍脑袋定的最好根据你自己的摄像头帧率和测试视频实际调整这一点到第 6 章再展开。3. 数据准备与预处理从原始图片到模型输入3.1 convert.py统一图片尺寸、灰度化与归一化数据集的原始图片通常大小不一、格式不一直接喂给 CNN 会出问题。convert.py做的就是标准化把图片统一读进来、转灰度、resize 到模型输入尺寸、归一化像素值最后打包成 numpy 数组存成.npz文件后续训练和推理都从这份处理好的数据读。import cv2 import numpy as np from pathlib import Path def convert_dataset(raw_dir, output_pathdata.npz, target_size(64, 64)): images, labels [], [] for label, sub_dir in enumerate([non_fatigue, fatigue]): # 0清醒, 1疲劳 for img_path in (Path(raw_dir) / sub_dir).glob(*.jpg): img cv2.imread(str(img_path), cv2.IMREAD_GRAYSCALE) img cv2.resize(img, target_size, interpolationcv2.INTER_AREA) img img.astype(float32) / 255.0 # 归一化到 [0,1] images.append(img) labels.append(label) images np.array(images).reshape(-1, target_size[0], target_size[1], 1) labels np.array(labels) np.savez_compressed(output_path, imagesimages, labelslabels) print(fsaved {len(images)} samples to {output_path})这段做了三件事灰度化去掉颜色信息让模型专注于纹理和形态resize统一尺寸/255.0归一化避免数值范围过大导致梯度震荡。interpolationINTER_AREA在缩小时比默认的线性插值更抗锯齿。注意标签目录名要和实际数据集结构一致如果压缩包里的目录名不是这两个改成实际的即可。3.2 split_train_test.py分层划分训练/验证/测试集模型训练前必须把数据切块否则用同一批数据既训练又评估指标会虚高到没有参考价值。split_train_test.py负责把convert.py产出的数据按比例分成训练、验证、测试三部分import numpy as np data np.load(data.npz) images, labels data[images], data[labels] indices np.arange(len(labels)) np.random.seed(42) # 固定种子保证每次切分结果一致 np.random.shuffle(indices) train_end int(len(indices) * 0.7) val_end int(len(indices) * 0.85) train_idx indices[:train_end] val_idx indices[train_end:val_end] test_idx indices[val_end:] np.savez(train.npz, imagesimages[train_idx], labelslabels[train_idx]) np.savez(val.npz, imagesimages[val_idx], labelslabels[val_idx]) np.savez(test.npz, imagesimages[test_idx], labelslabels[test_idx])这里用 7:1.5:1.5 的划分比例测试集保留 15% 数据用于最终评估。np.random.seed(42)是关键不固定种子的话每次跑出来的模型对比结果都不一样写论文时数据就对不上了。顺序上先 shuffle 再切分避免目录是按时间排序导致某一类数据基本集中在一个区间。3.3 load_and_process.py 与 data_provider.py批量加载与训练数据供给load_and_process.py负责把切好的数据读进来并做标签编码。data_provider.py则实现一个生成器按批次产出数据——这是训练大一点的数据集时的标准做法一次性把所有图片读进内存可能会撑爆笔记本。def data_generator(npz_path, batch_size32, augmentFalse): data np.load(npz_path) images, labels data[images], data[labels] num_samples len(labels) while True: indices np.random.permutation(num_samples) # 每个epoch打乱顺序 for start in range(0, num_samples, batch_size): batch_idx indices[start:start batch_size] batch_x images[batch_idx] batch_y labels[batch_idx] if augment: # 数据增强随机水平翻转 flip_mask np.random.rand(len(batch_idx)) 0.5 batch_x[flip_mask] batch_x[flip_mask][:, :, ::-1, :] yield batch_x, batch_y生成器用while True无限循环产出批次配合fit_generator或model.fit的steps_per_epoch使用。augmentTrue时做随机水平翻转相当于把数据量翻倍对提升泛化能力很有帮助。如果你复现时发现训练集很小、模型明显过拟合优先打开这个开关。4. 模型训练与评估cnn.py、evaluate.py 与 check.py 的用法4.1 cnn.py训练 mini-XCEPTION 并保存最优权重cnn.py把模型定义、编译和训练串成一条流程。训练时用 Adam 优化器、二分类交叉熵损失并挂了三个常用回调ModelCheckpoint保存验证集最优权重、ReduceLROnPlateau在 loss 停滞时自动降学习率、EarlyStopping防止训练时间失控。from tensorflow.keras.optimizers import Adam from tensorflow.keras.callbacks import ModelCheckpoint, ReduceLROnPlateau, EarlyStopping model.compile(optimizerAdam(learning_rate1e-3), losssparse_categorical_crossentropy, metrics[accuracy]) callbacks [ ModelCheckpoint(best_model.hdf5, monitorval_loss, save_best_onlyTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience5, min_lr1e-5), EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) ] model.fit(train_gen, steps_per_epoch200, epochs80, validation_dataval_gen, validation_steps50, callbackscallbacks, verbose1)训练参数里值得注意的几个点learning_rate1e-3是此类小模型的常用起点如果 loss 震荡就降到 1e-4sparse_categorical_crossentropy配合整数标签可以省去 one-hot 的存储开销save_best_onlyTrue保证磁盘上永远是最优权重而不是最后一轮的结果。笔记本 CPU 上跑这种小模型单个 epoch 通常几十秒到几分钟几十个 epoch 下来半天以内能完成。4.2 evaluate.py闭眼召回率比准确率更重要训练完不能只看 accuracy 一个数尤其疲劳检测这种场景漏报的代价远大于误报。evaluate.py会在测试集上输出分类报告和混淆矩阵from tensorflow.keras.models import load_model from sklearn.metrics import classification_report, confusion_matrix model load_model(best_model.hdf5) test_data np.load(test.npz) y_pred model.predict(test_data[images]).argmax(axis1) y_true test_data[labels] print(classification_report(y_true, y_pred, target_names[清醒, 疲劳])) print(confusion_matrix(y_true, y_pred))重点关注“疲劳”类别的 recall召回率也就是所有真疲劳样本里有多少被正确找出来。这个数字越高说明系统漏报越少。如果模型整体准确率还行但疲劳类召回率偏低常见的补救手段是给疲劳类样本加权或者在损失函数里给少数类更高的权重这在毕业设计里也是一个很好的改进点。4.3 check.py换机器复现前的体检脚本同学之间互相传项目最常遇到的情况是代码拷过去了模型文件路径不对、TensorFlow 版本不兼容、输入 shape 对不上一跑就崩。check.py就是干这个的——加载模型、检查依赖、核对 shape全过了再进入推理。import os from tensorflow.keras.models import load_model def check_all(): assert os.path.exists(models/_mini_XCEPTION.102-0.66.hdf5), 模型权重文件不存在 model load_model(models/_mini_XCEPTION.102-0.66.hdf5) assert model.input_shape[-1] 1, f模型输入通道数应为1, 当前为 {model.input_shape[-1]} required [tensorflow, opencv-python, numpy] for pkg in required: try: __import__(pkg.replace(-, _)) print(f[OK] {pkg}) except ImportError: print(f[MISSING] {pkg}) print([OK] model loaded, input shape:, model.input_shape) if __name__ __main__: check_all()这段代码解决的是“换了一台电脑就起不来”的问题。load_model能跑通说明权重文件和当前 TensorFlow 版本兼容输入 shape 和预处理代码对得上依赖库齐全三层检查全过再启动 UI省得排错排半天。5. 常见问题与避坑复现这套系统最容易翻车的五个地方5.1 现象CascadeClassifier加载人脸检测器返回空detectMultiScale直接报错我在不少机器上复现过这种问题原因基本都是一个代码里写死了相对路径haarcascade_frontalface_default.xml而当前工作目录不在haarcascade_files文件夹下OpenCV 找不到文件又不会明确报错只返回一个空的检测器对象。解决方式是用 OpenCV 自带的包路径import cv2 face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml )cv2.data.haarcascades指向 OpenCV 安装目录里的data/haarcascades/只要 OpenCV 装好了就一定存在。项目压缩包里自带的haarcascade_files目录也可以直接用但要确保路径存在建议在代码开头加一行os.path.exists断言文件找不到时立刻暴露问题而不是等后面报错。5.2 现象load_model(models/_mini_XCEPTION.102-0.66.hdf5)报错提示Unknown layer或找不到某个自定义层这个坑在网络上下载的 Keras 模型里非常常见。_mini_XCEPTION.102-0.66.hdf5是 TensorFlow 1.x 时代用 Keras 保存的权重直接拿到 TensorFlow 2.x 下加载有时会因为DepthwiseSeparableConv2D等层的实现在新旧版本中注册名不同而失败。解决方式一般是升级或降级 TensorFlow 到与权重匹配的版本或者在加载时加兼容处理。更稳妥的做法是绕开直接加载改成用cnn.py里的build_mini_xception重建模型然后按权重名加载from tensorflow.keras.models import load_model from tensorflow.keras.utils import CustomObjectScope from tensorflow.keras.layers import DepthwiseSeparableConv2D with CustomObjectScope({DepthwiseSeparableConv2D: DepthwiseSeparableConv2D}): model load_model(models/_mini_XCEPTION.102-0.66.hdf5)如果还不行就用build_mini_xception()重建结构再model.load_weights(...)这个方案对版本差异的容忍度高很多。复现时先用check.py测一遍能省掉大量无头绪的排查。5.3 现象摄像头打开失败cap.read()一直返回 False或者界面画面明显卡顿摄像头打不开多数是设备被占用——比如开了会议软件、相机权限被系统设置关掉了。如果卡顿则大概率是推理太慢或输入帧分辨率太高。我的做法是先把摄像头输入分辨率调低比如从 1280×720 降到 640×480再在送入检测器之前缩放一版人脸检测和眼部检测都在小图上做最后才把检测框映射回原图。低分辨率下人脸检测的帧率能明显提升疲劳检测这种场景完全不需要高清。如果是台式机没有摄像头直接用项目里的测试视频文件替换输入源就行把cv2.VideoCapture(0)改成cv2.VideoCapture(test.mp4)这样至少能把整套检测和报警逻辑跑通。5.4 现象Tkinter 界面能启动但标题栏、按钮上的中文全显示成方块这个问题只要是用 Tkinter 做界面的项目基本都会遇到。Windows 下 Tkinter 默认字体是不支持中文的解决方法是显式指定一个中文字体。在tkinter_UI.py里创建控件时统一设置字体或者在创建Tk()实例后设置默认字体import tkinter as tk from tkinter import font root tk.Tk() default_font font.nametofont(TkDefaultFont) default_font.configure(familyMicrosoft YaHei, size12) root.option_add(*Font, default_font)这段放在 UI 初始化最前面界面上的中文就能正常显示了。如果你用中文 Linux 桌面环境可以把Microsoft YaHei换成WenQuanYi Micro Hei道理一样。5.5 现象训练时 loss 不降或者几个 epoch 后直接变成 nan这类问题在我看过的大量 CNN 复现里很常见原因几乎固定在三个位置。第一是数据没有归一化像素值直接从 0-255 喂进去梯度数值过大导致震荡第二是学习率太高尤其是用 Adam 且初始学习率大于 1e-2 时很容易在训练早期就崩掉第三是标签和损失函数不匹配比如用categorical_crossentropy却传了整数标签。解决方式是确认数据已经/255.0学习率降到 1e-4 到 1e-3 之间标签形式与损失函数对齐。如果用了sparse_categorical_crossentropy标签保持整数如果用了categorical_crossentropy标签要做 one-hot。改完这三个地方绝大多数 loss 不降的问题都能解决。6. 跑通后的验证技巧没有摄像头怎么确认报警逻辑真的生效6.1 用视频文件替代摄像头离线跑完整链路项目收敛到能出界面之后最应该做的不是继续加功能而是验证报警逻辑本身。我一般会先准备一段包含“正常驾驶 模拟疲劳闭眼”两个片段的短视频把输入源从VideoCapture(0)换成VideoCapture(test.mp4)在baojin.py里临时加一行日志输出print(fframe_no{frame_no}, is_close{is_close}, ratio{close_ratio:.2f}, alarm{alarm})跑完一遍视频后对着日志检查两个点正常片段里有没有误报疲劳片段里报警有没有触发、触发时机是不是在连续闭眼约 0.5 秒之后。这一步能在不打扰任何人的情况下把阈值调到一个合理区间。6.2 从日志里反推阈值而不是凭感觉调参我见过很多人在界面里反复试阈值试了十几个数还是觉得“哪里不对”。正确做法是把日志里ratio和continuous_close的分布拉出来看一眼取正常驾驶时的最大值和疲劳驾驶时的最小值把报警阈值定在这两者之间。早期复现这套系统时我把CLOSE_RATIO直接设成 0.5结果正常驾驶时低头看手机也能触发报警后来统计了 3 分钟正常视频才发现正常状态下窗口内闭眼比例基本不会超过 0.2这才把阈值压到 0.4并加了CONTINUOUS_FRAMES兜底。从那以后我每次拿到这类检测系统都会先强制自己走一遍“视频采集 → 日志统计 → 反推阈值”的流程确认报警边界是数据算出来的而不是肉眼估出来的再上界面。希望帮到你。本文还有配套的精品资源点击获取