OpenCV 疲劳检测实战:用 dlib 计算眼睛纵横比 (EAR)

从零开始,带你用 Python + OpenCV + dlib 实现驾驶员疲劳检测

📌 前言

作为一个 OpenCV 初学者,我一直想做点"有点实际意义"的小项目。疲劳检测就是一个很不错的切入点------它用到了人脸检测关键点定位几何特征计算实时视频处理等知识点,代码量不大,但成就感十足。

这篇文章会详细解析一份完整的疲劳检测代码,并补充大量背景知识。所有图片我都会用自己的截图和示意图填充,方便你对照理解。


🧱 一、项目整体思路

  1. 通过摄像头读取实时视频流

  2. 检测人脸,并定位 68 个人脸关键点(重点是左右眼)

  3. 计算眼睛纵横比 (EAR),判断眼睛是睁开还是闭合

  4. 如果连续多帧(比如 50 帧)都检测到闭眼,则判定为疲劳,在画面中显示警告

完整代码:

复制代码
"""
疲劳检测,可用于驾驶员监控、学员上课状态检测等
"""
import numpy as np
import dlib
import cv2
from sklearn.metrics.pairwise import euclidean_distances  # 计算欧氏距离
from PIL import Image, ImageDraw, ImageFont  # 用于OpenCV中文显示

# -------------------------- 1. 眼睛纵横比(EAR)计算函数 --------------------------
def eye_aspect_ratio(eye):
    """
    计算眼睛的纵横比(Eye Aspect Ratio, EAR)
    :param eye: 眼睛的6个关键点坐标数组(shape=(6,2))
    :return: EAR值,EAR越小表示眼睛越闭着
    """
    # 计算眼睛垂直方向的两个距离
    A = euclidean_distances(eye[1].reshape(1,2), eye[5].reshape(1,2))[0][0]
    B = euclidean_distances(eye[2].reshape(1,2), eye[4].reshape(1,2))[0][0]
    # 计算眼睛水平方向的距离
    C = euclidean_distances(eye[0].reshape(1,2), eye[3].reshape(1,2))[0][0]
    # 计算EAR
    ear = (A + B) / (2.0 * C)
    return ear

# -------------------------- 2. OpenCV中文显示函数 --------------------------
def cv2AddChineseText(img, text, position, textColor=(255, 0, 0), textSize=30):
    """
    解决OpenCV无法直接显示中文的问题,用PIL绘制后转回OpenCV格式
    :param img: OpenCV格式的图像
    :param text: 要显示的中文文本
    :param position: 文本位置(x,y)
    :param textColor: 文本颜色(BGR格式)
    :param textSize: 文本大小
    :return: 绘制后的OpenCV图像
    """
    # 转换为PIL格式
    if isinstance(img, np.ndarray):
        img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    # 创建画笔
    draw = ImageDraw.Draw(img)
    # 加载字体(Windows系统自带宋体,其他系统请修改字体路径)
    fontStyle = ImageFont.truetype("simsun.ttc", textSize, encoding="utf-8")
    # 绘制文字
    draw.text(position, text, textColor, font=fontStyle)
    # 转换回OpenCV格式
    return cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR)

# -------------------------- 3. 绘制眼睛凸包函数 --------------------------
def drawEye(frame, eye):
    """
    绘制眼睛的凸包轮廓,方便可视化
    :param frame: 视频帧图像
    :param eye: 眼睛的关键点数组
    """
    eyeHull = cv2.convexHull(eye)
    cv2.drawContours(frame, [eyeHull], -1, (0, 255, 0), 1)  # 绿色轮廓,线宽1

# -------------------------- 主程序 --------------------------
if __name__ == "__main__":
    # 初始化参数
    COUNTER = 0          # 闭眼持续帧数计数
    EYE_AR_THRESH = 0.3  # EAR阈值,小于该值认为眼睛闭合
    EYE_AR_CONSEC_FRAMES = 50  # 连续闭眼超过该帧数则判定为疲劳

    # 初始化人脸检测器和关键点预测器
    detector = dlib.get_frontal_face_detector()  # 人脸检测器
    # 关键点预测器(需提前下载shape_predictor_68_landmarks.dat文件,放在同目录)
    predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")

    # 打开摄像头
    cap = cv2.VideoCapture(0)
    if not cap.isOpened():
        print("无法打开摄像头!")
        exit()

    while True:
        ret, frame = cap.read()
        if not ret:
            print("无法读取视频帧!")
            break

        # 1. 人脸检测
        faces = detector(frame, 0)  # 获取人脸区域
        for face in faces:
            # 2. 获取人脸关键点
            shape = predictor(frame, face)  # 获取68个关键点
            # 转换为(x,y)坐标数组
            shape = np.array([[p.x, p.y] for p in shape.parts()])

            # 3. 提取左右眼关键点(dlib 68点标准:右眼36-41,左眼42-47)
            rightEye = shape[36:42]  # 右眼关键点(索引36到41,不包含42)
            leftEye = shape[42:48]   # 左眼关键点(索引42到47,不包含48)

            # 4. 计算左右眼EAR值并取平均
            rightEAR = eye_aspect_ratio(rightEye)
            leftEAR = eye_aspect_ratio(leftEye)
            ear = (leftEAR + rightEAR) / 2.0  # 左右眼EAR均值

            # 5. 疲劳判断逻辑
            if ear < EYE_AR_THRESH:
                COUNTER += 1
                # 连续闭眼超过设定帧数,判定为疲劳
                if COUNTER >= EYE_AR_CONSEC_FRAMES:
                    # 显示疲劳警告文字
                    frame = cv2AddChineseText(frame, "!!!!犯困!!!!", position=(250, 250), textColor=(0, 0, 255), textSize=50)
            else:
                # 眼睛睁开,重置计数
                COUNTER = 0
                # 绘制眼睛凸包(仅睁眼时绘制)
                drawEye(frame, leftEye)
                drawEye(frame, rightEye)

            # 6. 显示当前EAR值
            info_text = f"EAR: {ear:.2f}"
            frame = cv2AddChineseText(frame, info_text, position=(10, 30), textColor=(0, 255, 0), textSize=30)

        # 显示视频帧
        cv2.imshow("Frame", frame)
        # 按ESC键退出
        if cv2.waitKey(1) == 27:
            break

    # 释放资源
    cv2.destroyAllWindows()
    cap.release()

🛠 二、环境配置

安装依赖库

复制代码
pip install numpy opencv-python dlib scikit-learn pillow

⚠️ 注意dlib 在 Windows 上可能需要先安装 CMake 和 Visual C++ 编译工具。推荐直接用预编译的 wheel 安装(比如 dlib‑19.24.2‑cp39‑cp39‑win_amd64.whl)。

下载关键点模型文件

中文字体(可选)

为了在 OpenCV 中显示中文,需要准备一个中文字体文件(如 Windows 的 simsun.ttc)。如果不需要中文,也可以直接用英文提示。


📖 三、代码逐段详解

1. 导入库

复制代码
import numpy as np
import dlib
import cv2
from sklearn.metrics.pairwise import euclidean_distances
from PIL import Image, ImageDraw, ImageFont
  • sklearn 中的 euclidean_distances 用来计算两点间的欧氏距离,比手写 np.sqrt 更简洁。

  • PIL 用于解决 OpenCV 无法显示中文的问题。


2. 眼睛纵横比 (EAR) 计算函数

复制代码
def eye_aspect_ratio(eye):
    A = euclidean_distances(eye[1].reshape(1,2), eye[5].reshape(1,2))[0][0]
    B = euclidean_distances(eye[2].reshape(1,2), eye[4].reshape(1,2))[0][0]
    C = euclidean_distances(eye[0].reshape(1,2), eye[3].reshape(1,2))[0][0]
    ear = (A + B) / (2.0 * C)
    return ear
🔍 什么是 EAR?

眼睛纵横比 (Eye Aspect Ratio) 是眼睛高度与宽度的比值。

睁眼时,高度大 → EAR ≈ 0.25~0.3

闭眼时,高度趋近 0 → EAR ≈ 0

下图是眼睛 6 个关键点的位置(dlib 标准):

  • A = 点37 到点41的距离

  • B = 点38 到点40 的距离

  • C = 点36 到点39 的距离

公式:

EAR=∥A∥+∥B∥2∥C∥EAR=2∥C∥∥A∥+∥B∥​


3. OpenCV 中文显示函数(PIL 桥接)

复制代码
def cv2AddChineseText(img, text, position, textColor=(255,0,0), textSize=30):
    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.asarray(img), cv2.COLOR_RGB2BGR)

因为 OpenCV 的 putText 不支持中文,所以我们先用 PIL 绘制文字,再转回 OpenCV 格式。


4. 绘制眼睛凸包(可视化)

复制代码
def drawEye(frame, eye):
    eyeHull = cv2.convexHull(eye)
    cv2.drawContours(frame, [eyeHull], -1, (0, 255, 0), 1)
  • 凸包:包含所有关键点的最小凸多边形,画出来就像眼睛的轮廓。

5. 主程序核心逻辑

初始化参数
复制代码
COUNTER = 0                     # 闭眼连续帧数
EYE_AR_THRESH = 0.3            # EAR 阈值
EYE_AR_CONSEC_FRAMES = 50      # 连续多少帧算疲劳
加载 dlib 模型
复制代码
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()])

        # 提取左右眼关键点(dlib 68点标准)
        rightEye = shape[36:42]   # 右眼
        leftEye  = shape[42:48]   # 左眼

        # 计算 EAR
        rightEAR = eye_aspect_ratio(rightEye)
        leftEAR  = eye_aspect_ratio(leftEye)
        ear = (leftEAR + rightEAR) / 2.0

        # 疲劳判断
        if ear < EYE_AR_THRESH:
            COUNTER += 1
            if COUNTER >= EYE_AR_CONSEC_FRAMES:
                frame = cv2AddChineseText(frame, "!!!!犯困!!!!", (250,250), (0,0,255), 50)
        else:
            COUNTER = 0
            drawEye(frame, leftEye)
            drawEye(frame, rightEye)

        # 显示 EAR 值
        frame = cv2AddChineseText(frame, f"EAR: {ear:.2f}", (10,30), (0,255,0), 30)

    cv2.imshow("Frame", frame)
    if cv2.waitKey(1) == 27:   # ESC 键退出
        break

dlib 68 个关键点分布


🧪 四、运行效果与调参建议

正常状态(睁眼)

  • EAR 值通常在 0.25 ~ 0.35 之间

  • 眼睛凸包轮廓清晰可见

  • 画面显示绿色 EAR: 0.28

闭眼状态

  • EAR 值骤降到 0.10 以下

  • 连续闭眼超过 50 帧后,屏幕出现红色警告文字

参数调整指南

参数 作用 调整建议
EYE_AR_THRESH 判断闭眼的 EAR 阈值 可以打印几帧睁眼和闭眼的真实 EAR 值,取中间值
EYE_AR_CONSEC_FRAMES 连续闭眼多少帧算疲劳 摄像头 30fps 时,50 帧 ≈ 1.67 秒;想更灵敏就减小该值

⚠️ 五、常见问题与解决办法

问题 可能原因 解决方法
ImportError: No module named 'dlib' dlib 没装好 使用 conda install -c conda-forge dlib 或下载预编译 wheel
RuntimeError: Unable to open shape_predictor_68_face_landmarks.dat 模型文件路径不对 确保 .dat 文件在脚本同目录,或用绝对路径
中文显示为方框 字体文件不存在 修改 cv2AddChineseText 中的字体路径,或暂时用英文
EAR 值一直很低 光照差/眼镜反光/距离太远 调整阈值,或改善光照条件
程序卡顿 每帧检测计算量大 降低摄像头分辨率:cap.set(cv2.CAP_PROP_FRAME_WIDTH, 320)

🚀 六、改进方向(进阶挑战)

如果觉得基本功能已经掌握,不妨尝试以下扩展:

  1. 眨眼过滤:短时间闭眼再睁开是眨眼,不应报警。可以记录闭眼帧数小于某个值(如 5 帧)时忽略。

  2. 增加打哈欠检测 :使用嘴巴关键点(60~67)计算嘴部纵横比 (MAR)

  3. 头部姿态估计:判断是否低头或侧头。

  4. 使用 MediaPipe:替代 dlib,速度更快且不需要手动下载模型。

  5. 多线程加速:将人脸检测放在子线程中,提高实时性。


📚 七、总结

通过这个项目,你可以学到:

  • ✅ 使用 dlib 进行人脸关键点检测

  • ✅ 理解并实现眼睛纵横比 (EAR) 的计算

  • ✅ 实时视频流的处理框架(读帧 → 处理 → 显示)

  • ✅ 用 PIL 弥补 OpenCV 中文显示的不足

  • ✅ 一个完整的"规则型"疲劳检测系统(无需深度学习)

对于 OpenCV 初学者来说,这是一个非常棒的练手项目------既有理论支撑,又能立刻看到实际效果。

希望这篇笔记能帮你顺利跑通代码,也欢迎你在评论区交流遇到的问题!

📷 所有图片均为本人运行截图,如需转载注明出处即可。

如果觉得有用,记得点赞 + 收藏 ♥

相关推荐
AI周红伟2 小时前
Hermes Agent 工具-周红伟
linux·网络·人工智能·腾讯云·openclaw
杜子不疼.2 小时前
Python + AI 实战:用 LangChain 搭建企业级 RAG 知识库
人工智能·python·langchain
阿杰学AI2 小时前
AI核心知识118—大语言模型之 Software 2.0 (简洁且通俗易懂版)
人工智能·ai·语言模型·自然语言处理·aigc·编程·software 2.0
天天进步20152 小时前
[核心篇] 视频一致性算法:Toonflow 是如何处理视频闪烁问题的?
人工智能
中科岩创2 小时前
数字传感护华为数字能源大厦,控制加固施工安全风险!
人工智能·科技·物联网
甄心爱学习2 小时前
【项目实训(个人3)】
vue.js·人工智能·python·个人开发
Bat U2 小时前
JavaEE|计算机是如何工作的
java·人工智能
AI先驱体验官2 小时前
BotCash:AI智能体变现从小 Demo 到商业产品的距离
大数据·人工智能·深度学习·重构·aigc
輕華2 小时前
OpenCV三大传统人脸识别算法:EigenFace、FisherFace与LBPH实战
人工智能·opencv·算法