初识OpenCV——疲劳检测

一、引言:将疲惫描述成数字

在上一篇博客中,我们学习了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库的ImageFontImageDraw实现中文绘制

  • 核心步骤: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库绘制中文
相关推荐
byte轻骑兵1 小时前
【BlueZ 】Linux 内核蓝牙子系统入门:hci_core 模块与 BlueZ 的交互
linux·人工智能·bluez·电脑蓝牙·嵌入式蓝牙
Dr_Fourier1 小时前
AWQ量化
c++·人工智能·pytorch·ai
Casbin开源社区1 小时前
OpenAgent 详解:单二进制自托管 AI Agent 平台,30+ 模型接入、RAG 知识库、MCP 工具调用与 Casbin 工具权限
人工智能·golang·开源
BJ_Bonree1 小时前
博睿数据加入ITSS分会,成为国家级信息技术服务标准化体系单位成员!
大数据·运维·数据库·人工智能·可观测性
河图洛水1 小时前
Behavior-1k:2026 挑战赛纯仿真 benchmark
人工智能·机器人
光锥智能1 小时前
Agentic Cloud,云厂商们的新叙事
人工智能·华为
ZDN_is_beauty1 小时前
綦江烟草部署(在wsl2里部署)
人工智能·python
子非鱼eva1 小时前
昇腾开源仓Issue分析解答-mindspore精选(二)·mindformers深耕与三大户续采
人工智能·ai·gitcode
QXWZ_IA2 小时前
新建公路验收的路面质量检测方法
人工智能·科技·安全