简介:疲劳驾驶检测数据集以Pascal VOC格式组织,收录4362张驾驶状态图片及对应XML标注文件,面向计算机视觉方向的算法工程师与研究人员,可直接用于疲劳驾驶检测、驾驶员状态分析等模型的训练与评估。标注工作基于labelImg完成,采用矩形框标记眼睛与嘴部状态,共设置closed_eye(闭眼)、open_eye(睁眼)、closed_mouth(闭嘴)、open_mouth(张嘴)4个类别,对应标注框数量分别为2485、3343、4903、936,模型可借助这些框学习睁闭眼、张闭嘴的关键视觉特征,判断驾驶员是否疲劳。压缩包内含4362个jpg图像、4362个xml标注文件以及1个说明txt,资源体量约368.57MB;VOC格式可直接对接多数目标检测框架,便于进一步转换为YOLO、MMDetection等常用训练格式。整套标注遵循合理准确原则,只确保标注质量,不对训练效果作额外承诺;目前已有2800余人浏览学习,适合需要标准VOC格式数据的目标检测、图像分类等任务作为基础数据集使用。
1. 4类别4362张VOC疲劳驾驶数据集:它到底能做什么
我在做车载DMS(驾驶员监控系统)第一版原型时,最卡的不是模型选型,而是训练数据从哪来。疲劳驾驶数据集以VOC格式把4个类别共4362张已标注图片整理好,正好解决从零起步的问题:不用先雇人标注,拿过来就能进训练管线。它适合的目标很明确——想做疲劳驾驶行为检测、手里没有标注团队、又想尽快跑出一版看得见mAP指标的工程师。
4个类别通常是打哈欠、闭眼、打电话、抽烟,前两个对应疲劳,后两个对应分神。4362张对随机初始化偏小,配合预训练权重微调足够跑通第一版。VOC是目标检测的通用交换格式,YOLO、Detectron2都能接住。下面按格式拆解、转换脚本、训练避坑、进阶验证的顺序走一遍。
2. VOC格式拆解:目录结构到XML标注的每处细节
VOC格式诞生于Pascal VOC竞赛,是目标检测领域流传最广的标注规范之一。很多标注工具导出时默认给VOC的XML,公开数据集也大量沿用这套结构。所以拿到一包VOC格式数据,第一件事不是急着写训练脚本,而是把三块内容看清楚:图片目录、XML标注、划分文件。这三块对应到训练环节里,分别决定输入图像、目标框和数据集切分。
2.1 四个疲劳类别怎么定,业务上怎么对应
一个疲劳驾驶数据集整理成4个类别,常见划分是打哈欠、闭眼、打电话、抽烟。从业务上看,闭眼和打哈欠是判断疲劳状态最直接的外显特征,DMS靠它们计算PERCLOS这类疲劳指标;打电话和抽烟属于驾驶分神,法规监管里同样是重点抓拍对象。
类别名本身没有统一标准,有的数据集叫yawning,有的叫mouth_open,有的把闭眼叫eye_closed。这不是问题,真正的问题在于一致性:XML里的name、训练配置里的类别列表、推理时显示出来的标签,三处必须完全一样。我在项目里的习惯是统一用英文短名:yawn、closed_eye、phone、smoke。不用中文有实际原因:XML文件编码一旦在Windows下被存成gbk,Linux训练环境读出来就是乱码,整个类别映射直接作废。
2.2 JPEGImages、Annotations、ImageSets/Main 各自管什么
标准VOC数据集在磁盘上是三个层次的目录:
fatigue_voc/ ├── JPEGImages/ # 图片原件,4362张jpg ├── Annotations/ # 同名的xml标注,4362个 └── ImageSets/ └── Main/ # 数据集划分 ├── train.txt ├── val.txt └── trainval.txtJPEGImages只放原始图片,不要提前resize、不要转格式。所有预处理放到训练时的dataloader里做,这样原始标注坐标始终对应原始分辨率,后面想换模型输入尺寸,都还有后悔药。Annotations里每个xml和图片同名,比如000042.jpg对应000042.xml。XML的filename字段虽然是冗余信息,但训练脚本通常不读它,而是用文件系统里的文件名配对。
ImageSets/Main下的txt是纯文本清单,每一行一个不带扩展名的文件名。train.txt写进训练集,val.txt写进验证集,trainval通常是两者并集的别名。这三个txt决定了模型被喂进什么、在什么上评估,比XML本身更容易被忽略,也更容易出错。
2.3 XML里的关键字段:filename、size、object、bndbox
一个典型的VOC标注XML长这样:
<annotation> <folder>fatigue_voc</folder> <filename>000042.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>yawn</name> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>770</xmin> <ymin>420</ymin> <xmax>900</xmax> <ymax>530</ymax> </bndbox> </object> </annotation>size里的width和height是坐标换算的唯一参照。bndbox的四个值是绝对像素坐标,左上角为原点,x向右、y向下。object每出现一次,代表图里有一个目标。一张图里如果有两个人同时在打电话,就会有两个object,两个bndbox。
difficult字段是VOC里的老概念,代表这个目标因为遮挡、极小或模糊而难以辨认。很多标注工具导出时会把难例标成1。训练时如果不过滤difficult=1,模型会被残缺框持续误导,对疲劳这种小目标场景影响更大。我转换时一律跳过difficult=1,只保留正常框。
读XML还有两个隐藏坑。一是size里的宽高有时和图片实际分辨率不一致,常见于数据被脚本重排过目录;二是bndbox坐标可能越界。所以转换脚本里必须做坐标裁剪,同时以size字段而不是以opencv读图结果为准。原因是:标注框本来是画在标注工具当时看到的那张图上的,尺寸记录在size里,图片文件被二次压缩过而XML没更新时,以XML为准才不会框漂移。
2.4 先数数每个类别有多少框:一个统计脚本
拿到数据集先别急着训练,跑一个类别统计,搞清楚每一类有多少个目标框。这一步对疲劳驾驶数据尤其重要:闭眼目标小、容易被漏标,打哈欠框数多、标注相对充分,四类数量严重失衡是常态。
import glob import xml.etree.ElementTree as ET stats = {} for xml_path in glob.glob("Annotations/*.xml"): root = ET.parse(xml_path).getroot() for obj in root.findall("object"): name = obj.find("name").text.strip() stats[name] = stats.get(name, 0) + 1 total_imgs = len(glob.glob("JPEGImages/*.jpg")) total_boxes = sum(stats.values()) for name, cnt in sorted(stats.items(), key=lambda x: -x[1]): print(f"{name}: {cnt} boxes") print(f"images: {total_imgs}, avg boxes per image: {total_boxes / total_imgs:.2f}")脚本逻辑很简单:遍历每个xml,累加各name出现次数,最后输出每类框数、图片总数和平均每张图的框数。如果平均框数低于0.5,说明大量图片是空图(没有任何行为发生),这类空图在训练时会被当作纯负样本,但验证时会导致模型偏向少报,需要结合第4章的空帧验证一起考虑。
3. 把VOC转成YOLO训练格式:转换脚本、类别映射与划分逻辑
YOLO训练不直接读VOC的XML,它要的是images目录加labels目录,每张图配一个同名txt。转换的本质就是把XML里的绝对像素框换算成归一化中心点和宽高。这一步脚本一旦写好,之后不管是yolov5训练自己的数据集还是yolov8,标签目录结构完全通用。
3.1 类别映射和YOLO的txt标注约定
YOLO的txt每一行格式是五个数:类别id、中心点x、中心点y、框宽、框高,全部归一化到0到1。class_id是类别列表里的下标,从0开始。比如类别列表是["yawn", "closed_eye", "phone", "smoke"],那yawn就是0,closed_eye就是1。一行对应一个目标框,没有目标时txt为空文件。
归一化计算方式:取bndbox的xmin和xmax,中心点x=(xmin+xmax)/2再除以图片宽度,框宽=(xmax-xmin)/图片宽度,y方向同理。这个换算不复杂,但容易在除以谁上出错,前面说过,分母必须是XML里size的宽高。
类别列表建议单独写一个配置:
# classes.py CLASSES = ["yawn", "closed_eye", "phone", "smoke"]训练、转换、推理都import这同一个文件,保证顺序一致。我踩过顺序不一致的坑:训练时class_id=0是yawn,推理脚本里classes.txt却按phone开头,模型输出和真实类别整体错位,mAP看起来正常,实际全乱了。
3.2 完整的VOC转YOLO脚本
下面这段脚本可以直接对着fatigue_voc目录跑:
import os import glob import xml.etree.ElementTree as ET # 类别顺序即class_id顺序,训练验证推理三处保持一致 CLASSES = ["yawn", "closed_eye", "phone", "smoke"] def convert_xml(xml_path, out_txt_path): root = ET.parse(xml_path).getroot() size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) lines = [] for obj in root.findall("object"): # 难例直接过滤,不让残缺框进训练集 if int(obj.find("difficult").text) == 1: continue name = obj.find("name").text.strip() if name not in CLASSES: continue cls_id = CLASSES.index(name) box = obj.find("bndbox") xmin = max(0, int(float(box.find("xmin").text))) ymin = max(0, int(float(box.find("ymin").text))) xmax = min(img_w, int(float(box.find("xmax").text))) ymax = min(img_h, int(float(box.find("ymax").text))) # 裁剪后框非法则丢弃,避免面积为零的负样本 if xmax <= xmin or ymax <= ymin: continue x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") if not lines: return False os.makedirs(os.path.dirname(out_txt_path), exist_ok=True) with open(out_txt_path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) return True xml_files = glob.glob("Annotations/*.xml") skipped = 0 for xml_path in xml_files: name = os.path.splitext(os.path.basename(xml_path))[0] if not convert_xml(xml_path, f"labels/{name}.txt"): skipped += 1 print(f"converted: {len(xml_files) - skipped}, skipped: {skipped}")这段脚本处理了三件容易被忽略的事。第一,difficult=1直接跳过,不参与训练;第二,坐标裁剪,xmin、ymin只允许增大,xmax、ymax只允许减小,限制在图片尺寸内;第三,裁剪后xmax<=xmin或ymax<=ymin的框直接丢弃,避免模型接收无效目标。
跑完后核对labels目录数量和Annotations数量:
find labels -name "*.txt" | wc -l find Annotations -name "*.xml" | wc -l两个数字不一致时,去看skipped统计。如果一个xml里所有目标都被过滤了,说明这张图没有任何可训练标注,需要单独抽出来人工复查,而不是默默忽略。
3.3 训练/验证划分:别把同一个人的连续帧切到两边
4362张图属于小型数据集,划分方式对最终指标的影响甚至大过模型结构。如果数据来自连续视频录制,同一人的相邻帧高度相似。按单张随机切分,会出现训练集和验证集里都有同一个动作的几乎相同画面,验证mAP虚高,上线后立刻露馅。
我经历过的真实翻车:一个疲劳检测模型验证mAP有0.87,换到另一段车内视频后漏检率飙升,最后定位到原因就是划分泄漏——训练集和验证集混进了同一段视频的帧。解决方案是分组划分,先按视频片段或拍摄场景把图片归组,再以组为单位切分,而不是以单张图为单位。如果文件名里能看到分组信息,比如同一场景共享前缀,可以用GroupKFold:
from sklearn.model_selection import GroupKFold file_names = [os.path.splitext(f)[0] for f in os.listdir("JPEGImages")] groups = [name.split("_")[0] for name in file_names] # 前缀作为组id,按实际命名规则改 gkf = GroupKFold(n_splits=5) for train_idx, val_idx in gkf.split(file_names, groups=groups): train_names = [file_names[i] for i in train_idx] val_names = [file_names[i] for i in val_idx] break # 只取第1折,演示用参数里的n_splits决定折数,一般5折够用。group条件必须根据实际文件名规则调整,如果文件名前缀是摄像头ID和时间戳混合,按前缀分组可能不合理,这时宁可手动维护一份每组包含哪些图片的清单。划分比例上,4362张按8:2切,训练约3490张、验证约872张。如果最后要做交付,建议再留出300张做独立测试集,完全不参与调参。
3.4 增强参数:对疲劳小目标什么该开,什么该关
疲劳驾驶检测的目标多数是小目标,闭眼框经常只有几十个像素,打哈欠的嘴部框也不大。YOLO自带的增强默认值按COCO的大目标调,直接套在疲劳场景上会出问题。我的经验是把色相增强调小:hsv_h=0.015,颜色偏差太大会让肤色失真;hsv_s和hsv_v各0.4,模拟不同光照够用,再大容易把画面调成过曝。
mosaic开启对4362张这种量级有帮助,但mosaic=1.0会把小目标的框切掉一部分,尤其类别样本少时,切没一个框等于少了一个训练信号。训练早期用mosaic=0.5,等loss进入平台期再调到1.0,是更稳的节奏。scale抖动不要超过0.5,闭眼目标本来就小,缩到几个像素后特征全丢。水平翻转对这种场景安全,打哈欠和闭眼在左右翻转后语义不变;上下翻转不建议开,车内视角里倒过来的脸并不常见。
4. 疲劳驾驶检测训练避坑排查:从loss异常到mAP虚高的5个典型问题
数据侧的坑通常比模型结构更早暴露,尤其在自己整理的VOC数据集上训练YOLO时,问题往往集中在标签、划分和类别分布三处。下面这5个问题按现象、原因、解决顺序写,都是我在疲劳检测项目里实际排查过的。
4.1 现象:训练loss持续下降,验证mAP却在0.5附近上不去
loss下降说明模型在训练集上拟合良好,mAP低说明学到的东西没有泛化。排在前面的原因通常是划分泄漏的另一面:验证集场景和训练集差异太大。4362张如果集中在同一个角度、同一个驾驶员,模型学到的是背景和肤色特征,而不是打哈欠这个动作本身。
解决:确认按3.3的分组方式重划分之后,再看验证集是否覆盖多角度、多光照。如果数据集本身场景单一,补数据的正确方向是增加不同机位和不同人的视频段,而不是继续堆同场景图片。另一种常见原因是类别框数差距大,哪个类别框少,它的AP就会被拉低,进而拖累整体mAP,可以单独打印每类AP定位。
注意:mAP不达标时先查数据分布,再动模型结构。在4362张的数据规模下,结构换血往往不如把划分和标注搞干净。
4.2 现象:closed_eye类别的mAP明显低于其它三类
闭眼是疲劳检测里最重要也最难的目标。它尺寸小,标注框通常只有几十像素,经过YOLO的下采样后,特征图上的响应区域非常有限。再加上闭眼样本在采集时容易被漏标——人眼已经闭上了但图上没框,模型就在同样的区域学到了这里不该有目标的负反馈。
解决:先统计该类框数,确认是不是样本量太少。如果少,两个方向:一是给闭眼类更高的loss权重,或者在dataloader里对该类图片过采样;二是把输入分辨率从640提到960,小目标召回率通常立竿见影,代价是显存和推理速度。我一般先用960验证精度上限,再评估部署端算力是否允许。
还要复查闭眼框的标注范围。常见问题是框太大,把眉毛和眼袋都包进去,模型学到的不是闭眼而是眼周区域。标注规范应该是紧贴眼裂,上下眼睑闭合线附近,框得越紧,模型越容易抓住关键纹理。
4.3 现象:训练时大量图片报no labels,xml和jpg对不上
自采集数据在多次导出和搬运后,经常出现文件名错位。典型两个症状:XML里的filename写的是完整路径;或者图片经过二次重命名、XML没有同步更新。YOLO训练时按文件名去找txt,txt又是按XML名生成的,两边差一个字符,这张图就变成无标注图。
解决:转换脚本不要信任XML里的filename字段,以Annotations目录下的文件名为准。同时跑一个交叉校验:
import os imgs = {os.path.splitext(f)[0] for f in os.listdir("JPEGImages")} anns = {os.path.splitext(f)[0] for f in os.listdir("Annotations")} print("有图无标注:", sorted(imgs - anns)[:20]) print("有标注无图:", sorted(anns - imgs)[:20])这段代码输出两个集合的差集。有图无标注的,要么补标注,要么从训练清单剔除;有标注无图的,多半是图片被误删或重命名,直接去翻原始备份。处理原则是宁可少一张图,不用错一张图。把对不上的样本全部隔离到单独目录,等人工核对后再决定是否放回训练集。
4.4 现象:验证集mAP不错,视频流里却每几秒乱报一个框
这是疲劳检测上线前最容易被忽略的场景。验证集的图片几乎全部是有行为的正样本,模型从头到尾没见过大量空背景帧,于是对画面里没有目标这件事没有概念,推理时在挡风玻璃、方向盘、路面上乱出框。
解决:训练和验证时都加入无行为帧。从车内视频里截取驾驶员正常驾驶、没有打哈欠闭眼打电话的片段,混进验证集,统计每帧平均误检框数。如果误报明显,先把conf阈值从0.25提到0.4,快速压掉低置信度乱报;更彻底的做法是把误检帧导出,人工确认后作为负样本补进训练集。
提示:mAP只反映目标出现在图上时找没找到,反映不了没有目标时别乱说。疲劳驾驶场景里,空帧误报直接决定用户是否卸载这个功能。
4.5 现象:同一驾驶员换个角度就漏检,侧面和低头特别严重
固定机位的摄像头天然只能覆盖有限的人脸角度。训练集里如果90%都是正脸,模型会对侧脸、低头这些姿态下的闭眼和打哈欠特征失效。这不是模型不行,是训练分布里没有这些姿态。
解决:最便宜的手段是水平翻转,模拟左右转头;再多一点可以用小角度随机旋转,但别超过15度,否则人脸框语义漂移。真正治本的是补充多姿态数据,尤其是侧面30到60度、低头看手机这两个角度,疲劳检测在实际车里基本绕不开。我在交付项目时会把姿态覆盖写进数据验收标准,而不只是看总框数。一个数据集总框数再多,姿态单一,对疲劳检测的价值也有限。
5. 4362张的进阶用法:迁移学习、难例挖掘与空帧误检验证
基础管线跑通、mAP稳定在0.6以上之后,下一步不是无限调参,而是把这4362张数据的价值榨干。我按投入产出比排序,推荐做三件事:迁移学习、难例挖掘、空帧验证。
5.1 迁移学习:微调参数怎么定
疲劳驾驶检测都是小目标,从COCO预训练权重起步是必选项。4362张的规模,随机初始化训练几乎不可能收敛出可用模型,但基于预训练权重微调,通常几十个epoch就能看到稳定结果。用ultralytics的话,一个最小命令就能起训练:
yolo detect train \ data=fatigue.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ mosaic=0.5 \ freeze=10data/fatigue.yaml只需要五行:train和val指向转换好的images目录,nc=4,names顺序与CLASSES完全一致。imgsz先640,如果闭眼AP不理想再提到960。batch按显存定,16G显存跑nano没问题。freeze=10用来冻结backbone前10层,前20个epoch让检测头先适应疲劳场景,之后去掉freeze再做全量微调。
5.2 难例挖掘:把失败样本手工补进训练集
用训练好的模型对验证集推理,导出置信度在0.3到0.6之间的框。这些是模型觉得像但拿不准的候选,压缩成拼图后人工筛一遍:确认是真实目标的框补进训练集,确认是误检的存成负样本。一轮挖掘循环通常能把mAP抬2到4个点,而且不依赖外部数据。这个动作对疲劳小目标尤其有效,很多闭眼框被漏标,模型自己发现了、人再确认,等于给数据集做二次标注。
5.3 空帧误检验证:疲劳检测逃不过的一关
真实场景里驾驶员绝大多数时间是正常的,画面里没有任何疲劳行为。模型如果在空帧上报一个闭眼,系统就会推送一次无效疲劳提醒,几次之后整个功能就被判定为不可用。我习惯把空帧误检率直接写进验收标准。做法是从行车记录视频里抽200帧没有任何行为的画面,混进验证集,用conf=0.25跑推理,统计每帧平均误检框数。这个数要控制在0.1以下,也就是平均10帧最多误检1个框。如果超了,先提conf阈值,再做难例挖掘补负样本。
mAP好看但空帧乱报的模型不能上车;mAP略低但空帧干净的模型,配合疲劳判定的时序逻辑,反而更容易落地。这套顺序来自我几次在上线前被空帧误报打回的经历,现在每个疲劳检测项目都把空帧验证放在最前面。希望这里的转换脚本和验证方式能帮你在疲劳驾驶数据集上少走一段弯路。
本文还有配套的精品资源,点击获取