简介:基于Python卷积神经网络的人脸识别驾驶员疲劳检测与预警系统,配套完整源码与文档,可用于毕业设计、课程设计或项目开发。系统基于Pycharm和Python3.6环境,围绕人脸朝向、位置、瞳孔朝向、眼睛开合度、眨眼频率、瞳孔收缩率等多维数据,对打哈欠、眨眼、点头三类疲劳特征进行实时识别,并计算注意力集中程度,在疲劳驾驶时及时发出安全提示。压缩包共20个文件、大小78.33MB,其中11个Python脚本构成核心检测与界面逻辑,另含XML级联分类器、HDF5模型权重、可运行EXE窗口程序,以及系统说明、运行说明和README文档,便于直接运行或按需扩展。源码已经过严格测试,整体方案完整,既可作为毕业设计/课程设计的现成参考,也能帮助开发者熟悉CNN在驾驶员状态监测场景中的数据预处理、模型调用与结果可视化流程。目前已有723人学习下载。
1. 疲劳检测系统拆解:一份能跑的 CNN 驾驶员预警工程
深夜跑过高速的人都知道,眼皮往下掉的那两三秒,方向盘修正幅度能吓出冷汗。这份基于 Python 卷积神经网络的人脸识别驾驶员疲劳检测与预警系统,正好踩在毕业设计和课程设计的落点上:用摄像头实时抓取面部,通过打哈欠、眨眼、点头三种信号计算疲劳程度,超标就弹窗报警。压缩包里源码、模型权重、UI 界面、运行说明齐全,Pycharm + Python3.6 就能跑通,项目源码已经过严格测试,可以直接复现并在此基础上延申。对做毕设或课设的同学来说,这是一个能开二发的工程,不是只有几个 demo 文件的空壳。拆它之前,先把原理和数据流理清楚,后面改阈值、加功能才不会迷路。
2. 系统架构与检测原理:三个疲劳信号怎么变成预警
2.1 从摄像头画面到疲劳分数:数据流先理清楚
整个系统说穿了是一条流水线:摄像头采集画面,人脸检测器找人脸,眼睛检测器在脸框里找眼睛,裁剪出来的小图喂给卷积神经网络,输出眼睛开闭状态,再按帧统计眨眼频率和闭眼比例,最后加上打哈欠和点头的辅助信号,综合出疲劳分数。
这条链路里,haarcascade_frontalface_default.xml负责第一步,haarcascade_eye.xml负责第二步,models/_mini_XCEPTION.102-0.66.hdf5是第三步的核心。所以整个包的调用关系并不复杂,难点在于每一级的结果都会影响下一级的输入质量:人脸框偏了,眼睛就找不到;眼睛裁切尺寸不对,模型就判不准。理解这一点,后面排查"为什么一直误报"就会顺手很多。
核心检测循环的典型实现如下:
# detect_class.py 核心流程节选 import cv2 from keras.models import load_model from load_and_process import preprocess_eye def frame_pipeline(frame, face_cascade, eye_cascade, model): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, minSize=(80, 80)) for (x, y, w, h) in faces: roi_gray = gray[y:y+h, x:x+w] eyes = eye_cascade.detectMultiScale( roi_gray, scaleFactor=1.05, minNeighbors=3, minSize=(20, 20)) if len(eyes) >= 2: # 取面积最大的两只眼睛做判别,避免把眉毛当眼睛 eyes = sorted(eyes, key=lambda e: e[2]*e[3], reverse=True)[:2] state = [] for (ex, ey, ew, eh) in eyes: eye_roi = roi_gray[ey:ey+eh, ex:ex+ew] img = preprocess_eye(eye_roi, target_size=(48, 48)) pred = model.predict(img, batch_size=1) state.append(1 if pred[0][0] > 0.5 else 0) # 1=睁眼 break return state # [左眼状态, 右眼状态]这里的scaleFactor=1.1表示每次按 10% 比例缩小搜索窗口,值越小检测越精细但速度越慢;minNeighbors=5是周围至少要有 5 个候选框才确认为人脸,太小容易把背景误判成人脸,太大则容易漏检斜脸。preprocess_eye负责把裁切区域统一缩放成 48×48 并归一化到 0~1,这是模型输入层的硬性要求。
2.2 眨眼与打哈欠:PERCLOS 判据和嘴部开合度的实现
眨眼检测有两种常见路线。一种是用 dlib 的 68 点关键点算眼睛纵横比,走几何路线;另一种是这份资源采用的 CNN 分类路线——把眼睛小图直接丢给mini_XCEPTION模型,输出"闭合/睁开"的概率。几何路线的好处是不需要训练,坏处是依赖关键点检测精度,侧脸和低头时点会飘。CNN 路线的好处是对光照和小尺寸图像更鲁棒,坏处是你不知道模型的判定口径到底是什么,只能通过实验去摸。
疲劳判定的行业标准叫 PERCLOS,即单位时间内眼睛闭合时间所占比例,通常把 0.4 作为警戒线,也就是闭眼时间占检测窗口的 40% 以上就触发疲劳报警。这份资源没有直接暴露 PERCLOS 公式,但统计逻辑是一样的:维护一个滑动窗口,记录最近 N 帧里闭眼帧数占比,超过阈值则置BLINK状态。
打哈欠检测则是另一个维度。资源包没有单独的嘴部 haar cascade,所以常见做法是:在已检测到的人脸框内,把眼睛下方到下巴的区域做图像差分,嘴部张开时纵向结构变化会非常显著。说白了就是裁下半脸,算边缘像素密度,嘴张得越大,边缘点越多。
# 打哈欠检测示意:基于下半脸区域的边缘密度变化 def yawn_score(face_roi): # face_roi 来自 extract_face.py 裁剪出的人脸区域 gray = cv2.cvtColor(face_roi, cv2.COLOR_BGR2GRAY) h, w = gray.shape mouth_region = gray[int(h*0.5):, :] # 取下半部分 edges = cv2.Canny(mouth_region, 50, 150) return int((edges > 0).sum()) # 在检测主循环中,连续 n 帧 score 超过阈值则触发 YAWNCanny的第二个参数 50 是低阈值,150 是高阈值,低于 50 的梯度直接丢弃,高于 150 的必然算边缘。这个值受光照影响很大:画面太亮,边缘密度的基数就高;画面太暗,牙和嘴唇的分界就出不来。所以真正调参时不要只调 Canny 参数,优先检查人脸裁切区域是否包含了完整的嘴部。
2.3 点头检测:人脸位置时序曲线怎么用
点头和眨眼、打哈欠不一样,它不是单帧特征,而是时间序列上的运动模式。人点头时,人脸中心点的垂直坐标会在几百毫秒内先下降再回升(或者反过来,取决于摄像头朝上还是朝下)。实现上最容易的方式是维护一个固定长度的队列,存最近 N 帧的人脸中心 y 坐标,然后比较前半段和后半段的均值差。
import collections class NodDetector: def __init__(self, win_size=20, threshold=12): self.history = collections.deque(maxlen=win_size) self.threshold = threshold self.nod_count = 0 def update(self, face_center_y): self.history.append(face_center_y) if len(self.history) < self.history.maxlen: return self.nod_count half = self.history.maxlen // 2 first_half = sum(self.history[:half]) / half second_half = sum(self.history[half:]) / half if abs(first_half - second_half) > self.threshold: self.nod_count += 1 return self.nod_countwin_size=20意味着在 20 帧内观察头部运动,如果摄像头是 30fps,这个窗口大约 0.66 秒,正好覆盖一次点头的时长。窗口太小会把头部随意晃动算成点头,太大则两个动作叠在一个窗口里反而抵消。threshold=12的单位是像素,这个值直接受画面分辨率影响:720p 下 12~20 像素合适,1080p 下要放大到 20~30 像素,否则轻微点头也会频繁触发。
三个信号在实际代码里是加权合并的,常见权重是眨眼 0.5、哈欠 0.3、点头 0.2,总分超过设定阈值才弹窗。这样单信号误报被拉低,整体鲁棒性比只看闭眼比例好很多。这也是这份资源真正值得借鉴的地方:疲劳不是单指标能定义的,多信号交叉验证才是工程做法。
3. 环境部署与代码走读:从 requirements.txt 到 GUI 预警
3.1 环境搭建:Python3.6、Pycharm 与 requirements.txt
第一次复现这个项目,最容易卡住的地方不是代码,而是环境。资源包的运行说明里已经写明开发环境是 Pycharm + Python3.6,这个版本信息不要忽视——旧版 TensorFlow/Keras 在 Python 3.9 以上的环境里经常直接报TypeError或者DataCardinality之类看不懂的错误。我一般会先用 Anaconda 建一个干净环境,避免系统 Python 里的包互相污染。
conda create -n fatigue python=3.6 conda activate fatigue pip install -r requirements.txtrequirements.txt 里主要包含 opencv-python、tensorflow、keras、numpy、Pillow 这类依赖。装完后建议先检查版本是否和模型文件兼容:
python -c "import keras; print(keras.__version__); import h5py; print(h5py.__version__)"如果 h5py 版本高于 3.x 而 keras 还是 2.2.x,加载.hdf5模型时大概率会报HDF5 version mismatch。解决办法不是硬装最新版,而是退回pip install h5py==2.10.0。这种兼容性坑在打包给毕设导师演示时非常尴尬,提前锁定版本能省掉大半现场救火时间。
运行入口有两个:带界面的tkinter_UI.py和不带界面的check.py。新手建议先跑check.py,它只做摄像头验证,代码路径短,出了问题好定位;确认摄像头画面和检测框正常后,再跑tkinter_UI.py看完整 UI。
python check.py python tkinter_UI.py3.2 模型加载与特征提取:haarcascade 给坐标,CNN 给状态
_mini_XCEPTION.102-0.66.hdf5这个文件名的含义值得拆一下:mini_XCEPTION是网络结构,102是训练轮次,0.66大概率是验证集准确率。也就是说这个模型的准确率基线在 66% 左右,别指望它像商用产品一样精准,它的价值在于在小尺寸眼部图像上跑得快,适合实时检测场景。
模型加载的代码在load_and_process.py或detect_class.py里,核心逻辑如下:
from keras.models import load_model MODEL_PATH = "models/_mini_XCEPTION.102-0.66.hdf5" model = load_model(MODEL_PATH) model._make_predict_function() # TensorFlow 1.x 需要提前初始化 def predict_eye_state(eye_roi): img = cv2.resize(eye_roi, (48, 48)) # 模型输入尺寸是 48x48 img = img / 255.0 # 归一化到 [0, 1] img = img.reshape(1, 48, 48, 1) # 单通道灰度图 pred = model.predict(img, batch_size=1) return pred[0][0] # 输出值越接近 1 表示越像睁眼这里的reshape(1, 48, 48, 1)最后一位是通道数,因为模型输入是灰度图单通道。如果你从摄像头取到的是 BGR 三通道图,直接 reshape 一定报错,必须先用cv2.cvtColor(gray)转灰度。这是新手最容易翻车的位置,报错信息往往很长很吓人,其实核心就是通道数对不上。
haarcascade 在这里的角色是"定位器",负责框出人脸和眼睛的位置;CNN 是"分类器",负责判断框出来的小图里眼睛到底是睁还是闭。两者分工明确,速度上 haarcascade 很快,CNN 单次推理在 CPU 上大约几十毫秒,整体帧率取决于你每帧都跑还是隔几帧跑一次。
3.3 主循环与报警:tkinter_UI 的消息队列与 baojin 联动
tkinter_UI.py是面向答辩演示的主程序,它做的事情很朴素:开摄像头、每一帧调用检测类、把结果显示在 Label 上、疲劳时亮红灯并触发声音报警。tkinter 本身是 Python 自带 GUI 库,不需要额外安装,但有个特点是主线程不能阻塞——如果你的检测逻辑每帧耗时太长,界面就会卡成幻灯片。
所以这份资源的 UI 代码用了after()定时器机制,而不是 while 循环:
import cv2 import tkinter as tk from PIL import Image, ImageTk from detect_class import FatigueDetector class FatigueUI: def __init__(self): self.app = tk.Tk() self.app.title("驾驶员疲劳检测预警系统") self.video_label = tk.Label(self.app) self.video_label.pack() self.status_label = tk.Label(self.app, text="正常驾驶", font=("SimHei", 14)) self.status_label.pack() self.cap = cv2.VideoCapture(0) self.detector = FatigueDetector() self.update_frame() def update_frame(self): ret, frame = self.cap.read() if ret: state = self.detector.run(frame) # 返回'正常'/'眨眼'/'打哈欠'/'疲劳' self.status_label.config(text=state) rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img = Image.fromarray(rgb) imgtk = ImageTk.PhotoImage(img.resize((640, 480))) self.video_label.configure(image=imgtk) self.video_label.image = imgtk self.app.after(10, self.update_frame) # 10ms 后再次刷新 def run(self): self.app.mainloop() if __name__ == "__main__": ui = FatigueUI() ui.run()after(10)的含义是 10 毫秒后重新执行一次update_frame,这样每一帧都通过定时器驱动,不占主线程。你在百度的很多代码里会看到cv2.waitKey(1)加while True的写法,但 tkinter 里这样写会直接卡死窗口。另外注意img.resize((640, 480))只是为了显示效果,不代表检测也用这个分辨率——检测用的是原始帧,显示用的缩放帧,这两个别搞混。
baojin.py在检测到疲劳状态时触发声音报警,常见做法是winsound.Beep或playsound播放一段提示音。答辩演示时建议把报警阈值调得敏感一点,让评审老师能在几十秒内看到完整流程,而不是等三分钟才触发一次,这段经验放到第四章细说。
4. 参数调优与避坑指南:阈值、误报与工程排错
4.1 阈值参数怎么调:三个信号各自的判定口径
拿到这份资源后,第一件事不是看代码逻辑,而是找参数。疲劳检测这类系统,代码结构不复杂,真正决定效果的是阈值。下面的参数表是我在实机测试中整理出来的,位置和值都以这份资源的默认代码为参照:
| 参数 | 所在位置 | 默认参考值 | 调整方向 |
|---|---|---|---|
| 眼睛分类置信度 | detect_class.py 中 pred>0.5 | 0.5 | 调到 0.4 会更敏感,调到 0.6 更保守 |
| 眨眼连续闭眼帧数 | 眨眼计数逻辑 | 5~8 帧 | 值越小越容易触发疲劳 |
| PERCLOS 时间窗口 | 滑动窗口统计 | 30 秒 | 窗口越长,判定越稳定,响应越慢 |
| 哈欠触发阈值 | 下半脸边缘密度 | 连续 3 次 | 根据摄像头角度微调 |
| 点头幅度阈值 | NodDetector.threshold | 12~20 像素 | 720p 用 12,1080p 用 20 起跳 |
调参有个原则:一次只动一个变量。很多同学上来把置信度、窗口、阈值全改了,出问题根本不知道是哪个改坏的。我一般先调眼睛置信度,因为它直接影响眨眼检测;确认眨眼稳定后,再动 PERCLOS 窗口;最后才碰哈欠和点头的联动参数。
另外要注意模型输出值的分布。_mini_XCEPTION.102-0.66.hdf5的验证准确率只有 66%,意味着它的输出概率分布不会非常极端——睁眼的输出未必接近 0.9,闭眼的输出也未必接近 0.1。如果实测发现所有输出都集中在 0.4~0.6 之间,说明模型对这个场景的区分度不够,这时候硬调阈值没有意义,应该回到数据层面,参考第五章的扩充方法。
4.2 翻车现场:光照、侧脸与误报的真实案例
实机测试最大的敌人不是代码 bug,而是场景不可控。以下三个案例都是我在测试中真实踩过的,逐一说明现象和解决思路。
案例一:戴墨镜直接误报疲劳。现象是系统在用户睁眼状态下疯狂报警,完全不受控制。原因是眼睛区域被墨镜完全遮挡,裁切出来的 ROI 是一片黑色,CNN 分类器把"极暗区域"学成了"眼睛闭合"。解决思路是加一道亮度检测:当眼睛区域的平均灰度低于某个下限时,不进入分类流程,直接标记为"检测无效",并提示摘下遮挡物。这道逻辑很便宜,但能挡掉一半以上的误报。
案例二:侧脸状态下检测框完全丢失。haarcascade 是正脸检测器,稍微侧头超过一定角度就框不到脸了。现象是画面里人明明在,但 UI 上一个框都没有。解决思路是放宽minNeighbors,从 5 降到 3,能提升斜脸的召回率,但代价是背景里的类人脸物体会被误框。更稳妥的方案是间隔几帧存一次人脸框位置,检测丢失时沿用上一次坐标,用上一帧的位置做眼睛区域裁切。
案例三:打哈欠和点头互相触发。伸懒腰打哈欠时头会不自觉后仰,而点头检测看的是人脸中心 y 坐标的变化,两者在时序上重叠会导致双重报警。解决思路是先判断信号类型:哈欠的核心是嘴部区域变化,点头的核心是人脸整体中心变化,如果两个信号几乎同时触发,只计哈欠不计点头。用工程话说就是加一个互斥锁,避免一次动作劈成两个疲劳证据。
4.3 常见问题排查:黑屏、卡顿与模型加载失败
把排查记录写成踩坑笔记,按"现象→原因→解决"的顺序看,比翻代码更省时间。
坑一:启动后摄像头画面全黑,UI 正常运行。原因是摄像头被其他程序占用,cv2.VideoCapture(0)返回了空帧却没报错。解决:先单独跑一条命令测试摄像头是否可用,确认没被占用再开 UI:
python -c "import cv2; cap=cv2.VideoCapture(0); print(cap.isOpened()); cap.release()"坑二:帧率只有 5~8 FPS,画面严重卡顿。原因是每一帧都跑两个人脸检测器加一次 CNN 推理,CPU 版本 TensorFlow 扛不住。解决:跳帧推理,每 3 帧只取 1 帧做完整检测,中间帧沿用上一帧的人脸坐标,检测帧率立刻能回到 15 FPS 以上。对于疲劳检测这种低频事件,20 FPS 和 10 FPS 的区别几乎没有感知。
坑三:加载.hdf5时报Unexpected end of file或HDF5 version mismatch。原因是 h5py 和 TensorFlow 的版本不兼容。解决:按 requirements.txt 里的版本重装,尤其是 h5py,不要用 pip 自动拉的最新版。这属于环境问题不是代码问题,报错信息再长也别慌。
坑四:模型输出几乎全是同一个类别(全 0 或全 1)。原因是模型的训练数据分布和你的摄像头场景差异太大,比如训练集全是黑白证件照,实拍是暖色夜灯。解决:先不要改模型,把摄像头画面转成灰度再试试;如果还是全 0,可以在数据预处理里加归一化后的对比度拉伸。仍然无效再考虑用第五章的方法做微调。
坑五:GUI 点关闭按钮后进程不退,摄像头灯还亮着。原因是 mainloop 退出后没有释放摄像头资源。解决:在关闭事件里显式调用cap.release()和app.destroy()。这个问题在答辩演示时最容易暴露,投影亮着你没注意系统还在录,属于印象分硬伤。
5. 进阶:离线验证与数据扩充,把检测精度往前推一步
先离线验证,再上摄像头。这是我从这份资源里学到的最值钱的一条经验。很多人拿到项目第一反应就是跑起摄像头看效果,但摄像头测试只能暴露"能不能跑",回答不了"判得准不准"的问题。资源包里的split_train_test.py和evaluate.py就是干这个用的:用已有数据集划分训练集和测试集,跑一遍评估脚本,得到准确率和混淆矩阵,量化出模型的真实水平。
python split_train_test.py --data ./dataset --ratio 0.8 python evaluate.py --model models/_mini_XCEPTION.102-0.66.hdf5--ratio 0.8表示 80% 做训练集、20% 做测试集。跑完之后重点看混淆矩阵里的"闭眼类"召回率,而不是整体准确率——疲劳检测里漏报比误报危险得多:误报顶多烦人,漏报可能出事故。如果闭眼类的召回率低于 60%,这台模型在实车上就不够用。
提升精度最有效的手段是数据扩充。我一般会在原数据集上做三种增强:水平翻转、亮度扰动、局部遮挡。水平翻转在眨眼问题上其实是无效的——左右眼翻转不影响开闭状态,但能扩充其他表情样本的多样性;亮度扰动是为了模拟白天逆光和夜间路灯;局部遮挡是为了模拟墨镜和刘海。做完扩充后重新训练,再跑evaluate.py对比两组召回率。资源包的data_provider.py和convert.py已经预留了数据转换和样本加载的接口,直接在它基础上加预处理逻辑就行。
从那以后我每次拿到检测类项目,都强制自己先跑一遍离线验证脚本,确认基准确率和混淆矩阵,再上摄像头实拍。别嫌枯燥,这一步能帮你分清"模型不行"和"阈值没调对"到底是哪个问题,省下大量瞎试参数的时间。希望帮到你。
本文还有配套的精品资源,点击获取