ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于OpenCV与CNN的驾驶员疲劳检测系统设计与实现

基于OpenCV与CNN的驾驶员疲劳检测系统设计与实现 简介基于卷积神经网络的人脸识别驾驶员疲劳检测与预警系统毕业设计项目面向计算机相关专业毕业生、课程设计及期末大作业学生提供可直接运行与二次开发的完整方案。包内共19个文件包括11个Python源码脚本、2个XML人脸检测级联分类器、2个TXT说明文件、1个预训练模型文件hdf5及1个可执行程序等压缩包约78.33MB目录结构清晰。代码涵盖数据预处理、CNN模型训练与评估、基于haarcascade的人脸与眼部检测、mini_XCEPTION模型疲劳状态判定以及tkinter图形界面交互并配有运行说明与依赖清单便于快速搭建环境。项目经严格调试能直接作为毕业设计使用已有244人学习或下载适合需要完成毕设或提升深度学习项目实战能力的读者。1. 从一版能跑的毕设说起卷积神经网络怎么盯住驾驶员的眼睛这套基于卷积神经网络人脸识别驾驶员疲劳检测与预警系统的毕业设计核心思路一句话能讲完先用 OpenCV 的 Haar 级联器把驾驶员的脸和眼睛从视频帧里框出来再把眼睛小图交给 CNN 网络做睁眼/闭眼二分类最后用滑动窗口统计闭眼比例触发分级报警。它不是一个只能演示 demo 的玩具压缩包里同时带了数据预处理脚本、训练好的 mini-XCEPTION 权重、实时检测主程序和 tkinter 界面完整覆盖了一个图像分类项目从数据到落地的所有环节。适合三类人计算机相关专业做毕设或课程设计的学生想拿现成代码练一遍 CNN 工程流程的 Python 学习者以及需要一套可演示界面去答辩的同学。整套系统跑起来之后你能看到一个摄像头画面、一个状态标签和一个报警开关全部逻辑就发生在你眼皮底下。2. 数据链路与技术选型Haar 人脸框、眼睛 ROI 与 48×48 输入2.1 为什么是 Haar CNN而不是一步到位用 YOLO很多同学拿到题目第一反应是上 YOLO毕竟 YOLO 能直接把人和眼睛一起检测出来。但在这套系统里Haar 级联器 CNN 的组合是更务实的选型原因有三点。第一算力约束。YOLO 在普通笔记本 CPU 上跑实时检测帧率很难保证而驾驶员疲劳检测要求摄像头画面流畅处理Haar 级联器是 OpenCV 自带的经典检测器纯 CPU 也能跑得动实测在 i5 笔记本上处理 640×480 画面能达到 20 帧以上。第二可解释性。毕设答辩时评审老师会问“人脸区域是怎么提取的”Haar 级联器有清晰的滑动窗口 特征积分图原理比一个端到端的黑盒更容易讲清楚。第三任务拆分合理。这套系统真正判断疲劳靠的是眼睛闭合状态而不是整张人脸特征所以只需要 Haar 先定位人脸再在其内部定位眼睛最后让 CNN 判断小图类别。压缩包里自带两个 Haar 模型文件haarcascade_frontalface_default.xml 负责框人脸haarcascade_eye.xml 负责在脸框范围内找眼睛。这种两级级联的做法比直接在全图里找眼睛稳定得多因为眼睛检测对背景非常敏感把搜索范围限制在人脸内部后误检率会直线下降。import cv2 face_cascade cv2.CascadeClassifier(haarcascade_files/haarcascade_frontalface_default.xml) eye_cascade cv2.CascadeClassifier(haarcascade_files/haarcascade_eye.xml)参数上不需要动两个文件都是 OpenCV 官方预训练权重直接用即可。需要注意路径和你执行脚本时的工作目录保持一致我一般会在脚本开头写死绝对路径或者用 os.path.join 拼一次省得换目录后报错。2.2 数据集准备划分、归一化与图像增强压缩包内附带了眼睛开闭数据集原始图片按 open 和 closed 两个类别存放。训练前第一件事就是把数据按 8:2 划分成训练集和验证集split_train_test.py 干的就是这件事。需要注意划分时按类别分别切分避免某一类全部进了验证集导致训练时类别缺失。import os import random import shutil src_dir data/eyes train_dir data/train val_dir data/val val_ratio 0.2 for cls in [open, closed]: files os.listdir(os.path.join(src_dir, cls)) random.shuffle(files) split_idx int(len(files) * (1 - val_ratio)) for f in files[:split_idx]: dst os.path.join(train_dir, cls) os.makedirs(dst, exist_okTrue) shutil.copy(os.path.join(src_dir, cls, f), os.path.join(dst, f)) for f in files[split_idx:]: dst os.path.join(val_dir, cls) os.makedirs(dst, exist_okTrue) shutil.copy(os.path.join(src_dir, cls, f), os.path.join(dst, f))这里 val_ratio0.2 意味着 20% 的数据用于验证随机打乱后按类别复制到不同目录。为什么要用复制而不是移动因为原始数据后续可能还要用来重新切分或做坏样本剔除保留一份原图是给自己留后悔药。数据加载部分 load_and_process.py 用了 Keras 的 ImageDataGenerator它可以在训练时动态做归一化和数据增强每次 epoch 拿到的图都有随机变化能明显抑制过拟合。from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen ImageDataGenerator( rescale1./255, rotation_range10, width_shift_range0.1, height_shift_range0.1, zoom_range0.1, horizontal_flipTrue ) val_datagen ImageDataGenerator(rescale1./255) train_gen train_datagen.flow_from_directory( data/train, target_size(48, 48), batch_size32, class_modecategorical ) val_gen val_datagen.flow_from_directory( data/val, target_size(48, 48), batch_size32, class_modecategorical )几个参数值得细说。rescale1./255 是把像素值从 0-255 缩放到 0-1这是 CNN 训练的标准预处理不做的话网络收敛很慢。rotation_range、width_shift_range 这些增强参数是为了模拟驾驶员头部轻微转动时眼睛位置的偏移但角度不要设太大10 度以内比较合理转多了会把眼睛形变到失真。horizontal_flip 对眼睛图像安全左右眼翻转不影响类别。target_size 用的是 48×48这个尺寸是 mini-XCEPTION 原版论文里用的输入尺寸太小会丢失眼睑轮廓细节太大则增加计算量。2.3 人脸与眼睛提取extract_face.py 的级联用法数据准备好之后推理阶段需要从摄像头帧里实时抠出眼睛区域extract_face.py 承担这个任务。它的核心是先把彩色帧转灰度再做一次直方图均衡化提升对比度然后用人脸级联器定位最后在脸内搜眼睛。import cv2 def extract_eye_roi(frame): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray cv2.equalizeHist(gray) faces face_cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(60, 60) ) for (x, y, w, h) in faces: face_roi gray[y:yh, x:xw] eyes eye_cascade.detectMultiScale( face_roi, scaleFactor1.1, minNeighbors4, minSize(20, 20) ) if len(eyes) 2: ex, ey, ew, eh eyes[0] eye_roi face_roi[ey:eyeh, ex:exew] return cv2.resize(eye_roi, (48, 48)) return None这段代码里有几个调试时常踩的点。detectMultiScale 的 scaleFactor 表示每次缩放图像的比例1.1 意味着每次缩小 10%数值越小检测越精细但速度越慢1.1 是速度和召回率的折中。minNeighbors 控制一个候选框被保留需要满足的邻居框数量值越大漏检越多值越小误检越多5 这个值在全脸检测里比较安全。minSize 设成 60×60 是为了过滤掉距离摄像头很远处的小脸区域避免把后排乘客误判为驾驶员。提示如果只检测到一只眼睛常见做法是直接放弃这一帧而不是强行用单眼判断。因为闭眼状态下 Haar 对眼睛的召回率本来就下降单只眼睛的 ROI 很容易是眉毛或阴影喂给 CNN 只会增加错误分类。提取出的眼睛 ROI 统一 resize 成 48×48这一步必须和训练时保持一致。模型不关心原始眼睛在画面里是大是小只接受固定尺寸输入所有尺寸适配都在这个函数里完成。3. CNN 模型与训练评估mini-XCEPTION 结构、权重文件与评估指标3.1 为什么选 mini-XCEPTION 而不是 VGG眼睛状态分类是一个极度简单的二分类任务闭眼和睁眼在纹理上的差异不需要特别深的网络就能学出来。用 VGG16 甚至 ResNet50 当然能跑但参数量大、推理慢、在几千张的小数据集上极易过拟合。mini-XCEPTION 是 XCEPTION 的轻量版本核心是深度可分离卷积参数量只有同深度普通卷积的十分之一左右在 FER2013 人脸表情数据集上有过很好的验证迁移到眼睛开闭任务上同样合适。另一个实际原因在压缩包里的模型文件名上_mini_XCEPTION.102-0.66.hdf5。102 大概率对应训练轮次0.66 反映的可能是验证集上的某个精度指标。这个数字不算高拿到手可以直接用于演示但如果想写进论文或者追求更高准确率强烈建议用配套脚本重新训练一轮。3.2 cnn.py 网络结构拆解cnn.py 定义了完整的模型结构。上采样部分我直接把它理解为一组带残差风格的卷积堆叠每一层都紧跟 BatchNormalization 和 ReLU中间穿插 MaxPooling 降维最后用 GlobalAveragePooling 替代全连接层。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import ( Conv2D, SeparableConv2D, MaxPooling2D, BatchNormalization, ReLU, Dropout, GlobalAveragePooling2D, Dense ) def mini_xception(input_shape(48, 48, 1), num_classes2): model Sequential([ Conv2D(8, (3, 3), paddingsame, input_shapeinput_shape), BatchNormalization(), ReLU(), SeparableConv2D(8, (3, 3), paddingsame), BatchNormalization(), ReLU(), MaxPooling2D((2, 2), strides(2, 2)), Dropout(0.25), SeparableConv2D(16, (3, 3), paddingsame), BatchNormalization(), ReLU(), SeparableConv2D(16, (3, 3), paddingsame), BatchNormalization(), ReLU(), MaxPooling2D((2, 2), strides(2, 2)), Dropout(0.25), SeparableConv2D(32, (3, 3), paddingsame), BatchNormalization(), ReLU(), SeparableConv2D(32, (3, 3), paddingsame), BatchNormalization(), ReLU(), MaxPooling2D((2, 2), strides(2, 2)), Dropout(0.25), GlobalAveragePooling2D(), Dropout(0.5), Dense(num_classes, activationsoftmax) ]) return model解释几个关键设计。SeparableConv2D 是深度可分离卷积先对每个通道单独做空间卷积再用 1×1 卷积跨通道融合参数量大幅下降的同时保持了特征提取能力。GlobalAveragePooling2D 把最后一层特征图直接平均成一个向量替代 Flatten Dense 的结构这能天然抑制过拟合因为全连接层是参数量最大的地方GAP 直接把它删掉了。Dropout 分布在每个池化层之后训练时随机丢弃一部分神经元让网络不依赖单一路径。输入通道是 1对应灰度图。如果训练时用 ImageDataGenerator 加载的是三通道 RGB 图需要把 input_shape 改成 (48, 48, 3)否则加载预训练权重时维度不匹配会直接报错。3.3 训练配置与评估epoch、batch_size 和混淆矩阵训练脚本的配置在 CNN 项目中属于标准套路。Adam 优化器配交叉熵损失加上 ModelCheckpoint 和 EarlyStopping 两个回调训练过程中自动保存最优权重并在验证集不再提升时提前终止。from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping model mini_xception(input_shape(48, 48, 1), num_classes2) model.compile( optimizeradam, losscategorical_crossentropy, metrics[accuracy] ) callbacks [ ModelCheckpoint(best_weights.hdf5, save_best_onlyTrue, monitorval_accuracy), EarlyStopping(patience10, restore_best_weightsTrue) ] history model.fit( train_gen, validation_dataval_gen, epochs102, batch_size32, callbackscallbacks )batch_size32 在 48×48 的小图上完全够用显存占用很低。epochs 设 102 是参考原项目权重文件的命名习惯实际训练时 EarlyStopping 大概率在 40-60 轮就停下来了。关键是 monitor 参数盯着 val_accuracy只保存验证集上表现最好的那一轮权重而不是最后一轮的。评估阶段 evaluate.py 里不能只看 accuracy 一个数字。疲劳检测的特殊性在于漏报比误报严重得多——把闭眼判成睁眼意味着驾驶员已经在打瞌睡但系统没报警把睁眼判成闭眼最多是频繁误报让人心烦。所以评估时我建议额外看混淆矩阵里 false negative 的行也就是实际闭眼但被预测为睁眼的样本数。from sklearn.metrics import confusion_matrix, classification_report y_true [] y_pred [] for images, labels in val_gen: preds model.predict(images) y_true.extend(labels.argmax(axis1)) y_pred.extend(preds.argmax(axis1)) if len(y_true) val_gen.samples: break print(confusion_matrix(y_true, y_pred)) print(classification_report(y_true, y_pred, target_names[open, closed]))注意如果 val_gen 是无限循环的生成器for 循环必须带 break 条件否则会在验证集耗尽后从头开始导致评估结果虚高。4. 实时检测与预警主流程baojin.py 主循环、滑动窗口与 tkinter 界面4.1 detect_class.py单帧推理封装实时检测的第一步是把前面所有环节封装成一个可复用的函数。detect_class.py 做的事可以概括为输入一帧画面输出眼睛状态和置信度。逻辑上它把 extract_eye_roi 和模型预测串在一起并对模型输出做归一化处理。import cv2 import numpy as np from tensorflow.keras.models import load_model model load_model(models/_mini_XCEPTION.102-0.66.hdf5) def classify_eye(eye_roi): if eye_roi is None: return -1, 0.0 img cv2.resize(eye_roi, (48, 48)) img img.astype(float32) / 255.0 img np.expand_dims(img, axis0) img np.expand_dims(img, axis-1) prob model.predict(img, verbose0)[0] label int(np.argmax(prob)) confidence float(prob[1]) if label 1 else float(prob[0]) return label, confidence几个细节。predict 前要 expand_dims 两次第一次加 batch 维第二次加通道维得到 (1, 48, 48, 1) 的四维张量这是 Keras 模型输入的标准形状。predict 里的 verbose0 是关掉预测日志否则每帧都会往控制台刷一行进度条主循环会被拖慢。返回值 label 为 0 表示睁眼1 表示闭眼confidence 返回的是对当前预测结果的置信度后面可以用它做二次过滤置信度低于 0.6 的帧直接丢弃不参与疲劳统计。4.2 baojin.py疲劳判定策略与报警规则疲劳判定是整系统的核心逻辑。单帧的睁眼闭眼结果噪声很大直接拿一帧结果做报警会频繁误触发所以 baojin.py 里用了滑动窗口统计闭眼比例这其实就是 PERCLOS 指标的简化实现——在一段固定时间窗口内统计眼睛闭合时间占比超过阈值就判定为疲劳。from collections import deque WINDOW_SIZE 30 # 窗口帧数约等于 1 秒 30fps CLOSE_RATIO_THRESHOLD 0.4 # 闭眼比例阈值 ALARM_FRAME 8 # 连续判定疲劳多少次后触发强报警 state_queue deque(maxlenWINDOW_SIZE) consecutive_closed 0 alarm_level 0 while cap.isOpened(): ret, frame cap.read() if not ret: continue eye extract_eye_roi(frame) if eye is None: continue state, conf classify_eye(eye) if conf 0.6: continue state_queue.append(state) if len(state_queue) WINDOW_SIZE: continue close_ratio sum(state_queue) / WINDOW_SIZE if close_ratio CLOSE_RATIO_THRESHOLD: consecutive_closed 1 else: consecutive_closed 0 if consecutive_closed ALARM_FRAME: alarm_level 2 elif close_ratio CLOSE_RATIO_THRESHOLD: alarm_level 1 else: alarm_level 0这段逻辑里有三个参数需要按实际场景调。WINDOW_SIZE30 表示在 30 帧内统计按 30fps 算正好是 1 秒这个窗口太短会放大瞬时抖动太长则反应迟钝。CLOSE_RATIO_THRESHOLD0.4 意味着窗口内 40% 的时间闭眼就判定疲劳这个值参考了 PERCLOS 研究中常用的 P80 标准。ALARM_FRAME8 是防抖锁连续 8 帧都满足闭眼条件才触发强报警避免抽烟、揉眼睛等短暂遮挡造成误报。报警响应可以用 winsound 让系统发出蜂鸣声也可以串一个 LED 或者语音模块这里给出最简单的实现import winsound if alarm_level 2: winsound.Beep(1000, 300) # 1000Hz持续300ms else: passwinsound 只在 Windows 下可用如果需要跨平台可以换 pygame.mixer 或播放一个预录制 wav 文件。Beep 的前两个参数一个是频率一个是时长1000Hz 在嘈杂的车内环境也比较容易被注意到。4.3 tkinter_UI.py一屏展示检测过程的演示界面编程语言的选型里tkinter 是 Python 自带的 GUI 库不需要额外安装打包体积也小用在毕设答辩演示里完全够用。tkinter_UI.py 的界面布局很直接摄像头画面放在正中间下方是检测状态文字右侧是开始和停止按钮代码结构如下。import tkinter as tk from PIL import Image, ImageTk class FatigueApp: def __init__(self, window): self.window window self.window.title(驾驶员疲劳检测系统) self.video_label tk.Label(window) self.video_label.pack() self.status_label tk.Label( window, text状态等待启动, font(微软雅黑, 14) ) self.status_label.pack() self.start_btn tk.Button(window, text开始检测, commandself.start) self.start_btn.pack(sidetk.LEFT) self.stop_btn tk.Button(window, text停止, commandself.stop) self.stop_btn.pack(sidetk.RIGHT) self.cap None def start(self): self.cap cv2.VideoCapture(0) self.update_frame() def update_frame(self): ret, frame self.cap.read() if ret: state, _ classify_eye(extract_eye_roi(frame)) self.status_label.config( text状态 (闭眼 if state 1 else 睁眼) ) frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img Image.fromarray(frame) imgtk ImageTk.PhotoImage(imageimg) self.video_label.imgtk imgtk self.video_label.configure(imageimgtk) self.window.after(10, self.update_frame) def stop(self): if self.cap: self.cap.release()这里有个非常容易被新手忽略的坑ImageTk.PhotoImage 对象如果没有被保存为实例属性Python 的垃圾回收机制会在函数退出时把它销毁画面显示不出来。所以代码里特意写了 self.video_label.imgtk imgtk把这个引用挂在 Label 上阻止回收。刷新用的是 window.after(10, self.update_frame) 而不是 while 循环加 time.sleep。after 是 tkinter 的事件循环调度器它不会阻塞主线程界面按钮还能正常响应如果在 update_frame 里用 while 死循环窗口会直接卡死点关闭都没反应。压缩包里的 tkinter_UI.exe 就是用 PyInstaller 把整个项目打包出来的成品答辩现场如果怕 IDE 出幺蛾子可以直接双击 exe 演示。5. 复现避坑指南五个最容易翻车的现场与参数修正5.1 摄像头索引错乱画面一片黑现象代码里写死 cv2.VideoCapture(0)运行时画面黑屏或者直接报错 -215。原因笔记本的摄像头索引不是固定的。很多游戏本带双摄像头0 是红外摄像头1 才是日常使用的 RGB 摄像头。虚拟机里有时 0 是虚拟摄像头1 才是宿主机透传的物理摄像头。这个问题换一台机器就翻车一次。解决写一个两行的小脚本循环尝试所有索引找到能读到画面的那个再把 baopin.py 里的索引改成对应的值。import cv2 for idx in range(3): cap cv2.VideoCapture(idx) ret, frame cap.read() if ret: print(f可用索引: {idx}) break cap.release()5.2 暗光环境下框不到眼睛后端直接断数据现象白天室内正常模拟夜间驾驶场景时光线变暗系统长时间没有任何状态输出因为 extract_eye_roi 返回的永远是 None。原因Haar 级联器对光照非常敏感低对比度下人脸和眼睛的特征响应值都会下降detectMultiScale 一个框都返回不出来。解决在转灰度之后先做直方图均衡化把对比度拉开代码在 2.3 节里已经写了。如果还漏检把眼睛检测的 minNeighbors 从 5 降到 3scaleFactor 从 1.1 降到 1.05。代价是误检会变多但误检的框往往在后续 CNN 分类阶段会被低置信度过滤掉所以整体效果反而更稳。5.3 换电脑后 hdf5 模型加载报错现象在原来项目环境里跑得好好的换一台电脑装好依赖后 load_model 直接抛异常报 Unknown layer 或者 Op type not registered。原因hdf5 权重文件对 TensorFlow/Keras 的版本敏感。原项目用的可能是 TensorFlow 2.6 左右的老版本新装的环境是 TensorFlow 2.16SeparableConv2D 这一层在新版本里注册名变了旧权重读不出来。解决两个方案。方案 A 是按 requirements.txt 里锁定的版本重建虚拟环境这是最省心的。方案 B 是不想折腾环境的话加载时加 compileFalse 绕开编译图再手动 compile 一次from tensorflow.keras.models import load_model model load_model( models/_mini_XCEPTION.102-0.66.hdf5, compileFalse ) model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy])方案 B 只解决加载问题如果某些层在新版本里已经被移除还是得靠方案 A。另外提醒一句项目里残留的pycache目录如果在拷贝时一起带走里面会有旧版本的字节码文件不影响运行但会干扰排查建议删掉再分发。5.4 报警状态来回跳睁眼闭眼乱切现象驾驶员正常睁眼开车系统隔几秒报一次疲劳蜂鸣器响一声又停再响一声。原因单帧分类的置信度本身有波动即使人眼明显睁着某一帧的闭眼概率也可能冲到 0.6 以上。病人监控的经典问题就是没有做时间维度上的平滑。解决两层防抖第一层是置信度过滤置信度低于 0.6 的帧直接丢弃第二层是连续帧计数也就是 4.2 节里的 consecutive_closed 逻辑。真正的报警必须连续多帧满足条件才触发单帧异常不改变状态。我在实际调参时还会加一个 3 秒的报警锁定期一旦触发强报警3 秒内不再重复报警避免蜂鸣器吵得人没法开车。5.5 自己训练准确率卡在 0.6 附近上不去现象用配套脚本重新训练训练集 accuracy 一路上升到 0.9验证集 accuracy 卡在 0.6 不动和模型文件名里的 0.66 差不多。原因验证集准确率上不去并且和训练集差距大说明过拟合但更常见的是数据本身有标签噪声。眼睛开闭数据集里大量存在“半睁眼”样本人眼标注时归为闭眼还是睁眼本来就存在主观分歧CNN 学不到一个稳定边界。解决第一步统计 open 和 closed 两个类别的样本数差的超过 1.5 倍就要做数据增广补齐。第二步把置信度在 0.4-0.6 之间难以判断的样本抽出来人工看一遍剔除掉那些明显标错的图。第三步把学习率从默认的 1e-3 降到 1e-4 再训练十个 epoch让模型在已有基础上微调而不是大步乱跳。做完这三步验证集准确率通常能提 3-5 个百分点。6. 进阶验证用 EAR 眼动比值对照 CNN 分类结果这套系统跑通之后你会开始怀疑一个问题CNN 说闭眼了真相真的是闭眼了吗单靠验证集准确率回答不了这个问题因为验证集和训练集同分布模型在里面犯错有规律。更好的做法是引入一个和 CNN 完全无关的生理指标做交叉验证。眼睛纵横比 EAREye Aspect Ratio就是这样一个指标它从人脸关键点中取眼睛轮廓的六个点用两个欧氏距离的比值描述眼睛张开程度闭眼时 EAR 会掉到阈值以下。EAR (||p2 - p6|| ||p3 - p5||) / (2 * ||p1 - p4||)p1-p6 是人脸 68 关键点中眼睛轮廓的六个标注点p1 和 p4 分别对应内外眼角p2-p3 对应上眼睑p5-p6 对应下眼睑。具体实现需要 dlib 的人脸关键点模型import dlib def eye_aspect_ratio(eye): a np.linalg.norm(eye[1] - eye[5]) b np.linalg.norm(eye[2] - eye[4]) c np.linalg.norm(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)拿到 EAR 之后再和 CNN 的输出做对照。你可以自己录一段 30 秒视频手动标注每一帧眼睛状态然后分别用 EAR 阈值和 CNN 二分类跑一遍算两个系统的 F1 分数。如果 EAR 判闭眼的帧 CNN 基本都判闭眼说明两个独立方法互相印证系统可靠如果大量不一致优先检查数据增强是否改变了眼睑形态。EAR 方案本身也可以作为疲劳检测的替代路线它和 CNN 的对比很直观对比项CNN 二分类EAR 阈值所需模型mini-XCEPTION 权重68 点关键点模型算力消耗中低光照影响中中头部侧转较鲁棒敏感侧脸关键点丢失可解释性黑盒几何指标易解释这个方法验证的顺序我建议放在主程序跑通之后、写论文实验章节之前因为它的结果会直接影响你对模型的信任度。从那以后我每次换电脑跑这个项目都强制走一遍三件事确认摄像头索引、核对模型输入尺寸、先录十秒视频再谈实时检测。这套流程让我在答辩前躲过不少冤枉路希望帮到你。本文还有配套的精品资源点击获取
返回列表