用Python+MediaPipe实现疲劳驾驶检测:从眼部关键点到EAR公式全解析

用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 关键点稳定性增强策略

在实际驾驶环境中,车辆震动、光照变化等因素都会影响关键点检测的稳定性。我通常采用以下几种策略来增强鲁棒性:

  1. 移动平均滤波:对连续多帧的关键点坐标进行平滑处理
  2. 异常值剔除:当某个关键点突然大幅度跳动时,使用历史数据替代
  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]) 
                          
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值