☰
基于Python卷积神经网络的人脸识别驾驶员疲劳检测与预警系统
2026/10/10 0:29:06 网站建设 项目流程

简介:这份资源面向计算机、人工智能方向的毕业设计学生与初学者,提供一套基于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 + Keras(TensorFlow 后端)的组合,原因有三:一是 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-python==4.8.1.78 pip install tensorflow==2.13.0 pip install numpy==1.24.3 pip install scikit-learn==1.3.0 pip install imutils==0.5.4 pip install pygame==2.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, scaleFactor=1.1, minNeighbors=5, minSize=(80, 80) ) for (x, y, w, h) in faces: cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.imshow('Face Detection', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

逻辑说明:detectMultiScale的scaleFactor=1.1表示每次图像缩小 10% 再检测,值越小越慢但越准;minNeighbors=5控制误检,值越大越严格;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:y+h, x:x+w] roi_color = frame[y:y+h, x:x+w] eyes = eye_cascade.detectMultiScale(roi_gray, 1.1, 10) for (ex, ey, ew, eh) in eyes: cv2.rectangle(roi_color, (ex, ey), (ex+ew, ey+eh), (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, my+h//2), (mx+mw, my+mh+h//2), (0, 0, 255), 2)

参数说明:眼睛检测的minNeighbors=10比人脸更严格,因为眼睛区域小、误检多;嘴巴检测的scaleFactor=1.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_classes=2): """ 输入:24x24 灰度图(眼睛或嘴巴 ROI) 输出:2 分类(疲劳/正常) """ model = models.Sequential([ # 第 1 个卷积块:提取边缘和纹理 layers.Conv2D(32, (3, 3), activation='relu', input_shape=input_shape), layers.MaxPooling2D((2, 2)), # 第 2 个卷积块:提取更复杂的局部模式 layers.Conv2D(64, (3, 3), activation='relu'), layers.MaxPooling2D((2, 2)), # 第 3 个卷积块:组合特征 layers.Conv2D(128, (3, 3), activation='relu'), layers.MaxPooling2D((2, 2)), # 展平后接全连接 layers.Flatten(), layers.Dense(128, activation='relu'), layers.Dropout(0.5), # 防止过拟合,毕设数据集通常不大 layers.Dense(num_classes, activation='softmax') ]) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), loss='categorical_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( rescale=1./255, rotation_range=10, width_shift_range=0.1, height_shift_range=0.1, zoom_range=0.1, validation_split=0.2 ) train_gen = datagen.flow_from_directory( 'dataset/eyes', # 目录下分 closed/ 和 open/ 两个子文件夹 target_size=(24, 24), color_mode='grayscale', batch_size=32, class_mode='categorical', subset='training' ) val_gen = datagen.flow_from_directory( 'dataset/eyes', target_size=(24, 24), color_mode='grayscale', batch_size=32, class_mode='categorical', subset='validation' ) history = model.fit( train_gen, epochs=30, validation_data=val_gen, callbacks=[ tf.keras.callbacks.EarlyStopping(patience=5, restore_best_weights=True), tf.keras.callbacks.ModelCheckpoint('fatigue_eye_model.h5', save_best_only=True) ] )

参数说明:EarlyStopping(patience=5)表示验证集损失连续 5 轮不下降就停,避免浪费时间;ModelCheckpoint只保存验证集最好的模型,这样最终拿到的.h5文件是泛化能力最强的那个。batch_size=32是 CPU 训练的平衡点,太大内存吃紧,太小梯度不稳。

3.3 疲劳判定逻辑:从单帧分类到时间窗口

CNN 输出的是单帧的「睁眼/闭眼」概率,但疲劳是一个时间累积概念。我一般用 PERCLOS(单位时间内闭眼帧占比)作为核心指标:

from collections import deque class FatigueDetector: def __init__(self, window_size=30, perclos_threshold=0.4): self.window = deque(maxlen=window_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_size=30, perclos_threshold=0.4)

逻辑说明:window_size=30表示看最近 30 帧(约 1 秒,按 30fps 算),perclos_threshold=0.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_size=30, perclos_threshold=0.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:y+h, x:x+w] 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:ey+eh, ex:ex+ew] 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, verbose=0) 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加了verbose=0避免刷屏。这里只用了眼睛模型,实际完整系统还会加嘴巴模型判断打哈欠,逻辑一样,只是多一个mouth_closed变量。

5. 避坑与排查:那些让模型「看起来能用实际翻车」的细节

5.1 坑一:白天准晚上废——光照变化导致 Haar 漏检

现象:白天测试人脸检测正常,晚上或隧道里频繁丢框,系统直接不工作。

原因:Haar 级联基于灰度对比度,暗光下人脸和背景对比度下降,detectMultiScale找不到足够强的边缘特征。

解决:加一步直方图均衡化(CLAHE),提升局部对比度。在cvtColor后加:

clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) gray = clahe.apply(gray)

clipLimit=2.0控制对比度增强上限,太大反而放大噪点。如果还不行,就换 DNN 人脸检测器,代价是推理慢 3~5 倍。

5.2 坑二:模型把「眯眼笑」判成疲劳——数据集偏差

现象:测试时驾驶员一笑,系统就报警。

原因:训练集里「闭眼」样本大多是疲劳闭眼,没有「眯眼笑」这种正样本,模型学到了「眼睛变窄=疲劳」的捷径。

解决:在数据集里补充「眯眼但正常」的负样本,或者用眼睛纵横比(EAR)做辅助判断——EAR 低于阈值且持续超过 1 秒才算疲劳,单纯眯眼一笑而过。EAR 计算需要面部关键点,可以用 dlib 或 mediapipe,但会增加依赖。

5.3 坑三:PERCLOS 阈值照搬论文,实际误报不断

现象:按论文设perclos_threshold=0.4,结果正常眨眼也报警。

原因:论文的帧率和你的摄像头帧率不一样。30fps 下 30 帧是 1 秒,但如果你摄像头只有 15fps,30 帧就是 2 秒,窗口太长导致反应迟钝;反之窗口太短则正常眨眼被算进去。

解决:先打印实际帧率,再按「窗口时长 = 1 秒」反推window_size。正常眨眼一次约 0.2~0.4 秒,所以窗口至少 1 秒才能区分「眨眼」和「闭眼」。我一般设window_size = fps,perclos_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', compile=False)跳过编译,再手动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_top=False, weights='imagenet' ) base.trainable = False # 冻结预训练层 model = models.Sequential([ base, layers.GlobalAveragePooling2D(), layers.Dense(64, activation='relu'), layers.Dropout(0.3), layers.Dense(2, activation='softmax') ]) model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])

注意输入尺寸从 24x24 变成 96x96,且要转成 RGB 三通道。冻结base后只训练新增层,trainable=False是关键,否则预训练权重会被小数据集带偏。

第二个是模型量化:把 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 曲线画出来。曲线比单帧准确率更能反映系统真实表现——如果曲线在正常驾驶时频繁冲高,说明阈值或窗口有问题;如果曲线一直贴地,说明模型太保守。这个习惯帮我省了无数次「改完感觉好了但说不清哪里好」的纠结。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询