产品 Demo 效果展示
上周 Virginia 那起客车追尾事故,司机“未减速”导致5人死亡。如果车上有一套 实时疲劳驾驶检测系统,在司机闭眼超过1.5秒或者头部持续低垂时立即报警,结果可能完全不同。
我花了一个周末做了个 Demo:用笔记本摄像头捕捉司机面部,实时计算 PERCLOS(单位时间闭眼比例)和头部俯仰角,一旦指标越限,就弹窗警告 + 记录日志。效果如下(当然我自己当司机):
- 眨眼检测:正常眨眼约200ms,连续闭眼超1秒触发警告
- 头部姿态:低头超过30°持续2秒触发“疲劳”告警
- FPS:在 MacBook M1 上能达到25帧,基本实时
技术选型(模型/框架/部署方案)
模型:YOLOv8n-face + 68点人脸关键点
选择 YOLOv8 的原因:
- 官方提供预训练的
yolov8n-face.pt(人脸检测),模型仅 5.8MB,推理速度极快 - 配合
dlib的 68点关键点检测器shape_predictor_68_face_landmarks.dat(约 100MB,但只加载一次) - 用眼睛6个关键点(36-47)计算 EAR(Eye Aspect Ratio),用鼻子和下巴点估算俯仰角
对比其他方案:
| 方案 | 模型大小 | FPS (M1) | 精度 | 是否需GPU |
|------|---------|----------|------|----------|
| YOLOv8n-face + dlib | 105MB | 25 | 高 | 否 |
| MediaPipe Face Mesh | 15MB | 30 | 中(易受角度影响) | 否 |
| OpenCV Haar Cascade | 1MB | 40 | 低(误检多) | 否 |
我最终选了 YOLOv8 + dlib,因为 关键点定位更稳,对头部旋转的容忍度更高,这对驾驶场景很重要(司机经常转头看后视镜)。
框架:PyTorch + OpenCV
- PyTorch 2.0+ 用于加载 YOLO 模型
- OpenCV 负责视频流读取、绘图、窗口显示
- 轻量,无额外依赖
部署方案
最终会打包为单文件 fatigue_detector.py,依赖的模型文件放在 models/ 目录下。上线到车内环境可以用 Raspberry Pi 4 + USB 摄像头,后续可接入蜂鸣器。
核心代码实现(关键片段)
1. 加载模型和初始化
import cv2
import dlib
import numpy as np
from ultralytics import YOLO
# 加载人脸检测模型(YOLOv8n-face)
face_model = YOLO('models/yolov8n-face.pt')
# 加载68点关键点检测器
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor('models/shape_predictor_68_face_landmarks.dat')
# 定义眼睛关键点索引(dlib 68点)
LEFT_EYE_POINTS = list(range(36, 42))
RIGHT_EYE_POINTS = list(range(42, 48))
注意:YOLOv8 返回的 bounding box 坐标格式为 xyxy,可直接送入 dlib 的矩形区域检测关键点,比 dlib 自己的检测器更快(因为 YOLO 先缩小搜索区域)。
2. EAR 计算(Eye Aspect Ratio)
def eye_aspect_ratio(eye_points):
# 计算垂直距离
A = np.linalg.norm(eye_points[1] - eye_points[5])
B = np.linalg.norm(eye_points[2] - eye_points[4])
# 计算水平距离
C = np.linalg.norm(eye_points[0] - eye_points[3])
ear = (A + B) / (2.0 * C)
return ear
EAR 正常值约 0.25~0.35,闭眼时降至 0.2 以下。我用 EAR_THRESHOLD = 0.2,连续 3 帧 EAR < 0.2 判定为一次“闭眼”。
3. 头部俯仰角估算
利用 30 号点(鼻尖)和 8 号点(下巴)的相对位置,计算俯仰角:
def head_pitch_angle(landmarks):
nose_tip = landmarks[30]
chin = landmarks[8]
dx = nose_tip.x - chin.x
dy = nose_tip.y - chin.y
angle = np.degrees(np.arctan2(dy, dx)) # 注意y轴向下
# 正常姿态角度约 60°~80°,低头增大
return angle
阈值:当 angle > 100°(几乎垂直低头)且持续超过 2 秒,判定为疲劳。
4. 主循环(实时检测)
cap = cv2.VideoCapture(0)
EAR_HISTORY = [] # 存储过去 30 帧的 EAR
CLOSED_FRAMES = 0
LOW_HEAD_FRAMES = 0
while True:
ret, frame = cap.read()
if not ret: break
# YOLO 检测人脸
results = face_model(frame, verbose=False)
boxes = results[0].boxes.xyxy.cpu().numpy() if results[0].boxes is not None else []
for box in boxes:
x1, y1, x2, y2 = map(int, box[:4])
# 扩大一点边界给 dlib
dlib_rect = dlib.rectangle(x1, y1, x2, y2)
# 用 dlib 检测关键点
shape = predictor(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY), dlib_rect)
landmarks = np.array([(p.x, p.y) for p in shape.parts()])
# 计算左眼和右眼 EAR(取平均值)
left_eye = landmarks[LEFT_EYE_POINTS]
right_eye = landmarks[RIGHT_EYE_POINTS]
ear = (eye_aspect_ratio(left_eye) + eye_aspect_ratio(right_eye)) / 2.0
EAR_HISTORY.append(ear)
if len(EAR_HISTORY) > 30:
EAR_HISTORY.pop(0)
# 判断闭眼
if ear < 0.2:
CLOSED_FRAMES += 1
else:
CLOSED_FRAMES = 0
# 判断低头
pitch = head_pitch_angle(landmarks)
if pitch > 100:
LOW_HEAD_FRAMES += 1
else:
LOW_HEAD_FRAMES = 0
# 报警逻辑
if CLOSED_FRAMES >= 5: # 约 0.2秒/帧,1秒闭眼
cv2.putText(frame, "WARNING! Eyes Closed!", (50,50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255), 2)
if LOW_HEAD_FRAMES >= 30: # 低头2秒(假设15fps)
cv2.putText(frame, "WARNING! Head Down!", (50,100), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255), 2)
# 绘制关键点和框
for (x,y) in landmarks: cv2.circle(frame, (x,y), 1, (0,255,0), -1)
cv2.rectangle(frame, (x1,y1), (x2,y2), (255,0,0), 2)
cv2.imshow("Fatigue Detector", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
注意:帧率影响报警阈值,实际部署时需根据摄像头 FPS 动态调整。我这里假设 15fps,你可以用 cap.get(cv2.CAP_PROP_FPS) 计算。
项目结构和配置
fatigue-detector/
├── models/
│ ├── yolov8n-face.pt # 5.8MB
│ └── shape_predictor_68_face_landmarks.dat # 99.7MB
├── main.py # 主程序
├── config.py # 阈值配置
└── README.md
config.py 示例:
# 疲劳检测参数
EAR_THRESHOLD = 0.2
CLOSED_FRAME_LIMIT = 5 # 连续闭眼帧数
HEAD_PITCH_THRESHOLD = 100 # 俯仰角阈值(度)
HEAD_DOWN_FRAME_LIMIT = 30 # 连续低头帧数
LOG_FILE = "fatigue_log.csv" # 日志文件
依赖安装(建议 Python 3.9+):
pip install opencv-python dlib numpy ultralytics
模型下载:
yolov8n-face.pt可以从 Ultralytics 官方 下载shape_predictor_68_face_landmarks.dat从 dlib 模型库 下载,解压后放入 models 目录
上线要注意的坑
1. 光照和遮挡
车内夜间光线不足,人脸检测精度下降。建议:
- 使用红外摄像头(已有车厂标配)
- 对输入帧做直方图均衡化:
frame = cv2.equalizeHist(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)) - 但我实测 YOLOv8 对光照鲁棒性不错,夜间低照度下仍能检出人脸,只是关键点稍有偏差。
2. 多人和乘客干扰
只检测主驾驶座的人脸:可通过最大人脸框的位置预设(例如画面左侧固定区域)来过滤。我在代码里加了 if x1 < frame.shape[1]//2: 只取左半边——当然前提是驾驶座在左侧。
3. 司机戴眼镜或墨镜
眼镜会遮挡眼睛,导致 EAR 偏低,从而误报。解决方案:
- 训练一个专用的疲劳模型,使用热成像(但成本高)
- 或者使用基于瞳孔检测的算法,但开发复杂度高
- 简单策略:检测到眼镜时降低灵敏度(通过 OCR 识别眼镜框?不现实)
- 我的建议:墨镜必须脱掉,或者系统改为检测嚼槟榔、打电话等其他行为。当前代码对于透明眼镜仍可工作,但墨镜会使 EAR 始终接近 0,误判为闭眼。可在检测到人脸但关键点异常时给出提示:“请摘下墨镜”。
4. 帧率波动
车内环境、CPU 负载变化可能导致帧率忽高忽低,固定帧数阈值失效。改写为时间阈值:
import time
sleep_start_time = None
if ear < EAR_THRESHOLD:
if sleep_start_time is None:
sleep_start_time = time.time()
elif time.time() - sleep_start_time > 1.5: # 闭眼超过1.5秒
alarm()
else:
sleep_start_time = None
5. 模型文件大小
dlib 的 shape_predictor 约 100MB,部署到树莓派可能占空间。替代方案:
- 用 MediaPipe Face Mesh(约 15MB)替换,但关键点定位精度略差
- 或者使用 OpenCV 的
LBF模型(约 16MB),但只能检测 5 个点,不够计算 EAR - 如果能在 Jetson Nano 上运行,直接使用 MTCNN + 68点网络,体积可控 5MB,但需要自己训练
6. 隐私合规
车内摄像头数据不能外传。务必:
- 全部本地计算,不上传任何图片到云端
- 日志中只记录事件时间、检测结果,不保存视频
- 符合《个人信息保护法》要求,用户需知情同意
再说回 Virginia 那起事故
上面这套系统只是技术实验,但已经能说明一件事:当前最便宜的方案(USB摄像头 + 树莓派4B)成本不到 500 元,如果每台客车配备,像“未减速追尾”这种低级错误完全有可能被预警拦截。当然,司机也可能忽视报警,但至少多一道保险。
从开发者角度看,这套代码 2 小时就能跑通,真实落地还需要:
- 行业标准:ECE R46 中针对疲劳驾驶的测试方法
- 硬件老化测试(85°C 车内高温)
- 抗震动、电磁干扰
但这些不是我一个博客能解决的。你如果感兴趣,可以先用我的 Demo 在你的笔记本上测试,然后考虑移植到嵌入式设备。代码已开源在 GitHub(链接见文末)。
附:完整 main.py 下载
为避免代码过长,我把完整程序放在 GitHub Gist。包括错误处理、日志记录等。
最后说一句:技术不能拯救所有生命,但多一个报警,也许能救一个家庭。 如果你对代码有任何改进建议,欢迎 PR。