简介:基于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), activation='relu', input_shape=input_shape), MaxPooling2D(pool_size=(2, 2)), Conv2D(64, (3, 3), activation='relu'), # 第二层开始组合眼部/嘴部局部纹理 MaxPooling2D(pool_size=(2, 2)), Conv2D(128, (3, 3), activation='relu'), MaxPooling2D(pool_size=(2, 2)), Flatten(), Dense(128, activation='relu'), Dropout(0.5), # 数据量小时防过拟合 Dense(2, activation='softmax') # 索引0为正常,索引1为疲劳 ]) return model逻辑说明:第一层 32 个卷积核负责提取眼睑边缘、瞳孔和皮肤的光影差异;第二、第三层把通道数放大到 64 和 128,用来组合眼睛轮廓和嘴部动作的局部特征。池化层逐层下采样,给特征一定平移不变性,摄像头画面里人脸稍微偏左偏右,网络仍能识别出闭眼。Dropout 放在全连接层前,训练时随机丢弃一半神经元,避免网络把训练集里的特定人脸直接背下来。
参数说明:输入统一缩放为 48x48 或 64x64,过大增加计算量,过小会让闭眼细节丢失;输出用 2 分类 softmax 时,真实驾驶场景里正常帧数量远多于疲劳帧,训练时必须用类别权重或用采样手段平衡,否则网络会滑向“永远输出正常”这个偷懒解。
2.3 选型决策:自建小型 CNN 还是迁移预训练模型
数据集量级决定选型方向。如果手上有 500 到 2000 张每类的自建数据集,自建小型 CNN 更稳,因为它参数少、收敛快,CPU 就能训练,也不需要折腾预训练权重下载。如果数据量到几千张以上,再考虑迁移学习,把在大规模数据上预训练过的视觉模型拿来微调,通常能拿更高精度。
| 选型 | 每类所需样本 | 训练成本 | 实时性 | 适用场景 |
|---|---|---|---|---|
| 自建小型 CNN | 500~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( rescale=1.0/255.0, horizontal_flip=True, rotation_range=10, brightness_range=[0.8, 1.2], validation_split=0.2 ) train_gen = datagen.flow_from_directory( 'dataset/train', target_size=(48, 48), batch_size=32, class_mode='categorical', subset='training' ) val_gen = datagen.flow_from_directory( 'dataset/train', target_size=(48, 48), batch_size=32, class_mode='categorical', subset='validation' )逻辑说明:flow_from_directory 会自动按文件夹名生成标签,省掉手写标注文件的步骤。ImageDataGenerator 里的 horizontal_flip 能立即把样本量翻倍,rotation_range 模拟头部轻微转动,brightness_range 模拟车内光照变化,这三项对疲劳检测这种场景敏感型任务非常有效。
参数说明:validation_split=0.2 表示从 train 里自动切出 20% 做验证;batch_size=32 是 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_FRAMES=40 是按照 20fps 摄像头帧率推算的,意味着连续闭眼约 2 秒才触发,这个时长能筛掉大部分瞬间眨眼。如果摄像头帧率是 30fps,建议把阈值改到 60;帧率越快,相同时间对应的帧数越多。主循环里没有把每帧显示和预警状态打到画面上的代码,实际工程里应该补上 imshow 和状态文本,方便现场调试。
4.2 预警判定逻辑:为什么必须用连续帧而不是单帧阈值
疲劳预警最忌讳的就是“见帧就报”。单帧 EAR 低于阈值可能是眨眼、低头看仪表盘、甚至摄像头丢帧,眨眼持续 100 到 400 毫秒,而疲劳闭眼通常持续 1 秒以上。用连续帧计数本质上是做一个时间窗口滤波,窗口长度决定了系统灵敏度。
这里的核心参数有三个:EAR 阈值、连续帧数、冷却时间。下面这张表是按不同灵敏度给出的一组起始值,实际项目里需要通过测试微调:
| 参数 | 保守模式 | 均衡模式 | 灵敏模式 |
|---|---|---|---|
| EAR 闭眼阈值 | 0.20 | 0.25 | 0.28 |
| 连续闭眼帧数 @20fps | 60 | 40 | 25 |
| 冷却时间(秒) | 8 | 5 | 3 |
表格里的“均衡模式”就是前面代码里用的那组参数。需要说明的是,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(target=play_alarm_task, daemon=True).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 分钟 | 0 | 0 | 0 | 0 |
| 闭眼 2 秒片段 | 3 | 3 | 0 | 0 |
| 打哈欠片段 | 2 | 1 | 1 | 0 |
如果误报多,优先调高连续帧数或调低 EAR 阈值;如果漏报多,反向调整。这个调试习惯帮我避免了很多次“换个测试者就翻车”的尴尬,也是疲劳检测系统里性价比最高的验证手段。另外如果想更进一步,可以尝试把每帧的 EAR/MAR 序列送入一维卷积网络做时序分类,但那是后话,先把离线回放跑通,系统才算真正能用。希望帮到你。
本文还有配套的精品资源,点击获取