
简介这份资源面向计算机、人工智能方向的毕业设计学生与初学者提供一套基于Python卷积神经网络的人脸识别驾驶员疲劳检测与预警系统完整实现。项目从人脸朝向、位置、瞳孔朝向、眼睛开合度、眨眼频率等特征入手实时计算驾驶员注意力集中程度并针对打哈欠、眨眼、点头三类疲劳表现作出安全提示。检测环节借助dlib的68点人脸关键点模型通过眼睛宽高比EAR判断闭眼状态结合连续帧数与阈值差值识别疲劳。压缩包共20个文件约78.32MB包含11个py源码、3个txt说明、2个xml级联分类器、1个hdf5模型权重、1个exe可执行程序及md文档等覆盖训练、评估、界面与检测全流程。已有61人学习适合需要完整代码、模型文件与配套资料快速完成毕设或课程项目的读者参考。1. 从一张打哈欠的截图说起这套疲劳检测系统到底在做什么凌晨两点的高速服务区我盯着笔记本上一张驾驶员打哈欠的截图发呆——那是某次模拟项目里跑出来的中间结果模型把张嘴动作判成了「疲劳」可实际上人家只是在跟副驾说话。这个误报让我意识到基于 Python 卷积神经网络的人脸识别驾驶员疲劳检测与预警系统难点从来不是「能不能识别出人脸」而是「怎么把眨眼、打哈欠、低头这些动作稳定地翻译成疲劳信号并且不误报」。这套系统要解决的核心问题很具体用摄像头采集驾驶员面部图像通过 CNN 提取特征判断眼睛闭合时长、嘴巴开合程度、头部姿态最终输出疲劳等级并触发预警。它适合两类人——做毕业设计需要一套完整可复现方案的学生以及想快速搭一个疲劳检测原型的工程师。整套东西通常包含源代码、训练好的模型文件.h5 或 .pt、数据集说明和部署脚本。下面我按「先跑通、再调优、最后避坑」的顺序把这条链路拆开讲。2. 环境搭建与最小可跑通链路先让摄像头认出你的脸2.1 依赖选型为什么是 OpenCV Keras/TensorFlow 而不是 PyTorch做疲劳检测第一件事是选框架。我一般推荐OpenCV KerasTensorFlow 后端的组合原因有三一是 OpenCV 的VideoCapture和CascadeClassifier在 CPU 上就能跑不需要显卡也能出结果二是 Keras 的SequentialAPI 写 CNN 极其直观适合毕设这种「要讲清楚每一层在干嘛」的场景三是模型文件.h5单文件保存方便打包进「全套资料」。如果你更熟 PyTorch换成torch.nn.Conv2d也一样但要注意模型文件格式变成.pt加载方式不同。下面这套依赖是我在 Python 3.8~3.10 上反复验证过的# 建议用虚拟环境避免和系统包打架 python -m venv fatigue_env source fatigue_env/bin/activate # Windows 用 fatigue_env\Scripts\activate # 核心依赖版本不要乱跳 pip install opencv-python4.8.1.78 pip install tensorflow2.13.0 pip install numpy1.24.3 pip install scikit-learn1.3.0 pip install imutils0.5.4 pip install pygame2.5.0 # 用于播放预警提示音这里有个血泪经验TensorFlow 2.13 和 numpy 1.24 是搭配的如果你装 numpy 1.26 会报np.object已弃用的错。很多人卡在这一步以为是代码问题其实是版本玄学。2.2 人脸检测用 Haar 级联还是 DNN 模型人脸检测是整条链路的第一环。常见做法有两种OpenCV 自带的 Haar 级联分类器和基于深度学习的人脸检测器如 ResNet SSD。Haar 的优点是快、零依赖、CPU 友好缺点是侧脸和暗光下容易漏检。DNN 检测器精度高但需要额外下载模型文件推理慢一些。对于疲劳检测我的建议是先用 Haar 跑通再考虑换 DNN。因为疲劳检测关注的是正脸驾驶员基本朝前Haar 足够用。下面是加载检测器和摄像头的代码import cv2 import numpy as np # 加载 OpenCV 自带的人脸检测器 # 路径在 cv2.data.haarcascades 下不用自己下载 face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) # 打开摄像头0 表示默认摄像头 cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while True: ret, frame cap.read() if not ret: break # 转灰度Haar 检测在灰度图上做 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # detectMultiScale 参数图像、缩放步长、最小邻居数、最小尺寸 faces face_cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(80, 80) ) for (x, y, w, h) in faces: cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) cv2.imshow(Face Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明detectMultiScale的scaleFactor1.1表示每次图像缩小 10% 再检测值越小越慢但越准minNeighbors5控制误检值越大越严格minSize(80,80)过滤掉太小的误检框。这三个参数是调优重点后面避坑章节会细说。2.3 眼睛和嘴巴定位把 ROI 切出来喂给 CNN人脸框出来后下一步是定位眼睛和嘴巴区域。常见做法是用 Haar 的haarcascade_eye.xml和haarcascade_smile.xml或者用面部关键点检测如 dlib 的 68 点。dlib 精度高但编译麻烦毕设场景我更推荐 Haar简单直接。eye_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_eye.xml ) mouth_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_smile.xml ) # 在人脸框内检测眼睛和嘴巴 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), (255, 0, 0), 2) # 嘴巴区域通常在人脸下半部分先裁剪再检测 mouth_roi roi_gray[h//2:, :] mouths mouth_cascade.detectMultiScale(mouth_roi, 1.7, 20) for (mx, my, mw, mh) in mouths: cv2.rectangle(roi_color, (mx, myh//2), (mxmw, mymhh//2), (0, 0, 255), 2)参数说明眼睛检测的minNeighbors10比人脸更严格因为眼睛区域小、误检多嘴巴检测的scaleFactor1.7是为了快速跳过因为嘴巴开合变化大不需要太精细。切出来的眼睛和嘴巴 ROI 会统一 resize 到 24x24 或 32x32作为 CNN 的输入。3. 用 CNN 做疲劳分类模型结构、训练流程与参数设置3.1 为什么不用传统 SVM而用卷积神经网络早期疲劳检测常用 SVM HOG 特征但 SVM 对眼睛开合这种细微纹理变化不敏感且需要手工设计特征。CNN 的优势在于自动提取局部纹理——闭眼时眼睑的横向纹理、打哈欠时嘴巴的纵向拉伸卷积核都能捕捉到。对于毕设来说CNN 还有一个隐性好处模型结构可视化后答辩时能讲出「每一层在学什么」比 SVM 好讲得多。我一般用一个小型 CNN参数量控制在 50 万以内CPU 推理单帧 20ms 左右足够实时。结构是3 个卷积块Conv ReLU MaxPool 2 个全连接层 Softmax 输出。3.2 模型定义与训练脚本import tensorflow as tf from tensorflow.keras import layers, models def build_fatigue_cnn(input_shape(24, 24, 1), num_classes2): 输入24x24 灰度图眼睛或嘴巴 ROI 输出2 分类疲劳/正常 model models.Sequential([ # 第 1 个卷积块提取边缘和纹理 layers.Conv2D(32, (3, 3), activationrelu, input_shapeinput_shape), layers.MaxPooling2D((2, 2)), # 第 2 个卷积块提取更复杂的局部模式 layers.Conv2D(64, (3, 3), activationrelu), layers.MaxPooling2D((2, 2)), # 第 3 个卷积块组合特征 layers.Conv2D(128, (3, 3), activationrelu), layers.MaxPooling2D((2, 2)), # 展平后接全连接 layers.Flatten(), layers.Dense(128, activationrelu), layers.Dropout(0.5), # 防止过拟合毕设数据集通常不大 layers.Dense(num_classes, activationsoftmax) ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losscategorical_crossentropy, metrics[accuracy] ) return model model build_fatigue_cnn() model.summary()逻辑说明Conv2D(32, (3,3))表示 32 个 3x3 卷积核MaxPooling2D((2,2))把特征图缩小一半。三层卷积后特征图从 24x24 降到 3x3再展平成 1152 维接全连接。Dropout(0.5)是关键——毕设数据集往往只有几千张图不加 Dropout 训练集准确率能到 99%测试集只有 70%这就是过拟合。训练时用ImageDataGenerator做数据增强把眼睛和嘴巴图片按 8:2 划分训练集和验证集from tensorflow.keras.preprocessing.image import ImageDataGenerator # 数据增强旋转、平移、缩放模拟不同角度和距离 datagen ImageDataGenerator( rescale1./255, rotation_range10, width_shift_range0.1, height_shift_range0.1, zoom_range0.1, validation_split0.2 ) train_gen datagen.flow_from_directory( dataset/eyes, # 目录下分 closed/ 和 open/ 两个子文件夹 target_size(24, 24), color_modegrayscale, batch_size32, class_modecategorical, subsettraining ) val_gen datagen.flow_from_directory( dataset/eyes, target_size(24, 24), color_modegrayscale, batch_size32, class_modecategorical, subsetvalidation ) history model.fit( train_gen, epochs30, validation_dataval_gen, callbacks[ tf.keras.callbacks.EarlyStopping(patience5, restore_best_weightsTrue), tf.keras.callbacks.ModelCheckpoint(fatigue_eye_model.h5, save_best_onlyTrue) ] )参数说明EarlyStopping(patience5)表示验证集损失连续 5 轮不下降就停避免浪费时间ModelCheckpoint只保存验证集最好的模型这样最终拿到的.h5文件是泛化能力最强的那个。batch_size32是 CPU 训练的平衡点太大内存吃紧太小梯度不稳。3.3 疲劳判定逻辑从单帧分类到时间窗口CNN 输出的是单帧的「睁眼/闭眼」概率但疲劳是一个时间累积概念。我一般用 PERCLOS单位时间内闭眼帧占比作为核心指标from collections import deque class FatigueDetector: def __init__(self, window_size30, perclos_threshold0.4): self.window deque(maxlenwindow_size) # 滑动窗口 self.threshold perclos_threshold self.eye_closed_frames 0 def update(self, eye_closed): 每帧调用一次eye_closed 是布尔值 self.window.append(1 if eye_closed else 0) if len(self.window) self.window.maxlen: perclos sum(self.window) / len(self.window) return perclos self.threshold return False detector FatigueDetector(window_size30, perclos_threshold0.4)逻辑说明window_size30表示看最近 30 帧约 1 秒按 30fps 算perclos_threshold0.4表示闭眼帧超过 40% 就判疲劳。这两个参数需要根据实际帧率和驾驶员习惯调后面避坑章节会讲怎么调。4. 预警模块与系统集成让检测结果真正「响」起来4.1 三级预警策略声音、弹窗、记录检测出疲劳后预警不能只是 print 一行字。我一般做三级一级轻度屏幕变黄 短提示音二级中度屏幕变红 连续蜂鸣三级重度弹窗 记录时间戳到日志。用 pygame 播放提示音用 OpenCV 的putText做屏幕叠加。import pygame import time import datetime pygame.mixer.init() # 准备两个音效文件短提示音和连续蜂鸣 alert_sound pygame.mixer.Sound(alert.wav) def trigger_alert(level, frame): level: 1 轻度, 2 中度, 3 重度 if level 1: cv2.putText(frame, MILD FATIGUE, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2) elif level 2: cv2.putText(frame, MODERATE FATIGUE, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 165, 255), 2) alert_sound.play() elif level 3: cv2.putText(frame, SEVERE FATIGUE!, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3) alert_sound.play() # 记录到日志方便事后分析 with open(fatigue_log.txt, a) as f: f.write(f{datetime.datetime.now()}: SEVERE\n) return frame逻辑说明预警等级由 PERCLOS 值映射——0.4~0.5 一级0.5~0.6 二级0.6 三级。日志记录是为了答辩时展示「系统有完整闭环」也是实际部署时排查误报的依据。4.2 主循环集成把检测、分类、预警串起来# 加载训练好的模型 eye_model tf.keras.models.load_model(fatigue_eye_model.h5) detector FatigueDetector(window_size30, perclos_threshold0.4) cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.1, 5, minSize(80, 80)) for (x, y, w, h) in faces: roi_gray gray[y:yh, x:xw] eyes eye_cascade.detectMultiScale(roi_gray, 1.1, 10) eye_closed False for (ex, ey, ew, eh) in eyes[:2]: # 只看前两只眼睛 eye_roi roi_gray[ey:eyeh, ex:exew] eye_roi cv2.resize(eye_roi, (24, 24)) / 255.0 eye_roi eye_roi.reshape(1, 24, 24, 1) pred eye_model.predict(eye_roi, verbose0) if pred[0][0] 0.5: # 假设索引 0 是闭眼 eye_closed True break is_fatigued detector.update(eye_closed) if is_fatigued: perclos sum(detector.window) / len(detector.window) level 1 if perclos 0.5 else (2 if perclos 0.6 else 3) frame trigger_alert(level, frame) cv2.imshow(Fatigue Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明每帧先检测人脸再在人脸内检测眼睛把眼睛 ROI 送进 CNN 分类结果喂给FatigueDetector更新滑动窗口。predict加了verbose0避免刷屏。这里只用了眼睛模型实际完整系统还会加嘴巴模型判断打哈欠逻辑一样只是多一个mouth_closed变量。5. 避坑与排查那些让模型「看起来能用实际翻车」的细节5.1 坑一白天准晚上废——光照变化导致 Haar 漏检现象白天测试人脸检测正常晚上或隧道里频繁丢框系统直接不工作。原因Haar 级联基于灰度对比度暗光下人脸和背景对比度下降detectMultiScale找不到足够强的边缘特征。解决加一步直方图均衡化CLAHE提升局部对比度。在cvtColor后加clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) gray clahe.apply(gray)clipLimit2.0控制对比度增强上限太大反而放大噪点。如果还不行就换 DNN 人脸检测器代价是推理慢 3~5 倍。5.2 坑二模型把「眯眼笑」判成疲劳——数据集偏差现象测试时驾驶员一笑系统就报警。原因训练集里「闭眼」样本大多是疲劳闭眼没有「眯眼笑」这种正样本模型学到了「眼睛变窄疲劳」的捷径。解决在数据集里补充「眯眼但正常」的负样本或者用眼睛纵横比EAR做辅助判断——EAR 低于阈值且持续超过 1 秒才算疲劳单纯眯眼一笑而过。EAR 计算需要面部关键点可以用 dlib 或 mediapipe但会增加依赖。5.3 坑三PERCLOS 阈值照搬论文实际误报不断现象按论文设perclos_threshold0.4结果正常眨眼也报警。原因论文的帧率和你的摄像头帧率不一样。30fps 下 30 帧是 1 秒但如果你摄像头只有 15fps30 帧就是 2 秒窗口太长导致反应迟钝反之窗口太短则正常眨眼被算进去。解决先打印实际帧率再按「窗口时长 1 秒」反推window_size。正常眨眼一次约 0.2~0.4 秒所以窗口至少 1 秒才能区分「眨眼」和「闭眼」。我一般设window_size fpsperclos_threshold从 0.4 开始根据实测微调。5.4 坑四模型文件加载报错——版本不匹配现象load_model(fatigue_eye_model.h5)报Unknown layer或bad marshal data。原因训练和推理的 TensorFlow 版本不一致或者模型保存时用了自定义层。解决训练和部署用同一个虚拟环境模型保存用model.save(model.h5)而不是model.save_weights()。如果必须跨版本用tf.keras.models.load_model(model.h5, compileFalse)跳过编译再手动compile。5.5 坑五多线程卡顿——CNN 推理阻塞视频流现象视频画面一卡一卡预警延迟明显。原因model.predict在主线程里同步执行每帧都要等推理完成。解决把推理放到独立线程主线程只负责采集和显示。用queue.Queue传递帧推理线程消费。或者降级模型——把Dense(128)改成Dense(64)参数量减半精度掉 1~2 个点但帧率翻倍。6. 进阶技巧用迁移学习和模型量化把系统压进树莓派如果你想把系统从笔记本搬到嵌入式设备比如树莓派有两个技巧值得试。第一个是迁移学习不要从零训练用 MobileNetV2 在 ImageNet 上的预训练权重把最后几层换成你的分类头只训练新增层。这样 5000 张图就能到 95% 以上准确率训练时间从几小时降到十几分钟。base tf.keras.applications.MobileNetV2( input_shape(96, 96, 3), include_topFalse, weightsimagenet ) base.trainable False # 冻结预训练层 model models.Sequential([ base, layers.GlobalAveragePooling2D(), layers.Dense(64, activationrelu), layers.Dropout(0.3), layers.Dense(2, activationsoftmax) ]) model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy])注意输入尺寸从 24x24 变成 96x96且要转成 RGB 三通道。冻结base后只训练新增层trainableFalse是关键否则预训练权重会被小数据集带偏。第二个是模型量化把 float32 权重转成 int8模型体积缩小 4 倍推理速度提升 2~3 倍精度通常只掉 1% 以内。converter tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations [tf.lite.Optimize.DEFAULT] tflite_model converter.convert() with open(fatigue_model.tflite, wb) as f: f.write(tflite_model)部署时用tf.lite.Interpreter加载.tflite文件树莓派 4B 上单帧推理能压到 30ms 以内基本实时。验证量化后精度的方法很简单拿 100 张测试图分别用原模型和 tflite 模型跑一遍对比预测标签一致率低于 95% 就说明量化损失太大需要调converter.representative_dataset做校准。最后说个我自己的习惯每次改完参数先拿一段固定视频跑一遍把 PERCLOS 曲线画出来。曲线比单帧准确率更能反映系统真实表现——如果曲线在正常驾驶时频繁冲高说明阈值或窗口有问题如果曲线一直贴地说明模型太保守。这个习惯帮我省了无数次「改完感觉好了但说不清哪里好」的纠结。希望帮到你。本文还有配套的精品资源点击获取