ARTICLE DETAIL

资讯详情

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

基于OpenCV与dlib的人脸关键点疲劳检测:从EAR/MAR到完整实现

基于OpenCV与dlib的人脸关键点疲劳检测:从EAR/MAR到完整实现 简介一套基于Python与OpenCV的疲劳检测实战项目面向计算机视觉初学者和需要实时安全监控的开发者可应用于驾驶员状态监测、工地安全值守等场景。资源包内含3个文件Python源码detect_blinks.py实现核心检测流程mp4示例视频用于效果演示dat文件为68点人脸关键点模型支撑眼睛、嘴巴等部位的精准定位。压缩包整体73.37MB结构紧凑、注释详细适合直接运行与二次开发。目前已有183人学习下载。项目代码覆盖图像采集、预处理、人脸检测、特征点定位、疲劳状态分析等关键环节通过眼睛闭合频率与打哈欠动作判断疲劳程度并给出实时视频处理与特征提取的完整参考实现既是入门OpenCV项目实战的优质范例也为进阶开发者提供了一套可复用的疲劳检测算法框架。1. 疲劳检测不是玄学先看这个项目的核心套路凌晨三点开长途眼皮开始打架等你反应过来去踩刹车可能已经撞上了护栏。市面上主流的疲劳检测方案动不动就上深度学习模型、红外传感器但张小龙不是靠这些做出来的。这个项目只靠一个普通 RGB 摄像头、Python 和 OpenCV就能把「困了」这个模糊的状态翻译成两个数字眼睛闭合持续帧数、嘴巴张开幅度。核心不是机器学习而是几何测量——用 dlib 的 68 点人脸关键点定位出眼睛和嘴巴的轮廓再算纵横比当眼睛闭合超过连续三帧、嘴巴张得过大就判定疲劳。它不需要 GPU不需要训练数据一个笔记本 CPU 跑 30 帧毫无压力。这套思路在驾驶员状态监控、在线考试防作弊、工位疲劳提醒里都能直接改来用。源码包里有检测脚本、测试视频和预训练模型文件从摄像头取帧到报警触发一条链路完整适合刚入门 OpenCV 的人看懂工程怎么落地。2. 视频流里先找到脸OpenCV 人脸检测与 dlib 68 点特征点定位疲劳检测的第一步不是检测疲劳而是先准确地找到脸、再找到眼睛和嘴巴。这个顺序不能乱。人脸检测Face Detection给出的是人脸外接矩形框告诉你脸在画面哪里人脸关键点定位Face Landmark Alignment则是在这个框里精确定位眉毛、眼睛、鼻子、嘴巴等部位的点坐标。项目里既要“看”到人又要精确测量眼睛闭合程度所以两步都要做。2.1 为什么选 dlib 的 frontal_face_detector 而不是 OpenCV 的 Haar CascadeOpenCV 自带的CascadeClassifier也能检测人脸速度很快但它返回的只是矩形框而且对小尺寸、侧脸、遮挡的鲁棒性一般。dlib 的get_frontal_face_detector()基于 HOG 线性分类器召回率和稳定性比 Haar Cascade 好不少尤其是对光照变化的容忍度更高。这个项目里最终需要的是每个关键点的精确像素坐标dlib 的shape_predictor直接和frontal_face_detector配合训练好的模型输出 68 个点正好覆盖眼睛、嘴巴的轮廓。shape_predictor_68_face_landmarks.dat是 dlib 官方提供的预训练模型约 100MB基于 iBUG 300-W 数据集训练。它输入一张人脸矩形框和对应的灰度图输出 68 个点坐标。文件大是因为内部是树形回归器组合Ensemble of Regression Trees每棵树保存了大量像素对比规则。用的时候直接加载不需要再训练。2.2 加载模型并逐帧检测上采样参数会直接影响检测距离拿到源码包先把模型文件和视频准备好。detect_blinks.py里第一步就是实例化检测器和预测器import dlib import cv2 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) cap cv2.VideoCapture(test.mp4) # 换成 0 就是读取摄像头 while True: ret, frame cap.read() if not ret: break # 灰度图dlib 检测器只接受单通道输入同时减少计算量 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 第二个参数 1 表示对输入图像做一次金字塔上采样 faces detector(gray, 1) for face in faces: shape predictor(gray, face) # 36-41 是右眼关键点42-47 是左眼关键点 for i in range(36, 48): x, y shape.part(i).x, shape.part(i).y cv2.circle(frame, (x, y), 2, (0, 0, 255), -1) cv2.imshow(Frame, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()关键参数是detector(gray, 1)中的1。这个参数表示对图像做几次上采样再检测。设置为1时dlib 会把原图放大一倍这样摄像头里稍微远一点的小脸也能被检测到代价是每帧多花约 20~40ms。如果你监控的是近景人脸可以改成0速度能明显提升。如果人物距离超过两米还经常丢脸可以改成2但 CPU 时间会翻倍。shape.part(i)返回的是 dlib 定义的point对象自带.x和.y。循环里取了 36 到 47 的点来标注因为这些点正好是双眼轮廓。测试视频test.mp4里的人脸靠近镜头帧率也不高跑这段代码你能直观看到红点跟随眼睛移动。2.3 68 点坐标的索引分布眼睛、嘴巴分别对应哪几个点拿到 68 个点后必须清楚每个点的索引对应哪个部位否则后续计算会取错坐标。dlib 的标准模型里索引是固定的。下面这张表是我平时查得最勤的点索引范围对应部位在疲劳检测里的用途0–16脸部轮廓一般不用头部姿态估计时才会引用17–21左眉毛不用22–26右眉毛不用27–35鼻梁与鼻翼辅助判断面朝方向36–41右眼轮廓六点计算右眼 EAR42–47左眼轮廓六点计算左眼 EAR48–59嘴巴外轮廓十二点上下唇开合距离60–67嘴巴内轮廓张嘴程度哈欠检测辅助注意predictor输出的坐标系对应的是灰度图里检测到的人脸区域如果后续要把关键点绘制到彩色原图直接使用shape.part(i).x和.y即可因为检测和预测都在同一坐标空间下。实际操作中35 点鼻尖和 8 点下巴之间的几何关系还可以用来做简单的头部低头检测这是另一个功能后面会提到。2.4 从视频到帧循环里的时间窗口和跳帧策略while cap.read()是逐帧阻塞读取。test.mp4的视频没有声音所以不需要音视频同步逻辑。对于 30fps 的输入如果单帧处理耗时超过约 33ms视频就会开始卡顿检测结果也会滞后。cv2.waitKey(1)里的参数控制着imshow的画面刷新等待毫秒数换成cv2.waitKey(30)会让处理慢下来但不会影响cap.read()已经取到的帧。如果接入实时摄像头我一般会加一个跳帧策略每两帧处理一次把上一帧的检测结果直接用于显示。这样既能保证脸不丢又不会让 CPU 打满。detect_blinks.py源码里没有做跳帧但你在实际部署时完全可以在循环开头加一个frame_id 1然后用if frame_id % 2 ! 0: continue跳过奇数帧。处理速度会快近一倍疲劳检测本身不是逐帧敏感的应用少处理一两帧不影响判断。3. 用 EAR 和 MAR 量化「困了」眼部纵横比与嘴巴开合度的计算找到眼睛和嘴巴的轮廓点之后下一步是把这些点坐标换算成能反映“闭合与否”的标量。直接用上下眼皮点之间的像素距离有一个致命问题人脸离摄像头远近不同同样的闭合状态会得出不同的像素值。比如靠近镜头时睁眼距离可能是 20 像素离远时变成 10 像素这样固定阈值就失效了。所以项目采用纵横比这个归一化指标。3.1 EAR 公式为什么它能做到与距离无关眼睛纵横比Eye Aspect Ratio, EAR最早由 Soukupová 和 Čech 在 2016 年的论文中提出针对 dlib 的 36–41右眼和 42–47左眼六个点计算。公式如下EAR (‖p2 − p6‖ ‖p3 − p5‖) / (2‖p1 − p4‖)对应到 dlib 索引p1 是眼角最外点36 或 42p4 是眼角最内点39 或 45p2、p3 是上眼皮中间两点p5、p6 是下眼皮中间两点。竖着取两个垂直距离的均值横着取一个水平距离两者相除后近大远小的尺度因素就被约掉了。看一眼代码实现from scipy.spatial import distance as dist def eye_aspect_ratio(eye): eye: 包含六个 (x, y) 坐标的列表按 dlib 索引顺序排列 返回 EAR 值正常睁眼约 0.28~0.34闭合时趋近于 0 # 计算垂直方向的两组欧氏距离 vertical_1 dist.euclidean(eye[1], eye[5]) vertical_2 dist.euclidean(eye[2], eye[4]) # 计算水平方向的欧氏距离 horizontal dist.euclidean(eye[0], eye[3]) ear (vertical_1 vertical_2) / (2.0 * horizontal) return earscipy.spatial.distance.euclidean接受两个元组或列表返回欧氏距离。用(vertical_1 vertical_2) / (2.0 * horizontal)分子分母的单位都是像素比例无量纲。当眼睛闭合上下眼皮距离接近 0EAR 值会降到 0.1 以下。单独算一只眼睛容易受眨眼、眯眼干扰所以实际使用时取左右眼平均值left_eye [(shape.part(i).x, shape.part(i).y) for i in range(42, 48)] right_eye [(shape.part(i).x, shape.part(i).y) for i in range(36, 42)] left_ear eye_aspect_ratio(left_eye) right_ear eye_aspect_ratio(right_eye) ear (left_ear right_ear) / 2.0注意索引顺序range(36, 42)取的是 36、37、38、39、40、41这六个点正好按顺时针从外眼角开始环绕右眼。range(42, 48)同理。顺序不能乱否则eye[1]和eye[5]就不是对应的上下眼皮点。3.2 MAR 公式嘴部开合度与打哈欠的捕捉嘴巴使用嘴部纵横比Mouth Aspect Ratio, MAR。dlib 的 48–67 点包含嘴唇内外轮廓我用外轮廓的 48–59 点来计算开合度。取 48 为左嘴角54 为右嘴角50 和 52 为上嘴唇外侧点56 和 58 为下嘴唇外侧点。公式形式上和 EAR 对称MAR (‖p2 − p8‖ ‖p3 − p7‖ ‖p4 − p6‖) / (2‖p1 − p5‖)其中 p2/p8、p3/p7、p4/p6 是上下唇对应的三组垂直点。**用了三组垂直距离是因为嘴唇张开时不是平整的椭圆中间比两边变化更大取平均值更稳定。**代码如下def mouth_aspect_ratio(mouth): mouth: 从 dlib 48-59 点中提取的 (x, y) 坐标列表 正常说话时约 0.2~0.5大张嘴打哈欠时通常超过 0.65 vertical_1 dist.euclidean(mouth[2], mouth[10]) # 50-58 vertical_2 dist.euclidean(mouth[4], mouth[8]) # 52-56 vertical_3 dist.euclidean(mouth[0], mouth[6]) # 48-54 horizontal dist.euclidean(mouth[0], mouth[6]) # 48-54嘴角宽度 mar (vertical_1 vertical_2 vertical_3) / (3.0 * horizontal) return mar这个函数里要注意索引用对了mouth[0]和mouth[6]是左右嘴角48 和 54mouth[2]是 50 点mouth[10]是 58 点mouth[4]是 52mouth[8]是 56。垂直距离用3.0 * horizontal做归一化与人脸到摄像头距离无关。MAR 对说话也很敏感所以疲劳检测里不会单靠一帧 MAR 超阈值就行而是跟眼睛闭合计数组合判断。3.3 阈值与连续帧单帧判断不可靠疲劳的视觉表征不是“某一帧闭眼了”而是“闭眼持续了一段时间”。正常眨眼时EAR 会瞬间掉到 0.1 以下但通常只在 100~400ms 内恢复对应视频里大约 3~12 帧。真正的疲劳闭眼EAR 低于阈值的时间会拉长到 500ms 以上。项目代码里的阈值设定参考如下指标常见初始值依据EYE_AR_THRESH0.25睁眼 0.3 左右闭眼低于 0.2取中间偏上能容忍轻微眯眼EYE_AR_CONSEC_FRAMES3连续 3 帧低于阈值约 100ms排除快速眨眼MOUTH_AR_THRESH0.65哈欠时 MAR 普遍超过 0.6~0.7普通说话时很少到 0.65MOUTH_CONSEC_FRAMES5哈欠至少要持续 5 帧以上代码里的判断逻辑是维护一个计数器只有连续达标的帧数超过阈值才触发报警EYE_AR_THRESH 0.25 EYE_AR_CONSEC_FRAMES 3 MOUTH_AR_THRESH 0.65 eye_counter 0 mouth_counter 0 alarm False # 主循环内的判断 if ear EYE_AR_THRESH: eye_counter 1 if eye_counter EYE_AR_CONSEC_FRAMES: alarm True else: eye_counter 0 if mar MOUTH_AR_THRESH: mouth_counter 1 if mouth_counter 5: alarm True else: mouth_counter 0计数器的作用是「延迟触发」。如果单帧 EAR 小于阈值就触发那日常眨眼一定会让系统疯了。阈值本身不是一个绝对精确的物理量它受分辨率、摄像头角度、是否戴眼镜影响所以更合理的是在系统长时间运行时记录正常状态下的 EAR 均值再在均值基础上下调 20%~30% 作为阈值。源码给的经验值 0.25 是在近距离、正脸、无眼镜场景下的一个起点不是银弹。4. detect_blinks.py 完整流程拆解从视频帧到报警触发接下来看资源包里的detect_blinks.py是怎么把前面这些零件拼成完整项目的。这个文件名是「检测眨眼」实际上实现的是疲劳判断——通过连续闭眼帧数和哈欠帧数来输出报警信号。整体流程并不复杂但每一段都有实际工程上的取舍。4.1 初始化阶段模型路径、参数、统计变量打开源码最早定义的是参数区。除了 Threshold 常量还会有VIDEO_PATH、SHAPE_PREDICTOR_PATH这类路径配置。我建议你拿到源码后第一件事就是确认路径和文件位置. ├── detect_blinks.py ├── shape_predictor_68_face_landmarks.dat ├── test.mp4 └── output.mp4 # 运行后生成的标注视频如果detect_blinks.py和模型文件不在同一个目录运行时会报RuntimeError: Unable to open。看到这个错先检查当前工作目录里有没有.dat文件。代码里加载模型用的相对路径我从终端执行python detect_blinks.py时工作目录就是终端当前目录所以要么把文件放在一起要么改成绝对路径。4.2 主循环核心一组眼睛点提取的封装源码在拿到face后会调用一个函数把shape对象转换成能直接用于 EAR 计算的坐标列表。手动写[(shape.part(i).x, shape.part(i).y) for i in range(...)]容易出错封装一下更好def shape_to_np(shape, dtypeint): 将 dlib shape 对象转成 (68, 2) 的 numpy 数组 coords np.zeros((68, 2), dtypedtype) for i in range(0, 68): coords[i] (shape.part(i).x, shape.part(i).y) return coords这样就不需要反复写part(i).x了。拿到的coords是 (68, 2) 数组后续取眼睛点写作left_eye coords[42:48] right_eye coords[36:42] mouth coords[48:60]用numpy数组切片代替列表推导式代码更紧凑计算欧氏距离时dist.euclidean(left_eye[1], left_eye[5])可以直接接受数组参数返回np.float64。源码里的注释也很清楚每个模块对应一段功能我用红框标注注释的位置你会发现它和流程图的顺序完全一致取帧、灰度化、检测、关键点、EAR/MAR、计数器、可视化。4.3 报警逻辑画面标注还是声音提醒项目里报警的可视化部分是在帧上画一个高亮矩形和文字提示。公开源码里最常见的是这样if eye_counter EYE_AR_CONSEC_FRAMES: cv2.putText(frame, FATIGUE - EYES CLOSED!, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) if mouth_counter 5: cv2.putText(frame, YAWN DETECTED!, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)cv2.putText的参数分别是图像、文本、左下角坐标、字体、缩放系数、颜色BGR、线宽。红色(0, 0, 255)用于报警提示。imshow显示窗口的标题可以改成当前 EAR/MAR 数值这样调试时能看到实时的量化变化。实际做驾驶员监控时画面里不能只靠文字因为司机不会盯着屏幕看。常见做法是联动蜂鸣器或输出串口信号。源码没有硬编码音频因为 OpenCV 本身不带音频播放但很容易扩展在报警条件里加一行os.system(start alarm.wav)可以用系统默认播放器播放音频。注意 Windows 下start会阻塞更可靠的是用playsound模块pip install playsound然后from playsound import playsound # 触发报警时 playsound(alarm.wav, blockFalse)我个人不建议在检测脚本里直接播放音乐因为音频线程会和视频循环打架。可以单独写一个报警进程检测到疲劳时用subprocess.Popen拉起播放这样视频帧率不会掉。4.4 依赖环境与运行命令dlib 是最容易卡壳的环节依赖项如下pip install opencv-python dlib imutils scipy numpyimutils是一个工具库里面提供resize等便捷函数源码里可能用到也可能没有装上无害。最容易翻车的是dlib。在 Windows 上pip install dlib经常因为缺少CMake和 C 编译环境而报错尤其是 Python 3.9 以上更容易踩坑。我常用的两个解决方案方案命令使用场景预编译 wheelpip install dlib19.24.2Windows Python 3.8/3.9wheel 能找到就最快conda 安装conda install -c conda-forge dlibconda 环境里最省心自动带 CMake 编译依赖跑起来后在终端运行python detect_blinks.py如果脚本里没有写-v参数控制视频路径那要先改代码里的cv2.VideoCapture(test.mp4)。test.mp4是打包人用摄像头自录的测试素材内容是模拟打哈欠和闭眼的动作。你可以先用它验证环境再换成摄像头cv2.VideoCapture(0)注意 0 表示默认摄像头笔记本内置通常就是 0外接摄像头可能是 1 或 2。运行时会看到实时窗口左上角显示 EAR 和 MAR 数值。EAR 低于阈值时你会看到计数从 0 跳到 1、2、3到 3 时屏幕上出现红色疲劳提示。整个脚本占用的 CPU 在笔记本 i5 上大约是百分之十几内存约 200MB 左右。5. 进阶疲劳检测的边界条件与调试技巧项目跑到能识别闭眼只是第一步。真正放在驾驶室或者教室里摄像头角度、戴眼镜、逆光、看了个侧脸都会让检测失效。这章的技巧是让你从「能跑」走向「能扛」。5.1 摄像头距离与阈值联动用睁眼基线动态调整 EAR 阈值固定 0.25 的阈值有个明显 bug如果摄像头架在仪表台上人脸距离比桌面近得多睁眼 EAR 可能只有 0.19闭眼 0.12用 0.25 阈值等于永远报警。反过来距离拉远后睁眼 EAR 变成 0.32用 0.25 又太迟钝。解决方法是在启动后前 50 帧记录ear的均值baseline_ear然后设定EYE_AR_THRESH baseline_ear * 0.75只在连续 50 帧都检测到人脸时计算基线避免把闭眼帧算进去。我通常在初始化前加一个状态机的逻辑前 50 帧只要ear 0.2就累积否则不参与平均。这样不同装设位置都能自校准。5.2 侧脸与遮挡丢失特征点时如何降级疲劳检测在头部转向一侧时单只眼睛的轮廓会被鼻梁遮挡dlib 给出的点会漂移EAR 可能变得很低造成误报。改进方案是计算左右眼各自 EAR并检测两眼 EAR 的差值如果差值大于 0.15说明很大概率是侧脸此时只看另一只眼的值。还可以用鼻子区域和脸部轮廓相对位置估算偏航角超过 30 度时停止输出报警避免误报。有人脸的置信度分数来辅助face.confidence在 dlib 新版本里可以拿到低于 0.8 就跳过这一帧。5.3 用眨眼频率做二次确认降低误报率疲劳的实质是一串异常信号叠加闭眼时间变长、眨眼频率降低、哈欠增多。检测到一次连续闭眼时先别急着报警。可以统计过去 60 秒内的眨眼次数blink_count正常人清醒时每分钟眨眼 15~20 次疲劳时降到 5 次以下。源码里维护一个deque来记录眨眼时间戳眨眼定义为从 EAR 低于 0.2 到重新高于 0.2 的一个完整过程并且在 400ms 内恢复。当eye_counter 3且blink_count显著偏低时才触发最终报警。这个二次确认逻辑能让误报率下降一个量级。5.4 性能优化把关键点定位限制在 ROI 区域当有一张人脸时下一帧的人脸位置和上一帧不会相差太远。与其每帧全图检测不如用上一步的人脸框扩大 20% 作为 ROI把检测直接作用在 ROI 上。dlib 的predictor本来就是在人脸框内预测的所以真正费时间的frontal_face_detector可以只在每 3 帧调用一次中间帧直接用上一帧的检测框加平移补偿。这样在树莓派或老式笔记本上帧率可以从 15fps 提升到 28fps。另外一个技巧是把cap.read()到的帧先缩小到宽度 480 再送检测器EAR 是比例值缩小后依然有效但检测和关键点定位的时间会大幅下降。shape_predictor_68_face_landmarks.dat这个模型在 CPU 上单次推理约 1~3ms瓶颈从来不在模型而在图像解码和整帧全图检测。用以上几个技巧组合起来能把这个 Demo 级别的项目改造成可以长时间运行的轻量级监控服务。最后提醒一句源码中的output.mp4建议用cv2.VideoWriter_fourcc(*mp4v)编码否则常见播放器解不了。本文还有配套的精品资源点击获取
返回列表