基于CNN的驾驶员疲劳检测系统:从人脸关键点到Python工程实现
发布时间:2026/9/28 21:17:56来源:尧图网络
简介面向计算机相关专业毕业生的Python毕业设计资料包围绕卷积神经网络与人脸识别技术实现驾驶员疲劳检测与预警系统。资源包含完整项目源码、配套数据集、权重文件及项目介绍文档既适合正在选题或推进毕设的学生参考也可作为课程设计、期末大作业的实战练习素材。压缩包共37个文件以Python脚本为主16个py涵盖模型训练、检测评估、摄像头及视频识别等模块另有训练权重pth、测试图片jpg与说明文档txt整体约500MB结构清晰便于直接运行调试。目前已有80人学习下载。项目经导师指导并获高分认可代码完整可运行从数据准备到模型加载、从单张图片检测到实时视频预警均有对应实现可帮助学习者快速理解SSD、VGG等网络在疲劳检测中的应用路径并在此基础上二次开发。1. 基于 CNN 的驾驶员疲劳检测毕业设计为什么都选这个方向每年毕业季计算机和电子类专业里都会出现一批基于 Python 的疲劳驾驶检测题目。原因很简单它同时踩中了三个容易出成果的点——有卷积神经网络CNN这个导师爱看的关键词有人脸识别和关键点估计这种可视化效果强的中间结果又有预警系统这种能接线、能演示、能写进论文的完整闭环。换句话说这不是一个纯算法题而是一个“算法工程”的复合题答辩时既有图可讲也有数据可晒。但要先纠正一个常见误解标题里的“人脸识别”在疲劳检测任务里绝大多数时候并不是门禁机那种“认出这个人是谁”的身份识别而是“在画面里找到人脸并用关键点描述面部状态”的广义人脸识别。疲劳的判断最终落在眼睛开合程度、嘴巴张合频率、头部姿态这几组数字上。CNN 在其中负责最硬的部分——人脸检测和眼睛状态分类而不是把驾驶员的面部特征拿去比对身份证照片。这套方案适合谁如果你正在做毕业设计手里有一块推理能力还行的 GPU或者愿意在 CPU 上牺牲一点帧率如果你想让项目里既有训练好的模型文件又有自己标注的数据集还有能现场运行的预警逻辑——那这个方向就比单纯做一个分类器或一个爬虫好落地得多。下面按我从零搭过一遍这套项目的顺序把方案选型、可复现代码和踩过的坑一次讲清楚。2. 方案选型CNN 负责哪些事EAR/MAR 阈值负责哪些事2.1 把疲劳检测拆成两个子问题人脸在哪、眼睛闭了多久疲劳检测的完整链路可以拆成四段人脸检测、人脸关键点定位、疲劳指标计算、预警触发。CNN 主要出现在前两段里后两段用传统几何算法。常见做法是级联式结构先用一个 CNN 检测器找出人脸框再在框内提取眼睛、嘴巴等区域的关键点最后用这些关键点计算眼睛纵横比EAR和嘴巴纵横比MAR根据持续帧数判断是否疲劳。另一种做法是用 CNN 直接做端到端的疲劳状态分类比如把眼睛区域裁剪出来丢进一个二分类网络输出“闭眼概率”。这种做法在第 4 章会展开它更贴近标题里的“卷积神经网络”也比纯阈值法更有论文可写。但第一版可演示程序建议用 EAR 阈值法先跑通因为它的误报原因明确参数好调展示时也更容易解释。先把流水线跑起来再往里塞 CNN 分类器这个顺序能少走很多弯路。2.2 关键点检测与 EAR/MAR 的数学模型假设已经拿到眼睛的六个关键点坐标EAR 的计算公式如下def eye_aspect_ratio(eye_points): # eye_points: 6 个 (x, y) 坐标顺序为上眼皮下沿、左眼角、外侧角等 A dist(eye_points[1], eye_points[5]) # 垂直距离1 B dist(eye_points[2], eye_points[4]) # 垂直距离2 C dist(eye_points[0], eye_points[3]) # 水平距离 return (A B) / (2.0 * C)这个比值反映眼睛的张开程度。眼睛自然睁开时垂直距离 A、B 较大EAR 通常在 0.25 到 0.35 之间闭眼时垂直距离趋近于 0EAR 会掉到 0.15 以下。MAR 换汤不换药把眼睛的 6 点换成嘴巴的 8 点坐标计算嘴巴纵横比用来识别打哈欠。EAR 单独用会漏掉“半闭眼”这种疲劳状态所以后面要配合连续帧计数和 CNN 分类器兜底。参数上最值得关注的是闭眼帧数阈值。假设摄像头是 30 帧每秒一次眨眼约持续 100 到 400 毫秒也就是 3 到 12 帧。真正的疲劳闭眼通常超过 500 毫秒因此连续闭眼帧数阈值设在 12 到 15 帧既不会把正常眨眼误判成疲劳又能抓住持续闭眼。这个数字后面要按实际摄像头帧率换算直接写在代码里不做换算是新手最常见的翻车点。2.3 三套人脸检测方案怎么选OpenCV DNN、MTCNN、MediaPipe人脸检测部分常见有三条路。第一条是用 OpenCV 自带的 DNN 人脸检测器模型是 SSD 结构配合 ResNet 基础网络一个大约 2MB 的 Caffe 模型文件就能跑CPU 上能做到实时集成最简单适合演示版。第二条是 MTCNN三个小网络级联精度高对人脸倾斜和大角度遮挡更稳但 O-Net 阶段比较慢CPU 上容易掉到 10 帧以下。第三条是 MediaPipe Face Mesh它直接给出 468 个关键点省去单独训练关键点模型的麻烦安装方便但对夜间暗光和大角度侧脸的稳定性稍弱。我的建议是第一版用 OpenCV DNN 检测人脸框MediaPipe 或 dlib 取关键点。原因很实际OpenCV DNN 的模型文件随 OpenCV 一起分发不用额外下载训练权重加载速度快不会在答辩现场因为模型初始化失败而尴尬。MediaPipe 的 Python 包安装简单不需要像 dlib 那样在 Windows 上经历漫长的 CMake 编译等待。等整套逻辑跑通再考虑换成 MTCNN 提升精度。2.4 报警策略为什么不能看单帧很多第一次做这个项目的人拿着 EAR 低于阈值就立刻报警结果摄像头前只要眨一下眼报警器就响一次。这是因为单帧判断没有时间维度。眼睛闭合可能是正常眨眼也可能是走神只有“闭合持续超过某个时间长度”才能作为疲劳证据。正确的报警策略是维护一个计数器EAR 低于阈值时加一高于阈值时清零当计数达到设定帧数才触发预警。同时设定提示、警告、报警三级状态分别对应短暂闭眼、持续闭眼、持续闭眼且频繁打哈欠。另外还要考虑复位逻辑。报警触发后如果驾驶员睁开眼睛一段时间状态应该自动回落而不是一直响到程序退出。复位时间一般取 3 到 5 秒也就是大约 90 到 150 帧眼神正常才把状态降回提示级别。这个逻辑看起来简单但面试时经常被追问“你怎么防止误报和连续报警”提前写好复位逻辑整个项目的完成度会明显不一样。3. 跑通第一版可演示程序环境、模型与最小代码3.1 环境准备与依赖安装实践中最稳妥的 Python 环境是 3.9 到 3.12 之间的 64 位版本用 Anaconda 创建独立虚拟环境别把包直接装进系统 Python。很多毕设翻车都是因为系统 Python 里已经有了一套 OpenCV再装一套导致 DLL 冲突画面刷不出来。创建环境后安装以下依赖conda create -n fatigue python3.10 -y conda activate fatigue pip install opencv-python opencv-contrib-python mediapipe numpy -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后先跑一个导入测试确认 opencv 和 mediapipe 能正常加载。如果你用的是 PyCharm记得在 Settings 里把项目解释器切到刚刚创建的 conda 环境否则代码里 import cv2 会显示红色波浪线。这一步偶尔带点玄学明明终端里能 importPyCharm 里却报错基本都是解释器没有切换而不是包装错了环境。3.2 用 OpenCV DNN MediaPipe 实现实时检测下面的代码是完整可运行的第一版核心是 OpenCV DNN 检测人脸框再把框内区域交给 MediaPipe 提取眼睛关键点计算 EAR。为了兼顾速度和精度每一帧先缩小到 480 像素宽再做检测。import cv2 import mediapipe as mp import numpy as np # 关键点索引MediaPipe FaceMesh 中的眼睛轮廓点 LEFT_EYE [33, 160, 158, 133, 153, 144] RIGHT_EYE [362, 385, 387, 263, 373, 380] def calc_ear(landmarks, idx_list, img_w, img_h): pts [(landmarks[i].x * img_w, landmarks[i].y * img_h) for i in idx_list] A np.linalg.norm(np.array(pts[1]) - np.array(pts[5])) B np.linalg.norm(np.array(pts[2]) - np.array(pts[4])) C np.linalg.norm(np.array(pts[0]) - np.array(pts[3])) return (A B) / (2.0 * C) face_proto deploy.prototxt face_model res10_300x300_ssd_iter_140000_fp16.caffemodel net cv2.dnn.readNetFromCaffe(face_proto, face_model) mp_face_mesh mp.solutions.face_mesh face_mesh mp_face_mesh.FaceMesh(static_image_modeFalse, max_num_faces1, refine_landmarksTrue, min_detection_confidence0.6) cap cv2.VideoCapture(0) frame_counter 0 EAR_THRESHOLD 0.22 CLOSE_FRAMES 12 while True: ret, frame cap.read() if not ret: break frame cv2.resize(frame, (480, 360)) 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]): conf detections[0, 0, i, 2] if conf 0.6: continue box detections[0, 0, i, 3:7] * np.array([w, h, w, h]) x1, y1, x2, y2 box.astype(int) x1, y1 max(0, x1), max(0, y1) x2, y2 min(w, x2), min(h, y2) face_roi frame[y1:y2, x1:x2] rgb_roi cv2.cvtColor(face_roi, cv2.COLOR_BGR2RGB) results face_mesh.process(rgb_roi) if results.multi_face_landmarks: lm results.multi_face_landmarks[0] ear_left calc_ear(lm.landmark, LEFT_EYE, w, h) ear_right calc_ear(lm.landmark, RIGHT_EYE, w, h) ear (ear_left ear_right) / 2.0 if ear EAR_THRESHOLD: frame_counter 1 else: frame_counter 0 state DROWSY if frame_counter CLOSE_FRAMES else NORMAL cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255) if state DROWSY else (0, 255, 0), 2) cv2.putText(frame, fEAR: {ear:.2f} Frames: {frame_counter}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.putText(frame, state, (10, 70), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 2) cv2.imshow(Fatigue Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()代码里 OpenCV DNN 的输入层做了 1.0 缩放和均值减除检测输出的 7 维向量中第四到第七个值是人脸框相对坐标乘以宽高就是像素坐标。MediaPipe 的 FaceMesh 默认返回的 468 个点中refine_landmarksTrue 会额外细化瞳孔区域对眼部关键点会更稳定。EAR 阈值先用 0.22 起步在光线正常的室内录一段视频观察数值分布再调整到略低于正常睁眼时 EAR 的值。3.3 演示视频、摄像头与界面参数设置第一版建议先用录好的视频验证别直接上摄像头。因为视频可以暂停、倒放答辩前可以用同一段视频反复测试参数避免现场因为光线、角度问题导致无法人脸框。代码里只需要把 VideoCapture 的参数改成视频文件路径cap cv2.VideoCapture(test_driver.mp4)摄像头实时模式有两个参数值得调一是分辨率建议 640x480 或更小很多笔记本摄像头标称 1080p但光线不好时实际有效分辨率很低放大画面只会拖慢帧率二是曝光时间OpenCV 里可以用 cap.set(cv2.CAP_PROP_EXPOSURE, -6) 调整数值越大画面越亮但运动模糊会更严重。实际演示时如果现场投影仪把画面色调压得很暗优先在摄像头前补光而不是拉曝光。补光能同时提升人脸检测置信度和关键点定位精度比调任何代码参数都有效。4. 把 CNN 用到底训练自己的眼睛状态分类器4.1 数据集怎么准备公开集与自拍标注的取舍EAR 阈值法有一个硬伤它假定眼睛是一个标准椭圆但真实驾驶场景里的眼型、眼妆、眼镜框、贴眼睫毛都会破坏这个假设。用 CNN 做眼睛状态分类的优势就是直接用图像像素判断开闭不看几何假设。数据集训练有两种渠道公开数据集如 CEW 闭眼数据集、NTHU Drowsy Driver Detection 视频集以及自己录制并裁剪。公开集的优点是样本量大、标注规范缺点是正脸多、戴眼镜样本少在真实场景里容易掉链子自拍集的优点是贴合自己的摄像头角度和光线缺点是工作量惊人。我推荐混合策略先用公开集训练一个基础模型再自己录五分钟正脸视频按帧裁剪眼睛区域手动分成 open 和 closed 两个文件夹加入训练集做增量训练。这样数据集目录里既有公开数据又有自己的标注记录论文里也能写清楚数据增强和样本分布。裁剪时把每只眼睛区域单独存成 24x24 的灰度图而不是把两只眼拼在一起因为两只眼的光照不同拼接会干扰分类器。4.2 CNN 结构设计与训练脚本下面是一个轻量 CNN两层卷积加一个全连接层输入 24x24 灰度图输出闭眼概率。网络刻意做小是为了在 CPU 上也能做到实时推理毕设答辩时没有必要追求大网络性能瓶颈在摄像头读取和两个人脸模型的叠加。import tensorflow as tf from tensorflow.keras import layers, models, optimizers def build_eye_cnn(): model models.Sequential([ layers.Input(shape(24, 24, 1)), layers.Conv2D(16, 3, paddingsame, activationrelu), layers.BatchNormalization(), layers.MaxPooling2D(2), layers.Conv2D(32, 3, paddingsame, activationrelu), layers.BatchNormalization(), layers.MaxPooling2D(2), layers.Flatten(), layers.Dropout(0.5), layers.Dense(2, activationsoftmax) ]) model.compile(optimizeroptimizers.Adam(learning_rate1e-3), losssparse_categorical_crossentropy, metrics[accuracy]) return model model build_eye_cnn() train_ds tf.keras.preprocessing.image_dataset_from_directory( dataset/, # 子目录: open/ closed validation_split0.2, subsettraining, seed42, image_size(24, 24), color_modegrayscale, batch_size32, label_modeint ) model.fit(train_ds, epochs30, validation_dataNone) model.save(eye_state_cnn.h5)训练参数里 batch_size 32 是 CPU 训练的安全值如果 GPU 显存充足可以提到 64加速不明显。epochs 设 30 但建议配合早停回调在验证集准确率连续 5 轮不上升时停止避免过拟合。Dropout 0.5 放在全连接层前面对抑制小数据集过拟合很有效。训练完成后打印一下每类的准确率如果 closed 类准确率明显低于 open 类需要增加闭眼样本或对闭眼样本做轻度随机旋转增强。4.3 把分类器接进检测主循环CNN 接入主循环的常见做法是替换单纯的 EAR 阈值判断而不是完全删除几何特征。先用 EAR 做一个快速预筛选当 EAR 低于阈值时裁出眼睛区域交给 CNN 计算闭眼概率只有概率超过 0.8 才计为一次闭眼。这样能挡住“EAR 低但眼睛实际半睁”的边界情况也不会让 CNN 在每一帧上都跑节省计算量。def infer_close_prob(cnn_model, eye_crop): gray cv2.cvtColor(eye_crop, cv2.COLOR_BGR2GRAY) gray cv2.resize(gray, (24, 24)) / 255.0 x np.expand_dims(gray, axis(0, -1)) prob cnn_model.predict(x, verbose0)[0][1] return prob # 在主循环的 EAR 阈值分支里调用 close_prob infer_close_prob(model, frame[y1:y2, x1:x2]) if close_prob 0.8: frame_counter 1这里有一件必须说清的事人均正常眨眼时也有 3 到 12 帧的闭眼直接用单帧概率做报警会把每次眨眼都算成疲劳。正确做法是把连续概率做滑动平均取最近 5 帧的平均闭眼概率作为判断依据再配合本来就在计的 frame_counter。这样代码里同时存在几何特征和 CNN 特征论文的分析部分可以对比两组指标的 ROC 曲线这也是这个方向最有内容可写的地方。5. 疲劳检测避坑与排查毕设答辩前必须知道的 5 个坑5.1 戴眼镜和暗光下关键点乱跳现象测试视频里没人闭眼界面上的 EAR 值却在 0.15 到 0.3 之间疯狂抖动偶尔弹出 DROWSY 状态。原因镜片反光会掩盖眼白和瞳孔区域MediaPipe 和 dlib 这类基于局部特征回归的关键点模型会把反光边缘误认成眼睑暗光下对比度不足时关键点定位结果本身就是噪声。解决在输入关键点模型前先做 CLAHE 直方图均衡化把局部对比度拉起来再用一个一阶低通滤波平滑 EAR 序列。滤波系数取 0.3 到 0.5也就是新值 0.3 * 当前值 0.7 * 上一次值能有效压掉高频抖动又不会让真实闭眼变得迟钝。5.2 眼睛是闭着但 EAR 值不低现象驾驶员小睡时眼睛半闭肉眼明显看出已经没在看路但程序判定为 NORMAL。原因半闭状态下垂直距离 A、B 没有归零EAR 可能还在 0.2 附近高于 0.22 的闭眼阈值。解决不要用一个固定阈值打天下。在程序启动后的前 30 帧标定睁眼基线取睁眼 EAR 的平均值再乘以 0.6 作为该驾驶员的个性化闭眼阈值。如果个性化标定仍然误判就启用第 4 章的 CNN 分类器在训练集里手动添加半闭眼样本而不是把所有半闭当睁眼。5.3 CPU 帧率上不去报警延迟明显现象现场演示时画面明显卡顿从闭眼到报警响应要等两三秒。原因每一帧同时跑 OpenCV DNN、MediaPipe 和窗口绘制CPU 单线程推理排队整体吞吐只有五六帧每秒。解决第一步把检测分辨率降到 320x240人脸框附近的 ROI 再适当放大第二步把模型的推理线程数设为 CPU 物理核数一半避免上下文切换开销第三步如果仍然卡顿把 MediaPipe 的 min_detection_confidence 从 0.6 降到 0.5人脸框稳定后甚至可以直接复用上一帧的人脸框只在丢失时才重新全图检测。这个策略在实际演示里能把帧率翻倍。5.4 打哈欠被当成转头或遮挡现象驾驶员张嘴打哈欠时脸部关键点大面积移动人头框宽度突然变化疲劳状态误报。原因MAR 和 EAR 都是基于关键点坐标的几何比值当嘴巴张大时下巴点下拉连带影响脸颊和眼周点位置且打哈欠时人脸框偶尔被手遮挡。解决给打哈欠判断增加一个约束条件——人脸宽度在连续 5 帧内变化不能超过 15%否则认为存在遮挡或剧烈姿态变化不计入疲劳计数。同时把打哈欠单独计数只有一分钟内出现 3 次以上哈欠才升级为警告避免一次张嘴就触发报警。5.5 自己拍的数据集训练模型在测试视频上翻车现象训练 CNN 时准确率 97%放到真实场景的视频里闭眼照片却被判成睁眼。原因自拍数据集常见的问题是场景单调——全是同一角度、同一光照、同一部手机前置摄像头模型学到的是“这个亮度下眼睛长这样”而不是“眼睛是否闭合”这个抽象概念。解决训练时加入随机亮度抖动、随机水平翻转、随机旋转 10 度把光照变化的期望打进数据里测试时用一段不同光线、不同角度的视频回归一遍如果差距仍然很大回到 4.1 混合公开数据集别只靠自拍样本。这是我反复踩过的坑数据量小的时候CNN 就是一个黑匣子你得用数据增强逼它学更本质的特征。6. 从检测到预警报警、日志与一次完整的验证流程6.1 三级预警输出与触发逻辑整套系统的最后一个闭环是预警输出。建议做三级状态而不是简单地响铃状态触发条件输出方式正常EAR 高于阈值界面绿框不输出声音提示连续闭眼超过 5 帧界面黄框显示“眨眼频率过高”警告连续闭眼超过 12 帧界面红框蜂鸣器短鸣报警警告后 5 秒内再次触发或 1 分钟内哈欠超过 3 次蜂鸣器长鸣语音提示“请停车休息”触发逻辑里要加一个冷却时间比如报警后至少 10 秒内不重复报警否则疲劳驾驶者在等红灯时的几次闭眼会让系统一直响失去可信度。日志输出建议写成 CSV记录每一帧的时间戳、EAR 值、闭眼帧数和报警状态答辩前把一段 30 秒视频跑出来的日志导出画一条 EAR 随时间的曲线图放进论文结果部分比截图有说服力得多。6.2 录一段自己的视频验证整套流程验证不要只在摄像头前摆拍按下面这套流程走能帮你少挨答辩老师的问询。第一步准备一段 90 秒的室内视频前 30 秒正常睁眼中间 30 秒模拟每 10 秒闭眼一次最后 30 秒连续闭眼 3 秒模拟疲劳状态。第二步跑一个统计脚本对比视频真实标签和系统输出标签统计误报次数和漏报次数。第三步把 EAR_THRESHOLD 和 CLOSE_FRAMES 两个参数做成带默认值的函数参数手动微调一遍记录哪组参数在误报和漏报上均衡最好。第四步换一个光线更暗的场景重复一次如果漏报率上升超过 20%优先调整 CLAHE 参数而不是阈值。我现在的习惯是每个项目先定下三个数字——阈值、持续帧数、报警冷却时间——再写代码。这三个数字决定了算法上限改代码只能修下限。疲劳检测这类和车窗、路况强相关的场景检测端再花哨也不如把这三件事在演示里讲清楚。希望这 6 章能帮你把这个毕设从原理到复现完整落地。按上面的顺序先跑通 OpenCV DNN 加 MediaPipe 的最小程序再加入你自己的 CNN 眼睛分类器最后把三级预警接上这套项目在答辩时展示和提问都撑得住。本文还有配套的精品资源点击获取
网站建设高端定制企业官网