简介:本资源是一套基于Python卷积神经网络的人脸识别驾驶员疲劳检测与预警系统,面向计算机、人工智能相关专业的毕业设计、期末大作业与课程设计场景,适合具备一定Python基础、希望快速完成高分项目的学生使用。压缩包共20个文件,约78.33MB,包含11个py源码文件、3个txt说明文档、2个xml人脸检测模型、1个hdf5训练权重、1个exe可执行程序及md说明等,覆盖模型训练、人脸提取、疲劳判别与界面交互等模块。已有72人学习下载。项目代码注释完整,新手也能看懂,附带系统说明与运行说明,部署后即可直接运行;其中tkinter界面美观、操作简单,CNN模型与检测流程清晰,可作为毕设或课程设计的完整方案,也便于在此基础上二次开发与功能扩展。
1. 从一张打哈欠的抓拍说起:这套疲劳检测系统到底在做什么
凌晨两点跑长途的司机被车内摄像头抓拍到连续闭眼 1.8 秒,仪表台立刻响起蜂鸣,同时手机端收到一条预警——这不是概念视频,而是基于 Python 卷积神经网络人脸识别驾驶员疲劳检测与预警系统最典型的落地场景。它要解决的核心问题很具体:用普通摄像头替代昂贵的红外眼动仪,靠 CNN 从人脸区域里判断眼睛开合、嘴巴张合,再叠加时间窗口统计,输出疲劳等级并触发预警。整套东西的技术栈是 Python + OpenCV + 卷积神经网络,适合做毕业设计、课程设计,也适合想入门深度学习落地的新手。它不追求工业级鲁棒性,但胜在链路完整、可复现、能讲清楚每一环为什么这么设计。下面我按自己搭过一遍的顺序,把选型、数据、模型、预警逻辑和踩过的坑讲透。
2. 为什么选 CNN 而不是传统特征:人脸疲劳检测的技术选型
2.1 传统 HOG+SVM 方案在疲劳场景下为什么容易翻车
早期做疲劳检测,很多人第一反应是 HOG 特征加 SVM 分类器,或者用 dlib 的 68 点关键点算眼睛纵横比(EAR)。这套方法在实验室光照稳定、头部姿态固定的条件下能跑出不错的结果,但一上车就原形毕露。原因有三:第一,HOG 对光照变化极其敏感,隧道进出口那种明暗突变会让特征分布整体漂移;第二,关键点检测依赖人脸对齐,司机侧脸、戴眼镜、低头看仪表盘时关键点直接丢失,EAR 值就成了噪声;第三,EAR 是手工设计的几何比值,它没法区分「正常眨眼」和「疲劳性微睡眠」,因为两者在单帧上的眼睛开合度可能一样,差别在时间维度的持续性和上下文。
CNN 的价值在于它把特征提取和分类揉进一个可学习的网络里。卷积核在训练中自动学到对眼睛闭合、嘴角下拉、眉间皱起这些疲劳相关纹理的响应,不需要你手写规则。更关键的是,CNN 对局部形变和光照有更强的容忍度,配合数据增强后,侧脸和戴眼镜的漏检率能明显压下来。这不是说 CNN 一定比传统方法准,而是在「摄像头位置不固定、光照不可控、司机姿态随机」的真实场景里,CNN 的鲁棒性上限更高,调参空间也更大。
2.2 用 Python 搭 CNN 疲劳检测的最小依赖清单
动手之前先把环境理清楚。我一般用 Python 3.8 到 3.10 之间的版本,太新的版本有些库轮子还没跟上,装起来会折腾。核心依赖就几个:OpenCV 负责读摄像头和做人脸检测,NumPy 做矩阵运算,TensorFlow 或 PyTorch 二选一搭 CNN,Matplotlib 用来画训练曲线。如果你只是想快速跑通,建议先用 TensorFlow/Keras,它的 Sequential API 写起来短,调试直观。
# 创建虚拟环境,避免污染系统 Python python -m venv fatigue_env # Windows 激活 fatigue_env\Scripts\activate # Linux/Mac 激活 source fatigue_env/bin/activate # 安装核心依赖,版本按自己环境微调 pip install opencv-python==4.8.0.74 pip install numpy==1.24.3 pip install tensorflow==2.13.0 pip install matplotlib==3.7.2 pip install scikit-learn==1.3.0这段命令的逻辑是:先隔离环境,再按「图像采集 → 数值计算 → 模型训练 → 可视化 → 评估」的顺序装库。参数上,opencv-python 选 4.8 是因为它的 DNN 模块对 Caffe 模型支持稳定,后面做人脸检测会用到;TensorFlow 2.13 是最后一个默认带 Keras 且对 CUDA 11.8 支持较好的版本,如果你没有 GPU,装 CPU 版也能跑,只是训练慢。装完用python -c "import cv2, tensorflow"验证一下,没报错就说明环境通了。
提示:如果你用的是 Apple Silicon 的 Mac,TensorFlow 要装 tensorflow-macos 和 tensorflow-metal,否则跑起来会退回 CPU,训练速度差好几倍。
2.3 人脸检测和 CNN 分类的分工怎么划
很多人一开始会把「人脸检测」和「疲劳分类」混在一起,想用一个网络端到端搞定。理论上可行,但实操里不划算。人脸检测用 OpenCV 自带的 Haar 级联或者 DNN 模块就够了,它的任务是框出人脸区域,把无关背景裁掉,降低后续 CNN 的输入噪声。CNN 只负责在裁好的人脸小图上判断「睁眼/闭眼」「张嘴/闭嘴」这类二分类。这样分工的好处是:人脸检测模型可以换,CNN 分类器可以单独训练和调优,两边解耦,出问题好定位。
我一般用 OpenCV 的 DNN 人脸检测器,加载 res10_300x300 的 Caffe 模型,它的速度和精度比 Haar 级联好不少,尤其在侧脸和暗光下。检测到人脸后,按比例裁出上半部分(眼睛和嘴巴都在这个区域),缩放到 64x64 或 96x96,送进 CNN。这个尺寸是权衡后的结果:太小会丢眼睛细节,太大推理慢,64x64 在树莓派上也能跑到 15 帧以上。
3. 数据集怎么准备:从公开数据到自采样本的完整流程
3.1 公开疲劳数据集的选用和局限
做毕业设计最省事的起点是公开数据集。常见的有 CEW(Closed Eyes in the Wild)和 ZJU 眨眼数据集,前者有几千张闭眼和睁眼图,后者是视频片段。CEW 的好处是干净、标注明确,坏处是它只覆盖眼睛状态,没有张嘴打哈欠的样本,而且都是正面人脸,缺少侧脸和戴眼镜的多样性。如果你只用 CEW 训练,模型在真实车内场景的泛化会打折扣。
我的做法是:用 CEW 做预训练,让 CNN 先学会区分睁眼闭眼的基本纹理,然后再用自己采集的样本做微调。自采样本不需要多,找个同学坐在工位上,用笔记本摄像头录几段视频,分别做「正常睁眼」「闭眼 2 秒」「打哈欠」「说话」这几个动作,每段 30 秒左右,总共几百帧就够。关键是覆盖不同光照和角度,比如白天靠窗、晚上开台灯、侧脸 30 度,这些多样性比数量更重要。
3.2 用 OpenCV 批量裁剪人脸并生成训练集
拿到视频后,第一步是把帧拆出来,检测人脸,裁出眼睛和嘴巴区域,按类别存到不同文件夹。下面这段脚本是我常用的批处理模板,它会遍历视频文件,用 DNN 检测器框人脸,然后按比例裁上下两半,分别存成睁眼/闭眼和张嘴/闭嘴的候选图。
import cv2 import os import numpy as np # 加载 OpenCV DNN 人脸检测器 prototxt = "deploy.prototxt" model = "res10_300x300_ssd_iter_140000.caffemodel" net = cv2.dnn.readNetFromCaffe(prototxt, model) def extract_faces(video_path, out_dir, label): cap = cv2.VideoCapture(video_path) os.makedirs(out_dir, exist_ok=True) idx = 0 while True: ret, frame = cap.read() if not ret: break h, w = frame.shape[:2] blob = cv2.dnn.blobFromImage(frame, 1.0, (300, 300), (104.0, 177.0, 123.0)) net.setInput(blob) detections = net.forward() for i in range(detections.shape[2]): confidence = detections[0, 0, i, 2] if confidence > 0.5: # 置信度阈值,低于 0.5 的框丢弃 box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) x1, y1, x2, y2 = box.astype("int") # 裁上半脸(眼睛区域)和下半脸(嘴巴区域) face = frame[max(0, y1):min(h, y2), max(0, x1):min(w, x2)] if face.size == 0: continue fh = face.shape[0] eye_region = face[0:int(fh * 0.5), :] mouth_region = face[int(fh * 0.5):fh, :] # 统一缩放到 64x64 eye_resized = cv2.resize(eye_region, (64, 64)) mouth_resized = cv2.resize(mouth_region, (64, 64)) cv2.imwrite(f"{out_dir}/{label}_eye_{idx}.jpg", eye_resized) cv2.imwrite(f"{out_dir}/{label}_mouth_{idx}.jpg", mouth_resized) idx += 1 # 每 5 帧取一帧,避免相邻帧太相似 cap.set(cv2.CAP_PROP_POS_FRAMES, cap.get(cv2.CAP_PROP_POS_FRAMES) + 4) cap.release() print(f"{video_path} 处理完成,共输出 {idx} 组样本") # 按类别调用,label 用 open/closed/fatigue 等 extract_faces("normal_drive.mp4", "dataset/open", "open") extract_faces("drowsy_drive.mp4", "dataset/closed", "closed")这段代码的关键参数有三个:confidence > 0.5是检测置信度阈值,调高会漏掉侧脸,调低会引入误检框,0.5 是实测比较平衡的值;int(fh * 0.5)是上下脸的分割比例,眼睛大致在上半部分,嘴巴在下半部分,这个比例对大多数正面人脸够用,侧脸时需要微调;cap.set那行是跳帧采样,因为相邻帧几乎一样,全采会导致训练集冗余,每 5 帧取一帧能让样本分布更均匀。跑完之后你会得到一堆 64x64 的小图,接下来要人工过一遍,把明显裁歪的、模糊的删掉,这一步不能省,脏数据对 CNN 的伤害比数据少更大。
3.3 数据增强和类别不平衡的处理
自采样本很容易出现类别不平衡,比如正常睁眼的帧远多于闭眼帧。直接拿去训练,模型会倾向于预测多数类,闭眼召回率上不去。我的处理方式是两步:先用数据增强把少数类扩增,再用类别权重在损失函数里补偿。增强用 Keras 的 ImageDataGenerator 就行,对眼睛区域做水平翻转、±10 度旋转、亮度微调,不要做垂直翻转,因为倒过来的眼睛在现实中不存在,会引入噪声。
from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen = ImageDataGenerator( rescale=1./255, # 像素归一化到 0-1 rotation_range=10, # 旋转 ±10 度 width_shift_range=0.1, # 水平平移 10% height_shift_range=0.1, # 垂直平移 10% brightness_range=[0.8, 1.2], # 亮度扰动 horizontal_flip=True, # 水平翻转 fill_mode='nearest' # 填充边缘像素 ) train_generator = train_datagen.flow_from_directory( 'dataset/train', target_size=(64, 64), batch_size=32, class_mode='binary' )参数上,rotation_range=10是保守值,再大可能把眼睛转出合理范围;brightness_range模拟车内明暗变化;fill_mode='nearest'保证旋转后边缘不留黑边。类别权重可以在model.fit里用class_weight参数传,比如闭眼类权重设为 2.0,睁眼类设为 1.0,让损失函数更关注少数类。这两招配合下来,闭眼召回率通常能从 70% 出头提到 85% 以上。
4. 卷积神经网络模型怎么搭:从 LeNet 到轻量级自定义结构
4.1 为什么不用 ResNet50 这种大模型
毕业设计里常见的一个误区是直接搬 ResNet50 或 VGG16,觉得网络越深效果越好。但在疲劳检测这个任务上,输入是 64x64 的小图,类别只有两三类,大模型的参数量严重过剩,训练慢、容易过拟合,部署到树莓派或手机端更是跑不动。我一般用一个 4 层卷积加 2 层全连接的自定义结构,参数量控制在 50 万以内,在 CPU 上单帧推理不到 10 毫秒,精度和 ResNet50 微调后差不了两个点。
这个结构的思路是:前两层卷积核小一点(3x3),抓边缘和纹理;后两层卷积核感受野大一些,抓眼睛整体形状和嘴巴开合;每层卷积后接 BatchNormalization 和 MaxPooling,BN 加速收敛,Pooling 降维。全连接层前加 Dropout,比例 0.5,防止过拟合。输出层用 Sigmoid 做二分类,如果要同时判断眼睛和嘴巴状态,就改成两个分支各输出一个 Sigmoid。
4.2 用 Keras 定义并编译疲劳分类网络
from tensorflow.keras import layers, models, optimizers def build_fatigue_cnn(input_shape=(64, 64, 3)): model = models.Sequential([ # 第一层卷积:32 个 3x3 核,抓基础纹理 layers.Conv2D(32, (3, 3), activation='relu', padding='same', input_shape=input_shape), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), # 第二层卷积:64 个 3x3 核,抓局部形状 layers.Conv2D(64, (3, 3), activation='relu', padding='same'), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), # 第三层卷积:128 个 3x3 核,抓眼睛嘴巴整体模式 layers.Conv2D(128, (3, 3), activation='relu', padding='same'), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), # 第四层卷积:128 个 3x3 核,进一步抽象 layers.Conv2D(128, (3, 3), activation='relu', padding='same'), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dropout(0.5), # 丢弃 50% 神经元,防过拟合 layers.Dense(256, activation='relu'), layers.Dropout(0.3), layers.Dense(1, activation='sigmoid') # 二分类输出 ]) return model model = build_fatigue_cnn() model.compile( optimizer=optimizers.Adam(learning_rate=1e-3), loss='binary_crossentropy', metrics=['accuracy'] ) model.summary()这段代码里,padding='same'保证卷积后尺寸不变,方便堆叠;BatchNormalization放在卷积和激活之后、Pooling 之前,能让训练更稳;Dropout(0.5)在全连接层前,是防过拟合的主力;输出层sigmoid把值压到 0 到 1,大于 0.5 判为闭眼或疲劳。编译时用 Adam,学习率 1e-3 是起点,如果训练损失震荡就降到 1e-4。binary_crossentropy是二分类的标准损失,如果你要三分类(正常/闭眼/打哈欠),就把输出层改成 3 个神经元的 softmax,损失换成 categorical_crossentropy。
4.3 训练时的回调配置和早停策略
训练不是跑满 epoch 就好,过拟合往往在验证损失开始上升时就发生了。我一般配三个回调:ModelCheckpoint 保存验证集上最好的权重,EarlyStopping 在验证损失连续 5 轮不降时停,ReduceLROnPlateau 在学习停滞时把学习率砍半。这三个配合,能省掉大量手动调参时间。
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau callbacks = [ ModelCheckpoint('best_fatigue_model.h5', monitor='val_loss', save_best_only=True, verbose=1), EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True, verbose=1), ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=3, min_lr=1e-6, verbose=1) ] history = model.fit( train_generator, epochs=50, validation_data=val_generator, callbacks=callbacks, class_weight={0: 1.0, 1: 2.0} # 闭眼类权重加倍 )patience=5表示验证损失 5 轮不降就停,这个值太小会早停,太大会浪费算力,5 是经验值。factor=0.5是学习率衰减比例,min_lr=1e-6是下限,防止学习率降到 0。class_weight里给闭眼类 2.0 的权重,是因为闭眼样本通常少,加权后模型会更重视漏检闭眼的代价。训练完把best_fatigue_model.h5留下来,后面推理用这个,不要用最后一轮的权重。
5. 预警逻辑怎么设计:从单帧判断到时间窗口统计
5.1 单帧分类结果为什么不能直接触发预警
CNN 输出的是单帧的闭眼概率,但疲劳是一个时间累积的概念。正常眨眼也会闭眼,持续 0.1 到 0.3 秒,如果单帧闭眼就报警,司机每眨一次眼就被吵一次,系统根本没法用。所以必须加时间窗口统计。我的做法是维护一个长度为 30 帧的滑动窗口(按 15 帧每秒算,约 2 秒),统计窗口内闭眼帧的占比,超过 70% 才判定为疲劳性闭眼,触发一级预警。如果连续闭眼帧数超过 45 帧(约 3 秒),直接触发二级预警,蜂鸣加语音提示。
打哈欠的判断类似,但窗口更长,因为一个哈欠持续 2 到 4 秒。我用 60 帧窗口统计张嘴帧占比,超过 60% 判为哈欠,连续 3 次哈欠触发预警。眼睛和嘴巴两个通道的预警可以叠加,比如闭眼占比高且哈欠频繁,就提升预警等级。这套逻辑不复杂,但它是把 CNN 的单帧输出变成可用系统的关键一步,少了这层,模型再准也没法落地。
5.2 用 OpenCV 串起摄像头、CNN 推理和蜂鸣预警
下面这段是主循环的骨架,它把摄像头采集、人脸检测、CNN 分类、滑动窗口统计和声音预警串在一起。实际跑的时候,CNN 推理和图像显示要分线程,否则显示会卡,但为了讲清楚逻辑,这里先写单线程版本。
import cv2 import numpy as np from tensorflow.keras.models import load_model import winsound # Windows 蜂鸣,Linux 用 os.system('beep') model = load_model('best_fatigue_model.h5') face_net = cv2.dnn.readNetFromCaffe("deploy.prototxt", "res10_300x300_ssd_iter_140000.caffemodel") eye_window = [] # 滑动窗口,存 0/1 MOUTH_WINDOW_SIZE = 60 EYE_WINDOW_SIZE = 30 EYE_THRESHOLD = 0.7 # 闭眼占比阈值 yawn_count = 0 cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break h, w = frame.shape[:2] blob = cv2.dnn.blobFromImage(frame, 1.0, (300, 300), (104.0, 177.0, 123.0)) face_net.setInput(blob) detections = face_net.forward() for i in range(detections.shape[2]): if detections[0, 0, i, 2] > 0.5: box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) x1, y1, x2, y2 = box.astype("int") face = frame[max(0, y1):min(h, y2), max(0, x1):min(w, x2)] if face.size == 0: continue fh = face.shape[0] eye = cv2.resize(face[0:int(fh*0.5), :], (64, 64)) mouth = cv2.resize(face[int(fh*0.5):fh, :], (64, 64)) # 归一化后送 CNN eye_input = np.expand_dims(eye / 255.0, axis=0) mouth_input = np.expand_dims(mouth / 255.0, axis=0) eye_pred = model.predict(eye_input, verbose=0)[0][0] mouth_pred = model.predict(mouth_input, verbose=0)[0][0] # 更新滑动窗口,1 表示闭眼/张嘴 eye_window.append(1 if eye_pred > 0.5 else 0) if len(eye_window) > EYE_WINDOW_SIZE: eye_window.pop(0) # 疲劳判定 if len(eye_window) == EYE_WINDOW_SIZE: closed_ratio = sum(eye_window) / EYE_WINDOW_SIZE if closed_ratio > EYE_THRESHOLD: cv2.putText(frame, "FATIGUE ALERT", (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3) winsound.Beep(1000, 500) # 1000Hz 响 500ms cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imshow('Fatigue Detection', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()这段代码里,EYE_WINDOW_SIZE=30对应约 2 秒窗口,EYE_THRESHOLD=0.7是闭眼占比阈值,这两个值要根据实际帧率调。如果你的摄像头是 30 帧每秒,窗口要翻倍到 60,否则时间窗口会缩短一半。winsound.Beep是 Windows 专用,Linux 下可以用os.system('echo -e "\a"')或者 pygame 播放音频。推理部分每帧调两次model.predict,在 CPU 上大概 20 毫秒,加上人脸检测,整体能跑到 20 帧以上,够实时用。
注意:
model.predict每次调用有固定开销,如果帧率上不去,可以把眼睛和嘴巴的输入拼成一个 batch 一次推理,能省一半时间。
5.3 预警等级和误报抑制的实用参数
预警不能只有「响」和「不响」两档,实际用起来需要分级。我一般分三级:一级是闭眼占比超阈值但未持续,只在屏幕上标红;二级是持续闭眼超 2 秒,蜂鸣短响;三级是持续闭眼超 3 秒或连续哈欠 3 次,蜂鸣加语音。误报抑制上,除了滑动窗口,还可以加一个「冷却时间」,触发预警后 5 秒内不再重复触发,避免蜂鸣一直响。另外,如果检测到人脸丢失超过 1 秒,清空滑动窗口,因为司机可能转头了,旧数据不再有参考价值。这些参数没有标准答案,要拿真人测试,记录误报和漏报的次数,再回头调阈值。
6. 避坑与排查:这套系统最容易翻车的五个地方
6.1 摄像头帧率不稳导致时间窗口失真
现象是预警时灵时不灵,同样的闭眼动作有时触发有时不触发。原因是滑动窗口按帧数算,但摄像头实际帧率会波动,光线暗时自动降帧,30 帧的窗口可能变成 4 秒而不是 2 秒。解决办法是不要按帧数,改按时间戳统计,记录每帧的time.time(),窗口只保留最近 2 秒内的帧,这样帧率变化不影响判定。改完之后预警一致性会好很多。
6.2 戴眼镜反光让闭眼被误判成睁眼
现象是戴眼镜的测试者闭眼时系统不报警。原因是眼镜片反光在眼睛区域形成高亮斑块,CNN 把高亮当成睁眼的眼白。解决办法有两个:一是在训练集里加入戴眼镜的样本,让模型学会忽略反光;二是在预处理时做自适应直方图均衡化(CLAHE),压一下高光。我一般两个都做,CLAHE 用 OpenCV 的cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)),对灰度图处理后转回 RGB,能明显改善反光场景。
6.3 模型在训练集上 99% 但实车漏检严重
现象是训练日志里准确率很高,一上车就频繁漏检。原因是训练集和真实场景的分布差异太大,公开数据集多是正面、均匀光照,车内是侧光、阴影、头部晃动。解决办法是拿真实车内视频做微调,哪怕只有几百帧,冻结前面卷积层,只训练全连接层,学习率调到 1e-4,跑 10 个 epoch,泛化会明显提升。另外检查一下输入归一化是否一致,训练时除以 255,推理时忘了除,这种低级错误也常导致漏检。
6.4 多线程下 OpenCV 显示卡死
现象是加了推理线程后,cv2.imshow窗口无响应。原因是 OpenCV 的 GUI 必须在主线程调用,推理放在子线程后,主线程被cap.read()阻塞。解决办法是把读取摄像头也放子线程,主线程只负责imshow和waitKey,线程间用queue.Queue传帧。或者更简单,不用多线程,把 CNN 输入尺寸从 64 降到 48,推理时间减半,单线程也能跑顺。
6.5 蜂鸣预警在 Linux 上没声音
现象是 Windows 上winsound.Beep正常,移到 Linux 或树莓派上没反应。原因是winsound是 Windows 专属模块。解决办法是换 pygame 播放 wav 文件,或者用os.system('aplay alert.wav')。树莓派上还可以接一个 GPIO 蜂鸣器,用 RPi.GPIO 控制,比音频更可靠。跨平台的话,建议一开始就用 pygame,虽然多装一个库,但省得后面移植时改代码。
7. 把预警从「响一声」做到「可追溯」:日志回放和阈值自整定
系统能跑起来之后,真正拉开差距的是可追溯性。我后来养成一个习惯:每次预警触发时,把前后各 5 秒的帧存成一个小视频片段,文件名带上时间戳和触发等级,同时把闭眼占比、哈欠次数、帧率写进 CSV。这样出问题可以回放,看是误报还是漏报,比盯着实时画面猜靠谱得多。存视频用cv2.VideoWriter,编码用 MJPG,5 秒 640x480 大概 2MB,跑一天也就几百 MB,完全可接受。
import csv import time # 预警触发时记录 def log_alert(level, closed_ratio, yawn_count, fps): with open('alert_log.csv', 'a', newline='') as f: writer = csv.writer(f) writer.writerow([time.strftime('%Y-%m-%d %H:%M:%S'), level, round(closed_ratio, 3), yawn_count, round(fps, 1)]) # 回放时按时间戳找对应视频片段 # 视频命名格式:alert_20250101_143022_level2.avi阈值自整定是另一个进阶点。固定阈值在不同人身上表现不一样,有人眼睛小,正常睁眼时 CNN 输出的闭眼概率就偏高,固定 0.5 会误报。我的做法是系统启动后先跑 30 秒「校准期」,让司机正常睁眼,统计这段时间 CNN 输出的均值加两倍标准差作为该用户的闭眼判定阈值。校准期结束后切换到正常检测。这个改动不大,但能显著降低个体差异带来的误报,实测误报率能降一半左右。
最后说个我自己的教训:一开始我追求模型精度,花了两周调网络结构,准确率从 92% 提到 94%,但实车测试时漏检还是多。后来发现问题不在模型,在数据——训练集里闭眼样本全是正面、均匀光照,而司机实际是侧脸、顶光。我回头补了 300 帧真实车内样本做微调,准确率只涨了 1 个点,但漏检率降了 40%。这件事让我记住,疲劳检测这种场景,数据的场景覆盖度比模型深度重要得多。先把数据采对,再谈调参。希望帮到你。
本文还有配套的精品资源,点击获取