☰
基于面部特征的疲劳检测系统:从EAR/MAR原理到OpenCV+dlib实战
2026/10/10 14:59:13 网站建设 项目流程

简介:这是一套面向高校学生与Python初学者的驾驶员疲劳检测系统源码,以人脸识别为核心,通过分析驾驶员面部特征判断其当前疲劳状态,适用于毕业设计选题、课程项目实战,也可迁移到交警摄像头、高速收费站等疲劳驾驶监测场景。压缩包共24个文件,约68.33MB,包含5个py主程序文件、10个xml配置与界面布局文件、iml与gitignore等工程配置、dat数据文件、mp3提示音、md说明文档及docx文档,覆盖从模型调用到界面交互的完整工程结构。目前已有1358人学习下载,说明该方向在毕业设计中具备一定关注度。源码可直接运行,读者能借此理解人脸检测、面部特征点定位与疲劳判定的实现思路,并在此基础上调整阈值、替换数据集或扩展报警逻辑,快速搭建属于自己的疲劳检测方案。

1. 从一张答辩现场的照片说起:这套疲劳检测系统到底在做什么

答辩季我见过太多类似的场景:PPT 上放着一张驾驶员打哈欠的截图,评委问「这个疲劳是怎么判定的」,学生答「眼睛闭着就是疲劳」。然后就没有然后了。这套基于驾驶员面部特征的疲劳检测系统,核心要解决的就是把「闭眼就是困」这种拍脑袋判断,换成一套有阈值、有计时、有误报抑制的可量化逻辑。它面向的是计算机、电子信息类毕业设计场景,也适合想入门 OpenCV 和 dlib 做实时视觉项目的同学。整套流程拆开看并不复杂:摄像头取帧、人脸检测、关键点定位、计算眼睛和嘴巴的开合度、连续帧计数、触发报警。真正拉开差距的地方在于参数怎么设、光照变化怎么扛、误报怎么压。下面我按自己实际跑通这套东西的顺序,把每个环节讲透。

2. 面部特征疲劳检测的原理链路:从像素到 EAR 和 MAR

2.1 为什么选面部特征而不是其他信号

疲劳检测的输入信号有很多种:方向盘转角、车道偏离、脑电、心率。对毕业设计来说,面部特征方案的优势很直接——只需要一个普通 USB 摄像头,不需要接触式传感器,算法链路全部在 Python 生态里能跑通,代码量可控,答辩时也容易可视化展示。方向盘和车道方案依赖车辆总线数据,脑电方案需要硬件采集设备,成本和调试门槛都高出一截。

面部特征方案的核心假设是:人在疲劳时,眨眼频率和闭眼时长会变化,打哈欠频率会上升,头部姿态会前倾或偏移。这三个现象分别对应眼睛、嘴巴、头部三个区域的特征。工程上最成熟、最容易复现的是眼睛和嘴巴两个维度,头部姿态作为辅助。所以整套系统的判定逻辑可以概括为:用眼睛开合度判断微睡眠,用嘴巴开合度判断打哈欠,两者加权后输出疲劳等级。

这里要强调一个容易被忽略的点:单帧判断没有意义。人正常眨眼也会闭眼,单帧检测到闭眼就报警,误报率会高到没法用。所以所有指标都必须做时间维度上的累积,这也是后面参数设置里帧计数窗口存在的原因。

2.2 EAR 和 MAR 的计算公式与几何含义

EAR(Eye Aspect Ratio,眼睛纵横比)的思路是:用眼睛周围 6 个关键点,算垂直方向距离之和与水平方向距离的比值。睁眼时垂直距离大,比值高;闭眼时垂直距离趋近于零,比值低。公式如下:

EAR = (|p2-p6| + |p3-p5|) / (2 * |p1-p4|)

其中 p1 到 p6 是眼睛轮廓上按顺序排列的 6 个点,p1 和 p4 是左右眼角,p2、p6 是上眼睑两点,p3、p5 是下眼睑两点。分母乘以 2 是为了归一化,让不同人脸尺寸下的 EAR 可比。

MAR(Mouth 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 Tools,Linux 下装 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, f"EAR: {ear:.2f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.putText(frame, f"MAR: {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(maxlen=YAWN_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, key=lambda r: r.width() * r.height())选出主驾驶。如果要做多人区分,需要加人脸识别,但毕业设计一般不需要,锁定最大人脸就够了。

5. 让这套系统更耐打的三个进阶技巧

第一个技巧是 EAR 基线自适应。不同人的眼睛大小差异很大,固定阈值对有些人偏严对有些人偏松。做法是启动后前 5 秒采集用户睁眼状态的 EAR 均值作为基线,闭眼阈值设为基线的 70%。这样每个人都有自己的阈值,误报率明显下降。代码上就是加一个校准阶段,把前若干帧的 EAR 存进列表求均值。

第二个技巧是头部姿态辅助判定。低头看手机和疲劳低头在视觉上很像,但疲劳低头通常伴随眼睛闭合。可以用 solvePnP 估算头部俯仰角,当俯仰角超过阈值且 EAR 偏低时,才判定为疲劳低头,单独低头不报警。这能把看手机的场景区分开。

第三个技巧是验证方法。别只用自己一个人测,找 5 个以上不同性别、是否戴眼镜的同学各测 5 分钟,记录误报次数和漏报次数。做一个简单的表格:

测试者是否戴眼镜误报次数漏报次数备注
A否10正常
B是30反光明显
C否01侧脸漏检

这张表在答辩时比任何描述都有说服力,也能帮你定位问题集中在哪类人群。我自己踩过的坑是只用自己的脸调参数,结果换个人就翻车。后来养成习惯,任何视觉项目都至少找三个人测,参数才敢定下来。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询