简介:面向驾驶员疲劳检测应用开发,此资源整合了基于YOLO算法的检测模型与配套数据集,可针对闭眼、打哈欠等疲劳行为进行识别,适用于计算机视觉学习者、智能座舱安全研发人员及算法调参工程师。压缩包共2000个文件,以1984个txt标签文件为主体,另含xml格式标注、yaml模型配置以及PDF和Markdown说明文档,整体约306MB;txt与xml分别存放,便于对比不同标注规范并灵活接入训练流程。目前已有1139人学习浏览,资源提供完整的标签组织方式和模型配置文件,配合文档中的代码说明与可视化参考,可帮助读者快速复现疲劳检测流程,也能作为算法优化与二次开发的数据基线。整体上既适合入门阶段理解YOLO检测任务的数据结构,也可直接作为实际项目的模型与训练样本基础。
1. yolo疲劳检测:为什么闭眼和哈欠是最难搞定的两个动作
驾驶员疲劳检测在ADAS项目里一直是个看着简单、做起来到处是坑的活。人好检测,但“闭眼”和“打哈欠”这两个疲劳信号,一个在画面里只有几十个像素,一个从张嘴到闭嘴的过渡极其相似,用普通目标检测思路去做,漏检和误报能把人逼疯。这套yolo算法驾驶员疲劳检测模型+数据集,把这两件事打包成了可直接落地的方案——数据集里同时给出txt和xml两种标签格式,分别存放在两个文件夹里,训练、验证、可视化一条链路不用自己再造数据。模型本身能识别驾驶员闭眼、打哈欠等行为,适合做ADAS预警模块的工程人员、搞边缘设备部署的开发者,以及需要一套完整数据做毕业设计的学生。下面从数据集结构开始,把版本选型、训练参数、踩坑点一次说透。
2. 数据集结构拆解:txt和xml双标签怎么用才不白拿
2.1 目录组织与两类标签的分工逻辑
下载解压后先看目录结构,别急着配环境。这套资源的核心结构一般是这样的:
driver_fatigue/ ├── images/ # 原始图片,jpg格式 ├── labels_txt/ # YOLO训练所需txt标签 ├── labels_xml/ # VOC格式xml标签 └── classes.txt # 类别清单txt和xml描述的是同一批图片的标注,区别只在于格式。具体类别名以解压后的classes.txt为准,通常是一套闭眼、睁眼、打哈欠的组合。YOLOv5、YOLOv8这类框架训练时直接读txt,每行一条目标记录,格式是class_id x_center y_center width height,全部做归一化;xml是VOC那套老格式,标签信息完整、人类可读,适合用LabelImg打开复查,或者写脚本做统计。有人图省事只留一种,我的建议是两个都留——xml是原始标注的母版,txt是编译产物,一旦txt坐标出了异常,拿xml对照还能恢复出原始像素坐标,这就是后悔药。
什么时候用txt?跑训练的时候用。什么时候用xml?做可视化复查、统计目标尺寸分布、排查标注问题的时候用。两个文件夹别合并,也别自己写脚本中途改路径。我见过有人把xml和txt放在同一个目录,训练脚本glob到了xml文件直接报错,低级翻车完全能避免。
2.2 xml标签字段逐项解读:bbox坐标藏在哪几个节点里
xml结构不复杂,但字段容易搞混。打开任意一个xml文件,核心结构是这样:
<annotation> <folder>images</folder> <filename>img_001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>closed_eye</name> <bndbox> <xmin>720</xmin> <ymin>340</ymin> <xmax>780</xmax> <ymax>390</ymax> </bndbox> </object> </annotation>字段含义对照:
| 字段 | 含义 | 易错点 |
|---|---|---|
| filename | 原始图片文件名 | 不带路径,只是纯文件名 |
| size/width | 图片真实宽度 | 必须和实际图片一致 |
| size/height | 图片真实高度 | 图片缩放后容易漏改 |
| object/name | 类别名 | 必须和classes.txt完全一致 |
| bndbox | 像素坐标框 | xmin < xmax,ymin < ymax |
这里有个最典型的坑:xml里的四个坐标是像素绝对值,txt里的坐标是归一化相对值,两者之间差一次除法。归一化分母必须用xml里size/width、size/height的值,不能用640,不能自己去cv2.imread一张图重新取尺寸。一旦图被预处理脚本改过分辨率而xml没同步更新,转出来的txt坐标整体偏移,模型训练时loss直接不收敛。
我建议拿到数据后先写脚本扫一遍所有xml,查xmin < xmax、ymin < ymax、width和height是否和实际图片一致。这类数据在早期标注时经常出现某一张图被缩放过但标注没跟着更新的情况,扫一遍能提前排除掉。
2.3 txt标签格式与可视化核验脚本
再看txt。每行格式固定为五个空格分隔的字段:
0 0.3906 0.3380 0.0312 0.0463第一个数字是类别ID,也就是类别下标,0表示names列表里的第一个类别;后面四个数字是归一化后的中心点x、中心点y、宽度、高度,理论上都落在[0,1]区间内。这里有一个经常被忽视的问题:如果某个txt里出现了-0.001或者1.05这种越界值,模型训练时大概率报错或者把这个目标当作背景忽略掉。
验证标签最靠谱的方式是可视化。拿一张图和它对应的txt画框,框的位置跟实际目标对得上,标签才算可用。我平时会快速跑一个核验脚本:
import cv2 def draw_labels(img_path, txt_path, class_names): img = cv2.imread(img_path) h, w = img.shape[:2] with open(txt_path, 'r') as f: lines = f.readlines() for line in lines: parts = line.strip().split() if len(parts) != 5: print(f'[WARN] 非法行: {line}') continue cls_id = int(parts[0]) x_center, y_center, bw, bh = map(float, parts[1:]) # 从归一化坐标反算像素坐标 x1 = int((x_center - bw / 2) * w) y1 = int((y_center - bh / 2) * h) x2 = int((x_center + bw / 2) * w) y2 = int((y_center + bh / 2) * h) color = (0, 255, 0) if cls_id < 2 else (0, 0, 255) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, class_names[cls_id], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 1) cv2.imshow('label_check', img) cv2.waitKey(0) cv2.destroyAllWindows() class_names = ['closed_eye', 'open_eye', 'yawn'] draw_labels('images/img_001.jpg', 'labels_txt/img_001.txt', class_names)这段脚本的逻辑:用cv2.imread拿到图片真实宽高,然后逐行解析txt的五个字段,再把归一化坐标反算成像素框。map(float, parts[1:])批量把后四个字符串转成浮点数,比逐个float()干净。if len(parts) != 5的检查能提前把脏数据暴露出来——很多训练翻车就是某一行只有4个数字或者行尾多了一个空格导致的。
跑完可视化,眼睛扫一遍,框的位置和类别对不对,十秒就能判断这套标签能不能用。如果发现很多框偏移或者尺寸不对,回到xml对照坐标,问题多半出在归一化分母上。另外,做目标尺寸统计的时候建议直接从xml算,不要从txt反推——txt已经归一化,乘回图片宽高才有像素尺寸,中间多一步取整误差。各类别的尺寸分布直接决定后面训练时要不要开mosaic、要不要上调输入分辨率。
提示:txt标签核验脚本跑完,如果发现大量坐标越界,不要单独手动改txt,用xml重新批量导出,保证一致性。
3. 模型选型与训练前准备:疲劳检测为什么务实选YOLOv5
3.1 YOLOv5和YOLOv8的选型对比
先说版本选型。训练这套疲劳检测数据,我优先推荐YOLOv5而不是YOLOv8,理由不是v8不好,而是这个场景有自己的特殊性。
看一眼对比:
| 对比维度 | YOLOv5 | YOLOv8 |
|---|---|---|
| 锚框机制 | Anchor-Based | Anchor-Free |
| 小目标表现 | 多尺寸先验框覆盖 | 中心点回归+距离预测 |
| 模型体积 | s约14MB | n约6MB |
| 部署生态 | ONNX/TensorRT资料多 | 较新但坑也新 |
疲劳检测里的闭眼目标,在640×640输入下经常只有20×20像素甚至更小,是典型的小尺寸目标。Anchor-Based的YOLOv5通过多尺度先验框的覆盖,对小目标更友好。v5的anchors参数默认给了三组尺寸,分别对应8×8、16×16、32×32的感受野,闭眼目标的尺寸正好落在最小档位上,先验框能直接接住。v8的Anchor-Free思路是用中心点加距离回归,对密集小目标更敏感,需要更长的训练时间才能拟合。如果你之前用yolov8训练自己的数据集习惯了,也不是不能跑这套数据,就是小目标收敛和部署的坑要多花时间。
部署链也要考虑。疲劳检测最终要落到实车或者边缘盒子上,YOLOv5的ONNX导出、TensorRT加速流程非常成熟,各种算子报错基本都有现成答案;v8的部署链也没问题,但相关资料里新坑的比例高一点。新手直接上手v5,翻车概率低,这条是血泪经验。
3.2 数据集划分脚本:一次切好训练集、验证集、测试集
选型定了之后先做数据划分。这套资源没有现成的train/val目录,需要自己切。我习惯按8:1.5:0.5分成训练、验证、测试三份,测试集必须留出来,后面验证模型泛化能力要用。
import os import random from shutil import move random.seed(42) IMG_DIR = 'images' TXT_DIR = 'labels_txt' # 建目录 for split in ['train', 'val', 'test']: os.makedirs(os.path.join(IMG_DIR, split), exist_ok=True) os.makedirs(os.path.join(TXT_DIR, split), exist_ok=True) imgs = [f for f in os.listdir(IMG_DIR) if f.endswith(('.jpg', '.png'))] random.shuffle(imgs) n = len(imgs) train_end = int(n * 0.8) val_end = int(n * 0.95) splits = { 'train': imgs[:train_end], 'val': imgs[train_end:val_end], 'test': imgs[val_end:] } for split, files in splits.items(): for img in files: name = img.rsplit('.', 1)[0] move(os.path.join(IMG_DIR, img), os.path.join(IMG_DIR, split, img)) move(os.path.join(TXT_DIR, f'{name}.txt'), os.path.join(TXT_DIR, split, f'{name}.txt')) print(f'train: {len(splits["train"])}, val: {len(splits["val"])}, test: {len(splits["test"])}')逻辑说明:random.seed(42)固定随机种子,这一步必须写——不然每次运行划分结果都不同,换机器结果也变。图片按比例切到三个子目录,同时把同名的txt也同步搬过去,这一步是关键:漏搬一个txt,那个样本在训练时就被当成背景样本,类别比例直接偏。
注意图片文件名的大小写。如果数据里出现IMG_001.JPG和img_001.jpg混存的情况,rsplit('.', 1)按最后一个点切分能拿到纯文件名,但前提是jpg和txt的前缀完全一致。建议划分前先把全部文件列出来,检查一遍有没有大小写不一致、后缀不同导致的同名异文件。划分完跑两条命令核对数量:
find images/train -name "*.jpg" | wc -l find labels_txt/train -name "*.txt" | wc -l两个数必须一致。如果txt少了,说明有图片缺标签,回到xml里把那一张的标注重新导出来补上。
3.3 data.yaml配置:类别顺序就是txt的class_id顺序
划分完写data.yaml。这是YOLOv5训练入口文件,内容不长但最容易出错:
# data.yaml train: /home/user/driver_fatigue/images/train val: /home/user/driver_fatigue/images/val nc: 3 names: ['closed_eye', 'open_eye', 'yawn']train和val指向图片目录的路径,YOLOv5会自动去同级的labels_txt目录找对应txt。这里有个隐藏约定:标签目录必须叫labels或者labels_txt,并且和图片目录同级。YOLOv5的查找方式是拿图片路径里的images替换成labels。如果你把标签目录命成annotations,训练时会报label file not found,看起来像数据缺失,实际是命名约定没满足。
names列表的顺序直接决定了txt里class_id的含义。names[0]对应class_id为0的类别,以此类推。如果你下载的classes.txt里类别顺序和我这里不同,以classes.txt为准,改data.yaml而不是改txt。我踩过这个坑:训练完的模型把睁眼全判成闭眼,把闭眼全判成哈欠,折腾三天才发现是names顺序和txt的class_id对不上。
另外,val目录在训练时默认用来算mAP。如果你把test也混进val,mAP会被刷高,看着好看但实际泛化能力没那么强。test单独留着,训完最后跑一次val.py --task test,那个指标才是能写进汇报的。
注意:data.yaml里names顺序一旦写入,训练过程中不要再改动。中途修改会导致ckpt的类别映射错乱,继续训练的结果不可信。
4. 训练参数详解:epochs、batch-size、img-size怎么设才不翻车
4.1 用显存反推batch-size的经验方法
训练显存不够是翻车最多的环节,没有之一。以YOLOv5s在img-size=640为例,我常用的对照表是这样:
| 显存 | 建议batch | 注意 |
|---|---|---|
| 4GB | 4-8 | 优先4 |
| 8GB | 16 | 最常用 |
| 16GB | 32 | 靠近上限 |
| 24GB | 32 | 可换v5m |
如果拿不准自己的卡能跑多大batch,有个快速估算方法:先设batch=2跑一个step,用nvidia-smi看显存占用。假设batch=2占用0.8GB,按线性反推batch=16需要6.4GB,8GB卡可以跑;如果batch=2占用1.2GB,反推batch=16需要9.6GB,超了,降成8。注意这只是线性估算,实际踩到CUDA out of memory就再减半。
特别提醒:不要为迁就显存把img-size降到416。640对闭眼小目标已经是下限,再低目标在特征图上只剩一个点,模型学不到纹理。低显存场景的正确做法是降batch、保持分辨率,而不是反过来。这也是我在4GB小卡上跑出来的教训。
4.2 关键超参数推荐值与训练命令
训练命令我建议固定成这样:
python train.py \ --data data.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 200 \ --patience 30 \ --lr0 0.01 \ --mosaic 1.0参数说明表:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| weights | yolov5s.pt | 官方预训练模型,迁移学习起步 |
| img | 640 | 闭眼小目标,别再低 |
| batch | 16 | 8GB显存安全值 |
| epochs | 200 | 这个规模的数据200轮够用 |
| patience | 30 | 连续30轮mAP不升就早停 |
| lr0 | 0.01 | YOLOv5s默认起点 |
| mosaic | 1.0 | 小目标增强刚需 |
epochs不是越大越好。疲劳检测数据集通常几千张图片,150到200轮基本收敛,300轮以后大概率过拟合。patience开省时间也防过拟合,它会在验证集mAP连续30轮不提升时自动终止训练,你睡觉时它自己停。
lr0=0.01是YOLOv5官方对s版本给的默认值,一般不用动。如果观察loss在中段震荡厉害,把lr0降到0.005再试,不要同时动其它参数,一次只改一个变量定位问题。
训练日志里还有三个loss值:box_loss、obj_loss、cls_loss。正常收敛趋势是三者一起下降,其中obj_loss降得最慢——它是前景/背景分类损失,疲劳检测这种小目标占比很少的场景里背景像素占绝对多数。如果box_loss降了但obj_loss纹丝不动,优先检查数据增强强度和负样本比例,把mosaic保持全开,必要时加随机裁剪。
提示:如果训练时
CUDA out of memory,优先降batch而不是降img,640分辨率是闭眼检测的下限。
4.3 训练日志里P、R、mAP到底信谁
训练日志里P、R、[email protected]会同步输出。新手常只盯mAP,在疲劳检测场景我更看R——漏报的代价远大于误报。漏掉一次闭眼,系统没有预警,可能就是一次事故,P略微放宽是可以接受的。
三个指标的关系:
- P(精确率):检出的框里真正正确的比例,框错了就是误报
- R(召回率):真实目标里被检出的比例,漏了就是漏报
- [email protected]:IoU阈值0.5下所有类别的平均AP,综合指标
疲劳检测建议盯R。R低于0.85,优先解决漏检问题;P低于0.8,再去处理后处理逻辑。不要先调参数追求mAP数字好看,实际场景里报警频次和漏报率才是用户拍板的标准。
训练完成后加一步独立验证。用test集重新评估一次,和val分开:
python val.py --data data.yaml --weights best.pt --task test如果val上mAP有90%+,test上只有70%,说明模型可能见过验证集内容,或者验证集分布太偏。这时候回头查划分脚本有没有混样本,而不是怀疑模型。
5. 疲劳检测避坑指南:五个典型翻车现场与排查方案
5.1 闭眼目标框过小导致漏检
现象:训练完在测试视频里一帧闭眼都没检出来,但打开训练集标签图,闭眼框明明都在。
原因:闭眼目标在原始图像里可能只有30×30像素,经过YOLO的多级下采样(640 → 320 → 160 → 80 → 40),在最深特征图上只剩不到1个像素的特征,模型根本学不到“这是闭眼”的纹理信息。
解决:优先把输入分辨率拉到960,代价是推理速度下降;或者用SAHI做切片推理,把原图切成512或640的块分别检测再合并结果,对极小型目标非常有效;也可以在训练阶段保持mosaic增强全开,让模型多见不同尺寸的闭眼样本。动手改之前先做一步统计,把xml里的闭眼框尺寸全部取出来算平均值,低于40×40像素的,直接判断小目标是主因,别瞎调学习率。
5.2 打哈欠误报:张嘴和哈欠的边界怎么处理
现象:模型把正常说话、张嘴笑、吃东西的动作全部判成打哈欠,报警频率高到没法用。
原因:单帧图像里“张嘴巴”和“打哈欠”几乎是同一个视觉特征,哈欠本质上是一个持续若干帧的张口过程,单帧检测天然丢掉了时间维度的信息。
解决:在后处理里加一个时间窗口逻辑——不是单帧出现yawn就报警,而是连续5帧里至少3帧检到才确认一次哈欠事件。用一个队列来维护最近几帧的状态:
from collections import deque yawn_history = deque(maxlen=5) def is_yawn_confirmed(yawn_detected): yawn_history.append(1 if yawn_detected else 0) return sum(yawn_history) >= 3deque(maxlen=5)只保留最近5帧的检测结果,每来一帧把新结果追加进去,旧的自动弹出。sum(yawn_history) >= 3表示5帧里至少3帧检到哈欠。这样处理后,误报告警能降到原来的十分之一,代价是报警延迟约0.5秒。疲劳预警这个场景完全能接受这个延迟。如果还是频繁误报,把条件收紧到连续5帧全部检到,或者结合闭眼帧占比再做一次交叉判断。
5.3 夜间红外场景识别率骤降
现象:白天测试mAP有90%以上,换到夜间红外图像直接掉到50%以下。
原因:训练集以白天RGB为主,模型实际在依赖颜色纹理做判断。红外图像只有灰度信息,颜色一没,特征分布完全变了。
解决:训练时打开颜色抖动和灰度增强。YOLOv5的hsv_h、hsv_s、hsv_v三个增强参数默认值调高一些,比如--hsv-s 0.8 --hsv-v 0.6,让模型对颜色变化不那么敏感。如果训练集本身光照单一,更直接的办法是在预处理阶段把一部分图随机转成灰度图参与训练,模型被迫去学形状和纹理而不是颜色。
夜间部署的另一个思路是采集少量红外数据做增量训练,几百张就够,不需要重新标全套。增量训练后模型在红外场景的掉点能拉回大半,这条建议来自实际项目经验。
5.4 训练loss不降或NaN
现象:训练到第30轮,loss卡在0.7上下不降了;或者某个loss直接变NaN。看起来是调参玄学,实际多半是数据的问题。
原因:最常见是标签文件脏了——txt里的归一化坐标越界,或者类别ID超出了names列表的长度;另一种是学习率太高,训练初期就震荡崩坏。
解决:先跑标签核验脚本,把txt里所有坐标打印出来扫描,检查有没有负数或大于1的值。发现越界行,用xml重新生成那一张的txt,不要手动改一个数字了事,像素坐标和归一化坐标之间换算错了整体还是歪的。如果标签没问题但依然NaN,把学习率降到0.001重启训练。注意:不要把越界的行直接删掉当不存在,模型会把那个位置当成背景,样本类别比例照样偏。
5.5 部署后推理速度不达标
现象:模型在Jetson Nano这类低算力设备上只有2-3 FPS,没法实时预警。
原因:FP32精度推理开销大,加上640分辨率对边缘设备的算力来说太重。
解决:按顺序做四步优化——先把模型切到FP16半精度(PyTorch里.half()),速度提升明显;再考虑TensorRT做INT8量化,速度能翻好几倍;如果量化后精度掉得可接受,把输入分辨率从640降到480;最后调NMS的IoU阈值,适当放宽让筛选更快。每做一步都在测试集上验一次R,R掉太多就回退。疲劳检测漏检的代价比速度大得多,这个优先级顺序别颠倒。
6. 用真实视频验证模型:从推理脚本到PERCLOS调优
6.1 视频推理脚本与参数设定
模型训练完别急着上实车,先用真实视频验证。真实视频和静态图片差距很大——运动模糊、光照变化、帧率不齐都会影响效果。我的验证脚本长这样:
import cv2 import torch model = torch.hub.load('ultralytics/yolov5', 'custom', path='best.pt') model.conf = 0.35 # 置信度阈值,疲劳场景优先召回 model.iou = 0.5 # NMS的IoU阈值 model.half() # 半精度推理 cap = cv2.VideoCapture('drive_test.mp4') closed_total = 0 frame_total = 0 yawn_total = 0 while cap.isOpened(): ret, frame = cap.read() if not ret: break results = model(frame) df = results.pandas().xyxy[0] if 'closed_eye' in df['name'].values: closed_total += 1 if 'yawn' in df['name'].values: yawn_total += 1 frame_total += 1 cap.release() perclos = closed_total / frame_total print(f'PERCLOS: {perclos:.2%}, 哈欠帧占比: {yawn_total / frame_total:.2%}')model.conf=0.35比默认的0.5低一些,疲劳检测宁可多检一点也别漏。model.half()切到FP16,速度提升明显,精度几乎无损。results.pandas().xyxy[0]把检测结果转成DataFrame,按name列直接统计类别,简洁好维护。
6.2 用PERCLOS指标验证模型是否可用
PERCLOS计算方式很简单:闭眼帧数除以总帧数。疲劳判定通常以PERCLOS超过0.4为警戒线,也就是40%的时间眼睛处于闭合状态。跑完脚本,数值在0.4以上说明模型确实抓到了闭眼规律,可以做预警前置;数值明显偏低而测试视频里确实有人打瞌睡,说明漏检,反过来调阈值或加大输入分辨率。
一个细节:如果闭眼检不到但正常眼睛能检到,大概率是置信度阈值太高,闭眼框全部被过滤掉了,把model.conf降到0.25再跑一遍。还是不检,回到5.1小目标方案。另外,验证用的视频最好是真实驾驶场景拍的,不要用实验室摆拍的片段——运动模糊和抖动对闭眼小目标的干扰,只有真实路测才看得出来。
最后说个我自己的翻车经历。之前有一回改标签格式时动了names顺序,忘了同步到txt,模型把睁眼全判成闭眼,闭眼全判成哈欠,折腾三天才发现源头就是类别顺序。从那以后,我拿到任何一套数据都强制先跑一遍标签核验脚本,确认类别顺序、坐标范围、文件数量全对,再进训练循环。这套检查流程到现在没再翻过车。希望帮到你。
本文还有配套的精品资源,点击获取