用Python+MediaPipe实现疲劳驾驶检测:从眼部关键点到EAR公式全解析
深夜的高速公路上,仪表盘微弱的蓝光映在驾驶员脸上,眼皮开始不自觉地耷拉下来——这个瞬间,可能就是一场悲剧的开始。疲劳驾驶早已成为全球道路安全的首要威胁之一,而计算机视觉技术正为我们提供一种非侵入式、实时监测的解决方案。今天,我们就来深入探讨如何利用Python和MediaPipe,构建一个精准、高效的疲劳驾驶检测系统。
这个项目的核心在于一个看似简单却极其巧妙的数学概念:眼睛纵横比。想象一下,当人眼睁开时,上下眼睑之间的距离与左右眼角之间的距离会形成一个特定的比例;而当眼睛闭合时,这个比例会急剧下降。通过实时追踪这个比例的变化,我们就能判断驾驶员是否处于疲劳状态。MediaPipe提供的面部网格检测能力,让我们能够以毫秒级的延迟获取眼部关键点坐标,而Soukupová和Čech在2016年提出的EAR公式,则为这一判断提供了坚实的数学基础。
对于智能驾驶场景的开发者而言,这不仅仅是一个学术课题,更是一个需要在实际车辆环境中稳定运行的工程挑战。我们需要考虑摄像头帧率、光照变化、头部姿态、甚至驾驶员佩戴眼镜等多种复杂情况。本文将带你从MediaPipe的基础配置开始,逐步深入到多线程处理、阈值优化等实战技巧,最终构建一个能够在真实场景中可靠工作的疲劳检测系统。
1. MediaPipe面部网格与眼部关键点定位
1.1 MediaPipe Face Mesh的核心机制
MediaPipe的Face Mesh解决方案提供了468个面部关键点的实时检测能力,这些点覆盖了面部的各个区域,从眉毛到下巴,从脸颊到嘴唇。但对于疲劳检测,我们真正关心的只是其中的一小部分——眼睛周围的12个关键点。
让我们先来看看MediaPipe的安装和基础配置。与传统的dlib库相比,MediaPipe在速度和精度上都有显著优势,特别是在移动设备和边缘计算场景中。
import cv2
import mediapipe as mp
import numpy as np
# 初始化MediaPipe面部网格
mp_face_mesh = mp.solutions.face_mesh
face_mesh = mp_face_mesh.FaceMesh(
static_image_mode=False, # 视频流模式
max_num_faces=1, # 只检测一张脸
refine_landmarks=True, # 使用精炼的关键点
min_detection_confidence=0.5,
min_tracking_confidence=0.5
)
这里有几个关键参数需要注意:
static_image_mode=False:设置为视频流模式,MediaPipe会在连续帧之间进行跟踪,大幅提升处理速度max_num_faces=1:在驾驶场景中,我们通常只关注驾驶员一人refine_landmarks=True:启用精炼的关键点检测,特别是眼部区域会更准确
1.2 眼部关键点的精确提取
MediaPipe的面部关键点有固定的索引编号体系。对于左眼和右眼,我们需要提取特定的6个点来计算EAR值。这些点的选择基于解剖学特征,能够最稳定地反映眼睑的开合状态。
# MediaPipe面部关键点索引定义
LEFT_EYE_INDICES = [362, 385, 387, 263, 373, 380]
RIGHT_EYE_INDICES = [33, 160, 158, 133, 153, 144]
def extract_eye_landmarks(landmarks, frame_shape):
"""从MediaPipe landmarks中提取眼部关键点坐标"""
h, w, _ = frame_shape
left_eye_points = []
right_eye_points = []
for idx in LEFT_EYE_INDICES:
landmark = landmarks.landmark[idx]
# 将归一化坐标转换为像素坐标
x = int(landmark.x * w)
y = int(landmark.y * h)
left_eye_points.append((x, y))
for idx in RIGHT_EYE_INDICES:
landmark = landmarks.landmark[idx]
x = int(landmark.x * w)
y = int(landmark.y * h)
right_eye_points.append((x, y))
return np.array(left_eye_points), np.array(right_eye_points)
这12个关键点的具体位置对应着眼部的重要解剖特征:
| 关键点索引 | 眼部位置 | 描述 |
|---|---|---|
| 362 (左眼) | p1 | 左眼外眼角 |
| 385 (左眼) | p2 | 左眼上眼睑中部 |
| 387 (左眼) | p3 | 左眼内眼角 |
| 263 (左眼) | p4 | 左眼内眼角下方 |
| 373 (左眼) | p5 | 左眼下眼睑中部 |
| 380 (左眼) | p6 | 左眼外眼角下方 |
| 33 (右眼) | p1 | 右眼内眼角 |
| 160 (右眼) | p2 | 右眼上眼睑中部 |
| 158 (右眼) | p3 | 右眼外眼角 |
| 133 (右眼) | p4 | 右眼外眼角下方 |
| 153 (右眼) | p5 | 右眼下眼睑中部 |
| 144 (右眼) | p6 | 右眼内眼角下方 |
注意:MediaPipe的坐标系统是归一化的,范围在[0, 1]之间。在实际使用时,需要根据图像的实际尺寸进行转换。同时,由于摄像头畸变和面部姿态变化,这些坐标可能会有轻微偏移,这也是为什么我们需要在后续步骤中进行校准和滤波。
1.3 关键点稳定性增强策略
在实际驾驶环境中,车辆震动、光照变化等因素都会影响关键点检测的稳定性。我通常采用以下几种策略来增强鲁棒性:
- 移动平均滤波:对连续多帧的关键点坐标进行平滑处理
- 异常值剔除:当某个关键点突然大幅度跳动时,使用历史数据替代
- 置信度加权:MediaPipe提供了每个关键点的可见性分数,可以用于加权计算
class EyeLandmarkStabilizer:
"""眼部关键点稳定器"""
def __init__(self, buffer_size=5):
self.buffer_size = buffer_size
self.left_eye_buffer = []
self.right_eye_buffer = []
def stabilize(self, left_points, right_points):
"""使用移动平均稳定关键点"""
self.left_eye_buffer.append(left_points)
self.right_eye_buffer.append(right_points)
# 保持缓冲区大小
if len(self.left_eye_buffer) > self.buffer_size:
self.left_eye_buffer.pop(0)
self.right_eye_buffer.pop(0)
# 计算移动平均
stabilized_left = np.mean(self.left_eye_buffer, axis=0)
stabilized_right = np.mean(self.right_eye_buffer, axis=0)
return stabilized_left.astype(int), stabilized_right.astype(int)
2. EAR公式的数学原理与实现细节
2.1 EAR公式的几何意义
眼睛纵横比公式的精妙之处在于它的几何直观性。Soukupová和Čech在2016年的论文中提出的公式如下:
EAR = (‖p2 - p6‖ + ‖p3 - p5‖) / (2 × ‖p1 - p4‖)
其中p1到p6是眼部周围的6个关键点。这个公式的分子计算了垂直方向上的两个距离(上眼睑中点到下眼睑中点,以及内眼角到外眼角的垂直分量),分母计算了水平方向上的眼宽。
让我用一个具体的例子来说明这个公式的稳定性。假设一个人的眼睛完全睁开时,垂直距离大约为15像素,水平距离为30像素,那么EAR值约为0.25。当眼睛半闭时,垂直距离可能减少到5像素,而水平距离基本保持不变,EAR值降至约0.083。当眼睛完全闭合时,垂直距离接近0,EAR值趋近于0。
def calculate_ear(eye_points):
"""
计算单只眼睛的纵横比
eye_points: 包含6个(x, y)坐标的数组,顺序为[p1, p2, p3, p4, p5, p6]
"""
# 计算垂直距离
A = np.linalg.norm(eye_points[1] - eye_points[5]) # p2到p6
B = np.linalg.norm(eye_points[2] - eye_points[4]) # p3到p5
# 计算水平距离
C = np.linalg.norm(eye_points[0] - eye_points[3]) # p1到p4
# 避免除零错误
if C == 0:
return 0.0
ear = (A + B) / (2.0 * C)
return ear
2.2 双眼睛EAR值的融合策略
在实际应用中,我们通常同时计算左右眼的EAR值,然后取平均值。但简单的算术平均并不总是最优选择,特别是在以下情况:
- 头部偏转:当驾驶员看向侧方后视镜时,一只眼睛可能被部分遮挡
- 光照不均:阳光从一侧照射时,可能影响单侧眼睛的检测精度
- 暂时性遮挡:驾驶员揉眼睛或调整眼镜时
针对这些情况,我推荐使用加权平均策略:
def calculate_weighted_ear(left_ear, right_ear, confidence_left=1.0, confidence_right=1.0):
"""
基于置信度的加权EAR计算
confidence: 基于关键点可见性或历史稳定性的置信度分数
"""
total_confidence = confidence_left + confidence_right
if total_confidence == 0:
return (left_ear + right_ear) / 2.0
weighted_ear = (left_ear * confidence_left + right_ear * confidence_right) / total_confidence
return weighted_ear
def calculate_eye_confidence(eye_points, historical_points):
"""
计算单眼检测的置信度
基于:1) 关键点可见性 2) 与历史数据的差异 3) 几何合理性
"""
# 1. 检查关键点是否在图像边界内
h, w = 480, 640 # 假设图像尺寸
out_of_bounds = sum(1 for p in eye_points if p[0] < 0 or p[0] >= w or p[1] < 0 or p[1] >= h)
bounds_score = 1.0 - (out_of_bounds / 6.0)
# 2. 与历史数据的差异
if historical_points:
mean_diff = np.mean([np.linalg.norm(eye_points[i] - historical_points[-1][i])


2117

被折叠的 条评论
为什么被折叠?



