基于Python CNN的驾驶员疲劳检测系统:从人脸识别到预警实战
简介这份资源是面向计算机专业毕业生与Python初学者的人脸识别疲劳检测毕业设计源码包围绕卷积神经网络实现驾驶员疲劳状态识别与预警可用于课程设计、毕设答辩或自学深度学习项目。压缩包共15个文件约2.8MB包含3个py主程序脚本、1个ui界面文件、1个whl离线安装包、4个xml配置、2个txt说明及md文档、png需求图等覆盖从界面到算法再到依赖安装的完整环节。其中main.py与main_ui.py承担检测逻辑与界面交互requirements.txt与dlib的whl文件方便快速配置环境README与需求图帮助理解系统设计思路。目前已有364人学习下载适合需要现成方案参考、想快速跑通疲劳检测流程并在此基础上二次开发的读者可从中获取项目结构组织、模型调用与预警逻辑的实践思路。1. 从一张答辩现场的照片说起这套疲劳检测系统到底在做什么答辩季我见过太多类似的场景PPT 上放着一段演示视频驾驶员眼睛一闭屏幕弹出红色预警框评委点点头学生松一口气。但真把笔记本摄像头对准自己稍微侧个脸、戴副眼镜、光线暗一点框就飘了预警要么不响要么狂响。这就是「基于 Python 卷积神经网络人脸识别驾驶员疲劳检测与预警系统」这类毕业设计最真实的落地状态——demo 能跑鲁棒性靠运气。这套系统要解决的核心问题其实很明确用普通摄像头不需要红外、不需要专用传感器实时判断驾驶员是否进入疲劳状态并在危险发生前给出预警。技术链路拆开就是三段人脸检测定位、眼部/嘴部状态识别、疲劳判据与预警触发。卷积神经网络CNN主要承担第二段——把「睁眼/闭眼」「打哈欠/正常」这种图像分类做准。适合谁适合正在做相关毕设的本科生、想入门计算机视觉落地的 Python 开发者以及需要给车队做低成本疲劳监控方案的一线工程师。它不追求论文级 SOTA追求的是「一台普通笔记本 一个摄像头就能跑起来」。2. 人脸检测与 CNN 分类的分工为什么不能一个模型全包2.1 检测和分类是两件事混在一起做会翻车很多人第一反应是「我训一个 CNN输入整张图直接输出疲劳/不疲劳」。这个思路在毕设里几乎必翻车原因有三个。第一整图里人脸占比太小CNN 的感受野被大量背景稀释学到的特征里一半是座椅、车窗、方向盘。第二疲劳是一个时序状态单帧图像信息量不足闭眼一帧可能是眨眼也可能是疲劳必须结合连续帧。第三整图分类的可解释性差答辩时评委问「你怎么知道是眼睛闭了」你答不上来。常见做法是拆成两级流水线第一级用 OpenCV 的 Haar 级联或 Dlib 的 HOG 检测器把人脸框出来再在框内定位眼睛和嘴巴区域第二级用一个小型 CNN 对裁剪出的眼部/嘴部小图做二分类。这样每个模型的任务都足够窄数据需求小训练快而且中间结果可视化答辩时能一张张图讲清楚。提示Haar 级联在侧脸和暗光下漏检明显如果条件允许第一级换成基于 CNN 的人脸检测器如 OpenCV DNN 模块加载的人脸检测模型召回率会好很多代价是 CPU 上帧率下降。2.2 环境搭建Python、OpenCV 与深度学习框架的版本对齐环境是第一个坑。热词里「python安装教程」「vscode python环境配置」「pycharm配置python环境」被反复搜说明大量人卡在这一步。我一般推荐 Python 3.8 到 3.10太新的版本某些深度学习框架的预编译轮子还没跟上。下面是一套我验证过能跑通的安装流程。# 创建独立虚拟环境避免污染系统 Python python -m venv fatigue_env # Windows 激活 fatigue_env\Scripts\activate # Linux / macOS 激活 source fatigue_env/bin/activate # 安装核心依赖版本号是踩坑后锁定的组合 pip install opencv-python4.8.1.78 pip install numpy1.24.3 pip install tensorflow2.13.0 pip install imutils0.5.4 pip install scipy1.11.4逻辑说明虚拟环境是为了让毕设的依赖和你机器上其他项目隔离答辩前重装系统也不怕。opencv-python负责摄像头读取、人脸检测和图像预处理tensorflow用来加载和推理 CNN 模型如果你用 PyTorch把对应行换成torch即可但要注意torchvision的版本匹配。imutils是个小工具库做图像缩放和坐标变换很省事。参数说明opencv-python的 4.8.x 系列在 Windows 上对cv2.VideoCapture的兼容性比 4.9 稳定这是我换过三个版本后的结论。numpy不要升到 2.xTensorFlow 2.13 还没适配。如果你用pip install时卡在下载换国内镜像源命令后面加-i https://pypi.tuna.tsinghua.edu.cn/simple。2.3 用 OpenCV 做实时人脸与眼睛定位的最小代码环境好了先跑通「摄像头 → 人脸框 → 眼睛框」这条链路这是后面所有工作的地基。import cv2 # 加载 OpenCV 自带的人脸和眼睛级联检测器 face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml) eye_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_eye.xml) cap cv2.VideoCapture(0) # 0 表示默认摄像头 while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 检测在灰度图上做更快 faces face_cascade.detectMultiScale(gray, 1.3, 5) for (x, y, w, h) in faces: cv2.rectangle(frame, (x, y), (xw, yh), (255, 0, 0), 2) roi_gray gray[y:yh, x:xw] roi_color frame[y:yh, x:xw] eyes eye_cascade.detectMultiScale(roi_gray, 1.1, 10) for (ex, ey, ew, eh) in eyes: cv2.rectangle(roi_color, (ex, ey), (exew, eyeh), (0, 255, 0), 2) cv2.imshow(fatigue-demo, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明detectMultiScale是滑动窗口检测gray是灰度图因为 Haar 特征本身就在灰度上计算。人脸框出来后只在人脸区域内再找眼睛缩小搜索范围减少误检。roi_color用来画框因为最终显示的是彩色帧。参数说明detectMultiScale的第一个数字参数是scaleFactor1.3 表示每次图像缩小 30%值越小检测越慢但越全第二个是minNeighbors5 表示一个候选框周围至少要有 5 个邻居才确认调大减少误检调小增加召回。眼睛检测我用了 1.1 和 10因为眼睛区域小需要更细的缩放步长和更严的确认条件。跑起来后如果眼睛框乱跳先把minNeighbors调到 15 试试。3. 训练一个能分清睁眼闭眼的 CNN数据、结构与参数3.1 数据集从哪来以及为什么必须自己做清洗公开的疲劳检测数据集不少但毕设里直接用往往水土不服因为采集设备、光照、人种和你的摄像头都不一样。我一般建议两条腿走路公开数据集打底自己用摄像头录几百张补足。睁眼闭眼二分类每类 2000 到 5000 张就够一个小 CNN 收敛。数据来源可以是公开数据集的眼部区域裁剪加上自己录制的视频抽帧。清洗比收集更重要。血泪经验是闭眼类里混进大量「半睁」「低头」「眼镜反光」的图模型学到的不是闭眼而是「暗」和「模糊」。清洗时把眼部区域统一缩放到 24x24 灰度图做一次直方图均衡化把过暗过亮的样本剔掉。下面这段代码是批量预处理脚本的核心。import cv2 import os import numpy as np def preprocess_eye(img_path, size(24, 24)): img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: return None img cv2.equalizeHist(img) # 直方图均衡缓解光照差异 img cv2.resize(img, size) # 统一尺寸CNN 输入要求 img img.astype(float32) / 255.0 # 归一化到 0-1 return img def load_dataset(root): X, y [], [] for label, folder in enumerate([open, closed]): folder_path os.path.join(root, folder) for fname in os.listdir(folder_path): feat preprocess_eye(os.path.join(folder_path, fname)) if feat is not None: X.append(feat) y.append(label) X np.array(X).reshape(-1, 24, 24, 1) # 增加通道维度 y np.array(y) return X, y逻辑说明equalizeHist是直方图均衡化把灰度分布拉平让暗光下拍的图和不暗的图特征更一致。resize到 24x24 是因为后面的 CNN 输入固定尺寸越小计算越快眼部特征在 24x24 下已经够用。归一化到 0-1 是 CNN 训练的常规操作能加速收敛。参数说明size可以改成 32x32 或 48x48但别超过 64否则全连接层参数暴涨小数据集容易过拟合。reshape(-1, 24, 24, 1)里的 1 是灰度通道数如果你用彩色图就改成 3。load_dataset假设目录结构是root/open/和root/closed/这是 KerasImageDataGenerator的常见约定自己整理数据时照着建文件夹。3.2 一个够用的小型 CNN 结构别一上来就 ResNet毕设里最常见的误区是拿 ResNet50 或 VGG16 直接上结果训练慢、显存爆、过拟合答辩前一周还在调。眼部二分类是个极窄的任务三层卷积足够。下面是我常用的结构。from tensorflow.keras import layers, models def build_eye_cnn(input_shape(24, 24, 1)): model models.Sequential([ layers.Conv2D(32, (3, 3), activationrelu, input_shapeinput_shape), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activationrelu), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activationrelu), layers.Flatten(), layers.Dense(64, activationrelu), layers.Dropout(0.5), # 防过拟合的关键 layers.Dense(1, activationsigmoid) # 二分类输出 ]) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy]) return model逻辑说明三层卷积逐层提取边缘、纹理、局部形状特征MaxPooling2D降维并保留最强响应。Flatten后接一个 64 维全连接层做特征整合Dropout(0.5)在训练时随机丢弃一半神经元这是小数据集上防过拟合最有效的一招。最后sigmoid输出 0 到 1 的概率大于 0.5 判为闭眼。参数说明卷积核数量 32、64、64 是经验值数据量翻倍可以加到 64、128、128。Dropout的 0.5 是常用起点如果训练集准确率远高于验证集调到 0.6如果两者都低说明欠拟合先减 Dropout 或加数据。adam学习率默认 0.001一般不用改训练不收敛时降到 0.0001。3.3 训练时的三个必调参数与早停策略训练脚本本身不复杂但三个参数决定成败batch_size、epochs和验证集比例。我一般用batch_size32epochs30验证集占 20%。关键是加EarlyStopping不然模型在验证集上开始变差后还在硬训最后保存的是过拟合的权重。from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint X, y load_dataset(dataset/eyes) split int(len(X) * 0.8) X_train, X_val X[:split], X[split:] y_train, y_val y[:split], y[split:] model build_eye_cnn() callbacks [ EarlyStopping(monitorval_loss, patience5, restore_best_weightsTrue), ModelCheckpoint(best_eye_model.h5, monitorval_loss, save_best_onlyTrue) ] history model.fit(X_train, y_train, validation_data(X_val, y_val), batch_size32, epochs30, callbackscallbacks)逻辑说明EarlyStopping监控验证集损失连续 5 个 epoch 不下降就停restore_best_weightsTrue保证模型回到最好的那一轮。ModelCheckpoint把最佳权重存成h5文件推理时直接加载这个文件不用重新训练。参数说明patience5是耐心值数据噪声大可以调到 8数据干净调到 3。batch_size受显存限制显存小就降到 16 或 8但太小会导致梯度震荡。epochs30配合早停实际可能十几轮就停了设大一点没关系。4. 从单帧分类到疲劳判据时序逻辑与预警触发4.1 用 EAR 和 MAR 做判据CNN 做兜底光有 CNN 分类还不够因为单帧闭眼不等于疲劳。工程上常用两个几何指标眼睛纵横比EAR和嘴巴纵横比MAR。EAR 是眼睛高度和宽度的比值睁眼时约 0.25 到 0.3闭眼时降到 0.1 以下。MAR 是嘴巴高度和宽度的比值打哈欠时明显升高。CNN 的作用是在 EAR 不可靠时比如眼镜反光导致关键点漂移提供第二判据。常见做法是连续 N 帧 EAR 低于阈值或者 CNN 连续 N 帧判为闭眼就计一次「闭眼事件」。单位时间内闭眼事件超过阈值或者单次闭眼持续超过 2 秒触发预警。打哈欠同理用 MAR 加 CNN 嘴部分类。import numpy as np from collections import deque class FatigueDetector: def __init__(self, ear_thresh0.2, consec_frames20, yawn_thresh0.6): self.ear_thresh ear_thresh self.consec_frames consec_frames self.yawn_thresh yawn_thresh self.closed_counter 0 self.yawn_counter 0 self.ear_history deque(maxlen30) # 保留最近 30 帧 def update(self, ear, mar, cnn_closed_prob): # CNN 概率和几何指标融合任一超标都计数 is_closed (ear self.ear_thresh) or (cnn_closed_prob 0.7) if is_closed: self.closed_counter 1 else: self.closed_counter 0 if mar self.yawn_thresh: self.yawn_counter 1 else: self.yawn_counter 0 # 闭眼持续超过阈值帧数或哈欠持续触发预警 if self.closed_counter self.consec_frames: return DROWSY if self.yawn_counter self.consec_frames: return YAWNING return NORMAL逻辑说明deque保留最近 30 帧的 EAR方便后面做平滑或画曲线。融合逻辑用「或」因为几何指标和 CNN 各有盲区宁可多报不可漏报这是安全类系统的通用原则。计数器在状态恢复时清零避免累积误报。参数说明ear_thresh0.2是常用起点但每个人眼睛形状不同最好在系统启动时做 5 秒校准记录用户睁眼时的平均 EAR阈值设为该值的 70%。consec_frames20在 30fps 下约 0.67 秒实际疲劳闭眼通常超过 1 秒可以调到 30。cnn_closed_prob的 0.7 是置信度门槛模型准的话可以降到 0.6 提高召回。4.2 预警输出声音、弹窗与日志三件套预警不能只是一个框。实际场景里驾驶员可能没看屏幕所以声音是必须的。我用pygame播报警音tkinter或cv2画红色遮罩同时把事件写进 CSV 日志方便答辩时展示「系统记录了 3 次疲劳事件」。import pygame import csv import time pygame.mixer.init() alarm pygame.mixer.Sound(alarm.wav) def trigger_alarm(state, log_pathfatigue_log.csv): if state in (DROWSY, YAWNING): alarm.play() with open(log_path, a, newline) as f: writer csv.writer(f) writer.writerow([time.strftime(%Y-%m-%d %H:%M:%S), state])逻辑说明pygame.mixer播报警音跨平台且延迟低。日志用追加模式每次事件一行包含时间戳和状态。答辩时打开 CSV 就能看到完整记录比口头说「它报警了」有说服力。参数说明报警音文件建议用 1 秒以内的短促蜂鸣太长会盖住后续判断。日志路径用相对路径方便打包。如果部署在 Linux 无音频设备的环境pygame.mixer.init()会报错加try/except降级为只写日志。5. 避坑与排查那些让 demo 当场翻车的细节5.1 摄像头帧率忽高忽低预警延迟严重现象演示时预警比实际闭眼慢半拍或者帧率从 30 掉到 5。原因通常是每帧都在做全图人脸检测且没有跳帧策略。解决人脸检测每 3 帧做一次中间帧复用上一次的人脸框只做眼部裁剪和 CNN 推理。CNN 推理本身很快瓶颈在检测。另外把摄像头分辨率从 1080p 降到 640x480帧率立刻回升。5.2 戴眼镜就误报摘了眼镜就正常现象戴眼镜的用户闭眼被频繁判为睁眼或者反光被当成闭眼。原因是眼镜框和镜片反光干扰了 Haar 眼睛检测和 EAR 计算。解决训练数据里必须包含戴眼镜的样本这是最根本的。临时方案是把 CNN 的置信度门槛从 0.7 降到 0.6让 CNN 在几何指标失效时多发挥作用。如果还不行换用基于关键点的方法如 Dlib 的 68 点计算 EAR对眼镜更鲁棒。5.3 模型在验证集上 99%实拍却一塌糊涂现象训练日志里val_accuracy很高但摄像头前表现差。原因是数据泄露或分布不一致——训练集和验证集可能来自同一段视频的相邻帧几乎一样或者训练数据是公开数据集的正脸实拍是侧脸暗光。解决划分数据集时按「人」划分同一个人不能同时出现在训练和验证集。另外用自己摄像头录一段独立测试视频别用训练数据评估。5.4 预警狂响驾驶员被吵到关掉系统现象稍微眨眼就报警或者打哈欠计数一直不清零。原因是阈值太敏感或者计数器逻辑有 bug。解决加一个「启动校准」阶段前 5 秒只采集不判断用这段时间的 EAR 均值动态设阈值。哈欠计数加一个冷却时间触发一次后 10 秒内不再重复触发。预警分级轻度只记录中度声音提示重度才持续报警。5.5 打包成 exe 后摄像头打不开现象PyCharm 里跑得好好的用 PyInstaller 打包后cv2.VideoCapture(0)返回空。原因是打包时漏了 OpenCV 的动态库或者摄像头权限在打包后被系统拦截。解决PyInstaller 加--add-data把cv2.data.haarcascades目录打进去或者改用绝对路径加载级联文件。Windows 上检查隐私设置里的摄像头权限别只盯着代码。6. 把系统做扎实的两个进阶方向模型量化与多指标融合如果你已经跑通了上面的流程答辩也过了想让这套东西真正能用在车上或者边缘设备上有两个方向值得投入。第一个是模型量化。上面那个小 CNN 在笔记本上跑没问题但放到树莓派或者低功耗开发板上TensorFlow 的浮点推理会吃力。用 TensorFlow Lite 把模型转成 int8 量化版本体积缩小到四分之一推理速度提升两三倍精度损失通常不到 1%。转换命令就一行converter.optimizations [tf.lite.Optimize.DEFAULT]但要注意量化需要一批代表性数据做校准别随便拿几张图糊弄。第二个是多指标融合。单靠眼睛和嘴巴遇到打哈欠同时揉眼睛、或者低头看手机的情况判据会打架。我一般会再加两个信号头部姿态用solvePnP估算俯仰角低头超过 20 度持续 3 秒算分神和方向盘握力如果有硬件。融合策略用简单的加权投票每个指标输出 0 到 1 的疲劳分加权求和后过阈值。权重不用拍脑袋拿一段标注好的视频做网格搜索找到使误报和漏报平衡的那组值。验证方法上别只看准确率。疲劳检测是典型的不平衡问题正常状态占 95% 以上全判正常也有 95% 准确率。要看召回率和误报率召回率低于 90% 说明漏报太多危险误报率高于每小时 5 次驾驶员会烦到关掉。我习惯用一段 10 分钟的模拟驾驶视频人工标出所有真实疲劳事件然后统计系统报了多少、漏了多少、误报多少这三个数比任何训练曲线都有说服力。最后说个我自己的习惯每次改完阈值或模型一定用同一段测试视频跑一遍把结果和上一版对比别凭感觉说「好像好点了」。疲劳检测这东西玄学的地方很多但能复现的对比数据是唯一的后悔药。希望帮到你。本文还有配套的精品资源点击获取