基于 dlib 与 OpenCV 的疲劳驾驶检测:眼睛纵横比(EAR)原理与代码逐段解析

1. 引言

疲劳驾驶是交通事故的重要诱因之一。本文介绍一种基于计算机视觉的疲劳检测方案:通过摄像头实时捕捉人脸,利用 dlib 定位 68 个人脸关键点,提取眼睛区域的 6 个关键点,计算眼睛纵横比(Eye Aspect Ratio,EAR),并根据 EAR 的持续变化判断驾驶员是否处于闭眼疲劳状态,从而触发警报。

本文将从原理出发,逐段讲解代码的调用逻辑与用法,帮助读者理解整个检测流程,并能够独立复现和扩展。

2. 原理概述

整个人脸检测与疲劳判断的流程可以概括为以下几个步骤:

  1. 使用 dlib 的正面人脸检测器定位画面中的人脸。
  2. 使用 dlib 的 68 点人脸关键点模型,获取每只眼睛周围的 6 个关键点坐标。
  3. 根据眼睛 6 个关键点的几何关系,计算眼睛纵横比(EAR)。
  4. 当 EAR 持续低于阈值(本文取 0.3)达到一定帧数时,判定为疲劳闭眼,触发警报。
  5. 在画面中实时绘制眼睛轮廓和 EAR 数值,便于观察。

其中,眼睛纵横比(EAR)是核心指标。它利用眼睛关键点之间的欧氏距离比值,刻画眼睛睁开程度,具有尺度不变性,即人脸离摄像头远近变化时,该比值仍然稳定。

3. 依赖库与导入

代码首先导入所需的第三方库:

python 复制代码
import numpy as np
import dlib
import cv2
from sklearn.metrics.pairwise import euclidean_distances  # 计算欧氏距离
# pip install scikit-learn -i https://pypi.tuna.tsinghua.edu.cn/simple
from PIL import Image, ImageDraw, ImageFont
# pip install pillow -i https://pypi.tuna.tsinghua.edu.cn/simple

各库的作用如下:

  • numpy:提供数组与矩阵运算,用于处理关键点坐标和数值计算。
  • dlib:提供人脸检测器与人脸关键点定位模型,是本方案的核心库。
  • cv2(OpenCV):负责摄像头读取、图像绘制与窗口显示。
  • euclidean_distances:来自 scikit-learn,用于计算两点之间的欧氏距离,这里用来计算眼睛关键点之间的距离。
  • PIL(Pillow):用于在图像上绘制中文文字,因为 OpenCV 自带的 putText 不支持中文。

安装依赖时,可以使用清华镜像加速下载,例如:

bash 复制代码
pip install scikit-learn -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install pillow -i https://pypi.tuna.tsinghua.edu.cn/simple

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

接下来定义计算眼睛纵横比的核心函数:

python 复制代码
def eye_aspect_ratio(eye):
    # --------计算眼睛纵横比--------
    #    1    2
    # 0        3  <---这是眼睛的6个关键点
    #    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 个关键点坐标数组,顺序对应上图标注的 0 到 5 号点。具体计算逻辑如下:

  • A:计算 1 号点与 5 号点之间的欧氏距离,即眼睛的垂直高度之一。
  • B:计算 2 号点与 4 号点之间的欧氏距离,即眼睛的垂直高度之二。
  • C:计算 0 号点与 3 号点之间的欧氏距离,即眼睛的水平宽度。
  • ear:取两个垂直距离的平均值,再除以水平宽度,得到纵横比。

当眼睛睁开时,垂直距离较大,EAR 值较高;当眼睛闭合时,垂直距离趋近于 0,EAR 值明显下降。因此,EAR 可以作为判断眼睛开合程度的有效指标。

这里使用 reshape(1,2) 是为了把一维坐标数组转换为二维矩阵,以满足 euclidean_distances 的输入格式要求。该函数返回的是一个 1×1 的矩阵,因此后续使用时需要通过索引取出数值。

5. 中文文字绘制函数

OpenCV 自带的 cv2.putText 无法直接显示中文,因此代码封装了一个使用 Pillow 绘制中文文字的函数:

python 复制代码
def cv2AddChineseText(img, text, position, textColor=(255, 0, 0), textSize=50):
    img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    draw = ImageDraw.Draw(img)
    fontStyle = ImageFont.truetype("simhei.ttf", textSize, encoding="utf-8")
    draw.text(position, text, textColor, font=fontStyle)
    return cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR)

该函数的调用逻辑如下:

  • img:输入的 OpenCV 图像(BGR 格式)。
  • text:要绘制的文字内容。
  • position:文字左上角的坐标。
  • textColor:文字颜色,默认红色。
  • textSize:字号,默认 50。

函数内部先将 BGR 图像转换为 RGB 格式,再转为 PIL 的 Image 对象,随后创建 ImageDraw 绘图对象,加载黑体字体文件 simhei.ttf,调用 draw.text 绘制文字,最后再转换回 OpenCV 的 BGR 格式返回。这样既保留了 OpenCV 的图像处理能力,又解决了中文显示问题。

需要注意的是,simhei.ttf 字体文件需要存在于当前工作目录或系统字体目录中,否则会报错。

6. 绘制眼睛轮廓函数

为了直观展示检测结果,代码定义了一个绘制眼睛凸包的函数:

python 复制代码
def drawEye(eye):#绘制眼眶凸包
    eyeHull = cv2.convexHull(eye)
    cv2.drawContours(frame, [eyeHull], -1, color=(0, 255, 0), thickness=1)

该函数接收某只眼睛的 6 个关键点坐标,调用 cv2.convexHull 计算这些点的凸包,即包围眼睛区域的最小凸多边形,然后通过 cv2.drawContours 在全局变量 frame 上绘制该轮廓,颜色为绿色,线宽为 1 像素。

这里直接使用了全局变量 frame,因此函数内部不需要返回值,绘制结果会直接反映在当前的视频帧上。

7. 全局变量与模型初始化

在进入主循环之前,代码完成了一系列初始化工作:

python 复制代码
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 提供的正面人脸检测器,用于在每一帧中定位人脸区域。
  • predictor :dlib 的 68 点人脸关键点预测器,需要加载预训练模型文件 shape_predictor_68_face_landmarks.dat。该文件可以从 dlib 官网下载。
  • cap:OpenCV 的视频捕获对象,参数 0 表示使用默认摄像头。

8. 主循环:逐帧处理

主循环是程序的核心,负责读取每一帧图像并执行完整的检测流程:

python 复制代码
while True:
    ret, frame = cap.read()
    faces = detector(frame, 0)   # 获取人脸
    for face in faces:  # 循环遍历每一个人脸
        shape = predictor(frame, face) #获取关键点
        # 将关键点转换为坐标(x,y)的形式
        shape = np.array([[p.x, p.y] for p in shape.parts()])
        rightEye = shape[36:42] # 右眼,关键点索引从36到41(不包含42)
        leftEye = shape[42:48]  # 左眼,关键点索引从42到47(不包含48)
        rightEAR = eye_aspect_ratio(rightEye)# 计算右眼纵横比
        leftEAR = eye_aspect_ratio(leftEye)# 计算左眼纵横比
        ear = (leftEAR + rightEAR) / 2.0 #均值处理
        if ear < 0.3:  # 小于0.3,认为闭眼,也可能是眨眼
            COUNTER += 1 # 每检测到一次,将+1
            if COUNTER >= 50: #持续50帧都闭眼,则警报
                frame = cv2AddChineseText(frame, text="!!!!危险!!!!", position=(250,250))
        else: #宽高比>0.3,则计数器清零,解除疲劳标志
            COUNTER = 0
        drawEye(leftEye)#绘制左眼凸包
        drawEye(rightEye)#绘制右眼凸包
        info = "EAR: {:.2f}".format(ear[0][0])
        frame = cv2AddChineseText(frame, info, position=(0,30))#显示眼睛闭合程度值
    cv2.imshow("Frame", frame)
    if cv2.waitKey(1) == 27:
        break
cv2.destroyAllWindows()
cap.release()

下面逐段分析主循环的调用逻辑:

8.1 读取视频帧与人脸检测

cap.read() 从摄像头读取一帧图像,返回布尔值 ret 和图像数据 frame。随后调用 detector(frame, 0) 检测该帧中的人脸,第二个参数 0 表示不使用图像金字塔的上采样,即按原始分辨率检测。检测结果是一个列表,每个元素对应一张人脸的区域。

8.2 关键点提取与眼睛区域切片

对于检测到的每一张人脸,调用 predictor(frame, face) 获取 68 个人脸关键点。通过列表推导式将关键点对象转换为 (x, y) 坐标数组。dlib 的 68 点模型中,右眼关键点索引为 36 到 41,左眼关键点索引为 42 到 47,因此通过切片 shape[36:42]shape[42:48] 分别取出左右眼的 6 个关键点。

8.3 计算 EAR 并取均值

分别调用 eye_aspect_ratio 计算左右眼的纵横比,再取两者的平均值作为当前帧的最终 EAR 值。取均值可以降低单只眼睛检测误差带来的影响,使判断更加稳定。

8.4 疲劳判断与警报触发

当 EAR 小于 0.3 时,认为眼睛处于闭合状态,计数器 COUNTER 加 1。如果连续闭眼帧数达到 50 帧,即认为驾驶员处于疲劳状态,在画面中绘制红色警告文字「!!!!危险!!!!」。当 EAR 恢复到 0.3 以上时,计数器清零,解除疲劳标志。这里的 0.3 和 50 是两个关键超参数,分别对应闭眼判定阈值和持续帧数阈值,实际使用中可以根据摄像头帧率和检测灵敏度进行调整。

8.5 可视化与退出

调用 drawEye 绘制左右眼的凸包轮廓,并在画面左上角显示当前 EAR 数值。最后通过 cv2.imshow 显示处理后的画面,cv2.waitKey(1) 等待键盘输入,当按下 ESC 键(ASCII 码 27)时退出循环。循环结束后释放摄像头并关闭所有窗口。

9. 完整代码

将上述所有部分组合在一起,得到完整的疲劳驾驶检测程序:

python 复制代码
import numpy as np
import dlib
import cv2
from sklearn.metrics.pairwise import euclidean_distances  # 计算欧氏距离
# pip install scikit-learn -i https://pypi.tuna.tsinghua.edu.cn/simple
from PIL import Image, ImageDraw, ImageFont
# pip install pillow -i https://pypi.tuna.tsinghua.edu.cn/simple
def eye_aspect_ratio(eye):
# --------计算眼睛纵横比--------
#    1    2
# 0        3  <---这是眼睛的6个关键点
#    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
def cv2AddChineseText(img, text, position, textColor=(255, 0, 0), textSize=50):
img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img)
fontStyle = ImageFont.truetype("simhei.ttf", textSize, encoding="utf-8")
draw.text(position, text, textColor, font=fontStyle)
return cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR)
def drawEye(eye):#绘制眼眶凸包
eyeHull = cv2.convexHull(eye)
cv2.drawContours(frame, [eyeHull], -1, color=(0, 255, 0), thickness=1)
COUNTER = 0      # 闭眼持续次数统计
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()
faces = detector(frame, 0)   # 获取人脸
for face in faces:  # 循环遍历每一个人脸
shape = predictor(frame, face) #获取关键点
# 将关键点转换为坐标(x,y)的形式
shape = np.array([[p.x, p.y] for p in shape.parts()])
rightEye = shape[36:42] # 右眼,关键点索引从36到41(不包含42)
leftEye = shape[42:48]  # 左眼,关键点索引从42到47(不包含48)
rightEAR = eye_aspect_ratio(rightEye)# 计算右眼纵横比
leftEAR = eye_aspect_ratio(leftEye)# 计算左眼纵横比
ear = (leftEAR + rightEAR) / 2.0 #均值处理
if ear < 0.3:  # 小于0.3,认为闭眼,也可能是眨眼
COUNTER += 1 # 每检测到一次,将+1
if COUNTER >= 50: #持续50帧都闭眼,则警报
frame = cv2AddChineseText(frame, te
相关推荐
我不会起名字3221 小时前
一天一道力扣Hot100(37):深度优先算法--括号生成
java·数据结构·c++·后端·python·算法·go
小白勇闯网安圈1 小时前
第5章 流式输出与人工审核
python·langchain
打工仔折腾 AI2 小时前
数据库上 K8s 之后谁来管?拆解金仓 KES-Operator 的声明式运维方案
人工智能·后端·python·性能优化·ai agent 实战
zx_741484812 小时前
【计算机视觉入门】OpenCV + MediaPipe + dlib:从仿射变换到换脸、手势、姿态与人脸网格
python·opencv·计算机视觉
PixelBai2 小时前
在线正则表达式测试工具推荐:实时高亮、支持 flags,写完先验一遍再上代码
python
正经教主2 小时前
【FDE系列】阶段2:Day 33:进阶查询 — 窗口函数与 CTE
人工智能·python·fde
小白勇闯网安圈2 小时前
第2章 状态 State 详解
python·langchain
Dr_Fourier2 小时前
AWQ量化
c++·人工智能·pytorch·ai
派大_星2 小时前
基于MediaPipe和传统机器学习的手势识别
python