多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于面部特征的疲劳检测系统:从EAR/MAR原理到OpenCV+dlib实战

基于面部特征的疲劳检测系统:从EAR/MAR原理到OpenCV+dlib实战 简介这是一套面向高校学生与Python初学者的驾驶员疲劳检测系统源码以人脸识别为核心通过分析驾驶员面部特征判断其当前疲劳状态适用于毕业设计选题、课程项目实战也可迁移到交警摄像头、高速收费站等疲劳驾驶监测场景。压缩包共24个文件约68.33MB包含5个py主程序文件、10个xml配置与界面布局文件、iml与gitignore等工程配置、dat数据文件、mp3提示音、md说明文档及docx文档覆盖从模型调用到界面交互的完整工程结构。目前已有1358人学习下载说明该方向在毕业设计中具备一定关注度。源码可直接运行读者能借此理解人脸检测、面部特征点定位与疲劳判定的实现思路并在此基础上调整阈值、替换数据集或扩展报警逻辑快速搭建属于自己的疲劳检测方案。1. 从一张答辩现场的照片说起这套疲劳检测系统到底在做什么答辩季我见过太多类似的场景PPT 上放着一张驾驶员打哈欠的截图评委问「这个疲劳是怎么判定的」学生答「眼睛闭着就是疲劳」。然后就没有然后了。这套基于驾驶员面部特征的疲劳检测系统核心要解决的就是把「闭眼就是困」这种拍脑袋判断换成一套有阈值、有计时、有误报抑制的可量化逻辑。它面向的是计算机、电子信息类毕业设计场景也适合想入门 OpenCV 和 dlib 做实时视觉项目的同学。整套流程拆开看并不复杂摄像头取帧、人脸检测、关键点定位、计算眼睛和嘴巴的开合度、连续帧计数、触发报警。真正拉开差距的地方在于参数怎么设、光照变化怎么扛、误报怎么压。下面我按自己实际跑通这套东西的顺序把每个环节讲透。2. 面部特征疲劳检测的原理链路从像素到 EAR 和 MAR2.1 为什么选面部特征而不是其他信号疲劳检测的输入信号有很多种方向盘转角、车道偏离、脑电、心率。对毕业设计来说面部特征方案的优势很直接——只需要一个普通 USB 摄像头不需要接触式传感器算法链路全部在 Python 生态里能跑通代码量可控答辩时也容易可视化展示。方向盘和车道方案依赖车辆总线数据脑电方案需要硬件采集设备成本和调试门槛都高出一截。面部特征方案的核心假设是人在疲劳时眨眼频率和闭眼时长会变化打哈欠频率会上升头部姿态会前倾或偏移。这三个现象分别对应眼睛、嘴巴、头部三个区域的特征。工程上最成熟、最容易复现的是眼睛和嘴巴两个维度头部姿态作为辅助。所以整套系统的判定逻辑可以概括为用眼睛开合度判断微睡眠用嘴巴开合度判断打哈欠两者加权后输出疲劳等级。这里要强调一个容易被忽略的点单帧判断没有意义。人正常眨眼也会闭眼单帧检测到闭眼就报警误报率会高到没法用。所以所有指标都必须做时间维度上的累积这也是后面参数设置里帧计数窗口存在的原因。2.2 EAR 和 MAR 的计算公式与几何含义EAREye Aspect Ratio眼睛纵横比的思路是用眼睛周围 6 个关键点算垂直方向距离之和与水平方向距离的比值。睁眼时垂直距离大比值高闭眼时垂直距离趋近于零比值低。公式如下EAR (|p2-p6| |p3-p5|) / (2 * |p1-p4|)其中 p1 到 p6 是眼睛轮廓上按顺序排列的 6 个点p1 和 p4 是左右眼角p2、p6 是上眼睑两点p3、p5 是下眼睑两点。分母乘以 2 是为了归一化让不同人脸尺寸下的 EAR 可比。MARMouth Aspect Ratio嘴巴纵横比同理用嘴巴内轮廓的上下距离和左右距离做比值。张嘴时上下距离增大MAR 升高。打哈欠的 MAR 通常明显高于正常说话。这两个公式的价值在于它们把「眼睛睁着还是闭着」这种模糊描述变成了一个可以设阈值的连续数值。阈值怎么定是后面避坑章节要重点讲的。2.3 关键点检测的选型dlib 还是 MediaPipe这是新手最容易纠结的地方。我两个都用过说下实际差别。dlib 的 68 点模型是经典方案眼睛 6 点、嘴巴 20 点的索引是固定的网上资料多公式直接套。缺点是模型较老侧脸和遮挡下容易丢点而且 dlib 的编译安装在某些 Python 版本上会翻车这是血泪经验。MediaPipe 的 Face Mesh 输出 468 个点精度更高对侧脸更稳安装是纯 pip 包不需要编译。缺点是点太多需要自己查索引眼睛和嘴巴的点位和 dlib 不一样公式里的点序号要重新映射。对毕业设计来说如果追求快速跑通和稳定安装我一般推荐 MediaPipe如果导师明确要求用 dlib 或者论文里要引用 68 点模型那就用 dlib。下面代码以 dlib 为例因为它的点位索引是标准化的便于讲解。2.4 最小可运行链路检测、定位、计算、判定先装依赖。注意 opencv 的包名是 opencv-python不是 cv2很多新手在 python 安装教程里看到 cv2 就 pip install cv2那是错的。pip install opencv-python dlib numpy scipy如果 dlib 安装报错常见原因是缺少 cmake 和编译器。Windows 下装 Visual Studio Build ToolsLinux 下装 build-essential 和 cmake再重试。实在装不上就换 MediaPipe。下面是核心计算代码包含 EAR 和 MAR 两个函数import dlib import cv2 import numpy as np from scipy.spatial import distance as dist # dlib 68 点模型中眼睛和嘴巴的索引 LEFT_EYE list(range(42, 48)) RIGHT_EYE list(range(36, 42)) MOUTH list(range(48, 68)) def eye_aspect_ratio(eye_points): # 垂直方向两组距离 A dist.euclidean(eye_points[1], eye_points[5]) B dist.euclidean(eye_points[2], eye_points[4]) # 水平方向距离 C dist.euclidean(eye_points[0], eye_points[3]) ear (A B) / (2.0 * C) return ear def mouth_aspect_ratio(mouth_points): # 取嘴巴内轮廓上下两点和左右两点 A dist.euclidean(mouth_points[2], mouth_points[10]) B dist.euclidean(mouth_points[4], mouth_points[8]) C dist.euclidean(mouth_points[0], mouth_points[6]) mar (A B) / (2.0 * C) return mar detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) for face in faces: shape predictor(gray, face) coords np.array([[p.x, p.y] for p in shape.parts()]) left_eye coords[LEFT_EYE] right_eye coords[RIGHT_EYE] mouth coords[MOUTH] ear (eye_aspect_ratio(left_eye) eye_aspect_ratio(right_eye)) / 2.0 mar mouth_aspect_ratio(mouth) cv2.putText(frame, fEAR: {ear:.2f}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.putText(frame, fMAR: {mar:.2f}, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow(Fatigue Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明detector 负责框出人脸predictor 输出 68 个关键点坐标coords 转成 numpy 数组后按索引切片。EAR 取左右眼平均值比单眼更稳因为侧脸时一只眼可能丢点。MAR 直接用嘴巴 20 点里的内轮廓点。参数说明detector 的第二个参数 0 表示不上采样检测快但小脸可能漏检如果摄像头离得远可以改成 1代价是速度下降。shape_predictor 的模型文件需要单独下载文件名固定放在脚本同目录。VideoCapture(0) 是默认摄像头外接摄像头改成 1。这段代码跑通后你会看到屏幕上实时显示 EAR 和 MAR 数值。先别急着加报警逻辑花几分钟观察自己睁眼、闭眼、说话、打哈欠时这两个值的变化范围这是后面定阈值的第一手依据。3. 把检测变成判定阈值、帧计数与报警逻辑3.1 阈值不是拍脑袋定的要先用数据说话上一章末尾让你观察数值就是为了这一步。我一般会做一个小实验正常睁眼录 30 秒记录 EAR 的均值和最小值闭眼保持 3 秒记录 EAR 的稳定值打哈欠一次记录 MAR 的峰值。有了这几组数据阈值就有依据了。经验值参考EAR 睁眼通常在 0.25 到 0.35 之间闭眼会掉到 0.15 以下所以闭眼阈值常设在 0.20 到 0.22。MAR 正常闭嘴在 0.2 到 0.4打哈欠峰值能到 0.6 以上阈值常设在 0.5 到 0.6。但这些只是起点不同人脸、不同摄像头、不同光照下都会漂移必须自己标定。这里有个反直觉的结论阈值设得越严格误报不一定越少。EAR 阈值设太低正常眨眼会被判成闭眼设太高真正的微睡眠反而漏报。关键是配合帧计数而不是靠单帧阈值卡死。3.2 帧计数窗口把单帧判断变成时间判断帧计数的逻辑是连续 N 帧 EAR 低于阈值才判定为一次闭眼事件。N 的大小取决于帧率。假设摄像头 30 帧每秒人正常眨眼闭眼时间约 0.1 到 0.3 秒对应 3 到 9 帧。微睡眠通常超过 0.5 秒对应 15 帧以上。所以闭眼帧计数阈值设在 15 到 20 比较合理能过滤掉正常眨眼。打哈欠持续时间更长通常 2 秒以上对应 60 帧。但打哈欠的 MAR 是逐渐升高再降低的不是一直高于阈值所以用「连续帧」判定打哈欠会漏。更稳的做法是滑动窗口统计最近 2 秒内 MAR 高于阈值的帧占比超过 60% 就算一次打哈欠。下面是加入判定逻辑的代码from collections import deque EYE_AR_THRESH 0.21 # 闭眼阈值 EYE_AR_CONSEC_FRAMES 18 # 连续帧数 MAR_THRESH 0.55 # 打哈欠阈值 YAWN_WINDOW 60 # 滑动窗口帧数 YAWN_RATIO 0.6 # 窗口内占比阈值 eye_counter 0 yawn_window deque(maxlenYAWN_WINDOW) fatigue_score 0 while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) for face in faces: shape predictor(gray, face) coords np.array([[p.x, p.y] for p in shape.parts()]) ear (eye_aspect_ratio(coords[LEFT_EYE]) eye_aspect_ratio(coords[RIGHT_EYE])) / 2.0 mar mouth_aspect_ratio(coords[MOUTH]) # 闭眼连续帧计数 if ear EYE_AR_THRESH: eye_counter 1 else: if eye_counter EYE_AR_CONSEC_FRAMES: fatigue_score 1 # 一次微睡眠事件 eye_counter 0 # 打哈欠滑动窗口 yawn_window.append(1 if mar MAR_THRESH else 0) if len(yawn_window) YAWN_WINDOW and \ sum(yawn_window) / YAWN_WINDOW YAWN_RATIO: fatigue_score 1 yawn_window.clear() # 避免同一哈欠重复计数 if fatigue_score 3: cv2.putText(frame, FATIGUE ALERT, (50, 100), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3) cv2.imshow(Fatigue Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break逻辑说明eye_counter 累计连续闭眼帧一旦睁眼就检查是否达到阈值达到就记一次微睡眠事件并清零。yawn_window 用 deque 维护固定长度窗口占比超阈值就记一次打哈欠然后清空窗口防止重复计数。fatigue_score 是疲劳积分累积到 3 就报警。参数说明EYE_AR_CONSEC_FRAMES 设 18 对应 30fps 下 0.6 秒能过滤正常眨眼。YAWN_WINDOW 设 60 对应 2 秒。YAWN_RATIO 设 0.6 是经验值太低会把说话误判成打哈欠。fatigue_score 阈值 3 意味着短时间内多次异常才报警降低误报。3.3 疲劳等级怎么分才合理只有「报警/不报警」两档太粗糙答辩时也不好看。我一般分三档正常、轻度疲劳、重度疲劳。轻度疲劳对应 1 到 2 次异常事件重度对应 3 次以上。也可以引入时间衰减如果 5 分钟内没有新的异常事件fatigue_score 减 1这样能反映疲劳的恢复。分档的价值在于报警策略可以差异化。轻度疲劳只做屏幕提示重度疲劳才触发声音报警。这样既不会频繁打扰又能在真正危险时提醒。3.4 报警输出与可视化报警不只是 print 一句话。毕业设计要展示效果可视化很重要。我通常做三件事在画面上画眼睛和嘴巴的轮廓线颜色随状态变化在角落显示 EAR、MAR 实时数值和疲劳积分触发报警时画面边框变红并叠加文字。轮廓线用 cv2.polylines 画眼睛用凸包 cv2.convexHull 更贴合。颜色逻辑EAR 正常绿色低于阈值红色MAR 正常绿色高于阈值橙色。这些细节在答辩演示时很加分评委一眼就能看懂系统在做什么。4. 避坑与排查这套系统最容易翻车的五个地方4.1 现象白天正常晚上或背光时疯狂误报原因dlib 的人脸检测基于灰度图光照不足时人脸对比度下降检测框会抖动甚至丢失导致关键点漂移EAR 和 MAR 数值乱跳。解决先做直方图均衡化再检测代码是gray cv2.equalizeHist(gray)。如果还不行加一个补光灯这是最直接有效的办法。另外可以在检测前做高斯模糊降噪。实在光照条件差换 MediaPipe它对光照的鲁棒性明显更好。4.2 现象戴眼镜的同学一用就报疲劳原因镜片反光会干扰眼睛关键点定位尤其是粗框眼镜上眼睑点经常被定位到镜框上导致 EAR 偏低。解决一是调整摄像头角度避开反光二是把 EAR 阈值适当下调比如从 0.21 降到 0.18三是用 MediaPipe 的 Face Mesh它对面部遮挡的处理更好。如果答辩现场有戴眼镜的评委要试提前准备一个「眼镜模式」的阈值配置。4.3 现象帧率不稳定帧计数阈值失效原因帧计数阈值是按固定帧率算的但实际运行时帧率会波动。摄像头标称 30fps实际可能只有 15 到 25fps导致同样的帧数对应的时间不一样。解决不要用固定帧数改用时间。记录每次闭眼的起始时间戳用time.time()算持续时长超过 0.5 秒才算微睡眠。这样帧率波动就不影响了。代码里把 eye_counter 换成 start_time 记录。4.4 现象dlib 安装报错卡在编译原因dlib 包含 C 代码pip 安装时需要本地编译缺少 cmake 或编译器就会失败。解决Windows 装 Visual Studio Build Tools 并勾选 C 生成工具然后pip install cmake再装 dlib。Linux 执行sudo apt install build-essential cmake。如果还是不行直接用 MediaPipe 替代功能一样安装省心。这是新手最容易卡住的地方别在这上面耗太久。4.5 现象多人出现在画面里系统不知道看谁原因detector 会检测出所有人脸代码里 for face in faces 会逐个处理导致疲劳积分被多个人脸共享逻辑混乱。解决只处理画面中面积最大的那张脸用max(faces, keylambda r: r.width() * r.height())选出主驾驶。如果要做多人区分需要加人脸识别但毕业设计一般不需要锁定最大人脸就够了。5. 让这套系统更耐打的三个进阶技巧第一个技巧是 EAR 基线自适应。不同人的眼睛大小差异很大固定阈值对有些人偏严对有些人偏松。做法是启动后前 5 秒采集用户睁眼状态的 EAR 均值作为基线闭眼阈值设为基线的 70%。这样每个人都有自己的阈值误报率明显下降。代码上就是加一个校准阶段把前若干帧的 EAR 存进列表求均值。第二个技巧是头部姿态辅助判定。低头看手机和疲劳低头在视觉上很像但疲劳低头通常伴随眼睛闭合。可以用 solvePnP 估算头部俯仰角当俯仰角超过阈值且 EAR 偏低时才判定为疲劳低头单独低头不报警。这能把看手机的场景区分开。第三个技巧是验证方法。别只用自己一个人测找 5 个以上不同性别、是否戴眼镜的同学各测 5 分钟记录误报次数和漏报次数。做一个简单的表格测试者是否戴眼镜误报次数漏报次数备注A否10正常B是30反光明显C否01侧脸漏检这张表在答辩时比任何描述都有说服力也能帮你定位问题集中在哪类人群。我自己踩过的坑是只用自己的脸调参数结果换个人就翻车。后来养成习惯任何视觉项目都至少找三个人测参数才敢定下来。希望帮到你。本文还有配套的精品资源点击获取
返回列表