如果你的毕设选题还悬在人工智能方向,又希望做出来以后既能现场演示、又能写成一篇像样的论文,睡意检测报警系统算是一个性价比很高的选择。它用摄像头实时捕捉人脸,靠迁移学习训练的模型判断眼睛是睁是闭,再根据闭眼持续时长给出疲劳预警。我前前后后带过几个同学做这类题目,今天就把完整技术路线、训练细节和踩坑记录一次聊透,给打算做或者正在做的同学一个可以直接参照的模板。
这套系统不是那种“只会跑个 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( rescale=1.0 / 255, rotation_range=10, width_shift_range=0.05, height_shift_range=0.05, brightness_range=[0.8, 1.2], horizontal_flip=True, zoom_range=0.1 ) val_datagen = ImageDataGenerator(rescale=1.0 / 255) train_flow = train_datagen.flow_from_directory( train_dir, target_size=(96, 96), batch_size=32, class_mode='binary' ) val_flow = val_datagen.flow_from_directory( val_dir, target_size=(96, 96), batch_size=32, class_mode='binary' ) base_model = MobileNetV2( weights='imagenet', include_top=False, input_shape=(96, 96, 3) ) base_model.trainable = False x = base_model.output x = GlobalAveragePooling2D()(x) x = Dropout(0.3)(x) x = Dense(1, activation='sigmoid')(x) model = Model(inputs=base_model.input, outputs=x) model.compile( optimizer=Adam(learning_rate=1e-3), loss='binary_crossentropy', metrics=['accuracy'] ) history = model.fit( train_flow, validation_data=val_flow, epochs=10 )第一阶段训练完,再解冻部分层做微调:
base_model.trainable = True # 只微调最后 30 层左右 for layer in base_model.layers[:-30]: layer.trainable = False model.compile( optimizer=Adam(learning_rate=1e-5), loss='binary_crossentropy', metrics=['accuracy'] ) model.fit( train_flow, validation_data=val_flow, epochs=5 )注意几个参数:
- 第一阶段学习率 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 可能只用一晚上,但要让它在不同光线下都稳定,我前前后后调了将近一周。毕业设计不只看你模型多复杂,更要看问题是否被真正解决掉了。宁愿做一个简单但可靠、能现场稳定运行的睡意检测报警系统,也别做一个花哨但动不动崩溃的演示品。答辩老师比你想象中更看重工程完成度。