简介:面向深度学习图像识别初学者的疲劳驾驶监测数据集,包含人处于疲劳状态下的实拍照片,以打哈欠张嘴、闭眼作为疲劳判断标准,可用于训练图像分类或目标检测模型。压缩包共256.28MB,内含2915张jpg原始图片与2914份xml标注文件,分别对应JPEGImages与Annotations目录,便于直接读取和训练。数据集中存在部分图片与标注不匹配的情况,资源附带了Python数据清洗脚本示例:程序会遍历图片目录并比对同名xml文件,自动删除无对应标注的图片,帮助用户快速获得干净有效的训练数据。已有4736人下载学习,侧面说明该数据集在人脸状态识别入门项目中具有较高参考价值。资源整体贴近驾驶安全真实场景,既能帮助理解VOC格式标注与目标检测流程,也能锻炼数据质量筛查与预处理能力。
1. 疲劳驾驶监测数据集:把它从压缩包变成能落地的检测模型
先给结论:疲劳驾驶监测数据集这类 zip 资源,核心价值不在“收集了多少张图”,而在你能不能把它快速转成自己训练框架能吃进去的格式。很多从业者拿到压缩包后的第一反应是解压、看两眼图片、扔进 YOLO 里开训,结果要么类别标签对不上,要么训练到一半发现数据集里全是同一段视频抽出来的相似帧,验证集指标虚高、上路就翻车。这个标题背后真正要解决的是三件事:数据怎么解压校验、标注怎么统一成 YOLO 格式、以及训练参数怎么适配疲劳驾驶这种“小目标 + 类不均衡 + 夜间低照度”的组合场景。本文按一条完整落地路径来写,适合准备用 YOLOv8 做疲劳驾驶检测、手头刚拿到一份 zip 数据集、正在纠结从哪一步开始的工程师;不建议纯算法研究型读者照搬全部参数,你们更关心消融实验的公平性,而这里更关心“能不能稳定跑通、部署后扛不扛得住”。
2. 解压不是双击完事:先做完整性校验与目录结构体检
2.1 为什么第一件事是校验而不是直接解压
zip 格式在传输过程中出现文件头损坏、截断、伪加密的概率,比大多数人想象得高。疲劳驾驶监测数据集的文件通常来自网盘、QQ 群文件或者标注公司的 FTP,经历过多次转存后,zip 的 central directory(中心目录)经常出现偏移。直接双击解压时,Windows 自带工具会静默跳过坏文件,然后你拿着缺失一半的图片集去训练,跑了两天发现某个类别样本数少得离谱,还以为是数据本身长这样,实际只是解压丢了文件。
常见做法是先用命令行工具做一次完整性和 CRC 校验,而不是依赖 GUI 工具。“我一般会”先看 zip 的文件总数和压缩包体积是否匹配,再抽取几个关键目录比对结构。这一步看起来多余,但在疲劳驾驶数据集上,它直接影响后续标注文件与图片的配对率——很多数据集解压后图片是 JPG、标注是 XML,如果 XML 丢了 30%,训练出来的模型会莫名其妙把背景当成人脸。
2.2 用 Python 写一个解压前体检脚本
这里给出一个不依赖第三方库的体检脚本,它可以列出 zip 内文件清单、识别伪加密、并自动检测解压后的文件完整性。
import zipfile import os import sys from collections import Counter ZIP_PATH = "疲劳驾驶监测数据集.zip" EXTRACT_DIR = "./extracted" def inspect_zip(zip_path): with zipfile.ZipFile(zip_path, "r") as zf: infos = zf.infolist() print(f"总文件数: {len(infos)}") # 统计扩展名分布,快速判断数据集构成 ext_counter = Counter() pseudo_encrypted = [] for info in infos: # 伪加密的标志:flag_bits 的 bit0 为 1,但实际数据未被加密 if info.flag_bits & 0x1: # 尝试读取该文件,如果能正常读出内容,说明是伪加密 try: zf.read(info.filename) pseudo_encrypted.append(info.filename) except RuntimeError: pass # 真加密,读取会抛错 ext = os.path.splitext(info.filename)[1].lower() ext_counter[ext] += 1 print(f"扩展名分布: {dict(ext_counter)}") if pseudo_encrypted: print(f"伪加密文件 {len(pseudo_encrypted)} 个,示例: {pseudo_encrypted[:3]}") # 检查是否有目录分隔符异常(跨目录攻击或路径穿越) for info in infos[:20]: if ".." in info.filename: print(f"警告: 存在路径穿越风险: {info.filename}") break def extract_and_verify(zip_path, extract_dir): """解压并二次校验:解压后文件大小与 zip 内记录对比""" os.makedirs(extract_dir, exist_ok=True) with zipfile.ZipFile(zip_path, "r") as zf: # 只解压非目录文件,忽略空目录 members = [m for m in zf.infolist() if not m.is_dir()] for member in members: # 防止路径穿越 safe_path = os.path.join(extract_dir, os.path.basename(member.filename)) with zf.open(member) as src, open(safe_path, "wb") as dst: dst.write(src.read()) print(f"解压完成: {len(members)} 个文件 -> {extract_dir}") if __name__ == "__main__": inspect_zip(ZIP_PATH) # 确认没问题后再解压 # extract_and_verify(ZIP_PATH, EXTRACT_DIR)代码逻辑说明:inspect_zip做了三件事——统计文件总数和扩展名分布、检测伪加密、检查路径穿越。伪加密的检测原理是读取flag_bits的 bit0,如果为 1 但文件内容仍能正常读取,说明只是标记了加密位而不是真正加密。这类 zip 在 Windows 上双击会要求输入密码,但用 Pythonzipfile库可以直接绕过。extract_and_verify里用os.path.basename做了一层防路径穿越处理,尽量避免在解压未知来源数据集时把文件写进其他目录。
参数调整建议:如果数据集动辄几十 GB,extract_and_verify的逐文件流式写入会慢。可以把zf.open(member)换成zf.extract(member, extract_dir),牺牲一点安全性换取速度;但如果压缩包来自不可信来源,建议保持上面的写法,不要跳过路径检查。
2.3 目录体检:标注与图片怎么配对是关键
解压完成后,下一步是确认图片和标注文件的对应关系。疲劳驾驶监测数据集的常见目录结构有两种:一种是images/和annotations/平级分开存放,另一种是每段视频一个文件夹,里面图片和标注混在一起。你需要写一个脚本统计“有图片没标注”和“有标注没图片”的文件数,这个比例直接决定了你能不能直接开训。
# 统计有图无标注的文件 find extracted/images -name "*.jpg" | sed 's/\.jpg$//' | sort > /tmp/img_list.txt find extracted/annotations -name "*.xml" | sed 's/\.xml$//' | sort > /tmp/ann_list.txt comm -23 /tmp/img_list.txt /tmp/ann_list.txt | head -20这个脚本的思路是用comm命令做两个文件列表的差集。sed 's/\.jpg$//'的作用是去掉扩展名,让图片和标注文件能按主文件名对齐。“我一般会”把head -20换成wc -l先看总数,如果超过 5% 的图片没有对应标注,就要考虑是不是解压丢失或文件名编码转换出了问题——常见的是 GBK 编码的文件名在 Linux 下解压变成乱码,图片和标注匹配不上。
3. 从 VOC/COCO 标注到 YOLO 格式:转换脚本与边界坑
3.1 疲劳驾驶数据集常用的三种标注格式
手里拿到的 zip 数据集,标注格式千差万别。最常见的三种:VOC 格式(每张图一个 XML 文件,存放object的name和bndbox坐标)、COCO 格式(一个大的 JSON 文件,用annotations数组存放bbox和category_id)、以及 YOLO 格式(每张图一个 TXT,每行是class x_center y_center width height,全部归一化到 0~1)。疲劳驾驶监测数据集里,VOC 和 COCO 占多数,直接能用的 YOLO 格式反而少——因为很多标注团队用的是 LabelImg(输出 VOC)或 CVAT(可以导出 COCO),而 YOLO 格式通常是使用者自己转换的。
转换时最容易出错的是坐标系的换算。VOC 的bndbox是绝对像素坐标,YOLO 需要归一化;COCO 的bbox是左上角坐标加宽高,YOLO 需要中心点坐标。如果图宽高读错或者除以了 0,训练时 loss 直接 NaN。
3.2 VOC XML 转 YOLO TXT 的完整脚本
import os import xml.etree.ElementTree as ET from pathlib import Path # 配置区 VOC_DIR = "extracted/annotations" # VOC XML 所在目录 IMG_DIR = "extracted/images" # 图片所在目录 OUT_DIR = "extracted/labels_yolo" # YOLO TXT 输出目录 CLASSES = ["open_eye", "closed_eye", "yawn", "normal"] # 按你的数据集类别调整 os.makedirs(OUT_DIR, exist_ok=True) def convert_voc_to_yolo(xml_path, img_width, img_height): tree = ET.parse(xml_path) root = tree.getroot() img_w = float(root.findtext("size/width")) # 优先用 XML 里记录的尺寸 img_h = float(root.findtext("size/height")) if not img_w or not img_h: img_w, img_h = img_width, img_height # 兜底:用实际图片尺寸 lines = [] for obj in root.findall("object"): name = obj.findtext("name") if name not in CLASSES: print(f"警告: 未映射类别 {name} in {xml_path}, 跳过该框") continue cls_id = CLASSES.index(name) bndbox = obj.find("bndbox") x_min = float(bndbox.findtext("xmin")) y_min = float(bndbox.findtext("ymin")) x_max = float(bndbox.findtext("xmax")) y_max = float(bndbox.findtext("ymax")) # 边界裁剪:防止标注框超出图像边界导致训练报错 x_min = max(0, x_min); y_min = max(0, y_min) x_max = min(img_w, x_max); y_max = min(img_h, y_max) # 过滤掉宽或高为 0 的无效框 if x_max <= x_min or y_max <= y_min: print(f"警告: 无效框 {x_min},{y_min},{x_max},{y_max} in {xml_path}") continue x_center = (x_min + x_max) / 2.0 / img_w y_center = (y_min + y_max) / 2.0 / img_h w = (x_max - x_min) / img_w h = (y_max - y_min) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") return lines def get_img_size_from_file(img_path): """兜底函数:用 PIL 读实际图片尺寸""" from PIL import Image with Image.open(img_path) as img: return img.width, img.height # 批量转换主流程 for xml_file in Path(VOC_DIR).glob("*.xml"): img_name = xml_file.stem + ".jpg" img_path = os.path.join(IMG_DIR, img_name) if not os.path.exists(img_path): img_path = img_path.replace(".jpg", ".png") # 有些数据集是 png if not os.path.exists(img_path): print(f"跳过: 找不到图片 {img_name}") continue img_w, img_h = get_img_size_from_file(img_path) try: lines = convert_voc_to_yolo(str(xml_file), img_w, img_h) except Exception as e: print(f"转换失败 {xml_file}: {e}") continue if not lines: print(f"警告: {xml_file} 无有效标注, 输出空文件") out_path = os.path.join(OUT_DIR, xml_file.stem + ".txt") with open(out_path, "w") as f: f.write("\n".join(lines)) print("转换完成。请人工抽查 5-10 个文件核对坐标。")代码说明:转换逻辑的核心在convert_voc_to_yolo函数里。先尝试从 XML 里读取size/width和size/height,但很多标注工具写错或者漏写这个字段,所以加了兜底逻辑——用get_img_size_from_file从实际图片读取宽高。x_center的计算是(x_min + x_max) / 2.0 / img_w,这里必须用浮点数除法,Python 3 默认没问题,但如果你在 Python 2 环境,整数相除会直接把坐标变成 0。边界裁剪那两行x_min = max(0, x_min)能有效避免标注框超出图像边界导致 YOLO 训练时 loss 异常放大。
参数调整建议:CLASSES列表的顺序就是最终模型的类别 ID 映射,一旦开始训练就不要随便改顺序,否则已保存的权重会全部错位。另外如果你的数据集里有hard_hat、vest这些非疲劳驾驶类别,要么在转换时过滤掉,要么保留并在CLASSES里注册,否则 YOLO 训练会因为类别数不匹配直接报错。
3.3 数据集切分:按视频而不是按帧随机切
疲劳驾驶数据集最常见的数据泄漏来源是切分方式错误。如果数据来自多段驾驶视频,相邻两帧几乎一模一样,按帧随机切分会导致验证集里出现训练集的“近亲”帧——模型其实在背相似背景而不是学疲劳特征。正确做法是按视频或按时间段切分。
import os import random from pathlib import Path IMG_DIR = "extracted/images" LABEL_DIR = "extracted/labels_yolo" TRAIN_RATIO = 0.8 # 假设文件名前缀是视频 ID,例如 video01_frame00123.jpg video_ids = set() for img_path in Path(IMG_DIR).glob("*.jpg"): # 用分隔符提取视频 ID,按实际文件名规则调整 video_id = img_path.stem.split("_")[0] video_ids.add(video_id) video_ids = sorted(video_ids) random.seed(42) random.shuffle(video_ids) split_idx = int(len(video_ids) * TRAIN_RATIO) train_videos = video_ids[:split_idx] val_videos = video_ids[split_idx:] def write_list(videos, output_path): with open(output_path, "w") as f: for vid in videos: for img_path in Path(IMG_DIR).glob(f"{vid}_*.jpg"): label_path = os.path.join(LABEL_DIR, img_path.stem + ".txt") if os.path.exists(label_path): f.write(str(img_path) + "\n") write_list(train_videos, "train.txt") write_list(val_videos, "val.txt") print(f"训练视频数: {len(train_videos)}, 验证视频数: {len(val_videos)}")这个脚本的思路是先用文件名前缀提取视频 ID,再在视频级别做 8:2 划分。random.seed(42)保证每次运行结果一致。glob(f"{vid}_*.jpg")把同一个视频的所有帧归入同一个集合,避免相似帧横跨训练集和验证集。如果你的文件名不是这种格式,可以自己改提取规则——关键是切分单位是“视频片段”而不是“单帧”。
4. 用 YOLOv8 跑通疲劳检测训练:data.yaml 与四组关键参数
4.1 从零写一个 data.yaml
训练之前需要把数据集描述文件配好。YOLOv8 的data.yaml是训练入口,它告诉训练器去哪里找图片列表、类别名是什么。很多新手栽在路径上:用了相对路径但工作目录不对,或者图片列表里写的是 Windows 路径而在 Linux 服务器上训练。
# data.yaml path: ./extracted # 数据集根目录,相对路径基于当前工作目录 train: train.txt # 训练图片路径列表(每行一张图片绝对或相对路径) val: val.txt # 验证图片路径列表 # test: test.txt # 可选,如果有测试集取消注释 nc: 4 # 类别数量,必须与 CLASSES 列表长度一致 names: 0: open_eye 1: closed_eye 2: yawn 3: normal参数说明:path字段是根目录,train和val可以是相对path的路径,也可以是绝对路径。nc必须和names里的键数量一致,写少了训练时类别索引越界,写多了会有大量空类别拉低 mAP。names的顺序必须和第 3 章的CLASSES顺序完全一致,因为每个 TXT 标注文件里的第一个数字就是类别 ID,而 YOLOv8 会按names索引顺序映射类别名。
4.2 训练命令与四个必调参数
yolo detect train \ data=./extracted/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ patience=15 \ workers=4 \ device=0命令拆解:model=yolov8n.pt选择 nano 版本作为预训练权重。疲劳驾驶检测的目标是眼睛和嘴部区域,属于小目标,建议优先用yolov8n或yolov8s,先用小模型跑通流程再换大模型;一上来就用yolov8x很容易 OOM,而且因为数据量不够导致严重的过拟合。四个必调参数:
| 参数 | 作用 | 疲劳驾驶场景建议 |
|---|---|---|
| epochs | 训练轮数 | 100 起步,数据量大可以降到 50,早停会帮你判断 |
| imgsz | 输入分辨率 | 640 是保底,眼睛区域太小建议 960;低配 GPU 别超过 1280 |
| batch | 批大小 | 显存允许尽量大,16 起步;小于 8 会导致 BN 层统计不稳定 |
| patience | 早停耐心轮数 | 15 是一个不算保守也不算激进的值,过小会在 mAP 还没到平台期就停 |
训练时的玄学在于imgsz和batch的关系。把imgsz从 640 提到 960,显存占用会变成原来的 2.25 倍,如果 OOM,优先减batch而不是降imgsz——在小目标检测任务里,分辨率对精度的贡献比 batch 大。device=0指定第一块 GPU,如果你有多个卡可以写device=0,1,YOLOv8 会自动做 DDP 训练。
4.3 训练过程中实时看什么:不看 loss 看 PR 曲线
训练日志里 loss 曲线在疲劳驾驶数据集上往往没有参考价值。因为类别不平衡严重,正常驾驶状态(normal)样本可能占总数的 60% 以上,模型只要学会输出normal就能拿到很低的 loss,但这对实际检测完全没用。你需要盯的是每个类别的 Precision 和 Recall。用yolo detect val单独评估验证集,看results.csv里的metrics/precision(B)和metrics/recall(B):
# 训练结束后用最佳权重评估 yolo detect val \ data=./extracted/data.yaml \ model=runs/detect/train/weights/best.pt \ imgsz=640best.pt是训练过程中验证集 mAP 最高的权重,last.pt是最后一轮权重。疲劳驾驶场景下,如果closed_eye的 Recall 低于 0.7,说明漏检太多——这在驾驶安全场景里是致命的。“我一般会”先按类别看 Recall,而不是只看整体的 mAP,因为 mAP 会被normal类的高基数掩盖。
5. 疲劳驾驶数据集训练避坑:把血泪经验固化成检查清单
5.1 zip 伪加密导致无法解压
现象:双击 zip 包弹出密码输入框,数据集发布者没给密码,或者给的密码不对。
原因:部分数据集的 zip 在打包时被工具错误标记了加密位,形成伪加密。文件本身没有真正加密,但 Windows 资源管理器和解压软件会按加密文件处理。很多网盘转存的数据集都有这个问题,转存过程篡改了 zip 的 flag bits。
解决:用第 2 节的inspect_zip函数检测伪加密文件,确认后直接用 Pythonzipfile库解压,它只按 flag 判断是否尝试解密,遇到伪加密可以正常读取内容。更快的方案是这个命令组合:
# 用 7z 绕过伪加密(如果 7z 能识别伪加密) 7z x 疲劳驾驶监测数据集.zip -oextracted -y # 如果 7z 也要求密码,用 zip -F 修复 zip -F 疲劳驾驶监测数据集.zip --out 修复后的.zip注意:zip -F是修复损坏 zip 的常用命令,它重建 central directory,但只适用于 zip 结构本身没被破坏的情况。如果修复后仍然失败,可以用zip -FF做深度修复,耗时更长但成功率更高。
5.2 中文文件名乱码导致图片和标注匹配失败
现象:解压后图片文件名是ç¡ç å¾ç_001.jpg这种乱码,或者图片能打开但 txt 标注文件找不到对应关系。
原因:zip 内文件名编码是 GBK,而 Linux/macOS 默认按 UTF-8 解压。Pythonzipfile库在解压时读取的是原始字节,不转换编码。Windows 下解压没问题的数据集到 Linux 上就翻车。
解决:解压时手动指定编码转换。
import zipfile import os ZIP_PATH = "疲劳驾驶监测数据集.zip" EXTRACT_DIR = "extracted" with zipfile.ZipFile(ZIP_PATH, "r") as zf: for info in zf.infolist(): # 尝试 GBK 解码,失败则保持原名 try: new_name = info.filename.encode("cp437").decode("gbk") except UnicodeDecodeError: new_name = info.filename # 已经是 UTF-8,不用转 target = os.path.join(EXTRACT_DIR, new_name) os.makedirs(os.path.dirname(target), exist_ok=True) with zf.open(info) as src, open(target, "wb") as dst: dst.write(src.read())代码逻辑:zipfile 在生成info.filename时,如果原始编码不是 UTF-8,会用 CP437 编码做解码兜底。所以encode("cp437")能找回原始字节流,再按 GBK 解码成正确的中文文件名。如果文件名是 UTF-8 编码的,decode("gbk")会报错,此时保持原名即可。
5.3 闭眼目标太小导致学习不到特征
现象:训练 loss 正常下降,但closed_eye类别的 AP 几乎为 0。查看预测结果,发现模型输出的框总是偏大或偏小,没有一个小框能准确定位眼睛区域。
原因:眼睛在 640×640 输入下可能只有 20×10 像素,而 YOLOv8 的检测头在不同尺度下负责不同大小的目标。小目标主要由 P3 层(最大特征图)负责,如果该层的 anchor 或标签分配策略没有适配小目标,模型很难学到闭眼和睁眼的区别。
解决:两个方向,第一个是用更高输入分辨率训练,第二个是开启 YOLOv8 的scale数据增强。
yolo detect train \ data=./extracted/data.yaml \ model=yolov8s.pt \ imgsz=960 \ batch=8 \ epochs=100 \ scale=0.3逻辑说明:imgsz=960把眼睛从 20×10 放大到 30×15,特征更明显。scale=0.3控制随机缩放的幅度,默认是 0.9,改成 0.3 能减少训练时目标被缩得过于模糊的情况——疲劳驾驶数据集很多是行车记录仪画面,本身就有运动模糊,缩放太大反而破坏细节。如果显存撑不住 960,另一个思路是用SAHI(切片辅助推理),但那是部署阶段的方案,训练阶段不适用。
5.4 类别严重不均衡:normal 类淹没了疲劳特征
现象:训练集里normal占 70%,其他三类加起来只有 30%。训练出来的模型对normal的 F1-score 很高,但部署到真实场景时经常把打哈欠漏检掉。
原因:默认的 BCEWithLogits 损失函数对类别频率敏感,多数类贡献的梯度远大于少数类,模型学会“无脑输出 normal”是最省力的优化路径。
解决:按类别做重采样或者修改损失权重。YOLOv8 自带class_weights支持,但需要手动计算权重文件。常见做法是统计每个类别的样本数,然后给少数类更高的损失权重;另一个实用方案是把视频中相邻且标注相同的帧做降采样——很多数据集里连续 30 帧都是同一个闭眼动作,抽掉 2/3 不仅能平衡类别,还能减少训练时间。
5.5 验证集和训练集相似帧泄漏
现象:训练 mAP 高达 0.95,验证集 mAP 也有 0.93,但拿到一段新的行车记录仪视频上去测,效果惨不忍睹。
原因:切分时按帧随机分,同一个视频的相邻帧同时出现在训练集和验证集里。模型相当于“见过”验证集的背景和人物姿态,测出来是背题成绩,不是真实泛化水平。
解决:用第 3 节的按视频切分脚本重新划分。还有一个更严格的做法——按驾驶时间段切分,比如前 20 分钟视频全给训练,后 5 分钟全给验证,这样可以进一步排除光照、路段变化带来的数据泄漏。
提示:不要只看
results.png里的 mAP 曲线。把训练完的权重拿到一段完全不相关(不同司机、不同车型)的视频上做推理,看到的效果才是真实水平。
6. 夜间与戴眼镜场景下的进阶验证:关闭数据泄漏后的真实测试
疲劳驾驶监测最容易翻车的场景是夜间低照度和戴眼镜。行车记录仪在夜间的画面噪声极大,眼睛区域几乎是一个黑块,人眼都分不清睁闭,模型大概率会把所有黑色区域都预测成closed_eye;戴眼镜时镜片反光又容易让模型把高光区域误判成normal。这两个问题不是靠调参能彻底解决的,但有一个技巧能显著提升测试的可靠性:把验证集按光照条件和是否戴眼镜分成子集,分别计算 mAP,而不是只看整体指标。
具体做法是在val.txt里按文件名前缀分组。假设发布者在文件名里标注过night_或day_,你可以直接筛出来分别评估:
grep "night_" val.txt > val_night.txt grep "day_" val.txt > val_day.txt yolo detect val data=./extracted/data.yaml model=best.pt val=val_night.txt yolo detect val data=./extracted/data.yaml model=best.pt val=val_day.txt这个输出的对比反映了模型的真实短板:如果val_night.txt的 mAP 只有 0.4 而白天有 0.85,说明问题出在图像质量而非模型结构。此时再去考虑增加夜间图像增强(随机亮度抖动、高斯噪声)或者更换输入分辨率才有意义;如果两个子集分数差不多,说明模型的瓶颈在特征提取层,优先换大模型或做知识蒸馏。我个人的习惯是每轮训练后都会跑一次这种显式测试,把夜间 mAP 作为发布模型的硬性门槛——疲劳驾驶场景里,用户不关心你白天跑多好,只关心晚上能不能用。希望帮到你。
本文还有配套的精品资源,点击获取