2026/9/28 13:38:21

YOLOV5+dlib驾驶员疲劳检测:从环境配置到EAR/MAR阈值标定实战

YOLOV5+dlib驾驶员疲劳检测:从环境配置到EAR/MAR阈值标定实战 简介这份资源是面向计算机视觉学习者与驾驶安全方向研究者的驾驶员疲劳检测实战项目包基于YOLOv5与Dlib构建可识别眨眼、打哈欠、抽烟、喝水、玩手机等行为并检测水瓶、手机、香烟等目标适合课程设计、毕业设计或算法入门练手。压缩包共115个文件约274.19MB以31个py源码、24个yaml配置、14个pyc缓存、2个pt权重及dat人脸关键点模型为主另含mp4演示、md说明与Dockerfile等部署文件覆盖数据预处理、模型构建、训练、目标检测与疲劳判定全流程。已有531人学习下载。读者可获得完整可运行源码、训练好的YOLOv5与Dlib模型以及讲解项目背景、代码结构、模型说明与结果分析的使用教程便于快速复现并迁移到实际驾驶安全场景。1. 驾驶员疲劳检测为什么总在真实车里翻车从 YOLOV5 加 dlib 的组合说起高速上跑两小时眼皮开始打架但车还在 120 巡航——这是疲劳驾驶最危险的时刻。基于 YOLOV5 加 dlib 实现驾驶员疲劳检测本质是把两个成熟工具拼成一条流水线YOLOV5 负责在画面里框出人脸dlib 负责在人脸上定位 68 个关键点再从眼睛和嘴巴的几何变化里算出疲劳指标。它解决的不是能不能检测到人脸而是在车内光照忽明忽暗、驾驶员低头抬头、戴不戴眼镜都变的条件下稳定判断出这个人是不是快睡着了。适合谁有 Python 基础、想跑通一个完整视觉项目、或者要拿它做课程设计/毕业设计的人。热搜里 yolov5 训练自己的数据集、yolov5 环境配置、yolov5 部署这几个词恰好对应这条流水线最容易卡住的三段。下面按先立住原理、再动手复现、最后讲坑的顺序拆开讲每一步都落到能抄的命令和参数上。2. 拆开这条流水线YOLOV5 找人脸、dlib 找眼睛各自负责什么2.1 为什么不是一个模型端到端就完事很多人第一反应是既然 YOLOV5 这么强直接训练一个疲劳/清醒二分类模型不就行了我一开始也这么想后来发现翻车点在于数据。端到端分类需要大量标注好的疲劳状态样本而疲劳是一个渐变过程标注边界极其主观——同一个打哈欠的动作有人标疲劳有人标清醒。而拆成人脸检测 关键点 几何判据这条链路每一段都有成熟方案人脸检测用 YOLOV5 或现成的人脸模型关键点用 dlib 的 shape_predictor_68疲劳判据用眼睛纵横比EAR和嘴巴纵横比MAR。好处是每一段可单独替换、单独调参坏处是误差会逐级累积——人脸框偏一点关键点就偏EAR 就抖。所以这条流水线的核心不是模型多强而是每一级的稳定性。YOLOV5 在这里的角色是粗定位。它输出人脸框的坐标把后续 dlib 的搜索范围从整张图缩小到一个框里。这一步很关键dlib 的 68 点检测器在整图上跑又慢又容易受背景干扰裁剪到人脸框后速度和准确率都上来了。常见做法是用 YOLOV5 训练一个单类别face检测器或者直接用人脸检测权重输入尺寸 640置信度阈值 0.5 起步。dlib 的角色是精定位。它拿到人脸框后用 68 点模型输出关键点索引36-41 是左眼42-47 是右眼48-67 是嘴巴。EAR 的计算就是取眼睛上下左右几组点的距离比值MAR 同理取嘴巴的垂直和水平距离比值。这两个比值是疲劳判据的物理基础跟模型无关纯几何。2.2 环境配置conda 建环境 装依赖的完整命令热搜里 yolov5 环境配置、conda yolov5 出现频率很高说明这一步劝退了不少人。我一般用 conda 建独立环境避免和系统 Python 打架。下面这套命令在 Windows 和 Linux 上都验证过PyTorch 版本按自己显卡选没有 GPU 就用 CPU 版。# 建一个 Python 3.8 的环境名字叫 fatigue conda create -n fatigue python3.8 -y conda activate fatigue # 装 PyTorch有 NVIDIA 显卡走 cu118没有就走 cpu # 具体命令去 PyTorch 官网生成这里给一个常见组合 pip install torch1.13.1 torchvision0.14.1 --index-url https://download.pytorch.org/whl/cu118 # 装 YOLOV5 依赖和 dlib pip install -r requirements.txt # YOLOV5 仓库根目录下的依赖清单 pip install dlib19.24.0 pip install opencv-python numpy scipy逻辑说明conda 环境隔离是为了防止 dlib 编译时找不到正确的 Python 头文件这是血泪经验——系统里多个 Python 版本时dlib 经常装到一半报 CMake 错误。dlib 用 pip 装预编译 wheel 最省事19.24.0 这个版本在 Python 3.8 上有现成 wheel不用自己编译。如果 pip 装 dlib 失败再考虑 conda install -c conda-forge dlib但 conda 源的版本可能偏旧。参数说明torch 版本要和 torchvision 匹配1.13.1 配 0.14.1 是官方对应关系乱配会报 undefined symbol。opencv-python 用默认最新即可但注意有些老教程用 opencv-contrib-python两者不要同时装会冲突。2.3 人脸检测这一级YOLOV5 推理代码与置信度阈值YOLOV5 推理有两种方式命令行 detect.py 和 Python 里加载模型。做疲劳检测要接后续逻辑必须用 Python 方式把检测结果拿到手里。import torch import cv2 # 加载 YOLOV5 模型weights 换成你自己训练的人脸权重或官方权重 model torch.hub.load(ultralytics/yolov5, custom, pathweights/face_best.pt) model.conf 0.5 # 置信度阈值低于这个的框丢掉 model.iou 0.45 # NMS 的 IoU 阈值人脸重叠时调这个 model.classes [0] # 只保留 face 这一类避免检测到别的物体 def detect_face(frame): # YOLOV5 接受 RGBOpenCV 读进来是 BGR要转 img_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results model(img_rgb, size640) # results.xyxy[0] 是 [x1, y1, x2, y2, conf, cls] boxes results.xyxy[0].cpu().numpy() return boxes逻辑说明torch.hub.load 第一次运行会去拉 YOLOV5 仓库代码网络不通就提前把仓库 clone 到本地把路径换成本地目录。model.conf 是置信度阈值人脸检测场景下 0.5 是个稳妥起点漏检多就降到 0.3误检多就升到 0.6。model.classes 限定类别很重要如果你用的是 80 类 COCO 权重不加这个会检测出一堆无关物体。参数说明size640 是推理输入尺寸和训练时一致。显存不够就降到 416 或 320但小脸会漏。results.xyxy[0] 里的坐标是原图尺度直接能用。如果一帧里有多个人脸boxes 会有多行疲劳检测一般只取面积最大的那个——驾驶员离摄像头最近。3. 从 68 个点到疲劳判据EAR 和 MAR 怎么算、阈值怎么定3.1 dlib 关键点检测与 EAR/MAR 计算公式拿到人脸框后裁剪出来送进 dlib。dlib 的 68 点模型输出的是 68 个 (x, y) 坐标索引固定。EAR 的经典公式是取眼睛的 6 个点算垂直方向三组距离之和除以水平方向距离的两倍。MAR 类似取嘴巴上下和左右的距离比值。import dlib import numpy as np detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) def eye_aspect_ratio(eye_points): # eye_points 是 6 个点的数组顺序左角、上左、上右、右角、下右、下左 # 垂直距离上左-下左、上右-下右、上中-下中这里用两组近似 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 def mouth_aspect_ratio(mouth_points): # 嘴巴 20 个点取上下唇中点和左右嘴角 A np.linalg.norm(mouth_points[13] - mouth_points[19]) # 上下唇中点 B np.linalg.norm(mouth_points[14] - mouth_points[18]) C np.linalg.norm(mouth_points[12] - mouth_points[16]) # 左右嘴角 mar (A B) / (2.0 * C) return mar def get_landmarks(frame, box): x1, y1, x2, y2 map(int, box[:4]) gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # dlib 用矩形检测这里直接用人脸框构造 rect跳过它自己的人脸检测 rect dlib.rectangle(x1, y1, x2, y2) shape predictor(gray, rect) points np.array([[p.x, p.y] for p in shape.parts()]) return points逻辑说明这里有个关键优化——dlib 自带的人脸检测器get_frontal_face_detector很慢既然 YOLOV5 已经给了框就直接用 dlib.rectangle 构造矩形跳过 dlib 的检测步骤只跑关键点预测。这一步能把单帧耗时砍掉一半以上。EAR 公式里 A、B 是两组垂直距离C 是水平距离正常睁眼时 EAR 在 0.25-0.35 之间闭眼会掉到 0.15 以下。参数说明shape_predictor_68_face_landmarks.dat 是 dlib 官方模型文件约 100MB需要单独下载。eye_points 的索引顺序要对左眼是 36-41右眼是 42-47嘴巴是 48-67。索引错一位EAR 就完全没意义这是新手最容易犯的错。3.2 阈值不是拍脑袋EAR/MAR 的标定方法与滑动窗口EAR 阈值不能直接抄网上的 0.2因为每个人的眼睛形状不同摄像头角度也不同。我一般让使用者先正常睁眼录 3 秒取 EAR 均值作为基准闭眼阈值设成基准的 60%。MAR 同理打哈欠时嘴巴张开MAR 会明显上升阈值一般设在基准的 1.5 倍以上。光有单帧阈值还不够眨眼是正常的闭眼超过一定帧数才算疲劳。这就用到滑动窗口维护一个长度为 N 的队列统计窗口内闭眼帧的比例。热搜里的滑动窗口滤波模型在这里就是干这个的。from collections import deque class FatigueDetector: def __init__(self, ear_thresh0.2, mar_thresh0.6, window30, eye_ratio0.7, mouth_ratio0.3): self.ear_thresh ear_thresh self.mar_thresh mar_thresh self.window window self.eye_ratio eye_ratio # 窗口内闭眼帧占比超过这个值判疲劳 self.mouth_ratio mouth_ratio self.eye_queue deque(maxlenwindow) self.mouth_queue deque(maxlenwindow) def update(self, ear, mar): self.eye_queue.append(1 if ear self.ear_thresh else 0) self.mouth_queue.append(1 if mar self.mar_thresh else 0) eye_score sum(self.eye_queue) / len(self.eye_queue) mouth_score sum(self.mouth_queue) / len(self.mouth_queue) # 两个指标任一超标就报警也可以加权 if eye_score self.eye_ratio or mouth_score self.mouth_ratio: return True return False逻辑说明window30 表示看最近 30 帧按 30fps 算就是 1 秒。eye_ratio0.7 表示这 1 秒里 70% 的帧都是闭眼才判疲劳——这样能过滤掉正常眨眼眨眼一般只占几帧。mouth_ratio0.3 是打哈欠的判据打哈欠持续时间长占比容易超。两个指标用 or 连接偏保守容易误报用 and 偏宽松容易漏报。实际项目里我一般用加权eye_score * 0.7 mouth_score * 0.3 0.5。参数说明window 太小会抖动太大反应迟钝30 是 30fps 下的经验值。如果摄像头只有 15fpswindow 要相应减半。ear_thresh 和 mar_thresh 必须按 3.2 节的方法标定不要用默认值直接上。4. 避坑与排查这条流水线最容易翻车的 5 个地方4.1 现象dlib 装不上报 CMake 或 boost 错误原因pip 在找不到预编译 wheel 时会尝试从源码编译 dlib而 dlib 依赖 CMake 和 C 编译环境Windows 上还依赖 Visual Studio Build Tools。Python 版本太新比如 3.11时wheel 往往还没发布。解决优先用 Python 3.8 或 3.9这两个版本 wheel 最全。确认 pip 版本够新pip install --upgrade pip然后 pip install dlib 让它直接下 wheel。如果还是编译Windows 装 Visual Studio Build Tools 并勾选 C 桌面开发Linux 装 cmake 和 build-essential。实在不行用 conda install -c conda-forge dlibconda 源的包是预编译的。4.2 现象YOLOV5 检测框在但 dlib 关键点乱飞原因YOLOV5 给的框可能偏大或偏小dlib 的 predictor 对框的位置敏感。框偏了68 点会整体偏移EAR 直接失真。另一个原因是灰度图转换时用了错误的色彩空间。解决把 YOLOV5 的框往里收 10%-20% 再送给 dlib给关键点检测留点余量。检查 cv2.cvtColor 用的是 COLOR_BGR2GRAY不是 RGB2GRAY。如果关键点还是乱打印出来可视化一下看 68 点是不是落在脸上。4.3 现象白天正常晚上或逆光时 EAR 剧烈抖动原因dlib 的关键点检测依赖图像梯度光照不足时梯度弱关键点定位精度下降。逆光时人脸变成剪影YOLOV5 都可能漏检。解决加一个简单的图像预处理——直方图均衡化cv2.equalizeHist或者 CLAHE提升暗部对比度。红外摄像头是更彻底的方案但成本高。另外把 EAR 的滑动窗口拉长到 45 帧用时间换稳定。4.4 现象戴眼镜的人 EAR 一直偏低频繁误报原因镜框和镜片反光会干扰眼睛关键点dlib 把镜框边缘当成眼睑导致 EAR 计算偏小。解决对戴眼镜场景EAR 阈值要单独标定通常比不戴眼镜低 0.03-0.05。或者在关键点检测前做一次眼镜区域掩膜但这会引入新误差。更稳的做法是降低 eye_ratio比如从 0.7 降到 0.5让判据更宽松代价是反应变慢。4.5 现象CPU 上跑不动一帧要几百毫秒原因YOLOV5 和 dlib 都是计算密集型CPU 推理时两者叠加帧率掉到个位数。解决YOLOV5 换 yolov5nnano 版输入尺寸降到 320能快 3-5 倍。dlib 那边把关键点检测的频率降下来——不用每帧都跑每 3 帧跑一次中间帧复用上一次的关键点。或者上 GPUYOLOV5 和 dlib 都能吃 CUDA帧率能回到 30fps 以上。树莓派 5 上部署自己训练的 yolov5 模型这个热搜场景基本必须用 nano 版加降频策略。5. 让疲劳检测真正可用从单帧判据到状态机与验证方法前面讲的都是单帧或短窗口的判据但真实驾驶里疲劳是一个持续状态不是某一帧的瞬时判断。我后来把判据升级成了一个简单的状态机清醒 → 疑似疲劳 → 疲劳 → 严重疲劳每个状态有进入和退出条件避免在阈值附近反复横跳。疑似疲劳是 EAR 窗口超标但还没持续够时间疲劳是持续超标 3 秒以上严重疲劳是叠加了打哈欠或头部下垂。状态机的好处是报警有层次不会一超标就尖叫也不会漏掉渐进式疲劳。验证方法上别只看准确率。我一般分三步第一步用录制好的视频回放人工标注每个疲劳片段看报警时间点和标注的偏差第二步找 3-5 个人做真实测试每人正常驾驶 10 分钟统计误报次数——误报比漏报更烦人一次误报就让人想关掉系统第三步做边界测试戴眼镜、戴口罩、夜间、侧脸看哪些场景会崩。这套流程跑下来才能说这个方案能用。一个具体技巧把 EAR 和 MAR 的原始值实时画成曲线叠加在视频上调试时一眼就能看出阈值设得对不对。我习惯用 OpenCV 的 cv2.line 在画面上画两条水平线代表阈值EAR 曲线用绿色MAR 用蓝色超标的部分标红。这个可视化花不了多少代码但省下的调试时间是以小时计的。# 在视频帧上叠加 EAR/MAR 曲线和阈值线 def draw_debug(frame, ear_history, mar_history, ear_thresh, mar_thresh): h, w frame.shape[:2] base_y h - 100 # 画阈值线 cv2.line(frame, (0, base_y - int(ear_thresh * 200)), (w, base_y - int(ear_thresh * 200)), (0, 255, 0), 1) cv2.line(frame, (0, base_y - int(mar_thresh * 200)), (w, base_y - int(mar_thresh * 200)), (255, 0, 0), 1) # 画历史曲线 for i in range(1, len(ear_history)): x1 int((i - 1) / len(ear_history) * w) x2 int(i / len(ear_history) * w) y1 base_y - int(ear_history[i-1] * 200) y2 base_y - int(ear_history[i] * 200) cv2.line(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) return frame逻辑说明ear_history 和 mar_history 是长度固定的队列存最近 N 帧的值。base_y 是曲线基准线乘以 200 是把 0-1 的比值放大到像素尺度。阈值线画出来曲线在阈值线以下就是闭眼/打哈欠。这个调试面板在项目初期帮我省了大量时间强烈建议加上。参数说明200 是放大系数EAR 一般在 0.2-0.4乘 200 后是 40-80 像素肉眼能看清。如果画面分辨率高系数可以调大。曲线长度和滑动窗口保持一致方便对照。最后说个我自己的习惯每次调完阈值我都会把当天的测试视频和参数记在一个表格里标注日期、光照、是否戴眼镜、误报次数。攒够十几条记录后阈值该怎么设基本就有数了比拍脑袋靠谱得多。这个项目不难难的是把每一级的误差控制住让整条流水线在真实场景里稳下来。希望帮到你。本文还有配套的精品资源点击获取