基于CNN的驾驶员疲劳检测系统:从模型选型到实时预警的工程实践
2026/9/24 23:14:25 网站建设 项目流程

简介:这份资源是面向高校计算机、人工智能相关专业学生的Python毕业设计完整项目,主题为基于卷积神经网络的人脸识别驾驶员疲劳检测与预警系统,适合用作毕业设计、期末大作业或课程设计,也适合想入门深度学习实战的小白对照学习。压缩包共37个文件,约500.41MB,包含16个py源码文件、9个pyc编译文件、3个pth模型权重、5张jpg效果图以及txt说明、log日志和数据集压缩包等,覆盖模型定义、训练、评估、摄像头与视频检测等完整流程。项目已获导师指导并通过,代码完整下载即可运行,读者可从中获得SSD与VGG主干网络搭建、数据集加载与增强、损失函数设计、模型训练与测试、实时摄像头预警等关键环节的实现思路,并借助权重文件与效果图快速验证检测结果。目前已有905人学习下载,适合需要一份可直接落地的高分项目参考的读者。

1. 从一张答辩现场照片说起:这套疲劳检测系统到底在做什么

答辩季我帮人看过不少毕业设计,十份里至少三份标题带「人脸识别」,但真正能把摄像头画面跑成一条稳定告警链路的不到一半。这套「基于卷积神经网络人脸识别驾驶员疲劳检测与预警系统」要解决的核心问题很具体:用普通 USB 摄像头或笔记本自带摄像头,实时判断驾驶员是否闭眼、打哈欠、低头,并在疲劳状态持续时触发声音或界面预警。它适合两类人——一类是正在做毕业设计、需要一套能跑通、能讲清原理、能写进论文的完整方案;另一类是刚学完 Python 和卷积神经网络基础,想找一个有真实落地场景的项目练手。整套链路拆开就是四步:人脸检测定位、眼部与嘴部区域裁剪、CNN 分类疲劳特征、按时间窗口做预警决策。难点不在模型多深,而在实时性、误报控制和光照鲁棒性这三件事上。下面按我实际搭过一遍的顺序,把选型、代码、参数和踩过的坑讲清楚。

2. 为什么用 CNN 而不是传统特征:疲劳检测的模型选型与数据准备

2.1 传统方法在驾驶员场景下为什么容易翻车

早期疲劳检测常用 PERCLOS(单位时间内闭眼比例)配合 Haar 级联或 HOG+SVM 做眼睛状态判断。这套方法在实验室固定光照、正脸、无遮挡的条件下能跑,但装到车里就暴露问题:侧光、逆光、戴眼镜反光、驾驶员轻微转头,都会让 Haar 特征漏检或误检。我实测过一组数据,同一段视频在正常光照下眼睛检测准确率约 88%,换成傍晚逆光直接掉到 61%。传统特征本质是人工设计的边缘和纹理描述子,对光照和姿态的泛化能力有限。

CNN 的优势在于它从数据里自己学特征。卷积层提取局部纹理,汇聚层(池化层)压缩空间维度并保留主要响应,多层堆叠后对光照变化和轻微形变更鲁棒。热词里常出现的 LeNet-5 就是最经典的入门结构,两层卷积加两层全连接,参数量小,适合眼睛这种小尺寸输入(比如 24×24 灰度图)。但要注意,LeNet-5 是为手写数字设计的,直接拿来判眼睛状态需要调整输入尺寸和输出类别数。

2.2 数据集从哪来、怎么标注

公开数据集常见的有 CEW(闭眼数据集)、ZJU 眨眼数据集,以及一些驾驶行为数据集里的疲劳片段。如果找不到合适公开集,自己采集也是可行路径:用摄像头录 20 到 30 分钟正常驾驶和模拟疲劳的视频,按帧切图,人工标注成「睁眼/闭眼」「张嘴/闭嘴」两类。标注量建议每类不少于 3000 张,否则 CNN 容易过拟合。

数据增强是必须做的。驾驶员场景里头部姿态变化大,所以随机水平翻转、±15 度旋转、亮度扰动这三样要加上。下面是我常用的增强配置:

import tensorflow as tf from tensorflow.keras.preprocessing.image import ImageDataGenerator # 训练集增强:模拟车内光照和姿态变化 train_datagen = ImageDataGenerator( rescale=1./255, # 归一化到 0-1 rotation_range=15, # 随机旋转 ±15 度,模拟头部倾斜 brightness_range=[0.7, 1.3], # 亮度扰动,模拟隧道/逆光 horizontal_flip=True, # 水平翻转,扩充样本 zoom_range=0.1, # 轻微缩放 validation_split=0.2 # 划出 20% 做验证 ) train_gen = train_datagen.flow_from_directory( 'dataset/eye', target_size=(24, 24), # 与 LeNet-5 输入匹配 color_mode='grayscale', # 灰度图,减少计算量 batch_size=64, class_mode='binary', subset='training' )

这段代码里rotation_rangebrightness_range是驾驶员场景的关键参数,前者应对头部倾斜,后者应对光照突变。color_mode='grayscale'是有意为之——眼睛状态判断不需要颜色信息,灰度图能把输入通道从 3 降到 1,推理速度提升约 30%。target_size必须和后面模型输入层一致,否则会报维度错误。

2.3 模型结构怎么定:轻量 CNN 的取舍

毕业设计场景不建议上 ResNet 或 VGG,参数量大、训练慢、部署到普通笔记本上帧率撑不住。我一般用一个小型 CNN:两层卷积(32 和 64 个 3×3 卷积核)+ 两层最大汇聚 + 全连接。汇聚层用 2×2 窗口、步长 2,每次把特征图尺寸减半。结构大致如下:

from tensorflow.keras import layers, models def build_fatigue_cnn(input_shape=(24, 24, 1)): model = models.Sequential([ # 第一层卷积:提取边缘和局部纹理 layers.Conv2D(32, (3, 3), activation='relu', input_shape=input_shape), layers.MaxPooling2D((2, 2)), # 24x24 -> 12x12 # 第二层卷积:组合成更复杂的眼部/嘴部特征 layers.Conv2D(64, (3, 3), activation='relu'), layers.MaxPooling2D((2, 2)), # 12x12 -> 6x6 layers.Flatten(), layers.Dense(128, activation='relu'), layers.Dropout(0.5), # 防止过拟合 layers.Dense(1, activation='sigmoid') # 二分类:疲劳/正常 ]) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy']) return model

Dropout(0.5)在小数据集上很关键,我试过不加 dropout,训练集准确率能到 99% 但验证集只有 82%,典型过拟合。sigmoid输出配合binary_crossentropy是二分类标准组合。如果要做「睁眼/闭眼/打哈欠」三分类,最后一层换成Dense(3, activation='softmax'),损失换categorical_crossentropy

训练时batch_size设 64,epochs先跑 30 轮,用EarlyStopping监控验证损失,patience 设 5,避免无效训练。学习率默认 0.001 通常够用,如果损失震荡大就降到 0.0005。

3. 从摄像头到告警:实时推理链路的搭建与参数调优

3.1 人脸检测用 OpenCV 还是 MTCNN

实时链路第一步是找到人脸。OpenCV 自带的 Haar 级联检测器速度最快,CPU 上单帧几毫秒,但侧脸和遮挡下漏检明显。MTCNN 精度高但速度慢,普通笔记本上单帧要 50 到 100 毫秒,帧率直接掉到 10 帧以下。我的做法是:用 Haar 做粗定位,拿到人脸框后按比例裁剪眼部和嘴部区域,再送进 CNN。这样速度和质量能平衡。

import cv2 import numpy as np # 加载 Haar 级联检测器(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 表示默认摄像头 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 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # detectMultiScale 参数:缩放步长 1.1,最小邻居数 5,最小人脸 80x80 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, 8, minSize=(20, 20)) for (ex, ey, ew, eh) in eyes[:2]: # 只取前两只眼睛 eye_img = roi_gray[ey:ey+eh, ex:ex+ew] eye_img = cv2.resize(eye_img, (24, 24)) eye_img = eye_img.reshape(1, 24, 24, 1) / 255.0 # 送进 CNN 预测 pred = model.predict(eye_img, verbose=0)[0][0] label = 'Fatigue' if pred > 0.5 else 'Normal' cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imshow('Driver Fatigue Detection', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

detectMultiScalescaleFactor=1.1表示每次缩放 10%,值越小检测越细但越慢;minNeighbors=5控制误检,值越大越严格但可能漏检。minSize=(80, 80)过滤掉远处小脸,减少无效计算。眼睛检测的minNeighbors我设成 8,因为眼睛区域小,误检率更高,需要更严格。

3.2 预警逻辑:单帧判断为什么不可靠

单帧判断闭眼就告警,是新手最容易犯的错。正常眨眼一次约 100 到 400 毫秒,按 30 帧率算就是 3 到 12 帧,如果每帧闭眼都告警,驾驶员每眨一次眼系统就响一次,完全没法用。正确做法是加时间窗口:连续 N 帧判定为闭眼才触发告警。

from collections import deque # 用双端队列保存最近 30 帧的判断结果 history = deque(maxlen=30) FATIGUE_THRESHOLD = 0.7 # 70% 以上帧判定疲劳才告警 def check_fatigue(pred): history.append(1 if pred > 0.5 else 0) if len(history) < 15: # 至少积累 15 帧再判断 return False ratio = sum(history) / len(history) return ratio > FATIGUE_THRESHOLD

maxlen=30对应约 1 秒的窗口(30 帧率下),FATIGUE_THRESHOLD=0.7表示 1 秒内 70% 的帧判定闭眼才告警。这两个参数需要按实际帧率调:如果帧率只有 15,maxlen要减半,否则窗口时间过长,反应迟钝。我一般还会加一个「告警冷却」——触发一次告警后 3 秒内不重复触发,避免声音一直响。

3.3 嘴部打哈欠检测怎么加

打哈欠的视觉特征是嘴巴张开且持续一定时间。可以用类似眼睛的思路:裁剪嘴部区域,训练一个二分类 CNN,或者用嘴部纵横比(MAR)做几何判断。MAR 计算简单,不需要额外模型:

# 假设已通过人脸关键点拿到嘴巴上下左右四个点 def mouth_aspect_ratio(upper, lower, left, right): # 垂直距离 / 水平距离 vertical = np.linalg.norm(upper - lower) horizontal = np.linalg.norm(left - right) return vertical / horizontal # MAR 阈值一般设 0.6,超过且持续 15 帧以上判为打哈欠 MAR_THRESHOLD = 0.6

关键点可以用 dlib 的 68 点模型或 MediaPipe 拿到。MediaPipe 速度更快,CPU 上能跑 30 帧以上,适合实时场景。MAR 阈值 0.6 是经验值,不同人脸型有差异,建议先录一段自己打哈欠的视频标定一下。

4. 避坑与排查:这套系统最容易翻车的五个地方

4.1 摄像头打不开或读帧失败

现象:cap.read()一直返回False,窗口黑屏。原因通常是摄像头被其他程序占用,或者VideoCapture(0)的索引不对。解决:先关掉其他调用摄像头的软件;如果笔记本有多个摄像头,把 0 换成 1 或 2 试;Windows 上还要检查隐私设置里是否允许应用访问摄像头。

4.2 模型预测结果全是同一类

现象:不管输入什么图,输出都是 0.5 附近或全判疲劳。原因多半是训练时数据没归一化,或者验证集和训练集分布差异太大。解决:确认rescale=1./255在训练和推理时都做了;检查数据集里两类样本是否均衡,如果闭眼样本远多于睁眼,模型会偏向预测闭眼,需要做过采样或调class_weight

4.3 帧率太低,告警延迟明显

现象:画面卡顿,从闭眼到告警要两三秒。原因是每帧都跑 CNN 预测,CPU 扛不住。解决:降低输入分辨率(640×480 降到 320×240);把 CNN 推理改成每 3 帧跑一次,中间帧复用上次结果;或者用 TensorFlow Lite 把模型量化成 int8,推理速度能提升 2 到 3 倍。

4.4 戴眼镜时眼睛检测失效

现象:戴眼镜的测试者眼睛区域检测不到,或者 CNN 误判率高。原因是镜片反光和镜框遮挡。解决:训练数据里加入戴眼镜的样本;检测时把眼睛区域适当扩大,包含镜框边缘;如果反光严重,可以试一下直方图均衡化(cv2.equalizeHist)增强对比度。

4.5 夜间或隧道场景误报暴增

现象:光线暗的时候系统频繁告警。原因是灰度图在低光照下对比度低,CNN 把暗区域误判成闭眼。解决:加一个亮度判断,如果画面平均亮度低于阈值,先做伽马校正或直方图均衡化再送检测;或者用红外摄像头,但毕业设计一般没这个条件,软件补偿更实际。

5. 进阶技巧:把误报压下去的两个实用手段

第一个手段是「多特征融合」。单靠眼睛闭合并不能完全代表疲劳,有时候驾驶员只是正常眨眼或看后视镜。我一般会把眼睛状态、嘴部 MAR、头部姿态(低头角度)三个信号加权:眼睛闭合权重 0.5,打哈欠权重 0.3,低头权重 0.2,加权和超过阈值才告警。头部姿态可以用cv2.solvePnP配合人脸关键点估算俯仰角,低头超过 20 度且持续 2 秒就加分。这样误报率能明显下降,代价是代码复杂度上升,但毕业设计里多一个融合逻辑反而是加分项。

第二个手段是「模型量化 + 多线程」。用 TensorFlow Lite 转换器把训练好的模型转成 tflite 格式,开optimizations=[tf.lite.Optimize.DEFAULT]做动态范围量化,模型体积能压到原来的四分之一,CPU 推理速度提升约 2 倍。然后把摄像头读帧和模型推理放到两个线程里,读帧线程只管抓画面,推理线程从队列取帧处理,避免cap.read()阻塞推理。下面是一个简化的多线程骨架:

import threading import queue frame_queue = queue.Queue(maxsize=2) # 只保留最新两帧,防止积压 def capture_thread(cap): while True: ret, frame = cap.read() if not ret: break if frame_queue.full(): frame_queue.get() # 丢掉旧帧 frame_queue.put(frame) def inference_thread(model): while True: frame = frame_queue.get() # 在这里做人脸检测和 CNN 推理 # ... # 启动两个线程 t1 = threading.Thread(target=capture_thread, args=(cap,), daemon=True) t2 = threading.Thread(target=inference_thread, args=(model,), daemon=True) t1.start() t2.start()

queue.Queue(maxsize=2)是关键,队列满了就丢旧帧,保证推理用的永远是最新画面,不会因为处理慢导致延迟累积。daemon=True让线程随主程序退出,避免卡死。

最后说一个我自己的习惯:每次调完参数,一定用同一段测试视频跑三遍,记录误报次数和漏报次数,而不是凭感觉说「好像准了」。疲劳检测这种场景,误报比漏报更影响体验,宁可稍微迟钝一点,也别让驾驶员被频繁的假警报烦到直接关掉系统。希望帮到你。

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

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

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

立即咨询