场景:每天重复的驾驶与重复的风险

2026年5月,一位来自Staten岛的公交司机在弗吉尼亚州因致命车祸被控过失杀人。事故具体原因官方尚未公布,但有一个事实不言自明:全球每年约130万人死于交通事故,其中疲劳驾驶占20%以上(WHO数据)。对于每天在路上跑8-10小时的公交司机来说,疲劳不是偶然,是职业常态。

你作为一个开发者,可能正在做车队管理、车联网甚至自动驾驶相关产品。但无论技术多炫,最薄弱的一环永远是驾驶员状态。今天这篇文章,我会教你用最朴实的工具——一台带摄像头的笔记本、OpenCV和MediaPipe——搭一个实时疲劳驾驶检测系统。读完你就能在自己的开发机上跑起来,甚至可以部署到树莓派上。

自动化后的效果

传统方式:靠调度员抽查或司机自查,几乎失效。

自动化后:摄像头实时分析驾驶员面部特征,一旦检测到闭眼超过1秒、连续打哈欠、或低头超过30度,立即发出蜂鸣声或推送警报。我们实测在NVIDIA Jetson Nano上,帧率可达25-30fps,延迟<200ms。

工具组合与流程图

  • OpenCV:摄像头采集与图像显示
  • MediaPipe Face Mesh:468个面部关键点检测,模型轻量(约4MB)
  • NumPy:几何计算(眼纵横比EAR、嘴纵横比MAR)
  • Pygame(可选):发出声音警报

流程图:

疲劳驾驶系统流程图:摄像头→MediaPipe Face Mesh→提取眼部/嘴部/头部关键点→计算EAR/MAR/俯仰角→阈值判断→触发警报

关键节点配置

1. 闭眼检测:EAR(Eye Aspect Ratio)

原理:计算眼周6个关键点的纵横比,正常睁眼时EAR约0.3-0.4,闭眼时接近0.1。

python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
import cv2
import mediapipe as mp
import numpy as np

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)

def eye_aspect_ratio(landmarks, eye_indices):
    p1 = np.array([landmarks[eye_indices[0]].x, landmarks[eye_indices[0]].y])
    p2 = np.array([landmarks[eye_indices[1]].x, landmarks[eye_indices[1]].y])
    p3 = np.array([landmarks[eye_indices[2]].x, landmarks[eye_indices[2]].y])
    p4 = np.array([landmarks[eye_indices[3]].x, landmarks[eye_indices[3]].y])
    p5 = np.array([landmarks[eye_indices[4]].x, landmarks[eye_indices[4]].y])
    p6 = np.array([landmarks[eye_indices[5]].x, landmarks[eye_indices[5]].y])
    # 计算垂直距离平均值 / 水平距离
    ear = (np.linalg.norm(p2-p6) + np.linalg.norm(p3-p5)) / (2.0 * np.linalg.norm(p1-p4))
    return ear

# 左右眼索引(MediaPipe Face Mesh标准)
LEFT_EYE = [33, 160, 158, 133, 153, 144]
RIGHT_EYE = [362, 385, 387, 263, 373, 380]
EAR_THRESHOLD = 0.23  # 关键阈值,根据光照和摄像头调整

2. 打哈欠检测:MAR(Mouth Aspect Ratio)

嘴部关键点:61, 146, 91, 181, 84, 17, 314, 405。

python
1 2 3 4 5 6
def mouth_aspect_ratio(landmarks, mouth_indices):
    # 类似EAR计算,用上嘴唇下沿与下嘴唇上沿的距离 / 嘴巴宽度
    p13 = np.array([landmarks[mouth_indices[0]].x, landmarks[mouth_indices[0]].y])
    p24 = np.array([landmarks[mouth_indices[1]].x, landmarks[mouth_indices[1]].y])
    # ... 完整实现省略,原理一致
    # 阈值约0.6,持续超过1秒记录为哈欠

3. 头部姿态估计:低头/偏头

利用MediaPipe的姿势模型(Pose),或直接从Face Mesh计算鼻尖相对两眼的俯仰角。更简单的方法:检测鼻尖关键点1(鼻尖)与左右眼中心的位置关系,当鼻尖Y坐标低于两眼Y坐标平均值时视为低头。

个人观点:纯2D方法对姿态变化敏感,实际部署建议结合IMU(惯性测量单元)或使用3D头部模型。但这个原型已能覆盖大部分工况。

完整流程的提示词(伪代码)

text
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
while True:
    frame = cap.read()
    rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    results = face_mesh.process(rgb)
    if results.multi_face_landmarks:
        landmarks = results.multi_face_landmarks[0].landmark
        ear_left = eye_aspect_ratio(landmarks, LEFT_EYE)
        ear_right = eye_aspect_ratio(landmarks, RIGHT_EYE)
        ear_avg = (ear_left + ear_right) / 2
        if ear_avg < EAR_THRESHOLD:
            # 累积睁眼计数器
            closed_frames += 1
            if closed_frames > 30:  # 约1秒(30fps)
                alert("闭眼疲劳!")
        else:
            closed_frames = 0

常见问题和调试技巧

  1. 眼镜反光/遮挡:使用红外摄像头+主动照明可以大幅提高鲁棒性。普通笔记本摄像头在逆光时表现很差,建议部署时选用支持宽动态的摄像头。
  2. 阈值调整:EAR阈值0.23是通用值,亚洲人眼裂较小,实际可能需要0.20-0.25。可以用标定模式记录用户正常睁眼和闭眼的EAR值。
  3. 性能优化:MediaPipe本身支持GPU加速(需CUDA),在树莓派上可将图像分辨率降至480p,同时使用refine_landmarks=False(降低精度换速度)。
  4. 误报抑制:突然的点头、大笑等动作容易被误判为疲劳。建议引入卡尔曼滤波平滑EAR/MAR曲线,或加入“持续时长”条件(如闭眼>2秒才触发)。

写在最后

Staten岛公交司机的悲剧是个警钟。作为开发者,我们也许无法直接改写法律或驾驶文化,但可以造一个低成本的原型,让车队管理者、创业公司甚至个人司机多一层保障。这套系统我已在GitHub开源(搜索"drowsy-driver-mediapipe"),配置清单、完整代码、和预训练模型都在。下周一就把它跑起来,给每天在路上的朋友们一个实打实的保护。