我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

基于迁移学习和人脸关键点的睡意检测报警系统

基于迁移学习和人脸关键点的睡意检测报警系统 如果你的毕设选题还悬在人工智能方向又希望做出来以后既能现场演示、又能写成一篇像样的论文睡意检测报警系统算是一个性价比很高的选择。它用摄像头实时捕捉人脸靠迁移学习训练的模型判断眼睛是睁是闭再根据闭眼持续时长给出疲劳预警。我前前后后带过几个同学做这类题目今天就把完整技术路线、训练细节和踩坑记录一次聊透给打算做或者正在做的同学一个可以直接参照的模板。这套系统不是那种“只会跑个 demo”的玩具它要处理视频流、识别人脸、判断眼睑状态、记录闭眼时长、发出报警提示每一环都涉及实际工程问题。把整个过程走完你会发现论文里的“创新点”“系统设计”“实验对比”全都有素材可用答辩现场也不怕老师追问细节。1. 项目整体设计与思路拆解1.1 系统要解决什么问题睡意检测的核心目的是捕捉人在疲劳状态下不由自主的闭眼行为。人在真正打瞌睡之前会出现闭眼时间变长、眨眼频率降低、头部下垂等信号如果能用摄像头实时抓到这些信号就有机会提前提醒用户。做一个毕设版本功能上可以只聚焦“眼睛状态判断 闭眼时长统计 报警”。也就是摄像头每秒捕获若干帧画面定位人脸并提取眼睛区域判断每只眼睛是睁开还是闭合连续闭眼达到阈值时触发分级报警提供可视化界面方便现场展示和参数调整这个范围刚好适合一个学生在一个学期内完成又不会显得单薄。往上可以扩展到头部姿态估计、打哈欠检测往下可以精简成嵌入式版可进可退。1.2 为什么要用迁移学习很多同学会问判断眼睛睁闭直接用传统规则不就行了吗确实纯规则的 EAR眼睛纵横比方案也能做出基本功能但鲁棒性很差。戴眼镜反光、侧脸、眯眼、光照变化都会造成误判。另一种思路是从零训练一个 CNN 做睁眼/闭眼二分类。问题是作为毕设你通常没有几千个小时的标注数据显卡也算不上顶级从零训练不仅收敛慢精度也很难看。迁移学习的价值就在这里把在大型数据集上预训练好的模型拿过来保留它前面已经学会的通用特征提取能力只替换最后几层去适配我们的任务。用少量数据也能训练出不错的分类器。我的类比是这就像你已经学过素描的明暗、透视、比例现在让你专攻“画眼睛”只需要在原有基础上加练眼睛局部就行不用从削铅笔开始重新学。1.3 整体模块划分整个系统我习惯拆成五个模块视频采集模块负责调用摄像头、设置分辨率、控制帧率人脸与关键点检测模块定位人脸提取眼睛关键点坐标用于裁剪 ROI眼睛状态分类模块迁移学习模型判断睁眼/闭眼疲劳判定模块基于连续闭眼帧数的状态机报警与人机交互模块声音报警、界面显示、阈值调节这五个模块各干各的后期替换算法、调参、加功能都很方便。我见过太多同学把所有逻辑揉在一个文件里最后改一个参数要翻半天代码非常痛苦。一开始就按模块写后面会省很多事。2. 核心技术拆解关键点、EAR 与迁移学习模型2.1 人脸 68 关键点与眼睛纵横比先讲传统方法里最经典的 EAR因为它可以作为辅助判据也能作为对照组写进论文。人脸关键点检测常用 dlib 的 68 点模型其中每只眼睛周围有 6 个关键点。EAR 的公式是[ EAR \frac{|p_2 - p_6| |p_3 - p_5|}{2 |p_1 - p_4|} ]简单说就是眼睛上下边缘的垂直距离之和除以眼睛左右端点的水平距离。人睁眼时这个值大概在 0.25 到 0.35 之间闭眼时会明显下降所以可以取一个阈值来判断是否闭眼。dlib 的 68 点模型中左眼关键点索引是 36 到 41右眼是 42 到 47。注意不同模型索引略有差异写代码前最好先把关键点可视化出来确认一遍别凭记忆写。EAR 方案的优点是很轻量纯 CPU 也能跑缺点是阈值不好定有人眼睛大有人眼睛小同一个阈值在不同人脸上表现完全不同。所以在实际系统里我建议把 EAR 当作辅助特征主判断交给迁移学习模型。2.2 为什么单独用关键点不够做毕设的时候如果只用 EAR论文很难写出深度答辩时也容易被问住。比如“同学戴眼镜怎么办”“光线变暗怎么办”这些问题用纯规则很难回答。眼睛状态分类模型可以回答这些问题。我们把眼睛区域裁剪出来交给卷积神经网络让模型学习“纹理形态”与“睁闭状态”的关系。闭眼和睁眼在视觉上有明显差异比如眼裂宽度、睫毛阴影、眼球反光区域等CNN 能学到这些特征比人工设计的规则更鲁棒。而且有了深度学习模型整个系统的技术含量就上去了。论文里可以多一个章节来写模型设计、训练、对比实验工作量看起来也更饱满。2.3 迁移学习模型选型与原理选型上毕设不需要追逐超大模型。MobileNetV2 是 I 几乎绕不开的选择轻量、CPU 推理也不慢、预训练权重好找。迁移学习实操时常见有两种玩法冻结全部骨干层只训练新加的分类头先冻结骨干训练分类头再解冻部分高维层进行整体微调第一种方式适合数据量很少的情况训练快但模型对目标域的适应性弱一些。第二种是更推荐的完整流程先用分类头把模型拉到“能收敛”的状态再用小学习率解冻部分层精调效果通常更好。损失函数用二分类交叉熵输出层用 sigmoid。输入尺寸我建议用 96x96 或 112x112 级别的眼图不需要 224 那么大因为眼睛区域本身很小过大反而增加计算量。3. 数据集准备与模型训练实操3.1 数据集从哪里来训练睡意检测模型的核心难点不是网络结构而是数据。公开数据集中比较常用的是包含眼睛区域图像的 MRL Eye Dataset以及一些睁眼/闭眼二分类数据集。建议的做法是从公开数据集中筛选睁眼、闭眼图片各一批自己用摄像头拍摄部分真实场景数据增加光照、角度的多样性把数据分成 train / val 两个目录按0_open和1_closed两个子目录组织比如我的目录结构是dataset/ train/ 0_open/ 1_closed/ val/ 0_open/ 1_closed/目录命名直接用 0 和 1配合flow_from_directory非常方便。3.2 自采数据与标注技巧公开数据集虽然量大但和你在宿舍里的光线、摄像头型号往往有差异。我强烈建议补充一部分自采数据只拍自己的眼睛就行不用全部人工截取。写一个小脚本用 dlib 检测人脸关键点把眼睛区域裁剪出来每隔几帧保存一张图。自己交替做“睁眼”和“闭眼”动作各拍几百张然后手动清理掉模糊和没对准的图片。这样采集几十张到几百张就够了。注意拍闭眼时不要眯眼要自然闭合否则模型会把“眯眼”和“闭眼”搞混。戴眼镜的同学一定要拍一组戴眼镜的图因为眼镜框和镜片反光对模型影响很大。3.3 模型训练代码与关键参数下面给一份基于 TensorFlow/Keras 的微调代码模板。先建立一个基础模型冻结权重只训练分类头。import tensorflow as tf from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras.layers import Dense, GlobalAveragePooling2D, Dropout from tensorflow.keras.models import Model from tensorflow.keras.optimizers import Adam from tensorflow.keras.preprocessing.image import ImageDataGenerator train_dir dataset/train val_dir dataset/val train_datagen ImageDataGenerator( rescale1.0 / 255, rotation_range10, width_shift_range0.05, height_shift_range0.05, brightness_range[0.8, 1.2], horizontal_flipTrue, zoom_range0.1 ) val_datagen ImageDataGenerator(rescale1.0 / 255) train_flow train_datagen.flow_from_directory( train_dir, target_size(96, 96), batch_size32, class_modebinary ) val_flow val_datagen.flow_from_directory( val_dir, target_size(96, 96), batch_size32, class_modebinary ) base_model MobileNetV2( weightsimagenet, include_topFalse, input_shape(96, 96, 3) ) base_model.trainable False x base_model.output x GlobalAveragePooling2D()(x) x Dropout(0.3)(x) x Dense(1, activationsigmoid)(x) model Model(inputsbase_model.input, outputsx) model.compile( optimizerAdam(learning_rate1e-3), lossbinary_crossentropy, metrics[accuracy] ) history model.fit( train_flow, validation_dataval_flow, epochs10 )第一阶段训练完再解冻部分层做微调base_model.trainable True # 只微调最后 30 层左右 for layer in base_model.layers[:-30]: layer.trainable False model.compile( optimizerAdam(learning_rate1e-5), lossbinary_crossentropy, metrics[accuracy] ) model.fit( train_flow, validation_dataval_flow, epochs5 )注意几个参数第一阶段学习率 1e-3第二阶段 1e-5。微调阶段学习率必须小否则预训练权重会被冲坏batch_size 根据显存调整我用 32 是偏保守的如果验证集准确率一直卡在某处先检查数据标注对不对别急着换网络3.4 模型评估与导出训练完成后不只是看整体准确率要重点看闭眼类别的召回率。睡意检测场景里漏报一次闭眼比误报一次严重得多。评估时用混淆矩阵和分类报告from sklearn.metrics import classification_report, confusion_matrix val_flow.reset() y_true val_flow.classes y_pred (model.predict(val_flow) 0.5).astype(int) print(classification_report(y_true, y_pred)) print(confusion_matrix(y_true, y_pred))导出模型时我建议转成 ONNX 格式方便用 OpenCV DNN 模块推理部署环境更干净。TensorFlow 侧可以先保存成 Keras 格式再转 ONNX。4. 实时报警系统集成与界面实现4.1 实时推理主流程整个实时检测部分最核心的就是主循环。我用 OpenCV 读摄像头dlib 做关键点检测OpenCV DNN 加载 ONNX 模型做眼睛分类。伪代码如下import cv2 import dlib import numpy as np from imutils import face_utils from scipy.spatial import distance as dist from winsound import Beep def eye_aspect_ratio(eye): A dist.euclidean(eye[1], eye[5]) B dist.euclidean(eye[2], eye[4]) C dist.euclidean(eye[0], eye[3]) return (A B) / (2.0 * C) detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) re_net cv2.dnn.readNetFromONNX(eye_classifier.onnx) cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) close_counter 0 frame_count 0注意分辨率不要贪高。640x480 足够检测人脸和眼睛分辨率越高人脸检测越慢系统整体帧率会掉得很厉害。4.2 眼睛裁剪与状态判定每一帧处理时先做人脸检测拿到面部区域后再预测关键点。拿到左右眼的 6 个关键点以后计算 EAR并裁剪眼睛矩形区域送进 CNN。裁剪眼睛区域有几个细节矩形不要贴得太紧留出 10% 到 20% 的边距统一缩放到 96x96归一化到 0~1和训练时一致左右眼分开判断只要某只眼被判定为闭合就算闭眼。这样即使侧脸时一只眼被遮挡另一只眼的状态仍然能提供信号。最后结合 EAR 和 CNN 结果做决策。CNN 概率大于 0.5 就认为闭眼EAR 作为辅助如果 EAR 非常低也往闭眼方向加权。4.3 疲劳状态机与报警逻辑直接把每一帧的睁闭状态交给阈值判断容易产生抖动。比如闭眼一帧中断了前面的计数就清零导致很难触发报警。我用一个简单的状态机来解决CLOSE_THRESHOLD_SLIGHT 30 # 约 1.2 秒 25fps CLOSE_THRESHOLD_SEVERE 60 # 约 2.4 秒 if eyes_closed: close_counter 1 else: close_counter 0 if close_counter CLOSE_THRESHOLD_SEVERE: level 重度疲劳 Beep(1500, 1000) elif close_counter CLOSE_THRESHOLD_SLIGHT: level 轻度疲劳 else: level 正常帧率不是稳定的 25fps 时按帧数累加并不科学。更稳妥的做法是记录上一次闭眼的开始时间然后用时间戳计算闭眼持续时长if eyes_closed and close_start is None: close_start time.time() elif not eyes_closed: close_start None if close_start is not None: duration time.time() - close_start if duration 2.0: alarm()时间阈值建议 1.5 秒到 2 秒之间。闭眼 1 秒是正常眨眼超过 1.5 秒已经能说明明显的困意。4.4 界面与交互界面用 Tkinter 就够不需要引入重量级框架。做一个窗口左边放实时视频画面右边显示 EAR、CNN 闭眼概率、疲劳等级、连续闭眼时间再加一个报警开关。报警方式我用了系统蜂鸣音简单直接。但实测发现持续“嘀嘀嘀”非常刺耳体验不好。后来改成第一级只显示文字并播放一次短提示音第二级才连续报警模拟“先提醒、后强提醒”的真实逻辑。这个分级设计在论文里写起来也很有说服力系统不是一刀切而是能区分轻度疲劳和重度疲劳。5. 常见问题排查与避坑指南这部分都是我自己和周围同学实际踩过的坑每一条都是真实教训比理论更容易让你少走弯路。问题现象可能原因解决方案模型对闭眼漏检严重训练数据里闭眼样本太少扩充闭眼样本特别是不同角度和光线下的闭眼图戴眼镜时误判严重镜框反光、镜片遮挡干扰自采戴眼镜数据推理时叠加 EAR 判断画面卡顿明显每帧都做人脸检测 CNN降低分辨率到 640x480人脸检测每隔一帧执行一次阈值不好调不同人眼睛形态差异大加一个用户校准步骤初始化时记录正常睁眼的 EAR 基线闭眼时长计数老被重置闭眼过程中偶发一帧误判为睁眼添加帧间平滑比如连续 2 帧都判定为睁眼才重置计数光照变化后准确率下降训练数据中光照多样性不足数据增强加亮度扰动推理时先做直方图均衡化训练准确率很高但实时效果差训练数据和摄像头画面分布差异大保证验证集里包含真实摄像头自采图帧率优化是一个重点。我之前见过同学用 1080p 视频流跑人脸关键点帧率只有个位数报警逻辑完全没法用。后来把分辨率降到 480p把关键点检测拆到每两帧做一次帧率直接提升一倍。另一个容易忽略的问题是报警的稳定性。报警不能每帧都触发也不能因为一帧误判就中断。我最后用的是“连续帧投票”机制如果最近 3 帧里有 2 帧判定为闭眼则当前状态算闭眼。这个简单的平滑策略大大减少了闪烁。光线问题也很关键。寝室里如果背光摄像头画面偏暗闭眼模型准确率会明显下降。我做了两个改进一是在训练数据里加入亮度扰动二是推理时对眼睛区域做 CLAHE 局部直方图均衡化。效果立竿见影。6. 从毕设到答辩论文亮点与演示经验6.1 论文怎么写才有说服力如果论文只写“我用了 MobileNetV2 微调”那和其他同学就拉不开差距。建议论文里至少包含三块实验内容传统 EAR 方案的检测结果作为基线迁移学习模型的检测结果作为主方案两者在自采测试集上的对比包括准确率、漏报率、误报率对比实验是答辩老师最感兴趣的部分也最能体现你对问题的理解。你可以自己跑一组真实数据把两种方案的指标列成表格。另一个可以写进论文的是“阈值分析”比如闭眼持续多长时间触发报警最合适报警灵敏度和误报率之间的权衡。这个内容老师很难挑出毛病因为你是在系统层面做分析而不是只堆模型。6.2 答辩演示的正确姿势答辩现场最容易翻车的点集中在摄像头和光线。我的建议是提前去答辩教室测试现场光线如果不理想带一盏小台灯补光演示前把报警声音调小放一个正常睁眼、一个闭眼、一个模拟打瞌睡的三个片段不要全程只放录好的视频老师更想看到实时画面但要有录播备份以防现场摄像头出问题准备好回答“阈值是多少”“为什么选 MobileNetV2”“如果光线很暗怎么办”这类常规问题展示闭眼状态的瞬间可以用屏幕上的指标变化来佐证让老师看到 EAR 下降、闭眼概率升高、疲劳等级切换这比自己干巴巴解释更有说服力。6.3 可扩展方向毕设做到这个程度已经可以交差了但如果学有余力还有几个方向能继续加分加上打哈欠检测用嘴部纵横比 MAR 判断张嘴幅度加上头部姿态检测检测低头动作把系统部署到树莓派或者 Android 上体现嵌入式能力把报警方式扩展成语音提示或蓝牙震动提醒每个方向都不复杂但能让整个系统的应用场景显得更完整。最重要的是这些扩展都不会推翻你已有的代码架构因为本来模块就是分开的。最后分享一个小经验这套系统真正耗时的地方不在训练模型而在数据清洗和阈值调优。头几次跑通 demo 可能只用一晚上但要让它在不同光线下都稳定我前前后后调了将近一周。毕业设计不只看你模型多复杂更要看问题是否被真正解决掉了。宁愿做一个简单但可靠、能现场稳定运行的睡意检测报警系统也别做一个花哨但动不动崩溃的演示品。答辩老师比你想象中更看重工程完成度。
返回列表