从特征到应用:用 dlib+OpenCV 实现实时疲劳检测(基于眼睛纵横比)
在前两篇内容中,我们完成了人脸检测、68 点关键点定位与面部特征可视化。本文将基于这些技术,聚焦一个实用场景 ——实时疲劳检测,通过计算眼睛纵横比(EAR)判断用户是否闭眼,并在持续闭眼时触发警报,可应用于驾驶安全、学习专注度监控等场景。
一、核心原理:眼睛纵横比(EAR)如何判断闭眼?
疲劳检测的核心逻辑是通过眼睛开合状态判断是否困倦,而眼睛纵横比(Eye Aspect Ratio,简称 EAR)是衡量眼睛开合程度的经典指标,由研究者 Tereza Soukupová 在 2016 年提出。
1. EAR 的计算逻辑
眼睛的 6 个关键点(如右眼 36-41、左眼 42-47)分布如下图所示,EAR 通过计算 “垂直方向距离之和” 与 “水平方向距离” 的比值,量化眼睛的开合程度:
1 2\ /
0 ----------+---------- 3/ \5 4
- 垂直距离:关键点 1 与 5 的距离(A)、关键点 2 与 4 的距离(B);
- 水平距离:关键点 0 与 3 的距离(C);
- EAR 公式:
EAR = (A + B) / (2 * C)
2. EAR 的判断标准
- 睁眼状态:EAR 值通常在 0.25~0.35 之间(具体数值需根据摄像头距离、人脸大小微调);
- 闭眼状态:EAR 值会急剧下降至 0.2 以下,甚至接近 0;
- 疲劳判断:若 EAR 持续低于阈值(如 0.3)超过一定帧数(如 50 帧,约 1 秒,因摄像头通常为 30~60 帧 / 秒),则判定为疲劳状态。
二、完整代码实现与逐模块解析
1. 完整代码
import numpy as np
import dlib
import cv2
from sklearn.metrics.pairwise import euclidean_distances # 计算欧式距离
from PIL import Image, ImageDraw, ImageFont # 处理中文显示# 模块1:计算眼睛纵横比(EAR)
def eye_aspect_ratio(eye):# 计算垂直方向两个距离:A(1-5)、B(2-4)A = euclidean_distances(eye[1].reshape(1, 2), eye[5].reshape(1, 2))B = euclidean_distances(eye[2].reshape(1, 2), eye[4].reshape(1, 2))# 计算水平方向距离:C(0-3)C = euclidean_distances(eye[0].reshape(1, 2), eye[3].reshape(1, 2))# 计算EAR(避免除以0,加微小值)ear = ((A + B) / 2.0) / (C + 1e-6)return ear# 模块2:OpenCV添加中文(解决cv2.putText不支持中文的问题)
def cv2AddChineseText(img, text, position, textColor=(255, 0, 0), textSize=50):if isinstance(img, np.ndarray): # 若为OpenCV的numpy数组格式,转换为PIL图像img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))draw = ImageDraw.Draw(img) # 创建绘图对象# 加载中文字体(需确保系统有simsun.ttc字体,Windows默认有,Linux/mac需手动安装)fontStyle = ImageFont.truetype("simsun.ttc", textSize, encoding="utf-8")draw.text(position, text, textColor, font=fontStyle) # 绘制中文return cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR) # 转回OpenCV格式# 模块3:绘制眼睛凸包(可视化眼睛区域)
def drawEye(eye, frame):eyeHull = cv2.convexHull(eye) # 计算眼睛关键点的凸包cv2.drawContours(frame, [eyeHull], -1, (0, 255, 0), -1) # 填充式绘制凸包(绿色)# 模块4:初始化与实时检测
if __name__ == "__main__":COUNTER = 0 # 闭眼持续帧数计数器EAR_THRESHOLD = 0.3 # EAR阈值(低于此值判定为闭眼)FATIGUE_FRAMES = 50 # 疲劳判定帧数(持续闭眼超过此帧数触发警报)# 初始化人脸检测器与关键点预测器detector = dlib.get_frontal_face_detector()predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")# 初始化摄像头(0表示默认摄像头,外接摄像头可改为1)cap = cv2.VideoCapture(0)# 设置摄像头分辨率(可选,根据硬件调整)cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)while True:# 读取一帧图像ret, frame = cap.read()if not ret: # 摄像头读取失败(如未连接),退出循环print("Error: 无法读取摄像头图像")break# 水平翻转图像(可选,解决摄像头镜像问题,更符合人眼习惯)frame = cv2.flip(frame, 1)# 1. 检测人脸faces = detector(frame, 0) # 0表示不上采样,平衡速度与精度# 2. 遍历每个人脸,处理眼睛关键点for face in faces:# 2.1 预测68点关键点shape_dlib = predictor(frame, face)shape = np.array([[p.x, p.y] for p in shape_dlib.parts()])# 2.2 提取左右眼关键点(索引对应68点分布)right_eye = shape[36:42] # 右眼:36-41(左闭右开)left_eye = shape[42:48] # 左眼:42-47(左闭右开)# 2.3 计算左右眼EAR,并取平均值(减少单眼误差)right_ear = eye_aspect_ratio(right_eye)left_ear = eye_aspect_ratio(left_eye)avg_ear = (right_ear + left_ear) / 2.0 # 最终EAR值# 2.4 绘制眼睛凸包(可视化,便于观察检测效果)drawEye(right_eye, frame)drawEye(left_eye, frame)# 2.5 疲劳判断逻辑if avg_ear < EAR_THRESHOLD:COUNTER += 1 # 闭眼帧数+1# 持续闭眼超过FATIGUE_FRAMES,触发警报if COUNTER >= FATIGUE_FRAMES:frame = cv2AddChineseText(frame, "!!!危险:请勿疲劳用眼!!!", position=(200, 200), textColor=(0, 0, 255), # 红色警报文字textSize=40)else:COUNTER = 0 # 睁眼时,计数器清零# 2.6 在图像上显示当前EAR值(便于调试阈值)ear_text = f"EAR: {avg_ear[0][0]:.2f}" # 格式化EAR值(保留2位小数)frame = cv2AddChineseText(frame, ear_text, position=(50, 50), textSize=30)# 3. 显示实时画面cv2.imshow("Real-Time Fatigue Detection", frame)# 4. 退出逻辑:按下ESC键(ASCII码27)关闭窗口if cv2.waitKey(1) & 0xFF == 27:break# 释放资源(摄像头+窗口)cap.release()cv2.destroyAllWindows()
2. 核心模块解析
(1)eye_aspect_ratio()
:EAR 计算函数
该函数是疲劳检测的 “核心算法”,关键逻辑如下:
- 欧式距离计算:使用
sklearn.metrics.pairwise.euclidean_distances
计算两点间距离(也可手动用np.linalg.norm(eye[1]-eye[5])
实现,效果一致); - 维度处理:
eye
是形状为 (6,2) 的 numpy 数组(6 个关键点,每个点含 x/y 坐标),需用reshape(1,2)
将单个点转换为 (1,2) 的二维数组,适配euclidean_distances
的输入要求; - 防除零处理:在分母
C
后加1e-6
(极小值),避免因水平距离为 0 导致程序报错。
(2)cv2AddChineseText()
:中文显示函数
OpenCV 的cv2.putText()
不支持中文字体,需通过 PIL 库转换图像格式实现中文显示,关键步骤:
- 格式转换:将 OpenCV 的 BGR 格式 numpy 数组,转为 PIL 的 RGB 格式图像;
- 字体加载:使用系统自带的 “宋体” 字体(
simsun.ttc
),Windows 默认路径为C:/Windows/Fonts/simsun.ttc
,Linux/mac 需手动安装该字体并指定路径(如/Library/Fonts/simsun.ttc
); - 格式转回:绘制中文后,再将 PIL 图像转回 OpenCV 的 BGR 格式,确保后续处理正常。
(3)实时检测逻辑
- 摄像头初始化:
cv2.VideoCapture(0)
调用默认摄像头,cap.set()
可调整分辨率(越高越清晰,但占用资源越多); - 镜像翻转:
cv2.flip(frame, 1)
实现水平翻转,解决摄像头 “左右颠倒” 的问题,让操作更自然; - 疲劳判定:
COUNTER
记录持续闭眼帧数,当COUNTER >= FATIGUE_FRAMES
时,用红色中文显示警报信息,直观提醒用户。
三、参数调试与效果优化
1. 关键参数调整
代码中的两个核心参数需根据实际场景微调,以达到最佳检测效果:
参数名称 | 作用 | 调整建议 |
---|---|---|
EAR_THRESHOLD | EAR 阈值,低于此值判定为闭眼 | - 若频繁误判 “睁眼为闭眼”:适当降低阈值(如 0.28);- 若频繁漏判 “闭眼为睁眼”:适当提高阈值(如 0.32);- 建议在光线充足环境下调试,先记录睁眼时的 EAR 值,取其 0.8~0.9 倍作为阈值。 |
FATIGUE_FRAMES | 疲劳判定帧数,超过此帧数触发警报 | - 摄像头为 30 帧 / 秒时:50 帧≈1.7 秒,适合驾驶场景(反应稍快);- 摄像头为 60 帧 / 秒时:100 帧≈1.7 秒,需按帧率等比例调整;- 若希望更灵敏:减少帧数(如 30 帧≈1 秒);若避免误报:增加帧数(如 80 帧≈2.7 秒)。 |
2. 环境优化建议
- 光线条件:避免逆光或过暗环境,光线不足会导致关键点检测偏差,建议在室内正常照明或室外阴影处使用;
- 人脸姿态:确保人脸正面朝向摄像头,侧脸或低头时眼睛关键点可能检测失败,可配合 “人脸跟踪” 功能优化(后续文章会介绍);
- 硬件性能:若画面卡顿,可降低摄像头分辨率(如 800x600),或减少
detector
的上采样次数(如保持detector(frame, 0)
,不改为 1)。
五、总结
本文从 “理论原理” 到 “代码实现”,完整讲解了基于 EAR 的实时疲劳检测系统,核心是将眼睛关键点的几何特征(EAR)转化为实用的状态判断指标。相比传统的 “基于像素亮度的闭眼检测”,该方法抗干扰能力更强,精度更高,且可移植到嵌入式设备(如树莓派),实现更广泛的应用。
至此,我们已完成从 “人脸检测→关键点定位→特征可视化→实用应用” 的完整技术链。后续文章将继续探索更复杂的计算机视觉场景,如人脸识别、表情分类等,敬请关注!