一、引言:将疲惫描述成数字
在上一篇博客中,我们学习了Dlib的人脸关键点定位与表情识别技术。今天,我们将把这些技术应用到一个极具现实意义的场景------疲劳检测。
疲劳驾驶是交通事故的主要诱因之一。据统计,全球每年因疲劳驾驶导致的交通事故占总数的20%以上。如果计算机能够实时监测驾驶员的眼睛状态,在疲劳初期就发出预警,就能有效避免悲剧的发生。
本篇博客将基于眼睛纵横比(EAR) 这一经典指标,构建一个完整的实时疲劳检测系统。我们将深入讲解EAR的计算原理、阈值设定、计数逻辑,以及如何用中文标注检测结果。
二、核心原理:眼睛纵横比(EAR)
2.1 什么是EAR
眼睛纵横比(Eye Aspect Ratio,EAR) 是由Soukupová和Čech在2016年提出的一个简单而有效的眼睛状态指标。它利用Dlib的68个关键点中眼睛的6个关键点,通过计算垂直距离与水平距离的比值来判断眼睛的睁开程度。
右眼关键点索引:36-41
python
36 37
38 39
41 40
-
水平方向:36号点(左眼角)到39号点(右眼角)的距离
-
垂直方向:37号与41号、38号与40号的距离
2.2 EAR计算公式
其中 p1p1 到 p6p6 分别对应眼睛的6个关键点:
| 关键点 | 索引 | 位置 |
|---|---|---|
| p1 | 36 | 左眼角 |
| p2 | 37 | 上眼睑左 |
| p3 | 38 | 上眼睑右 |
| p4 | 39 | 右眼角 |
| p5 | 40 | 下眼睑右 |
| p6 | 41 | 下眼睑左 |
代码实现:
python
from sklearn.metrics import euclidean_distances
def eye_aspect_ratio(eye):
"""计算眼睛纵横比"""
# 1 2
# 0 3
# 5 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 = euclidean_distances(eye[0].reshape(1, 2), eye[3].reshape(1, 2))
ear = (A + B) / 2.0 / C
return ear
参数说明:
-
eye:形状为(6, 2)的数组,包含6个关键点的(x, y)坐标 -
eye[1]和eye[5]:上下眼睑左 -
eye[2]和eye[4]:上下眼睑右 -
eye[0]和eye[3]:左右眼角
2.3 EAR的物理意义
| 眼睛状态 | EAR值范围 | 说明 |
|---|---|---|
| 正常睁眼 | 0.3-0.4 | 垂直距离较大 |
| 眯眼 | 0.25-0.3 | 垂直距离减小 |
| 闭眼 | <0.25 | 垂直距离趋近于0 |
| 眨眼 | 短暂<0.25 | 快速下降后恢复 |
核心原理:
-
睁眼时:上下眼睑距离大,EAR值高
-
闭眼时:上下眼睑距离趋近于0,EAR值低
-
眨眼:EAR短暂下降后迅速恢复
-
疲劳:EAR持续低于阈值,持续时间长
三、完整代码解析
3.1 导入与初始化
python
import numpy as np
import dlib
import cv2
from sklearn.metrics import euclidean_distances
from PIL import Image, ImageDraw, ImageFont
# 初始化
COUNTER = 0 # 闭眼持续次数统计
detector = dlib.get_frontal_face_detector() # 人脸检测器
predictor = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat') # 关键点预测器
cap = cv2.VideoCapture(0) # 打开摄像头
关键组件:
-
COUNTER:全局计数器,记录连续闭眼的帧数 -
detector:Dlib的HOG人脸检测器 -
predictor:68点关键点预测模型 -
cap:摄像头视频流
3.2 中文标注函数
python
def cv2AddChineseText(img, text, position, textColor=(0, 255, 0), textSize=30):
"""向图片中添加中文"""
if isinstance(img, np.ndarray):
img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img)
fontStyle = ImageFont.truetype('simsun.ttc', textSize, encoding="utf-8")
draw.text(position, text, textColor, font=fontStyle)
return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
为什么需要这个函数?
-
OpenCV的
cv2.putText()不支持中文 -
借助PIL库的
ImageFont和ImageDraw实现中文绘制 -
核心步骤:OpenCV图像 → PIL图像 → 绘制中文 → 转回OpenCV
3.3 眼睛凸包绘制
python
def drawEye(eye):
eyeHull = cv2.convexHull(eye)
cv2.drawContours(frame, [eyeHull], -1, (0, 255, 0), 1)
cv2.convexHull():计算点集的凸包(能包含所有点的最小凸多边形),用于可视化眼睛区域。
3.4 代码主体
python
while True:
ret, frame = cap.read()
faces = detector(frame, 0)
for face in faces:
shape = predictor(frame, face)
shape = np.array([[p.x, p.y] for p in shape.parts()])
# 提取左右眼关键点
rightEye = shape[36:42] # 右眼
leftEye = shape[42:48] # 左眼
# 计算EAR
rightEAR = eye_aspect_ratio(rightEye)
leftEAR = eye_aspect_ratio(leftEye)
ear = (rightEAR + leftEAR) / 2.0 # 取平均值
# 疲劳判断逻辑
if ear < 0.3: # 闭眼
COUNTER += 1
if COUNTER >= 50: # 持续50帧闭眼,报警
frame = cv2AddChineseText(frame, '!!!危险!!!', (250, 250))
else: # 睁眼
COUNTER = 0
# 可视化
drawEye(leftEye)
drawEye(rightEye)
info = "EAR: {:.2f}".format(ear[0][0])
frame = cv2AddChineseText(frame, info, (0, 30))
cv2.imshow('frame', frame)
if cv2.waitKey(1) == 27: # ESC退出
break
3.5 核心逻辑详解
第一步:获取人脸和关键点
python
faces = detector(frame, 0) # 检测人脸
shape = predictor(frame, face) # 获取68个关键点
shape = np.array([[p.x, p.y] for p in shape.parts()]) # 转为坐标数组
第二步:提取眼睛关键点
python
rightEye = shape[36:42] # 右眼,索引36-41
leftEye = shape[42:48] # 左眼,索引42-47
第三步:计算EAR
python
rightEAR = eye_aspect_ratio(rightEye)
leftEAR = eye_aspect_ratio(leftEye)
ear = (rightEAR + leftEAR) / 2.0
取左右眼平均值的意义:单只眼睛可能因为侧脸或遮挡导致EAR不准确,取平均值更稳定。
第四步:疲劳判断
python
if ear < 0.3: # 小于阈值,认为闭眼
COUNTER += 1 # 计数器+1
if COUNTER >= 50: # 持续50帧闭眼
frame = cv2AddChineseText(frame, '!!!危险!!!', (250, 250))
else: # 睁眼
COUNTER = 0 # 计数器清零
关键参数:
-
阈值0.3:EAR低于0.3认为闭眼
-
阈值50:持续50帧闭眼判定为疲劳
-
帧率30fps:50帧 ≈ 1.67秒
四、参数调优指南
4.1 EAR阈值的选择
| 阈值 | 灵敏度 | 误报率 | 适用场景 |
|---|---|---|---|
| 0.35 | 高 | 高 | 需要极度灵敏的监测 |
| 0.30 | 中 | 中 | 通用场景(推荐) |
| 0.25 | 低 | 低 | 避免误报的场景 |
调优建议:
-
不同人的眼睛形状不同,EAR基准值有差异
-
可以在正式使用前,先采集几秒睁眼数据,计算个人化的基准EAR
-
阈值 = 个人基准EAR × 0.7 左右
4.2 计数阈值的选择
| 帧数阈值 | 对应时间(30fps) | 说明 |
|---|---|---|
| 30帧 | 1秒 | 较敏感,可能因眨眼误报 |
| 50帧 | 1.67秒 | 推荐,平衡灵敏度和误报 |
| 75帧 | 2.5秒 | 较保守,适合高速驾驶 |
为什么需要计数?
-
正常眨眼:EAR短暂下降后立即恢复,约0.1-0.3秒
-
疲劳闭眼:EAR持续低于阈值,超过1秒
-
通过计数器区分眨眼和疲劳
五、完整代码
python
"""疲劳检测,可用于驾驶员监控、学员上课状态检测等。"""
import numpy as np
import dlib
import cv2
from sklearn.metrics import euclidean_distances
from PIL import Image, ImageDraw, ImageFont
def eye_aspect_ratio(eye):
"""计算眼睛纵横比"""
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 = euclidean_distances(eye[0].reshape(1, 2), eye[3].reshape(1, 2))
return (A + B) / 2.0 / C
def cv2AddChineseText(img, text, position, textColor=(0, 255, 0), textSize=30):
"""向图片中添加中文"""
if isinstance(img, np.ndarray):
img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img)
fontStyle = ImageFont.truetype('simsun.ttc', textSize, encoding="utf-8")
draw.text(position, text, textColor, font=fontStyle)
return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
def drawEye(eye, frame):
"""绘制眼睛凸包"""
eyeHull = cv2.convexHull(eye)
cv2.drawContours(frame, [eyeHull], -1, (0, 255, 0), 1)
# 初始化
COUNTER = 0
EAR_THRESHOLD = 0.3
FRAME_THRESHOLD = 50
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
faces = detector(frame, 0)
for face in faces:
shape = predictor(frame, face)
shape = np.array([[p.x, p.y] for p in shape.parts()])
rightEye = shape[36:42]
leftEye = shape[42:48]
rightEAR = eye_aspect_ratio(rightEye)
leftEAR = eye_aspect_ratio(leftEye)
ear = (rightEAR + leftEAR) / 2.0
# 疲劳判断
if ear < EAR_THRESHOLD:
COUNTER += 1
if COUNTER >= FRAME_THRESHOLD:
frame = cv2AddChineseText(frame, '!!!危险!!!', (250, 250),
textColor=(0, 0, 255), textSize=50)
else:
COUNTER = 0
# 可视化
drawEye(leftEye, frame)
drawEye(rightEye, frame)
info = "EAR: {:.2f}".format(ear[0][0])
frame = cv2AddChineseText(frame, info, (0, 30))
cv2.imshow('frame', frame)
if cv2.waitKey(1) == 27:
break
cv2.destroyAllWindows()
cap.release()
六、总结
本篇博客围绕疲劳检测这一实用场景,系统讲解了:
| 知识点 | 核心内容 |
|---|---|
| EAR指标 | 眼睛纵横比,衡量眼睛睁开程度 |
| 计算公式 | 垂直距离/水平距离 |
| 疲劳判断 | EAR<0.3且持续50帧 |
| 计数逻辑 | 区分眨眼和疲劳 |
| 中文标注 | PIL库绘制中文 |