简介:这份资源是面向高校计算机、人工智能相关专业学生的毕业设计完整项目包,聚焦基于卷积神经网络的人脸识别驾驶员疲劳检测与预警系统,适合用作毕业设计、期末大作业或课程设计,也适合想入门深度学习实战的初学者。压缩包共37个文件,约500.41MB,包含16个Python源码文件、9个编译缓存文件、5张效果图、3个模型权重文件以及数据集压缩包和说明文档,覆盖模型训练、推理检测、摄像头与视频流处理等模块。项目已通过导师指导并获高分评价,代码完整可直接运行,读者能拿到从数据准备、网络搭建、权重加载到疲劳状态判定与预警输出的整套方案,并借助效果图与日志快速验证结果。目前已有906人学习下载,可作为深度学习与计算机视觉方向毕业设计的实战参考。
1. 从一张“闭眼照”说起:这套疲劳检测系统到底在做什么
凌晨两点跑长途的司机,眼皮开始打架,头一点一点往下沉——这个瞬间如果被车载摄像头抓到,系统能在 0.5 秒内判定为疲劳并触发预警,这就是基于卷积神经网络的人脸识别驾驶员疲劳检测与预警系统要解决的核心问题。它把 Python、卷积神经网络、人脸识别、疲劳检测、预警系统这几个词串成了一条完整链路:先用摄像头采集驾驶员面部图像,再用 CNN 提取眼部、嘴部特征,判断闭眼时长、眨眼频率、打哈欠次数,最后通过声音或震动预警。这套方案适合做毕业设计的学生,也适合想入门深度学习落地的人——它不依赖昂贵硬件,一台带摄像头的笔记本加 Python 环境就能跑通。很多人以为疲劳检测要上红外或脑电波,其实普通 RGB 摄像头配合 CNN 就够用,关键在特征设计和阈值调参。
2. 疲劳检测的技术选型:为什么是 CNN 而不是传统方法
2.1 传统人脸检测与 CNN 的分水岭
早期疲劳检测用 Haar 级联加眼睛模板匹配,思路是先定位人脸,再在眼部区域做二值化,统计黑色像素比例判断睁闭眼。这套方法在实验室光照均匀时能用,但一上车就翻车:逆光、侧脸、戴眼镜、夜间红外补光不足,误判率飙升。CNN 的优势在于它不依赖人工设计的边缘和纹理规则,而是通过卷积核自动学习眼部区域的抽象特征。一个 3 层卷积加 2 层全连接的轻量网络,在自建数据集上就能把睁闭眼分类准确率做到 95% 以上。常见做法是用 MTCNN 或 RetinaFace 先做人脸检测和对齐,裁出眼部 ROI,再送入 CNN 分类。这样分工明确:人脸检测负责“找到脸”,CNN 负责“看懂眼”。
2.2 疲劳判定的三个核心指标
光判断单帧睁闭眼不够,疲劳是时序行为。我一般用三个指标联合判定:第一,PERCLOS(单位时间内闭眼帧占比),超过 0.3 持续 3 秒就报警;第二,最长闭眼时长,超过 1.5 秒直接触发;第三,打哈欠频率,用嘴部纵横比 MAR 判断,1 分钟内超过 3 次算疲劳。这三个指标分别对应微睡眠、重度困倦和缺氧性疲劳。参数不是拍脑袋定的,PERCLOS 的 0.3 来自 NHTSA 的疲劳驾驶研究,闭眼时长 1.5 秒是生理学上微睡眠的临界值。实际部署时可以根据驾驶员个体差异微调,但不要偏离太远,否则要么误报频繁,要么漏报危险。
2.3 预警系统的触发逻辑与去抖
预警不是一检测到闭眼就响,那样司机正常眨眼也会被吵死。我的做法是加一个状态机:连续 N 帧判定为闭眼才进入“疑似疲劳”状态,再持续 T 秒才升级为“确认疲劳”并触发预警。N 取 15 帧(约 0.5 秒),T 取 2 秒。预警方式分两级:一级是仪表盘闪烁加提示音,二级是座椅震动加语音播报。去抖的关键是引入“恢复帧”——如果中间有连续 5 帧睁眼,状态机回退一级。这套逻辑用 Python 的有限状态机库 transitions 实现最干净,也可以手写 if-else,但状态多了容易乱。
3. 用 Python 搭一套可跑通的疲劳检测流水线
3.1 环境准备与依赖安装
先确保 Python 版本在 3.8 以上,推荐 3.8 或 3.9,太新的版本有些深度学习库轮子还没跟上。安装依赖时注意 opencv-python 和 opencv-contrib-python 不要同时装,会冲突。我一般用虚拟环境隔离:
python -m venv fatigue_env source fatigue_env/bin/activate # Windows 用 fatigue_env\Scripts\activate pip install opencv-python==4.8.0.74 pip install tensorflow==2.13.0 pip install numpy==1.24.3 pip install scipy==1.11.1 pip install pygame==2.5.0TensorFlow 用来搭 CNN 和推理,OpenCV 负责摄像头采集和人脸检测,pygame 播放预警音。版本号不是随便写的:TensorFlow 2.13 对 Python 3.8 支持稳定,numpy 1.24 避免了和 TensorFlow 的 ABI 冲突。如果 pip 安装慢,换国内镜像源,但不要用来路不明的第三方轮子。
3.2 人脸检测与眼部 ROI 裁剪
用 OpenCV 自带的 DNN 人脸检测器,比 Haar 准,比 MTCNN 快。模型文件用 res10_300x300_ssd_iter_140000.caffemodel 和 deploy.prototxt,这两个文件网上能搜到,注意校验 MD5。代码逻辑是:读帧 → 转 blob → 前向推理 → 取置信度大于 0.7 的框 → 按比例裁出上半脸作为眼部区域。
import cv2 import numpy as np net = cv2.dnn.readNetFromCaffe('deploy.prototxt', 'res10_300x300_ssd_iter_140000.caffemodel') def detect_face(frame): h, w = frame.shape[:2] blob = cv2.dnn.blobFromImage(cv2.resize(frame, (300, 300)), 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.7: box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) x1, y1, x2, y2 = box.astype('int') # 裁上半脸作为眼部区域,比例可调 eye_roi = frame[y1:y1 + int((y2 - y1) * 0.5), x1:x2] return eye_roi, (x1, y1, x2, y2) return None, None置信度阈值 0.7 是平衡漏检和误检的经验值,光线差时降到 0.5,但误检会增多。裁上半脸的比例 0.5 覆盖了眉毛到鼻梁,包含双眼。如果驾驶员戴眼镜反光严重,可以先把 ROI 转灰度再做直方图均衡化,能提升 CNN 分类稳定性。
3.3 构建轻量 CNN 做睁闭眼分类
网络结构不用太深,输入 24x24 灰度图,两个卷积块加全连接就够。第一个卷积块 32 个 3x3 核,最大池化;第二个卷积块 64 个 3x3 核,最大池化;然后展平接 128 维全连接,Dropout 0.5,最后 softmax 输出 2 类。训练数据用 CEW 或自采数据集,正样本睁眼,负样本闭眼,各 2000 张起步。
from tensorflow.keras import layers, models def build_cnn(): model = models.Sequential([ layers.Conv2D(32, (3, 3), activation='relu', input_shape=(24, 24, 1)), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activation='relu'), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dense(128, activation='relu'), layers.Dropout(0.5), layers.Dense(2, activation='softmax') ]) model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy']) return modelDropout 0.5 是为了防止小数据集过拟合,如果数据量超过 1 万张可以降到 0.3。优化器用 Adam 默认学习率 0.001,训练 20 个 epoch 左右收敛。注意输入必须归一化到 0-1,否则梯度爆炸。训练时用 ImageDataGenerator 做随机旋转和亮度扰动,提升泛化。
3.4 时序判定与预警触发
把 CNN 的单帧输出接入状态机。维护一个长度为 30 的滑动窗口,记录每帧的睁闭眼标签,计算 PERCLOS 和连续闭眼帧数。
from collections import deque import pygame pygame.mixer.init() alert_sound = pygame.mixer.Sound('alert.wav') window = deque(maxlen=30) closed_count = 0 alert_level = 0 def update_state(eye_label): global closed_count, alert_level window.append(eye_label) if eye_label == 0: # 0 表示闭眼 closed_count += 1 else: closed_count = 0 perclos = window.count(0) / len(window) if closed_count >= 15 or perclos > 0.3: alert_level = 2 alert_sound.play() elif closed_count >= 8: alert_level = 1 else: alert_level = 0 return alert_level窗口 30 帧约等于 1 秒(30fps),PERCLOS 阈值 0.3 对应 9 帧闭眼。连续闭眼 15 帧约 0.5 秒触发二级预警。alert.wav 用短促蜂鸣声,频率 800Hz 左右,太刺耳反而让司机分心。预警播放用 pygame 而不是 playsound,因为 pygame 支持异步不阻塞主循环。
4. 避坑与排查:那些让我熬夜的翻车现场
4.1 摄像头帧率不稳导致 PERCLOS 失真
现象:白天跑得好好的,晚上一测 PERCLOS 忽高忽低,预警乱响。原因:USB 摄像头在低光照下自动降帧,从 30fps 掉到 15fps,滑动窗口的时间基准变了,30 帧实际代表 2 秒而不是 1 秒。解决:在代码里固定采集帧率,用 cv2.CAP_PROP_FPS 设 30,如果硬件不支持就丢帧补帧,保证窗口时间恒定。更稳妥的做法是用时间戳计算 PERCLOS,而不是帧数。
4.2 戴眼镜反光让 CNN 把睁眼判成闭眼
现象:驾驶员戴金属框眼镜,侧光一照,镜片反光形成白色亮斑,CNN 分类置信度直接倒向闭眼。原因:训练集里戴眼镜样本太少,模型没学过反光模式。解决:采集数据时专门录 500 张戴眼镜反光的负样本,标注为睁眼,重新训练。推理时先对眼部 ROI 做 CLAHE 限制对比度直方图均衡,削弱亮斑影响。如果还不行,加一个红外补光模块,但成本上去了。
4.3 多线程读帧和推理抢 GIL 导致卡顿
现象:主循环里同时做摄像头读取和 CNN 推理,画面卡成幻灯片,预警延迟超过 1 秒。原因:Python GIL 让 CPU 密集型推理阻塞了 IO 读取。解决:把摄像头读取放独立线程,用 queue 传帧给推理线程。或者更简单,每 3 帧做一次推理,中间帧复用上次结果,30fps 下推理降到 10fps 也够用。TensorFlow 的 session 不是线程安全的,多线程推理要加锁。
4.4 预警音播放阻塞主循环
现象:一触发预警,画面就冻住,声音播完才恢复。原因:pygame.mixer.Sound.play() 默认是同步的,或者用了 time.sleep 等音效结束。解决:用 pygame.mixer.Channel 异步播放,或者把播放逻辑丢到单独线程。另外音频文件采样率要和 mixer.init() 一致,否则播放失败还没报错,成了黑匣子。
4.5 模型在自采数据上过拟合
现象:训练集准确率 99%,一换人测试就掉到 70%。原因:自采数据里同一个人同一光照占太多,模型记住了人脸身份而不是睁闭眼特征。解决:数据增强加随机裁剪、旋转、亮度变化,训练集里至少 10 个不同人,每人不同光照和角度。验证集必须留出没见过的人,否则评估结果没有意义。
5. 进阶技巧:用迁移学习和模型量化把系统压进车载设备
如果这套系统要真上车,树莓派或 Jetson Nano 的算力有限,直接跑 TensorFlow 原模型推理一帧要 200ms,跟不上。我的做法是两步走:先迁移学习,再量化。
迁移学习用 MobileNetV2 做 backbone,它只有 300 万参数,比自建 CNN 还轻,而且在 ImageNet 上学到的底层特征对眼部纹理同样有效。把 MobileNetV2 的顶层换成全局平均池化加 2 类全连接,冻结前 100 层,只训练后 30 层,学习率降到 0.0001。这样 5000 张图训练 10 个 epoch 就能到 96% 准确率,比从头训快 3 倍。
from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras import layers, models base = MobileNetV2(input_shape=(96, 96, 3), include_top=False, weights='imagenet') base.trainable = False for layer in base.layers[-30:]: layer.trainable = True model = models.Sequential([ base, layers.GlobalAveragePooling2D(), layers.Dense(64, activation='relu'), layers.Dropout(0.3), layers.Dense(2, activation='softmax') ]) model.compile(optimizer=tf.keras.optimizers.Adam(1e-4), loss='sparse_categorical_crossentropy', metrics=['accuracy'])输入从 24x24 提到 96x96 是因为 MobileNetV2 有最小尺寸要求,而且更大输入对戴眼镜场景更鲁棒。训练完用 TFLite 量化:
converter = tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations = [tf.lite.Optimize.DEFAULT] tflite_model = converter.convert() open('fatigue_mobilenet.tflite', 'wb').write(tflite_model)量化后模型从 14MB 压到 3.5MB,推理速度在树莓派 4B 上从 200ms 降到 45ms,30fps 完全够用。注意量化会掉 1-2 个点准确率,如果接受不了就用 float16 量化而不是 int8。
验证方法很简单:找 5 个没参与训练的人,每人录 2 分钟模拟疲劳视频,统计预警触发时间和误报次数。我一般要求漏报率低于 5%,误报率低于 10%。如果达不到,优先调 PERCLOS 阈值和滑动窗口长度,而不是重新训练模型。
最后说个血泪教训:别在周五下午调阈值,调完就下班,周一回来忘了改了什么,结果演示时预警不响。每次改参数都记在 config.yaml 里,用 git 管起来。这套系统不难,难的是把每个环节的边界条件都想到。希望帮到你。
本文还有配套的精品资源,点击获取