ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

垃圾检测数据集VOC转YOLO格式转换与YOLOv8训练避坑指南

垃圾检测数据集VOC转YOLO格式转换与YOLOv8训练避坑指南 简介面向YOLO算法的垃圾检测数据集整体基于13707张带标签图像构建覆盖纸箱、纸张、塑料、铝、玻璃、硬纸板、瓶子、塑料瓶等常见可回收垃圾类别适合目标检测研究者、垃圾分类项目开发者及算法竞赛参与者使用。压缩包内共收录2000个XML标注文件资源大小约348MBXML文件记录目标类别与位置信息可配合图像完成模型训练与验证大幅减少人工标注工作量。目前已有346人学习下载说明该数据规模与类别覆盖在同类资源中具备一定实用价值。获取后可直接整理为YOLO所需标注格式用于模型训练、验证集划分和检测效果评估加快垃圾分类检测系统的搭建进度。1. 垃圾检测数据集到手先别急着训13707张图、8类标签到底能做什么做yolo算法垃圾检测项目的人十有八九第一步就把数据集解压后直接扔进训练脚本结果跑出来mAP惨不忍睹还以为是模型问题。这份13707张图像的垃圾检测数据集带完整XML标签覆盖纸箱、纸张、塑料、铝、玻璃、硬纸板、瓶子、塑料瓶8个类别本质上是Pascal VOC标注格式的存量数据不是开箱即用的YOLO训练格式。它解决的是垃圾分类场景下标注数据从零到一的问题——适合做智慧环卫、回收分拣、城市管理系统的视觉识别模块也适合拿来做YOLOv5/v8的微调实验。但要想让这批数据真正跑起来你必须先完成格式转换、类别映射、数据划分三件事这篇笔记就把完整流程和坑位拆给你。2. 先摸清家底VOC标注结构、8个类别与数据质量初检2.1 XML标注里到底有什么object框、name、difficult字段这份数据集的标注文件是xml后缀文件名形如img_0305_11337.xml对应同名的jpg图片。打开任意一个xml核心结构是annotation根节点下的object节点每个object代表图中的一个目标实例。一个典型object节点包含name类别名、bndboxxmin/ymin/xmax/ymax四个坐标值、difficult是否难例、truncated是否截断。annotation folderimages/folder filenameimg_0305_11337.jpg/filename size width1920/width height1080/height depth3/depth /size object nameplastic_bottle/name bndbox xmin412/xmin ymin208/ymin xmax634/xmax ymax521/ymax /bndbox difficult0/difficult /object /annotation这里有个容易忽略的点filename标签里的文件名和你实际解压出来的图片文件名可能对不上。比如xml里写的是img_0305_11337.jpg但图片文件是img_0305_11337.png或者文件名里有空格、下划线变体。我一般会先写一段脚本核对xml的filename和磁盘上实际存在的图片文件做一次全量比对把缺失或多余的项列出来不然转换到一半报FileNotFoundError才回头找很浪费时间。另外difficult字段建议保留转换后可以映射到YOLO格式的第四个标签位或者直接丢弃——如果你的场景里被遮挡、模糊的目标不算负样本就把它过滤掉训练时忽略这些框。2.2 类别名称归一化8类标注的命名不统一问题从数据集名称看类别包括纸箱carton/box、纸张paper、塑料plastic、铝aluminum、玻璃glass、硬纸板cardboard、瓶子bottle、塑料瓶plastic_bottle。但实际标注文件里同一类物品可能出现多种写法比如纸箱可能标成carton、cardboard_box、box塑料可能标成plastic、plastic_bag、plastic_wrap瓶子可能标成bottle、glass_bottle、plastic_bottle。这一步必须在转换格式之前做因为你后面训练脚本里data.yaml的类别列表是固定顺序的如果你不先把xml里的name统一成规范类别训练时类别索引就会乱——第一个类别对齐第一个标签错一位整个模型就废了。我踩过一次这个坑当时铝罐类标了aluminum_can和alu_can两个名字跑完一个epoch的loss曲线震荡得跟心电图一样后来用脚本统计所有xml里出现过的name值发现一共有14种写法映射归一化成8类后才稳定下来。推荐的做法是写一个类别映射字典把同义词归并到主类别上同时保留一个labels.txt记录最终8类的顺序后续YOLO格式的类别编号就是按这个顺序来的。2.3 数据质量初检脚本框越界、空标注、图片损坏排查拿到数据集后先跑一遍数据体检别急着转换。检查项目集中在四件事标注框坐标是否越界xmin小于0或大于图片宽度、是否有0宽高框标注时鼠标误点击、是否存在完全没有object节点的空xml、图片文件本身是否损坏用OpenCV读一下看返回是否为空。import cv2 import os import xml.etree.ElementTree as ET def inspect_dataset(img_dir, xml_dir): issues [] for xml_name in os.listdir(xml_dir): if not xml_name.endswith(.xml): continue xml_path os.path.join(xml_dir, xml_name) tree ET.parse(xml_path) root tree.getroot() filename root.find(filename).text img_path os.path.join(img_dir, filename) img cv2.imread(img_path) if img is None: issues.append(f图片缺失或损坏: {img_path}) continue h, w img.shape[:2] objs root.findall(object) if len(objs) 0: issues.append(f空标注: {xml_name}) for obj in objs: box obj.find(bndbox) xmin int(float(box.find(xmin).text)) ymin int(float(box.find(ymin).text)) xmax int(float(box.find(xmax).text)) ymax int(float(box.find(ymax).text)) if xmin 0 or ymin 0 or xmax w or ymax h: issues.append(f越界: {xml_name} - ({xmin},{ymin},{xmax},{ymax})) if xmax - xmin 0 or ymax - ymin 0: issues.append(f零面积框: {xml_name}) return issues issues inspect_dataset(images, annotations) print(f发现 {len(issues)} 个问题) for idx, issue in enumerate(issues[:30]): print(idx, issue)这段脚本的逻辑是先遍历xml目录逐个解析xml节点取filename去图片目录找对应图片用cv2读取验证图片完整性再检查每个object框的坐标是否落在图片尺寸范围内。脚本里float()转换是为了防止标注坐标带小数点有些标注工具导出的坐标值不是整数直接int()会截断。跑完一遍后如果问题数在几十个以内手工修掉即可如果超过上百个就要考虑是不是有某批图片和标注文件错位了通常是解压时目录结构被改动或者部分文件重命名导致。3. 把VOC转成YOLO格式路径修正、归一化与标签对齐3.1 为什么必须转格式YOLO需要的txt和VOC的xml差异YOLO系列训练时读的不是xml而是每张图片对应一个同名txt文件每行写一个目标class_id x_center y_center width height坐标全是归一化的0到1小数不是像素绝对值。归一化有两个好处一是不同分辨率的图片不用缩放就能喂给模型训练时随机resize不会导致标注错位二是txt文件体积小读取快。VOC的xml是像素绝对坐标直接用于YOLO会报格式错误所以必须写个转换脚本。转换的核心公式很简单x_center (xmin xmax) / 2 / img_width宽高同理除以图片宽度和高度。注意是除以图片本身的宽高不是除以统一scale。如果你转换前做了图片resize那坐标也要等比换算但这里数据集图片是原始分辨率直接除就行。3.2 转换脚本类别映射、坐标归一化、txt输出这一步直接给出完整脚本保存成voc2yolo.py运行。脚本支持两个参数一是xml目录二是图片目录输出写到labels目录每张图片对应一个txt。import os import xml.etree.ElementTree as ET from tqdm import tqdm # 类别映射表把xml里五花八门的name归并成8类 CATEGORY_MAP { carton: carton, cardboard_box: carton, box: carton, paper: paper, paper_board: paper, plastic: plastic, plastic_bag: plastic, plastic_wrap: plastic, aluminum: aluminum, aluminum_can: aluminum, alu_can: aluminum, glass: glass, glass_bottle: glass, hardboard: cardboard, cardboard: cardboard, corrugated: cardboard, bottle: bottle, plastic_bottle: plastic_bottle, pet_bottle: plastic_bottle } CATEGORIES [carton, paper, plastic, aluminum, glass, cardboard, bottle, plastic_bottle] CLASS_TO_ID {name: idx for idx, name in enumerate(CATEGORIES)} def convert_xml_to_yolo(xml_dir, img_dir, out_dir): os.makedirs(out_dir, exist_okTrue) skipped 0 for xml_name in tqdm(os.listdir(xml_dir)): if not xml_name.endswith(.xml): continue xml_path os.path.join(xml_dir, xml_name) tree ET.parse(xml_path) root tree.getroot() # 从xml里取图片名去掉扩展名作为txt文件名 filename root.find(filename).text base os.path.splitext(filename)[0] img_path os.path.join(img_dir, filename) # 读图片拿宽高也可以用xml里的size节点但建议以实际图片为准 from PIL import Image try: w, h Image.open(img_path).size except Exception: skipped 1 continue lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in CATEGORY_MAP: continue diff int(obj.find(difficult).text) if obj.find(difficult) is not None else 0 if diff 1: continue box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 坐标归一化中心点坐标和宽高全部除以图片宽高 cx (xmin xmax) / 2.0 / w cy (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h # 边界保护归一化后坐标超出[0,1]就截断 cx min(max(cx, 0.0), 1.0) cy min(max(cy, 0.0), 1.0) bw min(max(bw, 0.0), 1.0) bh min(max(bh, 0.0), 1.0) class_id CLASS_TO_ID[CATEGORY_MAP[name]] lines.append(f{class_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) out_path os.path.join(out_dir, base .txt) with open(out_path, w) as f: f.write(\n.join(lines)) print(f转换完成跳过 {skipped} 个无法读取图片的xml) convert_xml_to_yolo(annotations, images, labels)脚本里几个关键参数值得单独说明。CATEGORY_MAP把xml原始类别名归并成8个主类别这一步是全局的如果你在训练时发现某类目标数量特别少回来看这里很可能是某些相近类别的目标没有被正确归并。坐标计算用的是float()而不是int()因为有些标注工具导出的坐标带小数点直接int会导致框偏移几个像素这个误差在小目标上会被放大。min/max(0,1)截断是在处理标注越界问题要是某张图的标注框本身就画出了图片边缘归一化后会出现大于1的坐标YOLO训练时坐标超出边界会计算异常先截断能保证训练不崩但这只是兜底。最后输出的txt每行是class_id加四个浮点数注意浮点保留6位小数保留位数太少会影响小目标的精度6位在1920宽度下大约对应1毫米精度足够用。3.3 目录组织与labels.txt训练脚本直接能读的结构转换完成后目录结构应该是这样dataset/ ├── images/ │ ├── img_0305_11337.jpg │ └── ... ├── labels/ │ ├── img_0305_11337.txt │ └── ... ├── labels.txt └── data.yamllabels.txt内容就是类别名一行一个顺序和CATEGORIES一致YOLOv5/v8读data.yaml时指定names格式上两边类别顺序必须完全一致。这里有个很隐蔽的坑如果你用了第三方工具导出数据集labels.txt的位置可能不一样有的放在dataset/classes.txt有的直接放在data.yaml里写死。我一般是生成一份labels.txtYOLOv8的data.yaml里用names:字段读取或者直接把类别写进yaml两种方式结果一样但千万别在训练脚本里同时引用两个不同顺序的类别文件不然训练出来的模型类别对不上。另外建议把转换脚本保留一份在数据集根目录以防后续补充数据需要重新转换或者你发现初始转换有误时能改脚本重新跑一遍。4. 训练与验证yolov8s参数设置、数据划分与训练曲线解读4.1 按7:2:1划分train/val/test注意图片和标签配对数据准备好后先做划分。划分时有一个易错点不是随机挑图片就完事必须保证图片文件和对应的txt标签文件成对移动不然训练时读到一张没有标签的图片就是一个空的训练样本白白浪费一个batch。我习惯用下面这个脚本基于图片文件名做划分标签目录保持不变只是生成train.txt/val.txt/test.txt三个列表文件而不是物理移动文件。import os import random random.seed(42) # 固定随机种子保证每次划分结果一致 img_dir images all_imgs [f for f in os.listdir(img_dir) if f.endswith(.jpg)] random.shuffle(all_imgs) total len(all_imgs) train_imgs all_imgs[:int(total * 0.7)] val_imgs all_imgs[int(total * 0.7):int(total * 0.9)] test_imgs all_imgs[int(total * 0.9):] def write_list(file_path, img_list): with open(file_path, w) as f: for name in img_list: f.write(os.path.join(img_dir, name) \n) write_list(train.txt, train_imgs) write_list(val.txt, val_imgs) write_list(test.txt, test_imgs) print(ftrain: {len(train_imgs)}, val: {len(val_imgs)}, test: {len(test_imgs)})划分时固定随机种子很有必要否则每次运行划分结果不同做完实验想复现就难了。这里也可以考虑按类别分布做分层抽样避免某一类集中出现在验证集里但这个数据集类别相对均衡简单随机划分够用。如果你的垃圾检测场景中某些类别数量差异很大比如塑料瓶有4000张、铝罐只有600张那就要用分层抽样保证每个类别在训练集和验证集中的比例一致不然模型对少数类别的泛化能力会被低估。4.2 data.yaml配置路径写法与类别列表的对应关系YOLOv8的data.yaml配置直接影响训练能否启动最常见的问题是路径用相对路径还是绝对路径还有类别顺序和labels.txt不一致。下面是这个数据集的data.yaml标准写法。# 训练/验证数据路径使用相对路径时以当前工作目录为基准 path: ./dataset train: train.txt val: val.txt test: test.txt # 类别名称顺序必须与labels.txt一致 names: 0: carton 1: paper 2: plastic 3: aluminum 4: glass 5: cardboard 6: bottle 7: plastic_bottle这里path字段是数据集的根目录train/val填的是列表文件的相对路径YOLOv8会自动把path和train拼接成完整路径。注意names的类别顺序不能变这个顺序决定了模型输出的类别编号也决定了后续推理时每个检测框对应的类别名。如果你在推理阶段发现检测结果和实际物体对不上回来看一下data.yaml的names顺序是不是和训练时一致——我曾经改过一边names导致模型把纸箱识别成玻璃排查了半天。4.3 训练命令与关键参数batch_size、imgsz、epochs怎么设使用YOLOv8训练命令如下。建议先跑一个epochs30的短实验验证数据流是否正确再跑正式的长训练。yolo detect train \ modelyolov8s.pt \ datadata.yaml \ epochs50 \ batch16 \ imgsz640 \ lr00.01 \ lrf0.01 \ optimizerSGD \ device0 \ projectrun/garbage_det \ nameexp_v1参数选择的思路说一下。modelyolov8s.pt是加载COCO预训练权重作为起点垃圾检测和COCO的80类有较大差异但底层的特征提取网络已经学会了边缘、纹理、颜色等通用特征从预训练权重开始收敛速度比从零训练快很多尤其是13707张图这个规模从零训练容易过拟合。如果显存不够把batch降到8或4如果物体普遍较小imgsz建议用640以上小目标在640分辨率下特征保留得更好但显存占用会上升。optimizerSGD适合微调场景AdamW收敛快但最终的mAP往往略低于SGD。训练过程中要盯两个指标一是loss/box和loss/cls是否平稳下降如果前几个epoch loss不降反升大概率是学习率太大把lr0改成0.001即可二是有没有nan出现一旦出现nan检查txt标签里有没有0宽度0高度的框或者图片读取失败混进了训练集。4.4 训练曲线怎么读loss下降趋势和val指标的关系训练结束后在run/garbage_det/exp_v1/目录下会生成results.csv和曲线图。重点看train/box_loss、train/cls_loss、val/box_loss、val/cls_loss四条曲线的走势。正常情况是训练loss和验证loss同步下降最后趋于平稳。如果你看到训练loss持续下降到很低但验证loss在后期反弹升高这就是典型的过拟合信号——模型把训练集中的背景纹理和光照条件死记硬背了解决办法是加数据增强hsv_h、hsv_s、flipud参数或减小模型规模。如果训练loss和验证loss都降不下去卡在一个高位那就要怀疑标签质量问题比如框偏移严重、类别标注错误这种数据问题靠调参是救不回来的要回头修正标注。这个数据集还有一个特点是室内外场景混杂有的图片光照偏暗有的带强反光所以训练时把yolov8的增强参数稍微调大比如hsv_h0.015、hsv_s0.7、fliplr0.5对提升鲁棒性有帮助。5. 避坑记录标注错位、类别数对不上、小目标漏检与AP为零5.1 现象训练时报错 box 坐标超出图片边界训练到一半日志里出现类似Box coordinates (nan nan w h)的warning或者loss直接跳成nan。原因是用脚本转换txt时某个xml的bndbox坐标值出现异常——例如xmax比xmin还小、坐标值带了奇怪的引号或乱码导致归一化后宽高为负数送入模型后梯度异常。解决方法是转换前在脚本里加一个坐标合法性校验如果xmax xmin或ymax ymin这条object直接跳过并打印xml文件名。另外如果xml里的坐标值是用int()读的但实际字符串是412.0Python会抛ValueError这就是我为什么在前面脚本里用float()接收坐标文本。检查时要同时看size里的图片宽高是否和真实图片一致有些xml的size节点写的是占位值和实际图片分辨率差很多这种数据转换出来的txt坐标全偏。5.2 现象训练的类别数量和实际标注对不上模型训练完成后推理发现有些类别从来没被识别出来。排查后原因多半是CATEGORY_MAP里没有覆盖到xml中出现的所有name变体比如某个xml里写的是alu_can但映射表里只有aluminum_can这个目标在转换时被丢弃了变成无标注背景模型自然学不到铝罐这个类别。解决办法是转换前先做一次全量name统计把xml里出现过但映射表缺失的name都找出来列出来逐个确认是哪个主类。统计脚本很简单遍历所有xml把name的text值去重收集打印出来对比一下。这一步值得做这个数据集命名有点混乱多跑一次统计能省后面反复重训的时间。5.3 现象小目标漏检严重玻璃瓶和塑料瓶混淆度高训练跑完看验证集发现小尺寸的瓶盖、易拉罐拉环几乎全部漏检玻璃瓶和塑料瓶混在一起分不清。原因是两个因素叠加第一小目标在640分辨率下占比太小特征提取层经过32倍下采样后只剩几个像素信息基本丢失第二玻璃瓶和塑料瓶在形状、颜色上高度相似数据集中标注的区分也模糊有些图里玻璃瓶的标签写成了bottle塑料瓶的标签写成了glass_bottle。解决小目标问题优先把imgsz从640提到960或1280代价是显存翻倍batch需要调小。其次在训练时开启yolov8的close_mosaic10最后10个epoch关掉Mosaic增强让小目标不被裁切得过于严重。解决玻璃和塑料混淆问题靠纯视觉特征确实难这类问题本质上是类别定义本身有歧义建议回数据集去看标注靠人工梳理。如果数据集里本身标错了那模型的输出也就将错就错这类混淆短期内无解。5.4 现象验证时mAP0.5一直为0训练loss却正常训练loss正常下降但每次Validate的mAP全为0。我遇到过最离谱的一次是因为data.yaml里names配错了顺序类别索引对不上模型输出的第0类是纸箱但验证脚本把第0类当成塑料瓶去算AP自然全是0。原因就是前面3.3节提醒过的类别对齐问题训练和验证时读取的names列表来自同一个data.yaml理论上不该错但我那一次是训练时用的yaml在run/exp目录下被yolov8复制了一份并做过改动验证时又用了另外一份旧yaml两边类别顺序不一致。另一个常见原因是验证集和训练集完全一样且验证集里某些图片没有对应的标签txt所有真值框都是空的算出来AP自然为0。检查方法是在跑验证命令前先执行yolo val并观察日志中instances的数量如果接近0说明标签没被加载检查txt里的class_id是否超出了0到7的范围如果转换时写了class_id8这种越界值验证时标签解析会直接丢弃。6. 验证指标与难例回补mAP看整体、混淆矩阵看瓶颈回标再训练训练完成后进入验证和迭代环节。用下面命令跑验证集重点关注mAP0.5、mAP0.5:0.95和每个类别单独的AP值。yolo detect val \ modelrun/garbage_det/exp_v1/weights/best.pt \ datadata.yaml \ splitval \ conf0.25 \ iou0.7验证输出会打印一张表格包含8个类别的precision、recall、mAP0.5。这12项指标里最值得盯的是每个类的AP值而不是只看平均值——平均值好看但某一类AP很低说明模型在该类上的泛化明显不足。如果某个AP很低的类正好是铝aluminum或玻璃glass回到训练集去看这类图片的标注分布大概率发现数量偏少或者标注框没有跟上物体的实际轮廓。我会再跑一次混淆矩阵yolov8会输出confusion_matrix.png从中能看到哪两类经常被相互误判。比如瓶子和塑料瓶如果混淆严重说明标注时这两类本身的边界就不清晰数据集中可能大量存在同一物体一会标bottle一会标plastic_bottle的情况。这种情况先修标注再重训光调模型参数是徒劳。确认模型质量可接受后还有一个值得做的技巧用训练好的模型去跑测试集前面划分出的10%把检测置信度低的样本和漏检样本导出成难例列表去人工查看是标注漏标还是模型误判。如果是数据漏标把这些图片补标后塞回训练集足够数量的难例回补往往比单纯增加epoch更有效。从那以后我每次训完模型都会强制走一遍难例回看再决定要不要收工而不是只看个mAP数字就完事希望帮到你。本文还有配套的精品资源点击获取
返回列表