VOC数据集转Darknet格式:垃圾分类目标检测训练实战指南

发布时间:2026/10/11 20:20:20
VOC数据集转Darknet格式:垃圾分类目标检测训练实战指南 简介VOC格式垃圾分类数据集共14963张图片与对应标注覆盖44个全英文类别适合使用YOLOv3/v4/v5及Darknet框架训练目标检测模型。面向计算机视觉学习者、算法工程师及需要垃圾分类训练数据的研究者可直接用于模型训练与效果验证。压缩包内含44891个文件主要由jpg原图、xml标注和yolo格式txt标注组成另附类别名称及中英文标签对照文件整体约791.41MB标注工具为labelImg以矩形框准确框出目标类别数据划分与格式规范便于接入常见训练流程。目前已有1900人学习下载。数据集提供完整标注信息省去手工整理与格式转换时间适合快速开展垃圾分类检测实验需注意训练精度与模型权重需自行验证数据集本身仅保证标注准确合理。1. 垃圾数据集难在标注14963 张 VOC 图的价值在省掉最贵的一步上个月做生活垃圾分拣预研要训练一个能识别电池、纸盒、塑料瓶、烟头的检测模型手里只有摄像头拍回的原始帧一张标注都没有。自己标 500 张图两个人折腾快两天类别还不齐。后来拿到这份 VOC 格式垃圾数据集14963 张图全部带 XML 标注直接省掉了最耗时的阶段。这个规模喂给 YOLOv3、YOLOv4 或 Darknet 原生训练属于「够用但不土豪」的起步量级图多来自真实街道和室内场景背景噪声大比干净的商品图更适合做环卫场景模型底子。适合正在做垃圾分类识别、环卫巡检或想用高质量数据跑通 Darknet 流程的人。我会把 VOC 转 Darknet 的格式转换、训练参数和踩过的坑一步步写清楚照着做能省下不少排查时间。2. VOC 格式与 Darknet 训练链路先搞清标注文件在管什么拿到这份数据集第一件事不是急着开训而是看目录结构。PASCAL VOC 是目标检测领域流传最广的标注规范之一早年检测竞赛都基于它大量开源数据沿用这套组织方式Annotations下每张图对应一个 XMLJPEGImages放原图ImageSets/Main放划分名单。这份垃圾数据集延续的就是这套结构熟悉之后以后拿到任何 VOC 风格数据都能快速上手。XML 里需要关心的节点不多filename是图片名size里是 width 和 heightobject每个目标一个里面name是类别名bndbox是 xmin、ymin、xmax、ymax 四个角点坐标单位是像素。问题在于 Darknet 训练流程根本不读 XML它认的是每张图一个同名.txt文件。这就是为什么任何 VOC 数据到手第一步都是格式转换。2.1 VOC XML 与 YOLO TXT 的差异同样的框两种描述方式写转换代码之前先明确两边在管什么转换时才知道每一步在干嘛。维度VOC XMLYOLO TXT坐标形式xmin/ymin/xmax/ymax绝对值像素cx/cy/w/h中心点加宽高相对值类别标识字符串如 battery数字 id对应 obj.names 行号标注单位每张图一个 XML多个 object 节点每张图一个 txt一行一个目标归一化否是除以图片宽和高Darknet 之所以用归一化坐标是为了在不同输入分辨率下复用同一份标签。YOLO 的 loss 是相对坐标回归框的位置和宽高都以网格为单位预测归一化后的值不随输入尺寸缩放失效。这也是为什么 Darknet 训练时改了 width 和 height 不用重新标数据只要 cfg 里同步改就行。如果你用的是 YOLOv5 或 YOLOv8 的 ultralytics 框架标注文件换成了对应格式但 VOC 数据做底子这个思路是通用的转换目标变了而已。2.2 类别顺序是命门labels.txt 与 XML 类别名的映射Darknet 的 obj.names 是纯文本文件第一行对应 id 0第二行对应 id 1以此类推。转换脚本必须先扫描全量 XML收集所有出现过的类别名固定排序后再分配 id否则训练完的模型预测结果会全部错位——框是对的标签全是张冠李戴。我一般第一步跑类别扫描脚本把全量 XML 里的 name 去重后写出来import xml.etree.ElementTree as ET from pathlib import Path xml_dir Path(VOCdevkit/VOC2007/Annotations) classes set() for xml_file in sorted(xml_dir.glob(*.xml)): root ET.parse(xml_file).getroot() for obj in root.findall(object): name obj.findtext(name) classes.add(name.strip()) with open(labels.txt, w, encodingutf-8) as f: for i, cls in enumerate(sorted(classes)): f.write(f{i} {cls}\n) print(f共发现 {len(classes)} 个类别) for i, cls in enumerate(sorted(classes)): print(i, cls)这段代码做了两件事统计所有 XML 里的 object name按字符串排序后分配数字 id。sorted()排序是为了让两次转换结果完全一致避免文件系统遍历顺序变化导致 id 漂移。name.strip()去空格很关键有些标注工具保存时会在类别名前后留空白字符不去掉的话后面匹配类别 id 时会直接漏掉整个目标。2.3 目录结构搭建VOC 风格目录与 Darknet 配置对齐Darknet 的 obj.data 里指定了训练图片路径清单、验证图片路径清单、类别名文件、备份目录四个关键路径train.txt 和 val.txt 里每一行是一张图片的路径。Darknet 会拿图片路径去掉扩展名再去拼.txt找标签文件这就是「同名机制」JPEGImages/000001.jpg对应labels/000001.txt。创建标准目录用这一句mkdir -p VOCdevkit/VOC2007/{Annotations,JPEGImages,ImageSets/Main,labels}解压后把数据集按对应关系放进去。labels目录是转换脚本输出目录后面转出来的 txt 统一放这里。obj.data 我习惯这样写classes 20 train /data/trash/train.txt valid /data/trash/val.txt names /data/trash/obj.names backup /data/trash/backup注意 train.txt 和 val.txt 里写的是 JPEGImages 下 jpg 文件的完整路径不是 labels 下 txt 路径txt 路径由 Darknet 按同名规则自己拼。图片和标签必须放在同一个顶层目录下一旦分散Darknet 会报「Couldnt open file」系列错误。3. 把 VOC 转成 Darknet 训练格式转换脚本与边界处理这一章给可以直接跑的转换脚本。设计思路遍历 Annotations 下所有 XML解析每个目标的类别和 bndbox归一化后写成同名 txt。不做数据增强只做格式纯转换保证转换过程可审计——万一标错了能倒查回原始 XML。3.1 坐标归一化换算从像素框到相对框XML 里存的是 xmin、ymin、xmax、ymax单位像素。YOLO 需要中心点坐标和宽高且全部除以图片宽高归一化。换算公式是四个cx (xmin xmax) / 2 / width cy (ymin ymax) / 2 / height w (xmax - xmin) / width h (ymax - ymin) / height这四行是整个转换脚本的心脏我写进独立函数输入是 XML 路径和类别映射表import xml.etree.ElementTree as ET from pathlib import Path def convert_xml_to_txt(xml_path, class_id_map, out_dir): root ET.parse(xml_path).getroot() size root.find(size) img_w int(size.findtext(width)) img_h int(size.findtext(height)) if img_w 0 or img_h 0: print(f[跳过] 异常尺寸: {xml_path}) return lines [] for obj in root.findall(object): name obj.findtext(name).strip() if name not in class_id_map: print(f[跳过] 未知类别 {name}: {xml_path}) continue 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, img_w)) xmax max(0.0, min(xmax, img_w)) ymin max(0.0, min(ymin, img_h)) ymax max(0.0, min(ymax, img_h)) if xmax xmin or ymax ymin: print(f[跳过] 空框: {xml_path} - {name}) continue cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{class_id_map[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) if lines: out_path out_dir / (xml_path.stem .txt) out_path.write_text(\n.join(lines) \n, encodingutf-8)两个参数要解释。class_id_map是从类别名到数字 id 的字典由上一章的 labels.txt 生成这样类别 id 和 obj.names 天然一致。out_dir是标签输出目录脚本不改任何原始文件绿色可逆。xmin max(0.0, min(xmin, img_w))是防越界的个别标注工具会给出超出图片边界的坐标不裁的话 Darknet loss 会在训练中途变成 NaN裁完再判断一次空框小于等于 0 的框直接丢掉。:.6f保留 6 位小数精度足够且文件不会膨胀。3.2 批处理与 train/val 划分一份清单走全程单张转换写好之后加上遍历和划分一次跑完整个数据集import random from pathlib import Path random.seed(42) annot_dir Path(VOCdevkit/VOC2007/Annotations) img_dir Path(VOCdevkit/VOC2007/JPEGImages) txt_dir Path(VOCdevkit/VOC2007/labels) txt_dir.mkdir(exist_okTrue) xml_files sorted(annot_dir.glob(*.xml)) random.shuffle(xml_files) n len(xml_files) train_files xml_files[: int(n * 0.8)] val_files xml_files[int(n * 0.8) : int(n * 0.9)] test_files xml_files[int(n * 0.9):] def write_path_list(file_list, out_path): with open(out_path, w, encodingutf-8) as f: for xml_path in file_list: img_path img_dir / (xml_path.stem .jpg) f.write(str(img_path) \n) write_path_list(train_files, train.txt) write_path_list(val_files, val.txt) write_path_list(test_files, test.txt) for xml_path in train_files val_files test_files: convert_xml_to_txt(xml_path, class_id_map, txt_dir)random.seed(42)固定随机种子保证每次划分一致复现实验时不至于因为数据切分不同解释不清。8:1:1 是通用做法1.4 万张样本足够支撑。xml_path.stem取不带扩展名的文件名用它去拼 jpg 路径和 txt 名从源头保证「同名机制」不出错绝不手动改文件名。默认划分的隐患是每类样本在每个集里占比可能失衡后续训练发现某类 AP 异常低先看该类是不是全挤在训练集或测试集里再考虑换成按类别分层抽样。提示转换脚本跑完训练前用find labels -name *.txt | wc -l对一下 txt 数量和 XML 数量数量不一致说明有 XML 没转成功先排查再训练。3.3 转换成败的四个边界检查点jpg 文件名与 txt 文件名必须完全一致包括大小写。Linux 下000001.JPG和000001.jpg是两个文件。XML 里 filename 节点可能与实际文件名不一致以 Annotations 目录下实际存在的文件为准信文件名不如自己扫一遍。转换后抽查 20 个 txt看坐标小数位是否正常、是否落在 0 到 1 之间只查三个文件暴露不了整体问题。如果一个 txt 行数比 XML 的 object 数少大概率有类别没进 class_id_map把「未知类别」的打印信息留好排查靠日志不靠猜。4. 数据清洗与标注质量排查训练翻车最常出在这里格式转换完数据集看起来能用了但直接开训常常翻车。原因是 VOC 格式只规定了标注怎么存没保证标注质量过关。垃圾数据集如果是众包或爬取来源混着错标、漏标、重复框是常态。这一章讲开训前怎么做数据质量排查以及我实际踩过的几条问题记录。4.1 为什么先清洗而不是先训练模型上限由标注质量决定。一万张图里有 5% 的框出问题看起来不多但 YOLO 的 loss 对错框非常敏感一个 xmax/xmin 写反的框梯度会直接往错误方向拉一截。而且 Darknet 训练一次要几小时等 loss 震荡再回头查数据时间成本已经出去了。我现在习惯是任何 VOC 数据入手先跑统计脚本再抽检 30 到 50 张图然后才谈训练。4.2 批量巡检脚本类别分布、异常标注、损坏文件一次查清下面脚本会做四件事统计每个类别目标数量、检测损坏 XML、检测越界框、检测异常尺寸import xml.etree.ElementTree as ET from collections import Counter from pathlib import Path annot_dir Path(VOCdevkit/VOC2007/Annotations) class_counter Counter() bad_xml [] out_of_range [] zero_size [] for xml_path in sorted(annot_dir.glob(*.xml)): try: root ET.parse(xml_path).getroot() except ET.ParseError: bad_xml.append(xml_path.name) continue size root.find(size) img_w int(size.findtext(width)) img_h int(size.findtext(height)) if img_w 0 or img_h 0: zero_size.append(xml_path.name) continue for obj in root.findall(object): name obj.findtext(name).strip() class_counter[name] 1 b obj.find(bndbox) xmin float(b.findtext(xmin)) ymin float(b.findtext(ymin)) xmax float(b.findtext(xmax)) ymax float(b.findtext(ymax)) if xmin 0 or ymin 0 or xmax img_w or ymax img_h: out_of_range.append((xml_path.name, name)) print(类别分布:) for name, cnt in class_counter.most_common(): print(f{name}: {cnt}) print(f\n损坏 XML: {len(bad_xml)} 个) print(f尺寸为 0 的标注: {len(zero_size)} 个) print(f越界框: {len(out_of_range)} 个) for item in out_of_range[:10]: print(item)most_common()按数量降序输出一眼看出类别是否极端不平衡。越界判断用xmax img_w而不是因为边界粘连的框不算错完全超出才算。print 只输出前 10 个越界框避免刷屏完整清单重定向文件再逐个修。脚本跑完得到三个结论类别是否均衡、有没有解析不了的 XML、坐标有没有越界任何一个有问题都值得先修数据再训练。4.3 可视化抽检画框回原图错标一眼现形统计脚本能抓格式问题但抓不到语义错误——把塑料瓶标成玻璃瓶这种只有人眼看才知道。抽检方法随机抽 30 张图用 OpenCV 把 bbox 画回原图import cv2 import xml.etree.ElementTree as ET from pathlib import Path xml_path Path(VOCdevkit/VOC2007/Annotations/000001.xml) img_path Path(VOCdevkit/VOC2007/JPEGImages/000001.jpg) img cv2.imread(str(img_path)) root ET.parse(xml_path).getroot() for obj in root.findall(object): name obj.findtext(name) b obj.find(bndbox) xmin int(float(b.findtext(xmin))) ymin int(float(b.findtext(ymin))) xmax int(float(b.findtext(xmax))) ymax int(float(b.findtext(ymax))) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, name, (xmin, max(0, ymin - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(check_ xml_path.stem .jpg, img)画框统一绿色类别名红色标签放框左上角。抽检重点看两类问题框是否把目标完整包住类别名和框内容是否对得上。批量看几十张图标注质量就有数了。这一步花十五分钟能省下后面好几轮无效训练。4.4 踩坑记录五条实测翻车经验现象Darknet 训练 loss 一开始就 NaN。原因某张标签 txt 里出现 0 宽高或坐标大于 1 的归一化值。解决转换脚本里加边界裁剪和空框过滤训练前再跑巡检脚本检查 txt 中所有 w、h 是否大于 0 且小于等于 1。现象train.txt 里某行图片路径报 fopen 失败。原因图片文件名大小写和 XML 不一致或文件名带空格Linux 下路径被截断。解决把全部图片和标签统一重命名为纯数字如000001.jpg不保留原名。现象换了一批数据重新训练mAP 反而断崖下跌。原因obj.names 类别顺序变了模型输出的 id 和旧标签对不上。解决obj.names 用转换脚本吐出的 labels.txt 直接生成每次转换后对比类别顺序变了就重新生成 cfg 里的 names 路径。现象某个类别 AP 几乎为 0但训练图里该类别数量不少。原因目标普遍很小比如烟头占整图不到 1%网络在低分辨率下根本分辨不出来。解决检查该类 bbox 面积分布中位数小于 20×20 像素就考虑提高输入分辨率或先做切片推理。现象标注里存在重复框同一目标被标了两个几乎重合的框。原因多人标注合并时没去重Darknet 会把两个框当两个目标优化。解决写脚本算两框 IoU大于 0.8 的保留其中一个处理完目标数会明显减少。这五条前三条是格式问题后两条是数据问题但表现都是「训练结果不对」。经验是先查数据数据查完再动网络。5. 让这批数据真正出效果Darknet 训练参数与收尾验证5.1 cfg 里必须改的参数不同 YOLO 版本 cfg 略不同但必改参数是同一批参数建议值说明batch64大 batch 收敛快显存不够降到 32subdivisions8 或 16决定实际单次送入 GPU 的量batch 除以 subdivisionswidth/height608 或 640分辨率越高小目标越好检8G 显存以下回到 416max_batchesclasses × 2000且不小于 600020 类就是 40000stepsmax_batches × 0.8 和 × 0.9学习率两个台阶下降classes数据实际类别数每个 [yolo] 层都要改filters3 × (classes 5)每个 [yolo] 层前那个卷积层要改YOLOv3 和 YOLOv4 沿用 Darknet 系 cfgfilters 按公式 3×(classes5) 算20 类就是 75。注意不是把所有 convolutional 的 filters 都改只有每个 [yolo] 层前面那个卷积层改。YOLOv5 用的是 ultralytics 项目参数写在 yaml 里和 Darknet cfg 不是一套别混着改。5.2 验证技巧与我的收尾习惯训练收尾不要只盯 loss。Darknet 自带 mAP 计算训练完跑一句./darknet detector map data/obj.data cfg/yolov3.cfg backup/yolov3_final.weightsmAP 之外再挑 20 张测试集图做推理用detector test看实际框效果。mAP 高不代表错标少错标在测试集里表现不会太好验证环节宁可多花半小时。从那以后我每次拿到新的 VOC 数据集第一件事永远是跑类别统计和边界值巡检脚本确认 labels.txt 顺序没变再开始转换这个习惯替我挡住了至少三次白跑训练。希望帮到你。本文还有配套的精品资源点击获取