☰
VOC垃圾分类数据集:解压验收、格式转换与训练避坑指南
2026/10/26 1:11:15 网站建设 项目流程

简介:面向垃圾分类目标检测与YOLOv5入门应用的VOC格式数据集,包含烟蒂、纸箱、电池、蛋壳、大骨头等40余类常见垃圾的实拍图片,共1.5万张,均已用labelImg逐一人工标注,适合需要现成训练集或想学习数据标注流程的深度学习初学者。资源共5个文件,整体约953MB,其中两个zip分别存放VOCdevkit_trash完整标注数据与测试图片,一个rar放置yolov5-5.0-trash训练工程,另附PDF版小白训练教程和用于划分训练集/验证集的prepare_data.py脚本,可帮助读者快速跑通从数据集划分、模型训练到效果验证的完整链路,并附有训练好的模型权重供对比参考。目前已有2659人学习下载,是一个数据、教程与工具齐备的训练起步包。

1. 1.5万张VOC垃圾分类数据集:先别急着解压,先查这三件事

拿到一个写着“1.5万张VOC已标注数据集”的rar压缩包,最常见的做法是双击解压、点开文件夹看一眼,然后直接扔给训练脚本。我见过不少人在这一步省了五分钟,后面赔进去两三天:xml和jpg对不上、标注框出了图片边界、类别名中英文混着写,最后loss不收敛,还要回头怀疑自己的网络结构有问题。VOC格式本身不复杂,复杂的是“已标注”这三个字背后隐含的验收责任。这份数据适合做垃圾桶识别、智慧环卫、校园试点这类目标检测项目,也适合刚入门的朋友拿它跑通“数据→训练→部署”的最小闭环。就算rar是现成的、数据是免费的,第一步永远是问自己:里面的类别清单,我核对过吗?

2. VOC标注格式先读透,再谈训练:目录、XML和你最该关心的类别清单

2.1 一张JPEG配一个XML,标注信息全在object节点里

Pascal VOC是目标检测领域流传最广的标注格式之一,绝大多数标注工具(比如labelImg)都能直接导出这种结构。一个标准的VOC数据集,通常包含三个核心目录:JPEGImages存放原始图片,Annotations存放与图片同名的xml标注文件,ImageSets/Main存放train.txt、val.txt这类划分清单。文件名一一对应,这是VOC格式的基本约定。

xml里真正关键的内容集中在object节点。一个典型的object节点长这样:

<object> <name>recyclable</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>102</xmin> <ymin>85</ymin> <xmax>318</xmax> <ymax>402</ymax> </bndbox> </object>

这里的name是类别名,bndbox里的四个值是目标框的左上角和右下角像素坐标。注意一个细节:xmin/ymin/xmax/ymax在很多标注工具里是整数,但也可能是浮点数,解析时统一转成float再处理,别用int()直接截断。另外务必看一下difficult字段,它标记了“难以辨认”的目标,转换格式时通常会选择跳过这类框。

拿到数据集后第一件事,不是训练,而是打开几个xml看一眼name字段到底怎么写的。垃圾分类数据集的类别命名非常混乱,有的直接写“可回收垃圾”“有害垃圾”,有的写英文“recyclable”“hazardous”,还有的写拼音缩写。这直接决定了后续data.yaml怎么写,也决定了一旦转成YOLO格式后class_id是否对应得上。

2.2 先搞清楚你拿到的VOC是“检测框”还是“多边形”

VOC这个格式在不同的标注工具里含义不完全一样。大多数情况下,bndbox提供的是矩形检测框,适合目标检测任务。但有些工具导出时会写入polygon节点,存一串点坐标,比如:

<polygon> <point><x>120</x><y>80</y></point> <point><x>180</x><y>76</y></point> <point><x>200</x><y>150</y></point> </polygon>

这种数据用于实例分割任务。如果你手里的xml包含polygon,而你的目标是做检测框模型,要么写脚本把多边形的最小外接矩形转出来,要么就按分割任务做。最怕的是数据集里既有bndbox又有polygon,两个都存在时,优先用bndbox,并检查两者是否一致。

我一般会写个两三行的脚本,先扫描全部xml,统计“含bndbox的文件数”和“含polygon的文件数”,确认数据格式统一。这一步花不了两分钟,但能避免后面转换脚本跑一半报KeyError。

2.3 类别清单先提取出来,训练任务长什么样就清楚了

别信rar里的README,也别信下载页面的简介,一切以xml内容为准。写个统计脚本,把全部xml里的name字段扫出来,顺便统计每个类别的框数量。这一步能同时回答两个问题:这个项目是几分类,以及类别是否严重不平衡。

import os import glob import xml.etree.ElementTree as ET from collections import Counter ann_dir = "Annotations" # 改成你的实际路径 cat_counter = Counter() for xml_path in glob.glob(os.path.join(ann_dir, "*.xml")): tree = ET.parse(xml_path) root = tree.getroot() for obj in root.findall("object"): name = obj.find("name").text.strip() cat_counter[name] += 1 for name, count in cat_counter.most_common(): print(f"{name}: {count}")

这个脚本的逻辑很简单:遍历每个xml,找到所有object节点,读出name并计数。输出的结果里,重点关注两个信息。一是类别名的写法是否统一,比如“可回收垃圾”和“recyclable”是否同时存在,如果混用就必须建立别名映射。二是数量分布,通常垃圾分类数据集中“其他垃圾”会占大头,“有害垃圾”可能只有几百个框,这种不平衡会直接影响训练收敛速度。如果发现某个类别的框数量少于500,后续要做重采样或重复增强,别指望模型自己能学好。

3. 解压与验收:7-Zip命令行、孤儿文件检查和可视化抽检

3.1 别急着双击解压:7-Zip命令行、密码与完整性校验

很多人的习惯是双击rar包,用系统自带的压缩工具或第三方解压软件直接拖出来。这里我建议换成命令行操作,原因很简单:命令行可以顺便做完整性校验,而且能保留目录结构。7-Zip完全可以解压rar文件,不需要额外装那些带弹窗广告的“专用解压软件”。

# 先测试压缩包完整性,不实际解压 7z t 垃圾分类1.5万张VOC已标注数据集.rar # 测试通过后,正式解压,保留完整目录结构 7z x 垃圾分类1.5万张VOC已标注数据集.rar -o/home/user/garbage_dataset -p你的密码

7z t是测试模式,会逐个文件校验CRC,任何损坏在这里就会暴露,而不是等训练到第300轮才报错。7z x表示解压并保留目录结构,如果换成7z e,所有文件会被摊平到一个目录里,jpg和xml同名不同目录时就会互相覆盖,这个坑非常隐蔽。-o参数指定输出目录,注意-o和路径之间不能有空格。如果压缩包设了密码,-p后面直接跟密码;如果忘了密码,网上那些“强制解压”工具基本都是钓鱼或捆绑,与其冒险不如回头找数据提供方核对。

解压完之后还有一个容易忽略的点:检查文件夹名是否乱码。Windows下用中文目录名解压的文件,传到Linux服务器上很容易出现GBK与UTF-8编码错乱,导致代码里os.path.join拼出的路径根本不存在。我一般会在一开始就把整个数据集改名成纯英文路径,避免后期所有脚本踩编码的雷。

3.2 图片和XML一一对应:用30行脚本找出孤儿文件

VOC格式要求jpg和xml一一对应,但实际交付的数据集里经常出现“有图无标注”或“有标注无图”的情况。前者会让训练集里混入无标签图片,后者会导致转换脚本报错中断。扫一遍全目录,几秒钟就能定位问题。

import os img_dir = "JPEGImages" ann_dir = "Annotations" imgs = set(os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith(".jpg")) anns = set(os.path.splitext(f)[0] for f in os.listdir(ann_dir) if f.endswith(".xml")) only_img = imgs - anns only_ann = anns - imgs print(f"有图无标注: {len(only_img)} 个") for name in sorted(only_img)[:20]: print(f" {name}.jpg") print(f"有标注无图: {len(only_ann)} 个") for name in sorted(only_ann)[:20]: print(f" {name}.xml")

脚本逻辑不复杂:分别读取两个目录下的文件名,去掉扩展名后做集合差。输出结果里,only_img是图片存在但xml缺失的文件,这些图在训练时不会产生任何loss贡献,严格来说属于无效样本;only_ann更危险,转换脚本遍历Annotations时遇到它,会因为找不到对应图片而直接抛出异常。

处理原则我也说一下:如果数量很少,比如几十个,直接删掉或移到_broken目录;如果数量很大,比如几百个,就要怀疑是不是解压不完整,重新检查rar分卷是否齐全。文件名大小写不一致也常见,注意os.path.splitext后统一做lower()再比较。

3.3 可视化抽检:把标注框画出来,人眼扫一遍

脚本检查能发现文件缺失,但发现不了标注质量差。标注框偏移、框过大包住整张图、类别标反,这些只有把标注画到图上肉眼才能看出来。写一个画框脚本,每类随机抽几十张,拼成一张大图快速浏览。

import os import random import cv2 import xml.etree.ElementTree as ET def draw_annotation(img_path, xml_path, out_path): img = cv2.imread(img_path) if img is None: return False tree = ET.parse(xml_path) for obj in tree.getroot().findall("object"): name = obj.find("name").text.strip() bnd = obj.find("bndbox") xmin = int(float(bnd.find("xmin").text)) ymin = int(float(bnd.find("ymin").text)) xmax = int(float(bnd.find("xmax").text)) ymax = int(float(bnd.find("ymax").text)) color = (0, 255, 0) if "hazardous" in name.lower(): color = (0, 0, 255) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), color, 2) cv2.putText(img, name, (xmin, max(ymin - 5, 15)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imwrite(out_path, img) return True img_dir = "JPEGImages" ann_dir = "Annotations" out_dir = "check_vis" os.makedirs(out_dir, exist_ok=True) xml_names = random.sample(os.listdir(ann_dir), 100) for idx, xml_name in enumerate(xml_names): xml_path = os.path.join(ann_dir, xml_name) img_name = os.path.splitext(xml_name)[0] + ".jpg" img_path = os.path.join(img_dir, img_name) if os.path.exists(img_path): draw_annotation(img_path, xml_path, os.path.join(out_dir, f"{idx:03d}.jpg"))

这段代码的核心是读取xml里的bndbox,用OpenCV在原图上画矩形和类别名,最后写到check_vis目录。看抽检结果时,重点看三件事:框是否紧贴目标边缘、目标是否完整包含在框内、类别名和画面内容是否一致。垃圾分类这类数据最容易出现的问题是“有害垃圾”里的电池被标成“可回收垃圾”,或者装着厨余垃圾的塑料袋把整个袋子标成“厨余垃圾”——后者原则上没错,但会让模型学到“塑料袋也是厨余垃圾”的错觉。别想着靠loss曲线发现这种问题,错标在训练集里是常态,只有人眼能判断。

4. VOC转YOLO格式:转换脚本与四个边界坑

4.1 坐标系差异:VOC是左上角右下角,YOLO要中心点和宽高

VOC存储的是(xmin, ymin, xmax, ymax),即框的左上角和右下角像素坐标。YOLO系列要求的标注是归一化后的(cx, cy, w, h),其中cx和cy是中心点坐标,w和h是宽高,四个值都除以图片宽高,范围0到1。转换公式是固定的:

cx = (xmin + xmax) / 2 / image_width cy = (ymin + ymax) / 2 / image_height w = (xmax - xmin) / image_width h = (ymax - ymin) / image_height

看起来简单,但有一个关键决策:image_width和image_height从哪里来。绝大多数VOC xml里有<size>节点,记录了图片原始宽高,但这个值不可靠。很多数据集被二次压缩过、resize过,xml里的size却还是初版标注时的值。稳妥的做法是直接用cv2.imread读取图片后用img.shape拿真实宽高,转换精度才保证正确。

4.2 voc2yolo.py:一张图对应一个txt的转换脚本

YOLO的标注格式是“一张图片对应一个txt文件”,文件名和图片同名,每一行表示一个目标,格式是“class_id cx cy w h”。下面这个脚本把整个VOC目录批量转成YOLO格式,同时输出类别列表。

import os import cv2 import glob import xml.etree.ElementTree as ET CLASSES = ["recyclable", "kitchen", "hazardous", "other"] # 如果第2.3节统计出的类别名和你预期不一致,先做别名映射再转换 def voc2yolo(xml_path, img_dir, out_dir): tree = ET.parse(xml_path) root = tree.getroot() img_file = root.find("filename").text.strip() img_path = os.path.join(img_dir, img_file) if not os.path.exists(img_path): # 有些xml的filename不带扩展名,自动补.jpg img_path = os.path.join(img_dir, os.path.splitext(img_file)[0] + ".jpg") img = cv2.imread(img_path) if img is None: return None, f"无法读取图片 {img_path}" h, w = img.shape[:2] lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in CLASSES: continue bnd = obj.find("bndbox") xmin = float(bnd.find("xmin").text) ymin = float(bnd.find("ymin").text) xmax = float(bnd.find("xmax").text) ymax = float(bnd.find("ymax").text) # 坐标裁剪到图片范围内 xmin = max(0, min(xmin, w - 1)) xmax = max(0, min(xmax, w - 1)) ymin = max(0, min(ymin, h - 1)) ymax = max(0, min(ymax, h - 1)) if xmax - xmin < 1 or ymax - ymin < 1: continue cx = (xmin + xmax) / 2 / w cy = (ymin + ymax) / 2 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h cls_id = CLASSES.index(name) lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") out_name = os.path.splitext(os.path.basename(xml_path))[0] + ".txt" out_path = os.path.join(out_dir, out_name) with open(out_path, "w") as f: f.write("\n".join(lines)) return out_path, None img_dir = "JPEGImages" ann_dir = "Annotations" out_dir = "labels" os.makedirs(out_dir, exist_ok=True) bad_count = 0 for xml_path in glob.glob(os.path.join(ann_dir, "*.xml")): result, err = voc2yolo(xml_path, img_dir, out_dir) if err: bad_count += 1 print(err) print(f"转换完成,失败文件数: {bad_count}")

脚本分四步走:先根据xml里的filename找图片,找不到就自动补.jpg后缀;然后用cv2.imread读真实宽高;接着遍历每个object,做坐标裁剪和格式转换;最后写入同名txt。CLASSES列表的顺序决定了class_id,也就是txt每行第一个数字的含义,这个顺序要和之后data.yaml里的names保持完全一致。

转换完成后立即做一次反向验证:随机挑10张图,读取对应txt,把归一化坐标放大回像素值画框,和原图对比。两个方向都对了,才说明转换脚本没问题。

4.3 四个必踩边界坑:越界坐标、空XML、类别不一致、损坏图片

第一,越界坐标。标注时鼠标拖拽可能拖出图片边缘,导致xmax > w或ymax < 0之类的情况。转换时不处理,YOLO训练时会出现AssertionError: bbox must be in [0, 1]直接中断。处理方式就是脚本里那四行max/min裁剪,再过滤掉宽高小于1像素的退化框。

第二,空xml。有些xml文件存在,但里面一个object都没有,可能是标注到一半就导出的半成品。转换脚本在这种情况下会生成一个空txt。YOLO训练遇到空txt没问题,但会浪费一个训练样本;更麻烦的是如果这个空xml对应的图片本身存在而其他图片又没标注,你会高估有效数据量。转换时顺手统计一下“无object的xml数量”,超过总数1%就要考虑这批数据是否合格。

第三,类别名不一致。这是最阴间的坑。同样一种垃圾,一部分xml里叫“kitchen_waste”,另一部分叫“kitchen”,还有一部分叫“厨余垃圾”。不建立别名映射直接转换,同一个类被拆成三个class_id,模型会彻底学乱。处理办法是写一个dict做归一化:

ALIAS = { "kitchen": "kitchen", "kitchen_waste": "kitchen", "厨余垃圾": "kitchen", }

转换前先统一名称,再进入CLASSES列表比对。

第四,图片损坏或解码失败。rar解压不报错不等于文件没坏,cv2.imread读jpg失败时会返回None而不会抛异常。脚本里那张if img is None的判断就是用来抓这种问题的。如果坏图数量大,优先怀疑rar分卷下载不完整或磁盘写入异常,重新解压对应分卷,而不是一张张手工修复。

5. 垃圾分类VOC数据的常见问题和避坑排查:从训练翻车到部署误检

5.1 现象:loss一直在降,但验证集mAP卡在0.2上不去

这种翻车最迷惑人,因为训练过程看起来一切正常,loss曲线很漂亮,可验证集的mAP就是不涨。原因多半出在标注框偏大,框里包含太多背景,模型被迫在“垃圾+背景”的混合特征里找规律。尤其是垃圾分类场景,很多员工标注时习惯把垃圾桶整体框进去,而不是只框垃圾本身。解决办法是回到第3.3节的抽检图,逐类看框的贴合度;如果大面积偏松,宁可花时间批量修正,也别调模型参数。另外可以尝试把imgsz从640提到768,框包含的背景占比会相对下降,多少能缓解一些。

5.2 现象:某一类垃圾完全学不会,预测时全部漏检

最典型的是“有害垃圾”,样本量可能只有几百个框,而“其他垃圾”有一万多个。类别比例悬殊时,模型会把所有和背景相似的像素都压向多数类,少数类的loss贡献几乎为零。训练日志里可以看到这一类别的mAP和小项一直偏低。解决思路有两个:一是在训练配置里把该类的loss权重调高,对应YOLOv8里的cls参数和类别权重;二是做数据层面的平衡,把少数类图片做随机裁剪、旋转、亮度调整后重复拷贝进训练集。我更推荐后者,因为权重调整只能缓解,不能让模型见过真正多样的少数类样本。

5.3 现象:中文类别名在YAML里写得好好的,输出却全是乱码

这个坑和数据集本身关系不大,但几乎每个做过中文标签项目的人都会遇到。Windows下用记事本保存的UTF-8编码文件,传到Linux上偶尔会被误读,或者OpenCV的putText根本不支持中文,画框时显示一串问号。解决办法是全程使用英文别名,比如“recyclable”“kitchen”作为内部标签,单独维护一个id_to_cn.json做映射,只在最终展示时转换中文。这个文件本身是UTF-8,读取时用json.load不会出现编码问题。

5.4 现象:训练到一半,cv2.imread突然返回None,进程报错

之前解压时7z t明明通过了,训练却在中途读到坏图。这种情况多半发生在rar分卷解压过程中,某张图片的尾部数据被截断但CRC校验没有覆盖到,或者存储介质出现了坏扇区。根治办法是在训练前加一道批量读取检测,遍历所有图片文件用cv2.imread验证一遍,把返回None的文件单独列出,删除或重新解压。相信我,这个步骤的花费时间不到五分钟,但能避免训练到第五个小时才发现数据集有问题的惨案。

5.5 现象:验证集精度高,但实拍场景里连矿泉水瓶都识别成“其他垃圾”

这通常是类别定义不一致导致的,不是模型问题。训练数据里的“可回收垃圾”可能只包含纸箱和易拉罐,而实拍场景里的矿泉水瓶形态差异太大,模型没见过就不敢认。检查第2.3节统计出的类别清单,看每个类的图片是否覆盖了足够多的材质、光照和拍摄角度。如果数据集中每个类别都只有一种典型形态,建议先扩充实例数量,再回去调模型结构。

6. 用这份数据集做一次最小验证:20轮短训与单图实测

6.1 先做分层划分,写data.yaml

别把全部数据扔进训练集就开跑。先用脚本按类别分层抽样,划分训练集和验证集,避免某一类的样本全部落在验证集里。最简单的做法是按文件名排序后隔N取1,或者用sklearn的train_test_split加stratify。划分完之后的目录结构大概这样:

garbage_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml

对应的data.yaml:

path: ./garbage_dataset train: images/train val: images/val names: 0: recyclable 1: kitchen 2: hazardous 3: other

names的顺序必须和转换脚本里的CLASSES完全一致,class_id错位是训练后误检率高的最常见原因,没有之一。

6.2 20轮短训:主要看趋势,不看绝对值

露出原生的YOLOv8小模型先跑20轮,验证数据集能不能撑起一个像样的训练。命令大致是:

yolo detect train data=data.yaml model=yolov8n.pt epochs=20 imgsz=640 batch=8

batch=8是针对显存不太充裕的情况,显存够大可以开到16或32。重点看三张曲线图:训练loss是否在10轮之后还在持续下降,val精度是否在爬升,以及混淆矩阵里哪个类别最容易互认。20轮的效果不代表最终效果,但如果20轮连“厨余垃圾”和“可回收垃圾”都分不开,说明数据标注质量有问题,先回去抽检,别急着换大模型。

6.3 导出模型做单图实测,部署前加一道置信度后处理

训练完成后导出普适格式做落地验证,输出ONNX或TensorRT都行。

yolo export model=runs/detect/train/weights/best.pt format=onnx imgsz=640 yolo detect predict model=runs/detect/train/weights/best.pt source=test.jpg conf=0.45

conf这里我习惯设到0.45以上。垃圾分类的场景和通用物体检测不太一样,垃圾桶边上的误检容忍度很低——你把树叶当厨余垃圾问题不大,但把电池当可回收垃圾就是安全隐患。实测时重点看两个方向:一是矿泉水瓶、易拉罐、塑料袋这几种高度反光的物体是否漏检,二是小尺寸垃圾(烟头、电池)是否完全没反应。如果小目标漏检严重,下一步的工作重心就别放在调参上,而是考虑增加小目标相关的数据增强,或者干脆用更高分辨率的输入图。

我自己的经验是:数据集到手后的第一周,至少一半时间花在验收、清洗和格式转换上,这比训练本身更值得投资。这份1.5万张的VOC垃圾分类数据集,如果你的类别清单核对得足够细致,20轮短训后看到一个像样的mAP曲线并不难。但如果你跟我第一次一样,跳过检查直接训练,那大概率会把“数据问题”误判成“模型问题”,最后绕一大圈回来,还是要面对那些本该在第一天就解决的xml。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询