多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

YOLO驾驶员疲劳检测模型实战:从数据集构建到PERCLOS告警

YOLO驾驶员疲劳检测模型实战:从数据集构建到PERCLOS告警 简介面向计算机视觉与智能驾驶安全领域的研究者、开发者该资料包含基于YOLO算法的驾驶员疲劳检测完整模型与配套数据集可识别驾驶员闭眼、打哈欠等疲劳行为适用于疲劳驾驶预警系统研发、算法课程教学、毕业论文或课题验证。压缩包共2000个文件涵盖1984个txt标注、13个md说明文档、2个pdf技术文档及1个yaml模型配置文件其中txt标注可直接用于YOLO系列模型训练yaml定义网络结构md与pdf可辅助理解算法原理、使用流程与可视化效果包体整体约306MB。目前已有1139人学习下载。数据集特意将标注按txt与xml两种格式分别归档于两个文件夹提供双格式训练素材省去标准转换步骤配合项目说明文档和可视化参考用户可快速完成环境配置、模型训练与效果评估并进一步部署到实际驾驶监测场景。适合希望快速上手驾驶员疲劳检测的中初级视觉学习者也为进阶调优提供良好基线。1. 用 YOLO 算法做驾驶员疲劳检测模型最该先想清楚的是数据而不是网络夜里两点挂着挂车的司机在高速匝道上闭眼超过三秒——车载摄像头拍到了但没人看回放。这正是 yolo算法驾驶员疲劳检测模型数据集 这类项目要解决的典型场景不依赖方向盘传感器、不依赖心率带只靠一个朝驾驶员方向的内视摄像头实时判断人是不是困了。做这件事的人通常是三类ADAS 供应商的算法工程师、做车队安全管理系统的开发者、准备把这个题目当毕设或参赛课题的学生。我的建议是先别纠结 YOLOv8 还是 YOLOv5先把“疲劳”定义成模型能监督的标签再把数据集按场景补齐。后面每一章都是一个能直接复现的节点从信号定义、模型选型、数据集构建一路做到部署避坑。2. 疲劳信号建模为什么盯眼睛最可靠PERCLOS 和 EAR 怎么落到 YOLO 上疲劳检测模型并不是在检测“困”这个抽象状态而是在检测一组肉眼可观察、可量化的表现眼睛闭合时间变长、打哈欠频率上升、头部不自觉地低头。把这三种现象落到模型输出上就是三类完全不同的监督任务。2.1 三种可选信号通道闭眼、哈欠、点头的稳定性与落地成本对比信号模型需要输出判定逻辑稳定性主要坑闭眼眼睛区域或眼睑关键点EAR 低于阈值、PERCLOS 超过比例高行业常以 PERCLOS 为金标准墨镜、强光、小目标漏检哈欠嘴部区域或嘴部关键点MAR 持续多帧超过阈值中等说话、吃东西干扰说话误判麦克风噪声耦合点头头部姿态角俯仰角连续低头中低依赖相机标定看仪表盘、看后视镜会误报闭眼通道是绝大多数商用 DMS 的主判据原因很简单人在真正入睡前闭眼时长和闭眼频次的变化是最早出现的生理信号而且和画面特征的耦合最直接。打哈欠和点头只能做辅助证据不能单独触发告警否则车内对话、低头看导航都会让系统疯狂误报。把闭眼量化成模型指标行业里有两套东西绕不过去。一套叫 PERCLOS指单位时间内眼睑闭合时间所占的比例这是被广泛引用的疲劳判据另一套叫 EAREye Aspect Ratio用眼睛 6 个关键点的距离比来估计眼睛睁开程度。EAR 的最大好处是把“闭眼”从分类问题变成了回归问题模型不用硬判断 open/closed而是给一个连续值阈值后期再说。落地时 YOLO 在这里负责两件事把人脸从整帧画面里框出来再把眼睛区域从人脸框里找出来。人脸检测用 YOLO 非常合适因为人脸尺度相对大、特征稳定眼睛和嘴部是否闭合则建议用关键点模型或者单独的眼睛分类模型去做而不是让一个大 YOLO 把所有活都干了原因在第 3 章展开。2.2 用 YOLOv8 在本地跑通最小链路人脸框、眼睛框与 EAR 计算第一步先写一个通用的 EAR 计算函数它接收 68 点人脸关键点坐标返回左右眼的平均 EAR。这里用 dlib 的 68 点索引做示例import numpy as np # dlib 68 点中左眼是 36~41右眼是 42~47 LEFT_EYE list(range(36, 42)) RIGHT_EYE list(range(42, 48)) def eye_aspect_ratio(landmarks): # landmarks: 68x2 的坐标数组来自人脸关键点模型 def ear(p): # 两条竖线距离的平均值除以水平线距离 a np.linalg.norm(p[1] - p[5]) b np.linalg.norm(p[2] - p[4]) c np.linalg.norm(p[0] - p[3]) return (a b) / (2.0 * c) return (ear(landmarks[LEFT_EYE]) ear(landmarks[RIGHT_EYE])) / 2.0这个函数每次只算一帧。关键点坐标必须与人脸框在同一坐标系里否则 EAR 会被框的偏移直接带偏。常见做法是先拿 YOLO 人脸检测把人脸框出来再在框内做关键点定位整套链路如下from ultralytics import YOLO import cv2 face_model YOLO(face_best.pt) # 只负责把人脸框出来 stream cv2.VideoCapture(0) # 内视摄像头 while True: ok, frame stream.read() if not ok: break results face_model(frame, imgsz640, conf0.5) for box in results[0].boxes.xyxy.cpu().numpy(): x1, y1, x2, y2 box.astype(int) # 先对 face_roi 做 10% 扩边防止人眼被裁掉一半 face_roi frame[max(0, y1):y2, max(0, x1):x2] # 把 face_roi 送到 68 点关键点模型或你自己的 eye_cls 模型 # 这里以关键点模型为例返回 68x2 坐标 pts landmark_net(face_roi) ear eye_aspect_ratio(pts) # ear 进入滑动窗口后面的 PERCLOS 告警逻辑统一处理 push_ear(ear)这段代码最容易被忽略的是扩边 10%人脸检测框有时恰好贴着眉毛或眼眶边缘直接把原框交给下级模型眼睛的上下眼睑会被截掉EAR 曲线会周期性出现尖峰最终表现为不明原因误报。扩边不是调参玄学是缺省步骤。EAR 的取值范围在正常睁眼时大约 0.25 到 0.35闭眼时掉到 0.1 以下。不要直接用 0.2 做全局阈值不同人种、不同相机安装角度会让基线差 30% 以上。我一般会在系统启动后先采集 10 到 15 秒正常睁眼画面把 EAR 的中位数作为个人基线再按基线乘 0.6 作为闭眼阈值这个逻辑留到第 6 章再给完整代码。3. 模型选型与训练参数单模型多类别为什么不如两级级联疲劳检测项目里最常见的方案有两个方向一个是用单个 YOLO 同时检测人脸、眼睛、嘴巴输出多个类别另一个是两级级联先人脸后局部部件。二选一的时候我强烈建议把预算留给级联方案。3.1 一张图里人脸和眼睛尺度差 10 倍单模型同时检测会互相拖累先算一笔账。1080p 的内视摄像头画面里驾驶员人脸高度通常在 200 到 400 像素而眼睛高度只有 20 到 50 像素。YOLOv8 默认输入 640x640经过 5 次下采样后特征图是 20x20一个 30 像素高的眼睛在最后的特征图上只占不到 1 个像素。强行让一个模型的同一组 head 去预测 300 像素的人脸和 30 像素的眼睛小目标类别在训练时会持续被损失函数里的大目标梯度淹没。眼睛类别如果作为独立类别放进单模型它还会被背景里的阴影、方向盘轮廓干扰因为远距离下小目标的纹理信息本来就少。级联方案的思路是第一个 YOLO 只做人脸把人脸框裁出来缩放到 640 或 1280眼睛区域在裁剪后的人脸图里占的比例直接放大好几倍第二个模型再看这张大图时眼睛就不再是“小目标”了。这也是为什么人脸模型可以用轻量 backbone眼睛模型反而要保留足够分辨率的根本原因。3.2 可抄的训练配置face.yaml、eye.yaml 与这两条训练命令先准备两套数据集配置。人脸模型只检测一个类眼睛模型检测两个类open 和 closed。如果你的标注里只有眼睛 bbox 没有状态可以退一步用一个二分类模型处理眼睛区域但效果通常比不上直接让 YOLO 学习“睁/闭”的空间特征。# face.yaml path: /data/fatigue train: images/train val: images/val nc: 1 names: [face]# eye.yaml path: /data/fatigue train: images/train val: images/val nc: 2 names: [eye_open, eye_closed]训练命令用 ultralytics 的标准入口眼睛模型我建议把输入分辨率提到 1280# 人脸模型从预训练权重继续训练收敛快 yolo train dataface.yaml modelyolov8n.pt epochs100 imgsz640 batch16 patience20 # 眼睛模型目标是局部小区域分辨率提到 1280batch 相应减半 yolo train dataeye.yaml modelyolov8n.pt epochs100 imgsz1280 batch8 patience20参数上有四个点值得说明。第一两个模型都不要从随机权重开始用 yolov8n.pt 做预训练驾驶员疲劳数据集规模通常只有几千张随机初始化会让训练过程极其漫长。第二imgsz1280 对显存要求高如果显卡只有 8G可以把 batch 降到 4同时关闭 mosaic 增强因为 mosaic 拼贴会让小尺寸的眼睛区域出现拼接伪影反而损害精度。第三patience20 表示验证集连续 20 轮不提升就早停这是防止过拟合最便宜的手段。第四眼睛模型建议用 yolov8n 而不是 s/m/l因为裁剪后人脸图已经很干净模型容量不是瓶颈推理速度才是。训练完成后推理时按第 2 章的链路把两个模型串起来第一级只看整帧第二级只看人脸框。这里有一个很多人不知道的细节眼睛模型推理时输入图像分辨率要和你训练时一致如果训练用 1280推理就别为了省时间直接塞 640特征分布不一致会让置信度全线漂移。4. 疲劳数据集构建公开集、自采数据与 YOLOv8 训练前的一步转换数据集是这种项目真正的护城河。模型掉点八成是数据缺角数据问题会在装车后集中爆发而且排查成本极高。把公开集和自采数据统一成 YOLOv8 格式是进入训练前必须做对的一步。4.1 公开数据集能做什么不能做什么NTHU-DDD 与 YawDD 的边界公开集里比较常用的是 NTHU-DDD台湾清华大学驾驶模拟数据集包含几十位受试者在驾驶模拟器里的视频有正常、眨眼、打哈欠、困倦几类行为场景覆盖白天和夜晚。它的价值在于行为类别定义得比较接近真实疲劳状态适合拿来做预训练或者验证算法链路。YawDD 是另一个以打哈欠为主的驾驶室数据集摄像头装在车内后视镜位置视角和实车 DMS 摄像头接近但要确认它和你目标车型的安装高度是否一致视角差异会直接导致模型在实车上失效。公开集的边界也很明显模拟器环境背景单一多为实验室灯光或普通室内光缺少真实车队里的逆光、夜间红外补光、墨镜遮挡、颠簸运动模糊场景受试者以亚洲面孔为主欧美人种的眼部特征分布有明显差异。所以只靠公开集训练然后装车几乎必然翻车。常见做法是公开集 自采小样本的组合公开集做预训练自采数据按目标车型、目标安装位、目标光线条件补充 500 到 2000 张关键帧。4.2 把 VOC/自有标注转成 YOLOv8 格式转换脚本与四个边界坑很多公开集和标注工具导出的是 VOC 格式XMLYOLOv8 需要的是每张图一个同名 txt每行一个目标格式是class_id cx cy w h所有坐标都是归一化浮点数。下面这段脚本把 VOC 转成 YOLOv8 格式import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_path, class_names): tree ET.parse(xml_path) root tree.getroot() w int(root.find(size/width).text) h int(root.find(size/height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_names: continue # 只保留疲劳任务需要的类别 cls_id class_names.index(name) box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) # YOLO 标签是归一化后的 cx, cy, w, h全部除以图像宽高 cx (x1 x2) / 2.0 / w cy (y1 y2) / 2.0 / h bw (x2 - x1) / w bh (y2 - y1) / h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) out_name os.path.basename(xml_path).replace(.xml, .txt) with open(os.path.join(out_path, out_name), w) as f: f.write(\n.join(lines))这块代码逻辑本身不难难在边界坑。第一个坑是坐标越界标注软件偶尔会把框画出图像边界x2 大于 wy1 小于 0直接转出来后 anchor 计算会出 NaN训练直接中断。第二个坑是分母除零某些异常图片 width 字段缺失或为 0脚本要在转换前做校验。第三个坑是类别名大小写不一致比如Head和head在同一个数据集里混着写class_names.index 会抛异常。第四个坑是图片有 EXIF 旋转信息手机或运动相机拍摄的图在读取时会被自动旋转但 XML 里的坐标是按未旋转图像标注的最终结果是标签全部偏移 90 度这一步必须在预处理阶段统一处理。数据划分同样有讲究。疲劳视频是连续帧同一人在同一段路上的前后帧高度相似如果按帧随机划分训练集和验证集里会混入几乎相同的图像验证指标虚高到 0.98 都不奇怪。实际部署后换成新司机立刻掉到 0.6。正确的做法是按视频片段或人员 ID 划分保证同一个受试者的画面只出现在训练集或验证集之一。4.3 增广按疲劳场景配低光、墨镜与抖动模糊的参数怎么给YOLOv8 自带的增广对通用目标检测有效但对疲劳检测场景针对性不够。三个场景必须要手动补。第一是低光与夜间红外常见做法是把图像转灰度再叠加亮度扰动gamma 取值在 0.5 到 1.2 之间随机采样这样模型不会只依赖颜色信息判断眼睛闭合。第二是墨镜遮挡用随机大小和位置的黑块覆盖眼睛区域模拟太阳镜否则戴墨镜时模型会输出高置信度的 closed触发连续误报。第三是运动模糊用高斯模糊核模拟车辆颠簸和抖动核大小 3 到 7角度随机增强模型对模糊帧的鲁棒性。这些增广不用全堆在训练时做很多团队选择在数据准备阶段离线生成一部分再混合进训练集这样能直观看到各场景的样本占比而不是在训练日志里黑盒调参。做疲劳检测数据集时数量不是第一优先级场景覆盖度才是。5. 训练与部署最容易翻车的 5 个项目坑从标签到推理延迟这一章是踩坑记录全部来自实际项目中反复出现过的问题。每条按现象、原因、解决三步写照做能省掉至少一周的排查时间。5.1 验证集精度高装车后夜间场景却频繁误报现象模型在开源集和内部验证集上 mAP 都在 95 以上装到车内夜间开灯后告警每一两分钟触发一次。原因训练集里白天图像占 80% 以上夜间图像占比太少模型实际上学会了“低照度区域 闭眼”这个错误的捷径。验证集同样以白天为主所以指标完全失真。解决按场景维度统计训练集分布把白天比例控制在 60% 左右夜间和黄昏补足到 30% 到 40%。补不到实拍数据时用第 4.3 节的灰度增强和亮度扰动强行制造夜间样本但要注意灰度图只是模拟和真实红外补光下的成像质感仍有差距不能完全替代夜拍。5.2 戴墨镜时眼睛误检疲劳告警连续触发现象测试人员戴墨镜进入驾驶位系统立刻报警摘掉墨镜后恢复正常。原因墨镜区域在图像里是一块近乎纯黑的平面没有纹理也没有眼睑轮廓模型把它识别成闭眼是必然结果。这是数据缺陷不是模型缺陷。解决最稳的方案是增加一个墨镜检测状态先判断有没有戴墨镜如果戴了就不启用眼睛通道改用头部姿态和驾驶行为作为辅助判据。另一个折中方案是训练时做墨镜遮挡增广让模型对“纯黑区域”不再直接对应到闭合状态但这会牺牲一部分真实闭眼的召回因为真实闭眼和墨镜在视觉特征上确实接近只能靠上下文区分。5.3 眼睛目标太小两级级联后仍漏检现象人脸框裁切后眼睛模型在距离摄像头稍远的坐姿下频繁漏检尤其是倒装眼镜框或眯眼状态。原因人脸模型框住人脸后裁切区域的分辨率取决于人脸在原始图像中的高度。驾驶员座椅靠后时人脸高度可能只有 120 像素眼睛区域只剩十几个像素再送到眼睛模型里信息量不够。解决三步走。第一人脸推理时把输入分辨率从 640 提到 960人脸框尺寸会明显变大。第二眼睛模型训练和图 3.2 一致用 1280并且裁切后做人脸对齐把眼睛固定到图像中相对稳定的位置。第三如果还不行用 SAHI 切片推理的思路对眼睛区域做局部放大但推理成本会上升通常只在高端算力平台上使用。5.4 随机划分数据集导致同人同路段泄底指标虚高现象训练过程正常验证集 mAP 高达 98但模型在另一批陌生人身上效果极差回调一查发现验证集里混着训练视频的同帧或相邻帧。原因数据划分用了全局随机采样没有考虑视频的时间连续性和人员身份。连续帧之间的差异极小模型在训练时已经见过几乎一样的画面验证时只是“背答案”。解决划分单位从“帧”改成“视频片段”或“受试者 ID”。具体来说把同一个视频文件的所有帧或者同一个人在不同时间段的所有画面全部划到同一个集合绝不允许跨集合。这是疲劳检测数据管理里最容易忽视、又最影响指标可信度的一步。5.5 两个模型串行推理30fps 摄像头只跑到 8fps现象人脸模型跑 640 分辨率约 11ms眼睛模型跑 1280 分辨率约 18ms两模型串行后整体只有不到 9fps车规级平台更慢。原因人脸检测每帧都跑眼睛模型每帧都跑且眼睛模型的 1280 分辨率推理在高性价比边缘盒子上非常吃力。解决不要每帧都做完整推理。第一眼睛模型按 1/3 帧率运行即每 3 帧处理一次帧间用上一状态保持因为人眼闭合是一个持续过程200ms 间隔足够。第二人脸模型换更小输入 480x480对人脸这种大目标影响极小。第三最后再做 int8 量化和 TensorRT 转换ultralytics 的 export 命令直接支持精度损失在疲劳检测这种二分类任务里通常可以接受。6. 时序判定与阈值校准从单帧闭眼到 PERCLOS 告警的一次落地疲劳告警不能看单帧。人正常眨眼时闭眼只有 100 到 150 毫秒单帧误判会导致系统频繁打扰司机。把第 2 章算出的 EAR 送进滑动窗口做 PERCLOS 统计才能得到稳定可靠的告警信号。from collections import deque ear_buf deque(maxlen150) # 5 秒 x 30fps WARN_PERCLOS 0.4 # 闭眼帧占比超过 40% while True: ear next_ear_value() # 来自第 2 章或第 3 章模型的输出 ear_buf.append(ear) if len(ear_buf) ear_buf.maxlen: continue perclos sum(1 for v in ear_buf if v EAR_CLOSED) / len(ear_buf) if perclos WARN_PERCLOS: issue_warning()PERCLOS 的判定阈值建议在 0.35 到 0.4 之间过低容易把连续正常眨眼误报成疲劳过高会漏掉真正的困倦状态。更合理的做法是分两级告警PERCLOS 超过 0.35 持续 3 秒触发提示音超过 0.5 并持续 8 秒触发强烈告警。时长阈值比 PERCLOS 阈值更影响实际体验耐心观察真实数据再定。阈值校准是最后一道关键工序。不同人的 EAR 基线差异明显大眼睛和小眼睛在完全睁眼时的 EAR 可以差 0.1 以上固定阈值必然误报或漏报。我一般在系统启动后采集 30 秒正常状态数据取 EAR 中位数作为个人基线再乘 0.65 作为闭眼阈值效果比任何全局固定值都稳。这个校准逻辑也要考虑座椅位置和后视镜遮挡所以每次启动都重新校准一次。验证阶段建议录一段 15 分钟模拟驾驶视频找人逐帧标注闭眼区间再对比算法输出计算帧级 F1 而不是只看告警次数。第一次做的时候我在验证集上把 PERCLOS 阈值调到 0.3指标很好但装到车上全在误报最后发现是测试时摄像头安装角度比训练数据低人脸框裁切位置整体偏下EAR 曲线整体被抬高。后来我在代码里加了 30 秒自适应校准让不同座椅高度的人上车后都能自动纠正阈值问题才真正解决。这种问题不会在模型训练阶段暴露只能靠现场数据和耐心试。给自己留足现场调试的时间比调任何网络结构都值得。希望帮到你。本文还有配套的精品资源点击获取
返回列表