尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

VOC格式疲劳驾驶数据集转YOLO训练实战:4类别4362张全流程避坑

发布时间:2026/9/30 1:39:37

资讯中心
01
ARTICLE

VOC格式疲劳驾驶数据集转YOLO训练实战:4类别4362张全流程避坑

VOC格式疲劳驾驶数据集转YOLO训练实战:4类别4362张全流程避坑
简介面向疲劳驾驶检测研究或计算机视觉目标检测学习者这套VOC格式数据集专门用于训练识别闭眼、睁眼、闭嘴、张嘴四类状态以判断驾驶疲劳程度。资源包约368.57MB含4362张JPG图片、4362个对应XML标注文件及1份使用说明图片均以矩形框精确标注可直接用于YOLO、Faster R-CNN等常见检测模型的数据准备。四类标签分别为闭眼、闭嘴、睁眼、张嘴其中睁眼框数最多、张嘴框数较少样本覆盖真实驾驶场景差异便于训练疲劳状态识别模型虽不保证模型最终精度但标注准确合理可支撑算法验证。目前已有2800余人学习下载适合疲劳驾驶检测、目标检测模型训练及自动驾驶安全研究等场景使用。1. 疲劳驾驶数据集为什么选VOC格式一份4类别4362张的数据能干多少事VOC格式的疲劳驾驶数据集4个类别、4362张图拿到手第一反应往往是直接开训。但我见过太多项目死在这条捷径上——VOC格式的数据如果不先做格式体检和转换喂给YOLO系模型基本是盲人摸象要么类别标签对不上要么坐标框整体漂移要么训练集验证集划分完类别分布彻底失衡。这篇笔记就围绕这份「4类别4362张」的疲劳驾驶VOC数据集讲清楚它内部长什么样、怎么安全转成YOLO训练格式、哪些参数最值得调、哪些坑最好提前绕开。适合手里已经有或准备购入疲劳驾驶标注数据、想直接训练检测模型的工程师也适合第一次碰VOC格式的入门选手。2. 拿到手先拆包装VOC目录结构与xml标注里藏着什么一份标准的VOC格式数据集目录结构从来不是乱来的。拿到手先看这四件套是否齐全JPEGImages放原始图片、Annotations放对应的xml标注、ImageSets/Main放训练验证划分清单偶尔还会带一个JPEGImages的缩略版SegmentationClass之类的东西——这份疲劳驾驶数据如果没有做过分割任务后两个目录不存在是正常的别慌。我一般拿到手的第一件事不是看图片而是随机抽三个xml打开看字段。因为VOC标注文件是xml格式里面每个字段都决定了后面转换脚本怎么写。一个典型的标注文件长这样annotation folderJPEGImages/folder filename000001.jpg/filename source databasefatigue_driving/database /source size width1280/width height720/height depth3/depth /size segmented0/segmented object nameyawn/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin320/xmin ymin210/ymin xmax486/xmax ymax342/ymax /bndbox /object /annotation这里最关键的是三组字段filename决定图片名size决定图像的宽高object里的name加bndbox决定类别和坐标。其中bndbox存的是绝对像素坐标左上角和右下角后面转YOLO格式时要把它们换算成归一化的中心点加宽高。difficult字段表示这张标注是不是难以辨认的样本有的转换脚本直接跳过有的保留——具体怎么处理我放到第4章讲。2.1 一张xml标注到底写了什么核心字段逐个拆如果你不想手动一个个翻xml可以直接上脚本来解析顺便把整个数据集的关键信息批量摸一遍import os import xml.etree.ElementTree as ET anno_dir Annotations xml_files [f for f in os.listdir(anno_dir) if f.endswith(.xml)] for xml_name in xml_files[:5]: tree ET.parse(os.path.join(anno_dir, xml_name)) root tree.getroot() filename root.findtext(filename) size root.find(size) w int(size.findtext(width)) h int(size.findtext(height)) print(ffile: {filename}, size: {w}x{h}) for obj in root.findall(object): name obj.findtext(name) bndbox obj.find(bndbox) xmin float(bndbox.findtext(xmin)) ymin float(bndbox.findtext(ymin)) xmax float(bndbox.findtext(xmax)) ymax float(bndbox.findtext(ymax)) print(f class: {name}, bbox: ({xmin:.0f}, {ymin:.0f}) - ({xmax:.0f}, {ymax:.0f}))代码逻辑很直白先用ElementTree解析xml取filename和size再遍历所有object节点把类别名和四个坐标值取出来打印。注意坐标转换时我用float()再接format输出不用int()直接截断——因为有些标注工具会存类似320.5这样的带小数坐标直接int()会丢掉精度如果恰好落在边界上框会偏1像素。这个脚本跑完你能确认三件事图片是否都按000001.jpg这种序号命名、尺寸是否统一、类别名是否规范。这三件事里任何一件出问题后面转换都会踩坑。2.2 4个类别怎么定从标注口径到类别失衡标题写的是4类别但没写具体是哪四类。从疲劳驾驶检测的主流业务看常见的四类设计是打哈欠、闭眼、打电话、分心视线偏离或头部偏移。有的数据集会用「正常驾驶」当背景类有的把吸烟单独拆出来这取决于采集场景。我没有看过这份数据的标注规范但按行业惯例大概率是下面这种映射关系标签名含义标注范围说明yawn打哈欠嘴部区域要求嘴张开有明显特征eye_close闭眼眼部区域上下眼睑接触phone打电话手机贴近耳侧含手部distraction分心头部偏转或视线离开前方这里最值得留意的是「闭眼」和「打哈欠」的边界。打哈欠的时候人大概率也闭眼如果标注规范里没写清楚「打哈欠时眼部要不要单独标」你拿到的数据里很可能出现同一张图有的标了eye_close有的没标。这种标注口径不一致的问题是疲劳驾驶数据集最常见的隐形炸弹后面第5章我会专门讲怎么排查。另外4个类别的数量通常不会均匀。闭眼和打哈欠在连续驾驶监控里出现频率高打电话和分心相对少。拿到数据后第一件事就是统计类别频次如果某个类别只占5%以下训练时就要考虑欠采样或调loss权重。3. 训练前先做数据体检把4362张的底细摸清楚数据拿到手不等于能直接用。4362张图、4362个xml里面可能混着空标注、坏图、类别名写错的情况。先做一次全量体检半小时能省下后面三天的调试时间。3.1 用脚本统计类别分布与空标注数据有没有偏import os import xml.etree.ElementTree as ET from collections import Counter anno_dir Annotations class_counter Counter() empty_files [] total_boxes 0 for xml_name in os.listdir(anno_dir): if not xml_name.endswith(.xml): continue tree ET.parse(os.path.join(anno_dir, xml_name)) root tree.getroot() objs root.findall(object) if not objs: empty_files.append(xml_name) for obj in objs: name obj.findtext(name).strip().lower() class_counter[name] 1 total_boxes 1 print(total xml files:, len(os.listdir(anno_dir))) print(total boxes:, total_boxes) print(empty annotation files:, len(empty_files)) for cls, cnt in class_counter.most_common(): print(f{cls}: {cnt} ({cnt / total_boxes * 100:.1f}%))这个脚本要看的核心是两个输出。第一个是class_counter的分布如果某个类别的占比明显偏低比如phone只有3%训练出来的模型对该类别的召回率会很难看后面要考虑做类别重加权。第二个是empty_files的数量一份VOC数据里有几十个空xml并不罕见——采集时画面里确实没有目标标注员就留空了。这些xml对应的图片训练时可以直接丢弃也可以作为负样本背景图喂进去取决于你的训练管线是否支持负样本。另一个容易忽视的点是name字段的大小写。我在这段代码里做了.strip().lower()是因为真实数据里经常出现Yawn和yawn混写的情况。不做归一化的话转换脚本会把它们当成两个类别模型输出层就多出一个幽灵类别。3.2 图片尺寸与文件完整性检查别让一张坏图毁掉一次训练坐标转换依赖图片的真实宽高而不是xml里size字段写的宽高。size字段如果和实际图片对不上归一化坐标全会错位。所以体检时要用OpenCV把每张图真正读一遍以读取结果为准import cv2 import os img_dir JPEGImages bad_images [] size_set set() for img_name in os.listdir(img_dir): path os.path.join(img_dir, img_name) img cv2.imread(path) if img is None: bad_images.append(img_name) continue h, w img.shape[:2] size_set.add((w, h)) print(unreadable images:, len(bad_images)) print(distinct image sizes:, len(size_set)) for s in sorted(size_set): print(s)这里cv2.imread读不出来的图基本只有两种可能文件损坏或者后缀名与实际编码不符。常见做法是直接把这类图片从训练集里剔除顺便删掉对应的xml而不是去修图——修图的时间和收益不成正比。如果size_set里出现多种分辨率比如大部分是1280x720但有一批是1920x1080转换脚本里如果用xml的size做归一化这批大图的框位置会整体偏移。所以转换时无论如何都要以实际读图的高宽为准这是我在第4章代码里写死的一件事。还有一个小检查值得做对比JPEGImages和Annotations的文件名集合是否一致。用集合差集一跑就知道有多少xml没图、多少图没xml避免转换脚本在中间报FileNotFoundError。4. 把VOC转成YOLO格式转换脚本与四个必调参数VOC格式本身不能直接喂给YOLO系列训练yolov5、yolov8训练自己的数据集时用的都是txt标注每行一个目标格式是class_id center_x center_y width height四个坐标值全部归一化到0~1。这一步是整条链路里最容易出岔子的环节也是值得你反复核对的环节。4.1 VOC转YOLO的坐标换算从xml到txt的一行命令先上完整转换脚本再逐段讲参数。这份代码的输入是Annotations目录和JPEGImages目录输出的labels目录和images目录结构完全兼容YOLO系列训练框架import os import cv2 import xml.etree.ElementTree as ET # 类别映射表必须与你后续训练时的data.yaml保持一致 class_map { yawn: 0, eye_close: 1, phone: 2, distraction: 3, } anno_dir Annotations img_dir JPEGImages label_out labels os.makedirs(label_out, exist_okTrue) def convert_one_xml(xml_path, img_path, label_path): tree ET.parse(xml_path) root tree.getroot() # 关键点以实际读到的图片尺寸为准不信任xml里的size字段 img cv2.imread(img_path) if img is None: print(fskip unreadable image: {img_path}) return h, w img.shape[:2] lines [] for obj in root.findall(object): name obj.findtext(name).strip().lower() if name not in class_map: print(fskip unknown class: {name} in {xml_path}) continue cls_id class_map[name] bndbox obj.find(bndbox) xmin float(bndbox.findtext(xmin)) ymin float(bndbox.findtext(ymin)) xmax float(bndbox.findtext(xmax)) ymax float(bndbox.findtext(ymax)) # 越界裁剪坐标拉到图像边界内 xmin max(0.0, min(xmin, w - 1)) xmax max(0.0, min(xmax, w - 1)) ymin max(0.0, min(ymin, h - 1)) ymax max(0.0, min(ymax, h - 1)) if xmax xmin or ymax ymin: print(fskip invalid bbox: {xml_path} {name}) continue # VOC是绝对坐标(xmin,ymin,xmax,ymax)YOLO需要归一化的中心点宽高 cx (xmin xmax) / 2.0 / w cy (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) if lines: with open(label_path, w) as f: f.write(\n.join(lines)) else: # 没有有效目标的图片生成空标签文件 open(label_path, w).close() for xml_name in os.listdir(anno_dir): if not xml_name.endswith(.xml): continue stem xml_name[:-4] xml_path os.path.join(anno_dir, xml_name) img_path os.path.join(img_dir, stem .jpg) label_path os.path.join(label_out, stem .txt) convert_one_xml(xml_path, img_path, label_path)这段代码里有几个参数和设计决策值得展开说。第一个是class_map的顺序。这个顺序一旦确定后续训练的data.yaml必须一字不差地对齐否则类别标签全部错位。换句话讲yawn在第0位就是第0位训练时不能因为某个类别的框少就调换顺序——那会让已经转换好的所有txt全部作废。第二个是越界裁剪。VOC标注里偶尔会出现xmax比图片宽度还大的情况多半是标注工具拖拽时超出了画布边缘。如果不做裁剪归一化后width会大于1YOLO在解码时会有边界框超出图像的警告某些版本直接忽略这部分损失训练效果莫名缩水。裁剪到w - 1而不是w是因为图像像素坐标从0开始第w个像素实际上已经越界了。第三个是空标签文件的处理。有些xml里所有目标都是unknown class或无效框转换后没有任何有效行。我选择生成一个空txt而不是不生成文件——因为YOLO训练时会在images目录下找同名txt找不到文件会报错或跳过生成空文件则能保持一一对应。这个习惯能避免不少莫名其妙的训练中断。4.2 训练集/验证集划分按比例还是按场景转换完标签之后下一步是生成训练和验证清单。常见的做法是按9:1或8:2随机划分但随机划分对疲劳驾驶场景有个隐患同一段视频连续帧可能同时出现在训练集和验证集里导致验证mAP虚高部署时一换场景就崩。import os import random random.seed(42) img_files [f for f in os.listdir(JPEGImages) if f.endswith(.jpg)] img_files.sort() random.shuffle(img_files) val_ratio 0.1 val_count int(len(img_files) * val_ratio) val_files img_files[:val_count] train_files img_files[val_count:] with open(train.txt, w) as f: for name in train_files: f.write(fimages/{name}\n) with open(val.txt, w) as f: for name in val_files: f.write(fimages/{name}\n) print(ftrain: {len(train_files)}, val: {len(val_files)})这里有两个点需要按你的实际情况调整。第一random.seed(42)固定随机种子保证每次运行划分结果一致——这个习惯在复现实验结果时极其重要。第二如果这份数据的图片来自多个采集时段或多个司机更好的做法是按视频片段或司机ID划分而不是按图片随机划分。但VOC格式的xml里未必有这层元数据标题也没标注这一点所以无法直接按场景切分时随机划分是退而求其次的可靠方案。划分完以后要跑一个核对脚本确认验证集里4个类别都有样本且每个类别的占比和全量数据大致接近。如果验证集里恰好没有某个类别模型在该类别上的mAP就是0你会误判为「模型学废了」实际是划分运气太差。4.3 转换后的抽查方法转换器写完了怎么确认没转错转换完不能直接开训至少做一次可视化抽查。把txt标注画回图片上肉眼比对一下框的位置和类别这一步能发现绝大多数转换逻辑错误。import cv2 def draw_yolo_label(image_path, label_path): img cv2.imread(image_path) h, w img.shape[:2] with open(label_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id, cx, cy, bw, bh parts cx, cy, bw, bh float(cx), float(cy), float(bw), float(bh) xmin int((cx - bw / 2) * w) ymin int((cy - bh / 2) * h) xmax int((cx bw / 2) * w) ymax int((cy bh / 2) * h) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 0, 255), 2) cv2.putText(img, str(cls_id), (xmin, max(0, ymin - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(check_ os.path.basename(image_path), img) # 抽查训练集和验证集各5张 for f in train_files[:5] val_files[:5]: stem f.split(/)[-1][:-4] draw_yolo_label(fimages/{f}, flabels/{stem}.txt)抽查时重点看两个地方一是框是否贴合目标特别是eye_close这种小目标——如果框明显偏大或偏小说明坐标换算有系统误差需要回到第4.1节的脚本检查归一化分母用的是不是真实图片宽高二是类别ID是否对得上比如yawn的框上显示的应该是0而不是3这种错位基本就是class_map顺序不一致导致的。另外建议抽查夜里或逆光的图片。疲劳驾驶数据里暗光场景占比往往不低暗光下标注框的质量通常比白天差如果抽查只看白天的图容易漏掉一批坏标注。5. 转换与训练避坑5条踩过的坑帮你省掉三天调试时间这章写的是我在实际项目里踩过或排查过的具体问题。每条按「现象→原因→解决」的顺序写你可以直接对照自己的情况排查。5.1 xml里的size跟真实图片不一致框全部漂移现象训练时loss下降正常验证集mAP也不算低但把模型部署到实际摄像头画面里检测框的位置明显偏左上或右下尤其在某个分辨率段特别严重。原因标注工具或脚本处理图片时改了分辨率但xml里size字段没同步更新。转换时如果用的是xml里的宽高做归一化真实图片实际尺寸不同逆归一化后框自然偏移。解决转换脚本里严禁用root.find(size)里的宽高必须用cv2.imread读出来的img.shape。这也是我在4.1节代码里坚持读图取尺寸的原因。已经转完的txt可以批量重新生成不用手动改。5.2 类别名大小写混杂模型多出一个幽灵类别现象训练日志里显示的类别数比4多比如出现了5类其中某个类别样本数极少且loss在该类上一直很高。原因xml里有的写Yawn有的写yawn有的写eye_Close。转换脚本直接拿原始字符串做key大小写不同被当成不同类别模型被迫多学一个几乎没样本的类。解决在转换脚本里统一做.strip().lower()并在class_map查询前先归一化。如果已经训了一半把labels目录里所有txt的类别ID重新映射一遍即可不必重新转xml。5.3 绝对路径导致跨机器失效换台机器训练直接报错现象在自己电脑上跑得好好的把数据集拷到服务器上训练一开始就报FileNotFoundError或者读图读到一堆空图。原因生成的train.txt和val.txt里写的是C:\Users\xxx\fatigue_dataset\images\000001.jpg这种带盘符的绝对路径换机器后路径前缀全变了。解决txt里统一写相对路径比如images/000001.jpg然后训练脚本的工作目录固定指向数据集根目录。更稳妥的做法是连相对路径都不写只写文件名配合一个data.yaml里的path字段指定根目录。我一般用相对路径因为在命令行调试时更直观。5.4 打哈欠和闭眼互相污染标注口径的隐形炸弹现象模型训练完eye_close的precision很高但recall很低或者yawn的框经常把眼睛也包进去。看混淆矩阵两个类互相错分率超过30%。原因人在打哈欠时大概率闭眼标注员对「同一张图里能不能同时标两个类」理解不一致。有的图只标了yawn没标eye_close有的图两个都标有的图把打哈欠的嘴部画得巨大包含整个下半脸——这类标注口径问题在疲劳驾驶数据里几乎无法完全避免。解决第一看数据标注规范里有没有定义「标注范围」比如打哈欠的框只包嘴部、闭眼的框只包眼部第二做一次针对性统计随机抽样yawn和eye_close重叠的图人工核对50张第三如果错分率实在压不下来考虑把这两个类合并成一个fatigue类别做二分类检测牺牲细粒度换稳定性。这个决策要在训练前做训练后改类别等于重做。5.5 划分后验证集类别失衡mAP骗了你现象训练完模型在验证集上某个类别mAP是0但训练时该类别loss在正常下降看起来像「模型对这个类完全没学会」。原因全量数据里该类别本来就少随机划分后又恰好全部落在训练集验证集里一个该类的gt框都没有。mAP接口对没有gt框的类别直接给0分。解决划分时按类别做分层采样保证验证集里每个类别至少有一定数量的框。具体做法是遍历所有txt标签把包含某个类别的图片单独挑出来再按比例抽到验证集其余图片再做随机划分。代码不复杂但很多人不做这一步踩坑了才回头补。6. 用这份数据训出靠谱模型一个更聪明的训练与验证习惯数据转换干净了接下来才是训练本身。4362张图对疲劳驾驶检测来说属于中等规模第一轮训练建议用yolov5s或yolov8n这类轻量模型跑通全流程而不是直接上最大模型——先确认数据链路没有问题再考虑提升精度。训练时把imgsz设为640即可这份数据的原图如果是1280x720640尺寸下闭眼这种小目标的信息损失会比较大有条件可以试试768。增强策略要针对驾驶场景做取舍。Mosaic和随机平移缩放可以开但上下翻转必须关掉——驾驶画面里道路和车身的相对位置是物理约束上下翻转会把路面翻到天上模型学到的是完全错误的空间关系。左右翻转可以开但要注意phone这个类别左手举手机翻转为右手举手机对检测本身没影响检测框不会变错可以放心翻。验证时不只看总mAP重点看混淆矩阵里eye_close和yawn的双向错分率。我之前就吃过一次亏训练时mAP刷到了0.82兴冲冲拿去路测结果把眯眼频繁判成打哈欠才发现两个类的混淆矩阵一团糟。后来养成的习惯是每次训练完第一件事拉混淆矩阵而不是先看mAP——mAP骗人混淆矩阵不骗人。顺手也看一眼每个类别的recall疲劳驾驶场景漏报比误报恶劣得多。如果你手里这份数据的类别分布严重不均衡可以试试在data.yaml里给每个类配上不同的loss权重或者用focal loss但先别急着改跑一版baseline再看混淆矩阵。我自己的习惯是先不做任何花活用干净的数据跑出baseline再根据混淆矩阵的错分方向做针对性优化——是标注口径问题就修数据是光照问题就加增强是模型容量问题才换模型。把转换脚本、class_map映射表、随机种子一起存好三个月后回来复现实验时你会感谢自己当初的这个习惯。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。