ARTICLE DETAIL

资讯详情

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

基于Python的人脸识别疲劳驾驶检测与预警系统设计与实现

基于Python的人脸识别疲劳驾驶检测与预警系统设计与实现 简介基于Python的人脸识别驾驶员疲劳检测与预警系统毕业设计资源采用卷积神经网络实现人脸识别与疲劳状态判断适合计算机相关专业学生完成大作业、毕业设计也适合需要项目实战练习的开发者。资源包共37个文件含16个Python脚本、9个pyc编译文件、3个模型权重、5张测试图片、2个说明文本、1个数据集压缩包和1个训练日志压缩后约500MB训练、评估、摄像头检测、视频检测等流程完整齐全。目录按源码、权重和数据集等模块组织附项目介绍与说明文档模型定义、数据增强、损失函数、检测脚本等内容清晰可查。该毕业设计评审分为98分已有54人学习下载整体难度适中源码经过调试可运行能够帮助读者快速复现实验理解基于SSD/VGG的疲劳检测思路。1. 疲劳驾驶检测这么热你的毕设凭什么选这套Python方案跑长途的司机连续驾驶超过四个小时眼皮开始打架车辆偏离车道这时候如果有一套系统能提前两秒发出警报事故可能就不会发生。基于Python的人脸识别驾驶员疲劳检测与预警系统做的就是这件事用摄像头抓驾驶员的脸识别眼睛开合程度和打哈欠频率判断疲劳状态触发声光报警。这套方案在毕业设计里之所以受欢迎一是技术栈成熟Python生态里OpenCV、dlib、深度学习模型都是现成的二是可展示性强摄像头一开人脸框、眼睛关键点、EAR数值实时显示答辩现场效果直观三是数据好找公开数据集加上自己录一段就能凑出训练集。适合计算机、电子、自动化等专业做毕设或课程设计也适合想快速搭一个智能座舱预警Demo的从业者。2. 从摄像头到报警疲劳检测的技术选型与四个关键算法2.1 为什么是Python而不是C毕设场景下的开发效率账很多人在选题时会纠结工业级的疲劳检测系统确实多用C部署在车规级芯片上但毕设场景不一样。毕设的核心目标是在有限时间内把完整链路跑通并且能讲清楚每个环节的原理。Python在这条链路里的优势非常明显OpenCV、dlib、face_recognition、MediaPipe这些库都是Python优先支持写一个实时人脸检测加关键点提取核心代码不到五十行NumPy和SciPy处理数值计算完全够用不需要自己实现矩阵运算PyTorch或Keras训练一个眼睛状态分类模型用GPU训练半小时就能收敛。性能方面也不用担心。很多人误以为Python做实时视频处理会卡顿实际上瓶颈通常不在语言而在模型推理。dlib的人脸检测在CPU上跑一帧大约需要100到200毫秒OpenCV的DNN人脸检测器更快一帧几十毫秒完全满足15到20帧每秒的实时需求。真正需要优化的环节是模型选择和多线程缓冲这部分后面会展开讲。毕设答辩问性能直接说清楚检测帧率和报警延迟比堆砌C代码更有说服力。2.2 人脸检测选dlib还是OpenCV DNN精度、速度与部署成本的平衡人脸检测是整个系统的入口选型直接影响后续所有环节的稳定性。常见做法有三种dlib的HOG检测器、dlib的CNN检测器、OpenCV DNN加载Caffe模型。dlib的HOG检测器最轻量CPU上跑得很快但对侧脸、戴口罩、光线暗的情况比较敏感容易出现漏检。dlib的CNN检测器精度高很多但需要单独下载预训练模型在小尺寸人脸上效果不错速度比HOG慢一截。OpenCV DNN加载的res10_300x300_ssd模型是SSD架构模型文件小CPU推理速度快对遮挡和暗光有一定鲁棒性这是我在实际项目里比较推荐的做法。还要考虑依赖关系。dlib的HOG检测器不依赖额外模型文件但后续提取人脸关键点必须用到dlib的shape_predictor_68_face_landmarks.dat模型这个模型是绕不开的。既然关键点必须用dlib那么人脸检测直接用dlib的HOG反而能减少一次模型加载。所以选型不是孤立的要看整条链路。方案模型文件CPU单帧耗时侧脸/遮挡表现适用场景dlib HOG无约30-80ms较差正脸为主、追求轻量dlib CNN约14MB约300-500ms较好精度优先、硬件较强OpenCV DNN SSD约10MB约30-60ms中等实时性与精度兼顾我一般会这么做开发调试阶段用dlib HOG因为人脸框和68个关键点可以同时拿到排查问题方便部署演示阶段切成OpenCV DNN做人脸检测再用dlib做关键点提取速度能提升一截。切换成本也低两个方案都封装成一个detect_faces函数内部实现随便换。2.3 疲劳判定的两个经典指标EAR与PERCLOS眼睛状态是疲劳检测最核心的信号。学术界和工业界最常用的两个指标是EAR和PERCLOS。EAR的全称是Eye Aspect Ratio眼睛纵横比通过计算眼睛轮廓关键点之间的欧氏距离比值来反映眼睛的张开程度。EAR的公式是EAR等于眼睛高度和宽度的比值。具体来说左眼取6个关键点dlib索引36到41计算两个垂直方向距离的平均值除以水平方向的距离。人在正常睁眼时EAR大约在0.25到0.35之间闭眼时会掉到0.1以下。这个数值与人脸大小、摄像头距离无关因为做了归一化这是它最大的优势不需要针对不同人脸单独标定。PERCLOS的全称是Percentage of Eye Closure指在一段时间内眼睛闭合时间所占的百分比是疲劳驾驶领域公认的有效指标。计算逻辑是统计一个时间窗口比如30秒或60秒内EAR低于闭合阈值的帧数占总帧数的比例。如果PERCLOS超过设定阈值通常取0.4到0.5就判定为疲劳。只有EAR还不够。只看瞬间的EAR司机眨一下眼睛就可能被误判为疲劳。所以必须把PERCLOS作为判定基准再加上持续帧数判断也就是连续多帧EAR都低于阈值才触发报警。眨眼一般是200到400毫秒疲劳状态下的闭眼会持续1秒以上通过时间维度就能很好地区分正常眨眼和疲劳闭眼。2.4 数据从哪来公开数据集与自发采集的边界训练疲劳检测模型需要数据但很多人会在这一步卡住。其实公开数据集足够用NTHU-DDD是台湾清华大学发布的驾驶员嗜睡检测数据集包含不同人种、不同光照条件下的驾驶视频YawDD是加拿大数据集专门录制的驾驶员打哈欠和说话视频CEW是闭眼数据集包含睁眼和闭眼的人脸图片适合做眼睛状态二分类。这些数据集在学术圈公开已久不少论文和开源项目都在用下载渠道也相对稳定。自己录数据也是可行方案但要注意伦理和隐私边界。如果是毕设可以在实验室里找同学配合录制提前告知用途并征得同意不要拿涉密单位或特殊场所的场景当数据来源也不要自己去扒网上带人脸的视频。数据规模方面眼睛状态二分类其实不需要海量数据几千张图片就够因为这个问题相对简单关键是样本多样性要包含戴眼镜、戴墨镜、光照变化、不同角度的情况。数据集的使用方式也要合理划分。训练集占70%验证集占15%测试集占15%并且要保证同一个人不出现在两个集合里否则就是数据泄漏训练出来的模型在测试集上表现很好一到真实场景就崩这是很多人踩过的坑。3. 跑通第一个可用的疲劳检测程序数据、模型与代码3.1 环境准备与项目目录一版能直接用的结构先说环境。Python版本建议3.8到3.10OpenCV用4.xdlib需要cmake和C编译环境。Windows上装dlib最容易翻车常见做法是先装cmake和Visual Studio Build Tools再用pip安装dlib。如果编译太痛苦可以考虑用MediaPipe替代dlib提取关键点MediaPipe的FaceMesh能给出468个关键点而且pip安装是纯轮子不用本地编译但需要额外下载TensorFlow Lite模型。项目目录推荐按下述结构组织后面所有的代码和数据都往这个结构里放方便答辩时讲清楚层次。drowsiness_detection/ ├── config.py # 全局参数配置 ├── detection.py # 人脸检测与关键点提取 ├── fatigue_utils.py # EAR/PERCLOS/打哈欠计算 ├── alert.py # 报警模块 ├── train_eyes.py # 眼睛状态分类训练脚本 ├── realtime_demo.py # 主程序实时检测入口 ├── models/ # dlib模型、CNN模型存放目录 ├── datasets/ # 原始数据集存放目录 │ ├── train/ │ ├── val/ │ └── test/ ├── logs/ # 疲劳事件日志 └── requirements.txtrequirements.txt里至少包含opencv-python、dlib、imutils、scipy、numpy、tensorflow或pytorch。imutils这个库很多人不熟悉它的作用是对图像做缩放、旋转等预处理配合dlib使用可以显著减少样板代码建议安装。3.2 参数配置模块把阈值和路径集中管理把参数写死在代码里是新手最容易犯的毛病。疲劳检测涉及大量阈值比如EAR报警阈值、连续帧数、PERCLOS时间窗口这些参数在不同光照、不同摄像头角度下都需要调集中放在config.py里调参时只改一个文件非常方便。# config.py import os # 路径配置 BASE_DIR os.path.dirname(os.path.abspath(__file__)) DLIB_PREDICTOR os.path.join(BASE_DIR, models, shape_predictor_68_face_landmarks.dat) OPENCV_PROTOTXT os.path.join(BASE_DIR, models, deploy.prototxt) OPENCV_CAFFE os.path.join(BASE_DIR, models, res10_300x300_ssd_iter_140000.caffemodel) TRAINED_MODEL os.path.join(BASE_DIR, models, eye_state_model.h5) # 摄像头与画面参数 CAMERA_ID 0 # 内置摄像头为0USB摄像头通常为1 FRAME_WIDTH 640 FRAME_HEIGHT 480 DETECT_INTERVAL 2 # 每隔几帧做一次全图人脸检测 # EAR相关参数 EAR_THRESH 0.22 # EAR低于该值判定为闭眼 EAR_CONSEC_FRAMES 3 # 连续多少帧闭眼才触发疲劳判定 TIME_WINDOW 30 # PERCLOS统计时间窗口单位秒 PERCLOS_THRESH 0.4 # 窗口内闭眼帧占比超过该值判定疲劳 # 打哈欠相关参数 MAR_THRESH 0.6 # 嘴部纵横比阈值 MOUTH_CONSEC_FRAMES 10 # 连续打哈欠帧数 # 报警参数 ALERT_COOLDOWN 10 # 两次报警最小间隔单位秒这些参数不是拍脑袋定的EAR_THRESH取0.22到0.25是多数论文和开源项目采用的区间基于正常人睁眼EAR大概在0.25到0.35、闭眼在0.1以下的分布。你换成自己的摄像头上手测几次就能确认是否需要微调。DETECT_INTERVAL设为2是因为人脸检测比关键点提取更费时不需要每一帧都做全图检测检测到人脸后可以用追踪器接着跟踪。3.3 人脸检测与关键点提取基于dlib的最小实现下面这段代码完成了从摄像头读取画面、人脸检测、关键点提取的全过程是整套系统的基础。先加载dlib的人脸检测器和关键点预测器然后对每一帧做灰度转换、人脸检测、关键点提取。# detection.py import cv2 import dlib from imutils import face_utils import config # 加载dlib人脸检测器和68点关键点模型 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(config.DLIB_PREDICTOR) # 左右眼和嘴巴在68个关键点里的索引范围 (L_START, L_END) face_utils.FACIAL_LANDMARKS_IDXS[left_eye] (R_START, R_END) face_utils.FACIAL_LANDMARKS_IDXS[right_eye] (M_START, M_END) face_utils.FACIAL_LANDMARKS_IDXS[mouth] def get_face_and_landmarks(frame): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 设置检测参数放大倍数为1最小检测窗口为80x80 faces detector(gray, 1) if len(faces) 0: return None, None # 取检测到的最大人脸避免多人场景干扰 face max(faces, keylambda r: (r.right() - r.left()) * (r.bottom() - r.top())) landmarks predictor(gray, face) landmarks face_utils.shape_to_np(landmarks) return face, landmarks逻辑说明detector(gray, 1)中的第二个参数是图像金字塔的放大倍数数值越大能检测到的人脸越小但计算量也随之增加置1在640x480分辨率下表现最均衡。取最大人脸是因为驾驶员疲劳检测场景默认只有驾驶员一个目标如果后排坐了人且脸更大会抢走检测框。shape_to_np把dlib返回的shape对象转成numpy数组方便后面做数值计算。返回的landmarks是一个68x2的数组每一行对应一个关键点的x和y坐标。学术界和开源社区习惯把这段代码单独封装原因在于人脸检测策略随时可能需要替换成OpenCV DNN或MTCNN封装后只改函数内部实现外层调用完全不受影响。3.4 计算EAR和MAR把关键点变成可判定的数值关键点提取出来之后下一步就是计算EAR和嘴部纵横比MAR。这里有个细节需要特别注意两只眼睛的EAR要分开算再取平均因为人不可能两只眼睛同步完全闭合如果只算单眼容易因为角度问题误判。# fatigue_utils.py import numpy as np def eye_aspect_ratio(eye_points): # 垂直方向两个距离的平均值 vertical_1 np.linalg.norm(eye_points[1] - eye_points[5]) vertical_2 np.linalg.norm(eye_points[2] - eye_points[4]) # 水平方向距离 horizontal np.linalg.norm(eye_points[0] - eye_points[3]) # 加一个极小值防止除零 ear (vertical_1 vertical_2) / (2.0 * horizontal 1e-6) return ear def mouth_aspect_ratio(mouth_points): # 嘴巴的MAR与EAR计算逻辑类似 vertical_1 np.linalg.norm(mouth_points[13] - mouth_points[19]) vertical_2 np.linalg.norm(mouth_points[14] - mouth_points[18]) horizontal np.linalg.norm(mouth_points[12] - mouth_points[16]) mar (vertical_1 vertical_2) / (2.0 * horizontal 1e-6) return mar参数说明eye_points是左眼或右眼的6个关键点数组索引顺序是dlib的标准顺序从眼角外侧开始顺时针排列。EAR值越小人眼闭合程度越高。mouth_points取的是嘴部外圈12个关键点中的特定位置垂直方向的13和19两组点对应上下嘴唇MAR大于0.6就可以视为张嘴连续多帧大于阈值则判定为打哈欠。加1e-6是为了防止水平距离为0时除零虽然这在人脸检测正常的情况下几乎不可能发生但加上更稳妥。这个计算逻辑在工程上几乎没有优化空间就是标准的欧氏距离计算。如果你发现EAR计算出来总是偏低先检查关键点索引是否取错这是新手最容易犯的错误左眼取成右眼的索引算出来的数值完全没有参考意义。3.5 疲劳判定主逻辑把EAR、PERCLOS和报警串起来主程序的作用是把上面这些模块串成一个完整的实时链路。每帧读取画面检测人脸提取关键点计算EAR和MAR维护一个闭眼帧计数器同时维护PERCLOS统计的时间窗口。# realtime_demo.py import cv2 import numpy as np import time import config from detection import get_face_and_landmarks from fatigue_utils import eye_aspect_ratio, mouth_aspect_ratio from alert import trigger_alert def main(): cap cv2.VideoCapture(config.CAMERA_ID) cap.set(cv2.CAP_PROP_FRAME_WIDTH, config.FRAME_WIDTH) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, config.FRAME_HEIGHT) if not cap.isOpened(): print(无法打开摄像头) return ear_history [] window_frames 0 closed_frames 0 mouth_frames 0 alert_cooldown 0 start_time time.time() while True: ret, frame cap.read() if not ret: break face, landmarks get_face_and_landmarks(frame) if face is None: cv2.imshow(Drowsiness Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break continue # 分别计算左右眼的EAR取平均作为最终判定依据 left_ear eye_aspect_ratio(landmarks[config.L_START:config.L_END]) right_ear eye_aspect_ratio(landmarks[config.R_START:config.R_END]) ear (left_ear right_ear) / 2.0 # 嘴部MAR用于打哈欠检测 mar mouth_aspect_ratio(landmarks[config.M_START:config.M_END]) # 判定是否闭眼 if ear config.EAR_THRESH: closed_frames 1 else: if closed_frames config.EAR_CONSEC_FRAMES: pass # 这里把一次闭眼事件记录到日志 closed_frames 0 # PERCLOS统计按时间窗口滑动 window_frames 1 ear_history.append(ear) elapsed time.time() - start_time if elapsed config.TIME_WINDOW: closed_ratio sum(1 for e in ear_history if e config.EAR_THRESH) / len(ear_history) if closed_ratio config.PERCLOS_THRESH: print([警告] PERCLOS疲劳指数超标) ear_history.clear() start_time time.time() # 打哈欠检测连续多帧张嘴 if mar config.MAR_THRESH: mouth_frames 1 if mouth_frames config.MOUTH_CONSEC_FRAMES: print([提示] 检测到打哈欠) mouth_frames 0 else: mouth_frames 0 # 持续闭眼超过阈值帧数触发报警带冷却时间 if closed_frames config.EAR_CONSEC_FRAMES and alert_cooldown 0: trigger_alert(frame) alert_cooldown config.ALERT_COOLDOWN if alert_cooldown 0: alert_cooldown - 1 # 在画面上叠加当前指标方便答辩现场展示 cv2.putText(frame, fEAR: {ear:.2f}, (30, 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2) if closed_frames config.EAR_CONSEC_FRAMES: cv2.putText(frame, DROWSINESS ALERT!, (30, 70), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 3) if mouth_frames config.MOUTH_CONSEC_FRAMES: cv2.putText(frame, YAWN DETECTED, (30, 110), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2) cv2.imshow(Drowsiness Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows() if __name__ __main__: main()这段代码里最关键的逻辑是闭眼帧计数器与报警冷却时间的配合。闭眼帧计数器解决眨眼误判一个人正常眨眼最多持续几帧不会连续超过EAR_CONSEC_FRAMES设定的值所以不会触发报警。报警冷却时间是防止同一个疲劳状态下报警一次后不停重复响干扰司机。每隔10秒才能触发下一次报警这个值在config.py里可以调答辩时可以演示效果。PERCLOS统计的时间窗口用的是简单滑动窗口每过TIME_WINDOW秒统计一次窗口内的闭眼比例。这里的ear_history会一直保存窗口内的所有EAR值内存占用很小不用担心长期运行的性能问题。窗口结束时会打印一次PERCLOS指数可以把这个数据记录下来作为论文实验部分的支撑数据。3.6 训练一个眼睛状态分类模型基于TensorFlow的完整流程EAR阈值方案有个局限它对戴墨镜的场景几乎无效因为墨镜遮住眼睛后取不到关键点。针对这个问题常见的做法是再训练一个CNN分类模型直接对眼睛区域图片做睁眼闭眼二分类作为EAR方案的兜底。这里给出基于TensorFlow的完整训练流程用的数据集是CEW闭眼数据集也可以使用你手头自采的图片数据。# train_eyes.py import os import cv2 import numpy as np from sklearn.model_selection import train_test_split from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout from tensorflow.keras.preprocessing.image import ImageDataGenerator # 数据集目录结构假设 # datasets/train/closed/ 闭眼图片 # datasets/train/open/ 睁眼图片 def load_and_preprocess(path, target_size(48, 48)): images [] labels [] for label, subdir in enumerate([closed, open]): dir_path os.path.join(path, subdir) for fname in os.listdir(dir_path): if not fname.lower().endswith((.jpg, .jpeg, .png)): continue img_path os.path.join(dir_path, fname) img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img cv2.resize(img, target_size) # 归一化到0-1区间 images.append(img.astype(float32) / 255.0) labels.append(label) return np.array(images), np.array(labels) # 加载数据并按7:2:1划分训练集、验证集、测试集 X, y load_and_preprocess(datasets/train) X_train, X_temp, y_train, y_temp train_test_split( X, y, test_size0.3, random_state42, stratifyy) X_val, X_test, y_val, y_test train_test_split( X_temp, y_temp, test_size1/3, random_state42, stratifyy_temp) # 增加维度变成(样本数, 48, 48, 1)适配CNN输入 X_train X_train.reshape(-1, 48, 48, 1) X_val X_val.reshape(-1, 48, 48, 1) X_test X_test.reshape(-1, 48, 48, 1) # 数据增强小幅旋转和水平翻转防止过拟合 datagen ImageDataGenerator( rotation_range10, width_shift_range0.05, height_shift_range0.05, horizontal_flipTrue ) datagen.fit(X_train) # 一个轻量级CNN分类网络 model Sequential([ Conv2D(16, (3, 3), activationrelu, input_shape(48, 48, 1)), MaxPooling2D(2, 2), Conv2D(32, (3, 3), activationrelu), MaxPooling2D(2, 2), Flatten(), Dense(64, activationrelu), Dropout(0.5), Dense(1, activationsigmoid) ]) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy]) model.summary() # 用数据增强后的数据训练15轮 history model.fit(datagen.flow(X_train, y_train, batch_size32), validation_data(X_val, y_val), epochs15) test_loss, test_acc model.evaluate(X_test, y_test) print(f测试集准确率: {test_acc:.4f}) model.save(models/eye_state_model.h5)逻辑说明load_and_preprocess函数把图片统一缩放到48x48转成灰度图并归一化。灰度化是为了减少计算量眼睛状态分类只需要纹理和形状特征颜色信息帮助不大。数据增强里rotation_range10允许图片旋转最多10度宽度和高度平移各5%水平翻转randomly翻转图片这些都是模拟真实摄像头下司机头部的微小晃动能有效提升模型的泛化能力。网络结构设计得很小只有两个卷积层加一个全连接层。闭眼识别不是复杂任务用ResNet这样的深度模型反而容易过拟合毕竟CEW数据集总共就几千张图。Dropout(0.5)的作用是让全连接层随机丢掉一半神经元这是防止小数据集过拟合最有效的手段之一。bath_size32表示每轮更新梯度时用32张图epochs15在这个数据集上足够收敛轮数再多就容易记住训练集。最后保存的.h5模型可以通过TensorFlow或OpenCV DNN加载在实时检测时对人脸区域做眼睛状态判断。训练完成后在realtime_demo.py里加入一段逻辑先用dlib提取左眼和右眼的ROI区域缩放到48x48送入模型预测如果模型判定闭眼且连续多帧同样触发疲劳报警。这样即使司机戴墨镜导致关键点提取失败只要墨镜不是完全遮住眼睛CNN模型依然可以从眼睛区域图像判断状态。两者互为主备系统鲁棒性会明显提升。4. 毕设里最容易翻车的五个坑从数据标定到模型过拟合4.1 dlib装不上编译失败不是你的问题是环境链问题现象pip install dlib报错说需要Visual Studio C Build Tools或者编译时提示找不到cmake卡在安装步骤一两个小时。原因dlib的pip包需要本地编译C扩展没有对应的编译工具链就无法安装。在Windows上尤其容易出现这种问题Python版本太新也会导致找不到匹配的预编译包。解决先装cmake和Visual Studio Build Tools然后再pip install dlib。如果还是失败换Python 3.9或3.10版本这两个版本对dlib的支持最成熟。实在不想折腾直接用MediaPipe的FaceMesh替代dlibpip安装是预编译的wheel轮子无需本地编译至少能省掉半天时间。4.2 睁眼闭眼标定不准阈值照搬开源项目本地实测疯狂误报现象代码跑起来之后司机明明睁着眼系统频繁报警或者眼睛快闭上了系统毫无反应。原因EAR阈值是一个统计值不同人的眼型、摄像头安装角度、距离都会影响实际数值。开源项目给的0.2阈值是在他们实验环境下标定的直接拿来用在你的摄像头视角下误差必然不小。亚洲人单眼皮和欧洲人双眼皮的EAR分布有明显差异不加标定直接用翻车概率很高。解决花十分钟做一次简单标定。让测试者正常睁眼10秒记录EAR均值再闭眼10秒记录EAR均值取两者中间值作为报警阈值。把标定过程录下来写进毕设论文里这能成为你工作的加分项因为很多毕设根本没有标定环节。4.3 戴眼镜和墨镜让关键点漂移EAR算出来永远是异常值现象戴上黑框眼镜后关键点位置偶发跳变EAR时而极低时而正常系统不稳定。原因dlib的68点关键点模型是在大量人脸图片上训练的对镜框遮挡并不鲁棒。黑色镜框与瞳孔颜色接近关键点回归会把镜框边缘误认为眼睛轮廓。墨镜场景更极端关键点直接落在镜片上。解决在训练数据里加入戴眼镜的人脸图片对dlib模型做微调但这在毕设阶段成本偏高。更实际的方案是两条路并行EAR判定为主同时用CNN眼睛分类模型做辅助验证。当两个模型都判断为闭眼才触发报警。另外一个笨办法是调整阈值戴眼镜时EAR普遍偏高按4.2的方法重新标定一次也能缓解。4.4 测试集准确率95%实际一跑就废数据泄漏的坑现象CNN模型在测试集准确率超过95%放到摄像头实时检测时闭眼识别几乎全错看起来像随机猜测。原因数据划分没有按人隔离。同一个人的睁眼和闭眼图片同时出现在训练集和测试集里模型记住了这个人的人脸特征而不是学到睁眼闭眼的区别。换一个人就完全失效。还有一种情况是数据增强做得过猛图片被翻转后眼睛位置信息错乱模型学到的是位置特征而不是状态特征。解决划分数据集时保证同一个人只出现在训练集或测试集其中一个集合里。如果数据集里没有人物ID标签那就按文件夹划分一个文件夹一个人的命名规范在建数据集时就要做好。另外测试Augmentation后的图片肉眼检查一下增强结果是否还保持眼睛状态清晰可辨。4.5 报警响个不停没有冷却机制的预警系统等于没有预警现象第一次报警触发后系统在以毫秒级频率持续触发报警报警声连续不断实际使用时司机只会直接关掉系统。原因代码里只有闭眼帧数判断缺少报警去抖和冷却机制。只要闭眼状态不解除闭眼帧计数器一直在累加每次都触发trigger_alert而trigger_alert内部没有上一次报警时间的记录。解决给报警模块加全局冷却时间用config.ALERT_COOLDOWN控制两次报警间隔至少10秒。同时把报警阈值从EAR_THRESH改成EAR_THRESH加一个迟滞量比如EAR低于0.22判定闭眼但闭眼状态解除需要EAR恢复到0.25以上。迟滞设计的目的是防止EAR在阈值边界抖动时反复触发报警和取消报警这在工程上叫消抖属于经典的嵌入式系统设计思想写到论文里也是加分项。5. 从demo到能演示的系统提速、日志与多媒体报警做到第4章的程度你的系统已经能在答辩现场演示了。但如果想让评委觉得这是一个接近工程化的作品而不是课上大作业下面几个进阶点值得做。第一是MediaPipe替换dlib做人脸关键点提取FaceMesh模型在CPU上推理速度比dlib快而且对遮挡的鲁棒性更好还能输出468个关键点嘴部检测精度更高。替换后代码改动也不大只需要把detection.py里的predictor换掉再调整EAR计算时使用的关键点索引即可。第二是给系统加上疲劳事件日志。每次触发报警时把时间戳、EAR值、PERCLOS指数、当前帧截图保存到logs目录。导出一份CSV文件记录整个驾驶过程中疲劳事件的分布这个数据可以作为论文实验章节的有力支撑。下面是一段日志写入的参考实现。# alert.py import csv import os import time import cv2 import config LOG_FILE os.path.join(config.BASE_DIR, logs, fatigue_events.csv) def trigger_alert(frame, ear, perclosNone): # 保存当前帧截图文件名包含时间戳 timestamp time.strftime(%Y%m%d_%H%M%S) snapshot_dir os.path.join(config.BASE_DIR, logs, snapshots) os.makedirs(snapshot_dir, exist_okTrue) snapshot_path os.path.join(snapshot_dir, falert_{timestamp}.jpg) cv2.imwrite(snapshot_path, frame) # 把报警记录追加到CSV os.makedirs(os.path.dirname(LOG_FILE), exist_okTrue) is_new_file not os.path.exists(LOG_FILE) with open(LOG_FILE, a, newline, encodingutf-8) as f: writer csv.writer(f) if is_new_file: writer.writerow([timestamp, ear, perclos, snapshot]) writer.writerow([timestamp, f{ear:.3f}, perclos, snapshot_path]) # 报警提示打印文字、播放提示音 print(f[{timestamp}] 疲劳报警触发EAR{ear:.3f}) try: import winsound winsound.Beep(1000, 500) except ImportError: pass # 非Windows环境不播报可扩展为播放音频文件逻辑说明这段代码有一个值得注意的细节就是is_new_file变量的处理。第一次写入CSV时需要写表头后续追加不需要用文件是否存在来区分。截图保存到独立的snapshots目录命名带时间戳方便后续按时间线回顾报警时刻的画面。报警提示音在Windows上用winsound自带的BeepLinux环境下可以替换成playsound库播放一段音频文件这块可以根据你的部署环境灵活改。第三是引入多线程处理让视频I/O和算法处理解耦。摄像头读取帧是I/O操作经常因为USB带宽不足导致卡顿如果在主线程里读取画面再检测检测耗时会导致画面掉帧。常见做法是开一个生产者线程专门读摄像头把帧放进队列主线程从队列里取帧做检测。队列用deque或queue.Queue即可设置最大缓存帧数避免内存无限增长。我自己做这类实时系统时开头最先做的往往不是算法而是先把生产者消费者框架搭好因为帧率稳了后面算法调参才有意义。最后要提醒的是帧率监控。在画面左上角固定显示当前FPS设置一个目标帧率比如15FPS。如果发现实际帧率低于目标优先检查人脸检测是不是在每一帧都执行了全图检测通过设置DETECT_INTERVAL跳过部分帧或者改用更轻量的人脸检测模型。还有摄像头分辨率从1280x720降到640x480检测耗时能减少将近一半疲劳检测不需要那么高的分辨率关键是关键点坐标的相对精度而不是像素数量。做到日志、截图、多媒体报警这三件事都完成后你的系统就从单线程demo升级成了带数据记录能力的准工程化系统。把这些运行日志整理干净挑几次典型的报警事件截图放到论文的实验分析里评委会觉得你的工作量实实在在。这套方案我前后做过三轮第一轮也踩过不少坑比如照抄别人的阈值导致现场频繁误报比如在数据划分上偷懒导致模型一换人就废。后来我意识到所谓的高分毕设核心不在于代码多花哨而在于你能把每个环节为什么这么做讲清楚。建议你做的时候也养成习惯每次改参数记录下当时的环境和结果不玄学不靠猜。希望这篇笔记能帮到你把这条疲劳检测的路走通答辩顺利。本文还有配套的精品资源点击获取
返回列表