基于YOLOv5与dlib的驾驶员疲劳检测:从目标检测到PERCLOS预警

发布时间:2026/10/10 11:52:53
基于YOLOv5与dlib的驾驶员疲劳检测:从目标检测到PERCLOS预警
简介面向驾驶员疲劳状态监测场景该压缩包整合了基于YOLOv5与Dlib的完整检测识别方案适合希望掌握目标检测、人脸68点关键点标注及疲劳行为判别的开发者、学生和竞赛团队使用。源码覆盖数据预处理、模型构建、训练推理与疲劳判定可统计眨眼次数和打哈欠次数并对水瓶、手机、香烟等目标进行检测从而监测开车中的不良行为。压缩包共115个文件约274MB包括Python源码、YAML配置、训练好的pt模型、dlib人脸关键点dat模型、mp4操作演示、Markdown说明文档及Git相关文件等目录结构清晰便于按教程逐步复现。项目内置了背景目的、实现方法、代码结构、模型说明与结果分析等讲解帮助读者快速理解检测流程与调参思路。目前已有532人学习下载适合作为课程设计、毕业设计或驾驶安全方向研究的起步素材。1. 驾驶员疲劳检测为什么绕不开 YOLOv5 和 dlib这门技术到底怎么落地凌晨两点的绕城高速上方向盘后面的人眼皮开始打架。这时一套能实时喊醒他的疲劳检测系统比任何保险都值钱。基于 YOLOV5 加 dlib 的驾驶员疲劳检测是目前课程设计和入门 CV 落地里最稳的路线YOLOv5 负责把人脸从复杂车里框出来dlib 负责在清晰脸部区域里量出眼睛闭合程度和嘴巴张合频率两者分工明确也各自能替换。这套方案不挑显卡普通笔记本也能跑出可演示的效果适合做毕设、实训项目也适合想快速接触检测加关键点这类复合任务的开发者。2. 技术拆解YOLOv5 负责“找目标”dlib 负责“量疲劳”2.1 YOLOv5 在疲劳检测里到底检测什么人脸、眼睛还是嘴巴先说结论我一般让 YOLOv5 只做“定位人脸”疲劳状态交给 dlib 的 68 点关键点来算。这样做不是因为 YOLOv5 不能检测眼睛和嘴巴而是眼睛和嘴巴在 640 分辨率里往往是十几到几十像素的小目标训练成本高、漏检率高部署后还会被方向盘、雨刮器这些前景物体干扰。人脸框至少有 200×200 像素YOLOv5 的 anchor 和特征层能稳稳追上。用 YOLOv5 把全图缩小到人脸 ROI再让 dlib 在 ROI 里找关键点正好绕开 dlib 对整张图鲁棒性差的问题。如果你拿到的项目 zip 里没有单独训练人脸模型也可以用 YOLOv5 官方预训练权重里的 person 类先顶着因为驾驶员一定坐在驾驶位person 框和 face 框的中心点和长宽比都很接近。我自己在测试阶段就这么干过把 person 框裁剪出来再丢给 dlib 的 frontal_face_detector 二次检测。真正部署时再换成自己训的 face 权重yolov5s 的参数量只有 7.2M换模型不影响后续逻辑。2.2 dlib 的 68 点关键点与 EAR/MAR 疲劳指标计算dlib 的 68 点模型把一张正脸分成眉毛、眼睛、鼻子、嘴巴和下巴轮廓五组坐标索引区间是固定的。疲劳检测只关心两组眼睛周围的 6 个点左眼索引 36-41右眼索引 42-47从 0 开始计和嘴巴周围的 8 个点索引 60-67注意 48-59 是嘴唇外轮廓算 MAR 用内唇点更稳定。EAR眼睛纵横比的原理是眼睛闭合时上下眼睑距离缩小、内外眼角距离不变比值就会明显掉下去。正常睁眼 EAR 在 0.25 到 0.35闭眼会掉到 0.1 以下。MAR嘴巴纵横比同理打哈欠时嘴部高度变大、宽度不变比值抬升。import numpy as np def eye_aspect_ratio(eye_pts): # eye_pts 是 dlib 返回的 6 个关键点坐标 dist1 np.linalg.norm(eye_pts[1] - eye_pts[5]) dist2 np.linalg.norm(eye_pts[2] - eye_pts[4]) dist3 np.linalg.norm(eye_pts[0] - eye_pts[3]) ear (dist1 dist2) / (2.0 * dist3 1e-6) return ear def mouth_aspect_ratio(mouth_pts): # mouth_pts 取内唇 4 个点索引 61,62,63,64 对应上/下/左/右 dist1 np.linalg.norm(mouth_pts[1] - mouth_pts[3]) dist2 np.linalg.norm(mouth_pts[0] - mouth_pts[2]) mar dist1 / (dist2 1e-6) return marEAR 分母加 1e-6 是为了防止眼角两点距离为零时除零这种极端情况在眨眼瞬间完全可能出现。MAR 只用四个点比用全部 12 个嘴部点更快而且不受嘴唇外轮廓和下巴阴影干扰。这两个指标都是单帧值后面做状态判定时一定要用连续帧不能看单帧。疲劳检测里还有一个行业认可的指标叫 PERCLOS意思是单位时间内眼睑闭合时间的比例。驾驶安全研究里通常认为PERCLOS 超过 40% 就进入疲劳状态。def calc_perclos(ear_list, ear_thresh0.22): # ear_list 是滑动窗口内若干帧的 EAR 序列 closed_frames sum(1 for e in ear_list if e ear_thresh) return closed_frames / len(ear_list)这个函数实现并不复杂但工程上要注意一点ear_list 必须按时间顺序收集不能乱序否则 PERCLOS 没有任何意义。用队列做滑动窗口最省事窗口大小一般是 60 到 90 帧对应 2 到 3 秒。2.3 为什么选“YOLOv5 dlib”而不是纯 dlib 或纯深度学习纯 dlib 一条龙用 HOG 人脸检测器找人脸再算 EAR/MAR在清晰、正脸、光线均匀的实验室数据上完全够用代码量还少一半。但它对侧脸、低头、脸上有阴影、戴墨镜的情况会直接丢人脸。驾驶场景里司机低头看导航、扭头看后视镜、黄昏逆光都是日常HOG 检测器会频繁翻车。加一个 YOLOv5 人脸检测器先锁定目标区域相当于给整个系统上了一层保险。纯深度学习一条龙比如用 YOLOv5 直接训练 eye_open、eye_closed、mouth_open 三个类别不去碰 dlib。这条路不是不行但闭眼和打哈欠的标注量非常大而且眼睛只有十几个像素的时候即使调到 yolov5n 和 640 分辨率也只能达到“能跑”而达不到“好用”。关键点的最大优势是只用 68 个点就能连续量化疲劳程度不只是输出一个类别概率还能算 PERCLOS 这种行业认证指标。两个技术拼在一起各干各擅长的活这是项目能拿高分、能通过验收的根本原因。3. 从零跑通最小系统环境搭建与检测命令3.1 环境准备Python 版本、PyTorch 与 dlib 的安装组合疲劳检测对版本组合其实非常敏感。项目里常见做法是Python 3.8 或 3.9 PyTorch 1.13 前后的版本 dlib 19.22 前后这套组合在 Windows 和 Ubuntu 上都经过大量验证。Python 版本不要追新3.11 之后有些预编译 dlib wheel 还没有容易把自己卡在编译环节。conda create -n fatigue python3.8 conda activate fatigue pip install torch torchvision torchaudio pip install numpy opencv-python pyyaml tqdm matplotlib pandas requests pip install dlib提示torch 和 torchaudio 可以不装推理只用 torch 和 torchvision。如果你有 N 卡建议在装 torch 之前先看一眼驱动支持的 CUDA 版本再去找对应的 PyTorch 版本没有 N 卡就装 CPU 版后面把推理图片尺寸调小照样能演示。dlib 的安装是最容易翻车的点装不上时不要死磕 pip先看下一章避坑记录。3.2 下载模型与关键点文件第一次跑通人脸检测模型文件两个一个是 YOLOv5 的人脸检测权重假设项目里叫 best.pt放到 weights/ 目录另一个是 dlib 的关键点模型文件名固定为 shape_predictor_68_face_landmarks.dat约 100MB也放到 weights/。没有这两个文件后面所有代码都是空转。把 YOLOv5 源码目录准备好后先用一张驾驶室照片验证环境# 加载自定义人脸检测权重path 指向项目 weights 目录 model torch.hub.load(ultralytics/yolov5, custom, pathweights/best.pt) img demo/driver.jpg results model(img) results.show()这句代码乍一看简单背后逻辑是torch.hub 会判断你本地有没有 ultralytics/yolov5 的缓存没有就拉仓库有就直接复用。path 指向项目自带的人脸权重。results.show() 会弹出图片窗口第一次运行如果睁眼后没有窗口是因为 OpenCV 在无头服务器上不能用改成 results.save() 就能把结果写到 runs/predict/。参数方面custom 表示加载自定义训练权重不加的话会默认加载官方 COCO 预训练权重那检测的就不是 face 而是 person。3.3 把 YOLOv5 检测框和 dlib 关键点串起来完整推理脚本跑通单张图片只是热身。下面这个脚本才是疲劳检测的主干YOLOv5 推理 → 裁剪人脸 ROI → dlib 关键点 → 关键点转 numpy → 计算当前帧 EAR/MAR 和疲劳状态。import cv2 import torch import dlib import numpy as np model torch.hub.load(ultralytics/yolov5, custom, pathweights/best.pt) detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(weights/shape_predictor_68_face_landmarks.dat) cap cv2.VideoCapture(0) EAR_THRESH 0.22 CLOSED_FRAMES 3 def ear_from_shape(shape, left_start36, right_start42): left np.array([(shape.part(i).x, shape.part(i).y) for i in range(left_start, left_start 6)]) right np.array([(shape.part(i).x, shape.part(i).y) for i in range(right_start, right_start 6)]) return (ear(left) ear(right)) / 2.0 def ear(pts): a np.linalg.norm(pts[1] - pts[5]) b np.linalg.norm(pts[2] - pts[4]) c np.linalg.norm(pts[0] - pts[3]) return (a b) / (2.0 * c 1e-6) closed_count 0 while True: ret, frame cap.read() if not ret: break dets model(frame, size640).xyxy[0] for x1, y1, x2, y2, conf, cls in dets: if int(cls) ! 0: continue face frame[int(y1):int(y2), int(x1):int(x2)] gray cv2.cvtColor(face, cv2.COLOR_BGR2GRAY) rects detector(gray) if not rects: continue shape predictor(gray, rects[0]) current_ear ear_from_shape(shape) if current_ear EAR_THRESH: closed_count 1 else: closed_count 0 if closed_count CLOSED_FRAMES: cv2.putText(frame, FATIGUE!, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) cv2.imshow(fatigue, frame) if cv2.waitKey(1) 27: break cap.release() cv2.destroyAllWindows()这个脚本有两个关键点。第一YOLOv5 返回的 xyxy 坐标要裁剪成 int 再取图因为 numpy 索引不接受小数。第二放入 dlib 的矩形是从裁剪后的人脸区域坐标系统计算的也就是说 gray 是脸图rects[0] 的坐标也在这个脸图里shape 的 68 个点坐标自然也是脸图坐标不经过坐标偏移直接拿来用就行。CLOSED_FRAMES 等于 3 的含义是连续 3 帧闭眼才触发配合摄像头 30 帧率意思是不足 0.1 秒的眨眼不会报警。EAR_THRESH 0.22 是大多数中青年男性驾驶员的统计值戴了美瞳或眼睛小的司机要往下调最好实测标定。4. 训练自己的疲劳检测模型数据集、yaml 与超参数4.1 准备疲劳数据集标注规范与目录结构如果你不想用人脸检测 dlib 这条路线而是想让 YOLOv5 自己学疲劳状态数据集要按 YOLO 格式组织。常见做法是两个类别open_eye 和 closed_eye或者再加 mouth_open。标注时眼睛区域经常会和眉毛粘连我习惯把标注框多切进一点眼睑比只圈眼球更容易收敛。目录结构最好严格对齐 YOLOv5 的要求datasets/fatigue/ images/ train/ val/ labels/ train/ val/图片和同名的 txt 文件一对一对出现。训练集放八成验证集放两成不要把所有数据都丢进 train。txt 里每一行是一种目标的格式类别索引 x_center y_center width height四个坐标都除以图片宽高归一化到 0-1。用手工标注工具导出 YOLO 格式后最好抽查前几行确认坐标没有出界。4.2 数据集配置文件 data.yaml 怎么改YOLOv5 用 yaml 文件告诉训练脚本去哪里读数据。最容易踩坑的地方是路径写法要么写绝对路径要么把 data.yaml 放在 datasets/fatigue/ 下面写相对路径千万别把路径放到系统盘以外然后期待它自己找到。# data.yaml train: /home/user/datasets/fatigue/images/train val: /home/user/datasets/fatigue/images/val nc: 2 names: [open_eye, closed_eye]train 和 val 后面的路径只需要到 images 目录YOLOv5 会自动在相同位置找 labels 目录。nc 必须和 names 长度一致names 的索引顺序要和标注文件里的第一列数字对应。如果你后面还要用同一份数据训 mouth_open就要改成 nc: 3并且回看 labels 里有没有混入旧标签。4.3 训练参数与 yolov5 超参数怎么选才不翻车训练命令本身不难难在参数怎么设。起步阶段我一般用 yolov5s 权重作为预训练跑 50 个 epochbatch 根据显存定6GB 显存用 1612GB 用 32。imgsz 用 640疲劳状态里的眼睛是小目标如果用 320 会丢掉细节。python train.py --data data.yaml --weights yolov5s.pt --epochs 50 --batch 16 --imgsz 640 --device 0 --hyp hyp.scratch-low.yaml--hyp 参数指定超参数文件hyp.scratch-low.yaml 是 YOLOv5 官方为小数据集准备的起点。它对小目标的策略比较稳学习率较低不容易在数据量小的时候震荡。改超参数时我一般优先动这两个lr0初始学习率小数据集从 0.001 跑到 0.01 之间太高会出现 loss 一上来就爆掉。close_mosaic 相关YOLOv5 的 mosaic 数据增强对疲劳数据不一定友好闭眼区域会被切碎训练后期建议把 mosaic 关掉只留基本翻转和颜色抖动。训练完看 runs/train/exp/ 下的 results.png。正常情况 loss 应该前 10 个 epoch 快速下降20 个 epoch 后变缓如果到第 30 个 epoch loss 还在抖动检查是不是学习率太大或者验证集里混了标错的图。血泪经验先训一个只有 face 类别的模型验证 dlib 管线再训疲劳状态模型不要一上来就开双任务。4.4 模型轻量化与导出选项训练完的 best.pt 可以直接用但部署到摄像头时会发现 640 分辨率加 batch 1 的推理时间不够流畅。常见做法是把模型导出成 ONNX再用 ONNX Runtime 推理这样不需要 PyTorch 环境也能跑而且提速明显。python export.py --weights runs/train/exp/best.pt --include onnx --opset 12 --imgsz 640导出的 ONNX 文件可以用在推理脚本里也方便后续做 TensorRT 加速。如果你的开发板是树莓派这种 ARM 平台onnx 导出后还能换设备不换代码。轻量化还有一个更简单的方向训练时直接换 yolov5n.pt 作为预训练模型体积小一半以上检测精度损失不大因为疲劳检测场景的目标类别少、背景固定小模型的泛化压力比通用检测小很多。5. 避坑记录疲劳检测项目最常见的 5 个翻车现场5.1 现象dlib 安装报错提示需要 CMake 或 C 编译器我第一次在 Windows 上跑这个项目pip install dlib 直接报“Could not build wheels”。原因是 dlib 在 Windows 上没有预编译 wheelpip 只能从源码编译本机却缺了 Visual Studio Build Tools 和 CMake。解决方法是先执行 conda install cmake再装 Build Tools最后重新 pip install dlib。如果你完全不想碰编译换 Python 3.8 后去装网上现成的 dlib wheel 是最后手段但要注意版本必须和 Python 版本匹配装完一定要跑一句 import dlib 验证否则会在项目启动阶段才爆炸。5.2 现象关键点模型加载失败或者加载后一直搜不到人脸关键点模型 shape_predictor_68_face_landmarks.dat 有 100MB 左右下载过程中网络中断会得到大小不对的半截文件dlib 加载时不会立刻报错直到 predictor 第一次运行时才给出异常。解决方法是检查文件大小是否和官方一致不一致就删掉重新下载。搜不到人脸的最佳解决路径不是调 dlib 参数而是把 YOLOv5 检测到的人脸框适当外扩再裁剪比如上下左右各扩 20 像素给 dlib 留下完整脸轮廓HOG 检测器尤其需要看得到额头和下巴。如果你发现侧脸时 HOG 搜不到可以临时把 dlib 的 upsample 次数从 0 改成 1代价是每帧多耗几十毫秒。5.3 现象训练 loss 一直不降mAP 是 0这种情况八成是数据集配置出问题。我当时把 labels 和 images 放反训练程序没报错因为路径存在但内容是空的。先看 labels/train 里 txt 文件的数量和内容确保不是 0 字节。再看 data.yaml 路径是否正确YOLOv5 训练日志里会出现“train: images0”这样的提示别忽略。最后看标注坐标有没有超过 0-1 范围超范围的目标会被直接忽略。还有一个容易忽略的细节图片和标注文件的命名必须完全一致大小写也要一致Windows 上复制文件容易变成同一份文件两个扩展名也会导致训练时找不到标签。5.4 现象摄像头推理卡到 1.5 秒一帧没法实时纯 CPU 机器跑 YOLOv5s 加 dlib 本来就是这样。解决的顺序我建议先换 yolov5n 权重再把推理分辨率从 640 降到 480 或 320然后把 dlib 的输入从整帧改成 YOLOv5 框出的脸图最后用关键帧策略每 3 帧跑一次完整检测中间 2 帧直接沿用上一帧的疲劳状态。这样一次能省下七成计算量画面还能保持 10 帧以上。注意不要把 face detector 和 shape predictor 在循环里反复初始化这两个对象的加载非常重放到循环外面只加载一次。5.5 现象疲劳判定乱触发眨一下眼也报警原因通常是 EAR_THRESH 和 CLOSED_FRAMES 搭配得太宽松。我记得一个项目里把 CLOSED_FRAMES 设成了 1等于任何一帧眼睛小一点都会报警。解法是先记录这个人 10 秒正常驾驶的 EAR 分布把阈值设在正常均值减两倍标准差闭眼帧数设在 3 到 5。还有一个细节是 EAR 序列本身有噪声把原始 EAR 用 5 帧滑动窗口滤波平滑一下再判定误报会下降明显。滤波的窗口长度不要超过 10 帧否则真正的疲劳闭眼会被抹平掉报警延迟反而变大。6. 进阶验证从离线视频到实时预警怎么评估你的疲劳检测效果先用一段 10 分钟以上的真实驾驶视频做离线验证比直接上摄像头效率高。把推理脚本改成读视频文件同时在每一帧把 EAR、MAR、判定结果这些时间序列落盘成 CSV。用这段数据画一条 EAR 时间曲线你一眼就能看出疲劳段和非疲劳段之间的波形差异也能顺手把阈值调准。真实感很重要找驾驶视频时用人坐在桌前模拟犯困也比用实验室正脸视频好。疲劳判定还可以加一层状态机眼睛连续闭了 3 帧并紧接着一个 5 秒窗口内闭眼比例超过 40%才正式判定疲劳MAR 超过 0.6 的帧持续 10 帧先触发“打哈欠”提示而不是“疲劳”警报。这样能避免单一指标带来的误报。评估指标用三个就够检出率、误报次数、从闭眼发生到报警的延迟。报警延迟低于 1 秒、误报每 10 分钟低于 3 次就可以拿给项目验收了。我自己的教训是不迷信公共权重的默认阈值。项目包里带的最佳参数只是起点换个人、换摄像头角度、换一辆车都必须回到那 10 分钟视频里重新找阈值。希望帮到你。本文还有配套的精品资源点击获取