资讯详情 基于CNN的人脸识别驾驶员疲劳检测与预警系统实现
📅 2026/10/12 20:27:11
简介基于Python卷积神经网络的人脸识别驾驶员疲劳检测与预警系统完整项目面向毕业设计、课程设计、期末大作业等实际需求适合需要从零搭建AI视觉方案并快速出成果的学生与开发者。资源共37个文件包体约500.4MB涵盖16个py源码、9个pyc编译文件、3个pth模型权重、5个jpg测试图片、2个txt说明文档以及zip数据集与log训练日志代码、模型、数据、文档一应俱全。源码包含训练脚本、测试脚本、摄像头实时检测脚本、SSD目标检测模型和VGG16预训练权重完整覆盖从图像采集、数据预处理、CNN特征提取、疲劳状态分类到预警触发的全流程可直接运行调试也可根据自身场景改造优化。项目内置的数据集和介绍文档能帮助理解数据组织方式、权重文件用途及调参思路压缩包目录层次清晰便于按模块学习复用。目前已有233人学习是一份可作毕业设计或课程设计完整参考的高质量资源。1. 疲劳检测不是人脸识别这套毕设系统真正要解决什么基于卷积神经网络的人脸识别驾驶员疲劳检测与预警系统是毕业设计里把计算机视觉三个高频概念——卷积神经网络、人脸识别、驾驶员疲劳检测——串成完整闭环的典型项目它既有模型训练、又有实时检测、还能给出预警输出评审能直接看到成果。拿到这类源码后最该做的不是让 demo 跑起来而是先想明白四件事CNN 在这套系统里到底承担哪一段任务疲劳特征怎么量化预警怎么做到少误报以及数据集在哪个环节最容易拖后腿。下面按一条可复现的毕业设计落地路径来写先讲清楚为什么疲劳检测要依赖 CNN 而不是传统图像处理再给出疲劳特征提取、实时预警主循环的可运行代码和参数设置最后用一份避坑记录把常见翻车现场列出来。无论你打算用现成的预训练模型还是自己从头训练一个小型 CNN这条路径上的关键取舍基本一致适合准备做毕设、课程设计或者想用 Python 快速验证疲劳检测方案的从业者。2. 为什么是卷积神经网络从传统图像处理到疲劳状态分类的设计取舍2.1 疲劳迹象到底长什么样眨眼频率、眼睑开度与嘴部动作疲劳驾驶检测在行业里并不是靠“看脸色”而是看几个可量化的动作特征眨眼频率异常、眼睑开度持续偏低、嘴巴频繁张大打哈欠、头部出现不自主下垂。其中 PERCLOS单位时间内眼睛闭合时间所占比例是被研究最充分的指标它把疲劳定义成“闭眼时长在总时间里的占比超过阈值”很多商用方案都拿它当第一判据。所以这套系统的技术栈其实是两层底层要准确判断每一帧的眼睛是睁开还是闭合、嘴巴是张大还是常态上层再根据连续帧统计来判定疲劳状态。传统 OpenCV 方案通常用肤色检测、灰度投影或 Haar 特征来判断眼部开闭流程快但非常怕光照变化、镜框遮挡和侧脸角度而 CNN 能从局部图像里自适应学习稳定的纹理和形态模式换人换环境的鲁棒性明显更好这也是标题里为什么强调“卷积神经网络”而不是简单的图像差分。这里还要刻意区分一个常见的误解人脸识别和疲劳状态识别是两个任务。标题里的“人脸识别”通常指先用检测器框出人脸位置再对脸部区域做状态分类而很多人搜过的 ArcFace 人脸识别模型做的是判断“你是谁”它提取的是身份判别特征不是状态判别特征直接拿来跑疲劳检测反而浪费算力。真正管用的是“人脸在哪”和“眼睛嘴在什么状态”这两个信息。2.2 CNN 在检测流程里的两种角色从局部纹理到状态概率在常见的毕设方案里CNN 有两种承担方式。第一种是用 CNN 做人脸检测输出面部边界框或关键点坐标典型的实现有 MTCNN、RetinaFace 这类轻量检测网络。第二种是拿面部裁剪图做疲劳状态分类输出“正常 / 疲劳 / 打哈欠”的概率。多数毕业设计采用级联结构前置人脸检测器用 MTCNN 或 dlib后置状态分类器用一个小型 CNN。这样拆的好处是解耦清晰人脸检测判断位置状态分类只负责看裁剪图两者优化方向完全不同。如果从零开始训练小型 CNN网络可以做得非常浅因为输入是居中的眼部或面部裁剪图任务本质上是一个图像二分类。下面是一个我常用的结构输入 48x48 的 RGB 人脸图经过 3 层卷积和 2 层全连接参数量在百万级别CPU 上也能跑出可用的实时帧率from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout def build_fatigue_cnn(input_shape(48, 48, 3)): model Sequential([ Conv2D(32, (3, 3), activationrelu, input_shapeinput_shape), MaxPooling2D(pool_size(2, 2)), Conv2D(64, (3, 3), activationrelu), # 第二层开始组合眼部/嘴部局部纹理 MaxPooling2D(pool_size(2, 2)), Conv2D(128, (3, 3), activationrelu), MaxPooling2D(pool_size(2, 2)), Flatten(), Dense(128, activationrelu), Dropout(0.5), # 数据量小时防过拟合 Dense(2, activationsoftmax) # 索引0为正常索引1为疲劳 ]) return model逻辑说明第一层 32 个卷积核负责提取眼睑边缘、瞳孔和皮肤的光影差异第二、第三层把通道数放大到 64 和 128用来组合眼睛轮廓和嘴部动作的局部特征。池化层逐层下采样给特征一定平移不变性摄像头画面里人脸稍微偏左偏右网络仍能识别出闭眼。Dropout 放在全连接层前训练时随机丢弃一半神经元避免网络把训练集里的特定人脸直接背下来。参数说明输入统一缩放为 48x48 或 64x64过大增加计算量过小会让闭眼细节丢失输出用 2 分类 softmax 时真实驾驶场景里正常帧数量远多于疲劳帧训练时必须用类别权重或用采样手段平衡否则网络会滑向“永远输出正常”这个偷懒解。2.3 选型决策自建小型 CNN 还是迁移预训练模型数据集量级决定选型方向。如果手上有 500 到 2000 张每类的自建数据集自建小型 CNN 更稳因为它参数少、收敛快CPU 就能训练也不需要折腾预训练权重下载。如果数据量到几千张以上再考虑迁移学习把在大规模数据上预训练过的视觉模型拿来微调通常能拿更高精度。选型每类所需样本训练成本实时性适用场景自建小型 CNN500~2000 张CPU 可达分钟级收敛高毕设、快速原型验证迁移预训练模型2000 张以上建议 GPU分钟到小时级中精度要求高、数据充足纯传统视觉Haar/LBP不需要训练零成本高光照稳定环境仅作基准对比从这张表能看出毕设项目里大多数人会走第一行因为疲劳检测任务本身的难度并不高难的是稳定采集和标注质量而不是模型容量。把网络结构图画得再复杂数据不干净最终精度一样是玄学。所以下面两章先把特征计算和数据集准备讲透。3. 特征提取与数据集准备EAR 和 MAR 怎么算才稳定3.1 面部关键点定位与眼睛纵横比 EAR 的计算公式当前最主要的疲劳量化基础是 dlib 的 68 点面部关键点或者 MTCNN 输出的关键点坐标。关键点本身不是疲劳指标但能换算成几何比率其中使用最广泛的是眼睛纵横比简称 EAR。这个值不是 CNN 输出的而是用关键点坐标算出来的目的是把“眼睛闭合程度”变成一个随个体差异和距离远近相对稳定的数值。EAR 的公式如下上下眼睑两点距离的平均值除以左右眼角距离。睁眼时该值通常在 0.3 到 0.38 之间闭眼时迅速掉到 0.1 左右。常见的做法是把 0.25 作为“疑似闭眼”的阈值。下面是可以直接抄走的计算函数import numpy as np def eye_aspect_ratio(eye): 输入单眼6个关键点坐标顺序按 dlib 68点模型约定。 eye 形状为 (6, 2)每一行是 (x, y) v1 np.linalg.norm(eye[1] - eye[5]) # 上眼睑到下眼睑的垂直距离之一 v2 np.linalg.norm(eye[2] - eye[4]) # 上眼睑到下眼睑的垂直距离之二 h np.linalg.norm(eye[0] - eye[3]) # 左右眼角水平距离 return (v1 v2) / (2.0 * h)逻辑说明6 个关键点按顺序围绕眼睛一圈索引 1 和 5、2 和 4 分别构成两条垂直线段索引 0 和 3 是左右眼角。分母乘以 2 是为了与两个垂直距离的平均值对齐让 EAR 的范围保持在一个可解释的尺度上。关键点坐标的绝对像素值会随摄像头距离改变但这个比值基本稳定所以才能跨距离使用。参数说明EAR 的主要问题是个体差异。单眼皮、小眼睛人群睁眼 EAR 可能只有 0.22戴深色镜框时关键点略微偏移也会压低数值。因此不建议把所有用户的阈值写死后面避坑章节会专门讲校准方法。3.2 嘴巴纵横比 MAR打哈欠检测的第二个特征通道只看眼睛会有漏判比如有人疲劳时不闭眼而是频繁打哈欠所以主流方案会再加一个嘴巴纵横比简称 MAR。与 EAR 类似MAR 用嘴部外轮廓关键点计算垂直距离和水平距离的比值正常闭嘴时在 0.2 到 0.3 之间张大嘴打哈欠时明显超过 0.5。def mouth_aspect_ratio(mouth): 输入嘴部8个关键点坐标顺序按 dlib 68点模型约定。 mouth 形状为 (8, 2) v1 np.linalg.norm(mouth[2] - mouth[10]) # 上嘴唇到下嘴唇 v2 np.linalg.norm(mouth[4] - mouth[8]) # 上嘴唇到下嘴唇内侧点 h np.linalg.norm(mouth[0] - mouth[6]) # 左右嘴角距离 return (v1 v2) / (2.0 * h)逻辑说明嘴部关键点比眼睛多取两组对称的垂直距离做平均能抵消轻微角度偏转带来的误差。在实时主循环里通常每帧同时计算左眼 EAR、右眼 EAR、MAR 三个值左眼右眼再取平均作为最终 EAR这样一只眼被遮挡时另一只眼还能兜底。参数说明哈欠阈值一般设置为 0.5 到 0.6低于这个范围会把说话误判成哈欠。更稳妥的用法不是单帧判断而是连续多帧 MAR 都超过阈值或记录一次“从低位到高位再回落”的完整过程才算一次有效哈欠。这部分逻辑放到实时预警章节展开。3.3 数据集构成与标注正常、闭眼、打哈欠三类样本怎么配有了 EAR 和 MAR仍然需要 CNN 来做人脸状态分类因为关键点检测在某些角度下会失败而 CNN 可以直接对图像块做判断两种方式互为补充。数据集的质量直接决定 CNN 的上限这里最常见的问题就是类别不平衡正常驾驶帧占了绝大多数闭眼和打哈欠样本只有零头。建议的采集构成按三个类别来整理正常状态、闭眼状态、打哈欠状态。每个类别至少准备 800 到 1000 张图如果自行采集可以从视频里按帧提取再用关键点坐标自动裁剪出眼部和嘴部区域。标注格式建议和分类网络输入对齐直接按文件夹划分即 train/normal、train/eye_closed、train/yawn验证集和测试集同理。from tensorflow.keras.preprocessing.image import ImageDataGenerator datagen ImageDataGenerator( rescale1.0/255.0, horizontal_flipTrue, rotation_range10, brightness_range[0.8, 1.2], validation_split0.2 ) train_gen datagen.flow_from_directory( dataset/train, target_size(48, 48), batch_size32, class_modecategorical, subsettraining ) val_gen datagen.flow_from_directory( dataset/train, target_size(48, 48), batch_size32, class_modecategorical, subsetvalidation )逻辑说明flow_from_directory 会自动按文件夹名生成标签省掉手写标注文件的步骤。ImageDataGenerator 里的 horizontal_flip 能立即把样本量翻倍rotation_range 模拟头部轻微转动brightness_range 模拟车内光照变化这三项对疲劳检测这种场景敏感型任务非常有效。参数说明validation_split0.2 表示从 train 里自动切出 20% 做验证batch_size32 是 CPU 训练时的稳妥值显存充足可以调到 64。target_size 必须与前面 build_fatigue_cnn 的 input_shape 一致否则模型输入尺寸对不上。训练时如果闭眼样本仍然偏少可以再传 class_weight把稀少类别的权重调高后面避坑章节会再讲。4. 实时预警主流程连续帧判定、冷却时间与报警触发代码4.1 环境准备与主循环摄像头取帧、人脸检测、关键点特征计算正式写主循环之前先把依赖装好。常见启动顺序是先完成 Python 环境配置再执行 pip install opencv-python numpy dlib。dlib 在 Windows 上较容易遇到编译问题一般做法是用别人预编译好的 wheel 包或改用 conda 环境安装。实时主循环的逻辑很直接摄像头取一帧检测人脸若没检测到就跳过该帧否则定位关键点算出左右眼 EAR 均值与 MAR再交给状态判定逻辑。下面的代码是把前面两个特征函数串起来的最小闭环import cv2 import time import numpy as np import dlib from feature_utils import eye_aspect_ratio, mouth_aspect_ratio detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) FATIGUE_EAR_THRESHOLD 0.25 # EAR 低于该值视为闭眼 CONSECUTIVE_FRAMES 40 # 连续闭眼40帧触发预警约2秒 20fps COOL_DOWN_SECONDS 5.0 # 触发一次预警后冷却5秒防止连环报警 ear_frames 0 last_alarm_time 0.0 cap cv2.VideoCapture(0) while cap.isOpened(): ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) if len(faces) 0: ear_frames 0 continue face faces[0] landmarks predictor(gray, face) left_eye np.array([[landmarks.part(i).x, landmarks.part(i).y] for i in range(36, 42)]) right_eye np.array([[landmarks.part(i).x, landmarks.part(i).y] for i in range(42, 48)]) mouth np.array([[landmarks.part(i).x, landmarks.part(i).y] for i in range(48, 60)]) ear (eye_aspect_ratio(left_eye) eye_aspect_ratio(right_eye)) / 2.0 mar mouth_aspect_ratio(mouth) if ear FATIGUE_EAR_THRESHOLD: ear_frames 1 else: ear_frames 0 now time.time() if ear_frames CONSECUTIVE_FRAMES and now - last_alarm_time COOL_DOWN_SECONDS: trigger_alarm() ear_frames 0 last_alarm_time now逻辑说明dlib 68 点模型里索引 36 到 41 是左眼42 到 47 是右眼48 到 59 是嘴部轮廓。代码先把坐标抽成 numpy 数组再调用前面定义的两个特征函数。检测器没有找到人脸时直接清零 ear_frames避免把人脸丢失前后的特征残留算进疲劳计数。参数说明CONSECUTIVE_FRAMES40 是按照 20fps 摄像头帧率推算的意味着连续闭眼约 2 秒才触发这个时长能筛掉大部分瞬间眨眼。如果摄像头帧率是 30fps建议把阈值改到 60帧率越快相同时间对应的帧数越多。主循环里没有把每帧显示和预警状态打到画面上的代码实际工程里应该补上 imshow 和状态文本方便现场调试。4.2 预警判定逻辑为什么必须用连续帧而不是单帧阈值疲劳预警最忌讳的就是“见帧就报”。单帧 EAR 低于阈值可能是眨眼、低头看仪表盘、甚至摄像头丢帧眨眼持续 100 到 400 毫秒而疲劳闭眼通常持续 1 秒以上。用连续帧计数本质上是做一个时间窗口滤波窗口长度决定了系统灵敏度。这里的核心参数有三个EAR 阈值、连续帧数、冷却时间。下面这张表是按不同灵敏度给出的一组起始值实际项目里需要通过测试微调参数保守模式均衡模式灵敏模式EAR 闭眼阈值0.200.250.28连续闭眼帧数 20fps604025冷却时间秒853表格里的“均衡模式”就是前面代码里用的那组参数。需要说明的是EAR 阈值调高后正常眨眼也更容易被算作闭眼连续帧数就必须相应增加来抵消阈值调低则相反。冷却时间的作用是防止预警触发后检测状态还没恢复就再次计数导致一次疲劳事件响七八次警报。判断逻辑里还有一个容易被忽略的点ear_frames 在非闭眼帧要立即清零而不是缓慢递减。这样设计是为了让系统对“睁眼恢复”快速响应因为驾驶员只要睁眼就说明当前时刻还具备一定的控制能力系统应该立刻解除危机状态。4.3 报警不卡主线程把提示音播放拆到独立线程很多毕设主循环里直接把报警声音播放写在检测线程里结果触发报警瞬间画面卡顿严重的会把帧率拖到个位数。正确做法是把报警行为扔进独立的线程检测主循环只负责置一个标志位不直接播放声音。import threading import winsound alarm_playing False def play_alarm_task(): global alarm_playing alarm_playing True for _ in range(3): winsound.Beep(880, 200) # 880Hz持续200ms循环3次 time.sleep(0.15) alarm_playing False def trigger_alarm(): if not alarm_playing: threading.Thread(targetplay_alarm_task, daemonTrue).start()逻辑说明trigger_alarm 被调用时先检查 alarm_playing避免上一次报警还没播完又叠加一次线程。线程设为 daemon 模式主程序退出时不会因为报警线程残留而卡住。winsound 是 Windows 自带库Linux/macOS 可以用系统命令或 playsound 库替代接口保持一致。参数说明Beep 的频率 880Hz 在人耳可听范围内比较醒目200ms 的时长搭配 3 次循环整体不到 1 秒。冷却时间仍然由主循环里的 COOL_DOWN_SECONDS 控制线程内部不再做二次限制。除了声音还可以在画面上叠加“疲劳预警请休息”的红色文字这是我在实车测试里觉得最有用的组合因为现场环境嘈杂时声音未必听得见。5. 避坑记录从光照突变到帧率不足的 5 个常见问题5.1 光照突变导致 EAR 跳变误报现象白天在树荫下正常行驶系统突然开始频繁报警或者夜间经过路灯时报警声断断续续。原因EAR 是一个几何比值但关键点检测依赖图片纹理过曝时瞳孔和眼白区域几乎融为一体凌晨或逆光时人脸区域过暗关键点坐标会发生漂移EAR 被拉到阈值以下。解决在主循环里加入 CLAHE 直方图均衡化只对人脸检测框内部做增强不影响整帧明暗。另外阈值不要写死推荐把闭眼阈值设置为“用户睁眼基线值 × 0.65”基线在摄像头启动后前 3 秒计算动态校准能吸收大部分光照变化。5.2 单眼皮小眼睛人群闭眼漏检现象换了一个测试者后系统灵敏度整体下降闭眼 3 秒都没报警换回上一个测试者又恢复正常。原因不同人的睁眼 EAR 基线差异很大。单眼皮和小眼睛人群睁眼 EAR 可能只有 0.22而统一阈值是 0.25导致这个人永远处于“伪闭眼”状态反过来大眼睛人群闭眼 EAR 也可能到 0.2统一阈值又可能报得过早。解决把固定阈值改成动态校准启动程序后记录前 30 帧的 EAR 均值作为个人基线闭眼阈值设为基线的 0.6 到 0.7 倍。这个校准阶段要在界面里提示“请正视前方 3 秒”代码实现并不复杂但能让系统从“只能给一个人用”变成“可以换人用”。5.3 一次打哈欠触发多次报警现象测试者打了一个哈欠报警连响 3 次冷却时间形同虚设。原因打哈欠时嘴部张开会持续 1 到 3 秒MAR 多次穿越阈值每次触发都可能把 ear_frames 重新计数冷却时间被同一事件消耗完导致连环报警。解决打哈欠检测不要用单帧阈值加连续计数改用状态机检测到 MAR 超过阈值时记录“张嘴开始”等到 MAR 回落到阈值以下才记为一次哈欠事件。哈欠事件单独计数只和实际发生的次数耦合不和帧计数纠缠。5.4 数据集不平衡CNN 训练完只输出正常现象训练集准确率很高验证集也说得过去但把闭眼图片单独丢给模型预测结果全是“正常”。原因正常帧占比可能超过 80%网络只需要全部预测为正常就能拿到 80% 以上的准确率训练出的模型对少数类完全无感。这在疲劳检测里是最常见的“假成功”。解决给类别加权重是直接手段Keras 里可以通过 class_weight 参数实现将“闭眼”和“打哈欠”的权重设为正常类别的 2 到 3 倍。同时用水平翻转、轻微旋转做数据增强扩充闭眼和哈欠类样本。判断数据集质量有没有过关可以单独抽一批闭眼帧做混淆矩阵看召回率而不是只看整体准确率。5.5 训练时准确率 99%放到实车画面里惨不忍睹现象离线测试准确率很高接上行车记录仪视频后误报漏报全回来了特征好像完全失效。原因训练数据来自网络抓图或室内自拍测试场景是驾驶位视角光照、角度、背景全变了这叫数据分布偏移。模型在训练集上的能力并没有被迁移到目标场景。解决至少录制一段真实驾驶视角的视频按帧抽取并筛选出正常、闭眼、打哈欠三类样本加入现有数据集中重新微调。如果实车测试不方便用行车记录仪录像代替也可以关键在于训练数据的采集角度、摄像头安装位置和最终部署必须一致。帧率不足也会放大这个问题主循环里如果配置的是 20fps训练时就应该明确按这个帧率去计算连续帧阈值。6. 用离线回放把误报率压下来阈值参数化与验证步骤在把系统交给评审或做道路实测之前最有效的一个验证技巧是用录制的视频回放代替纯摄像头在线调试。离线回放的好处是同一个片段可以反复跑改一次参数重新放一遍能清楚地看到参数对报警时机的影响而不需要坐在车里反复试。具体操作分三步。第一步录制 3 到 5 个短视频片段每段 2 到 3 分钟覆盖正常驾驶、闭眼 2 秒以上、频繁打哈欠、低头看手机几种情况并在纸上记录每个片段里真正出现疲劳事件的时间点。第二步写一个脚本用视频文件替换 VideoCapture 的摄像头输入其余代码不动跑完把报警时间点输出成列表。第三步把系统报警时间和人眼记录做对比统计漏报次数和误报次数。建议把主循环里的所有阈值参数提取成配置文件或者一个参数字典而不是散落在代码常量里。我在实际项目中会把 FATIGUE_EAR_THRESHOLD、CONSECUTIVE_FRAMES、COOL_DOWN_SECONDS、MAR_THRESHOLD 全部放到一个配置区每次回放验证只改配置不动逻辑。回放两到三轮后用下表记录结果视频片段实际疲劳事件次数系统报警次数漏报误报正常驾驶 2 分钟0000闭眼 2 秒片段3300打哈欠片段2110如果误报多优先调高连续帧数或调低 EAR 阈值如果漏报多反向调整。这个调试习惯帮我避免了很多次“换个测试者就翻车”的尴尬也是疲劳检测系统里性价比最高的验证手段。另外如果想更进一步可以尝试把每帧的 EAR/MAR 序列送入一维卷积网络做时序分类但那是后话先把离线回放跑通系统才算真正能用。希望帮到你。本文还有配套的精品资源点击获取