YOLO垃圾分类数据集实战:VOC/COCO/YOLO三格式转换、划分与训练避坑指南
2026/9/23 16:48:38 网站建设 项目流程

简介:本资源为面向目标检测学习者的YOLO垃圾分类检测数据集,适用于课程设计、毕业设计及算法入门实战,帮助解决真实场景下垃圾分类数据难获取、标注格式不统一的问题。压缩包共2000个文件,约410.27MB,以1985个xml标注文件为主,另含少量txt、html与py脚本,分别对应标签数据、教程说明和划分工具。数据集采用labelimg标注,标注框质量高,同时提供voc、coco和yolo三种格式标签,分文件夹存放,可直接接入YOLO系列模型训练。资源还附赠环境搭建与训练案例教程,覆盖Windows与Linux版本,并提供训练集、验证集、测试集划分脚本,可按需自行切分数据。目前已有1346人学习下载,适合希望快速跑通垃圾分类检测流程、掌握数据格式转换与训练配置的读者参考使用。

1. 拿到一个 10000 张的 YOLO 垃圾分类数据集,先别急着开训

你从群里或者某个资源站下到一个压缩包,名字叫「YOLO垃圾分类检测数据集(含10000张图片)+对应voc、coco和yolo三种格式标签+划分脚本+训练教程.rar」。解压之后大概率是一堆文件夹:JPEGImages、Annotations、images、labels、train、val,还有几个 .py 脚本和一个 Word 教程。很多人第一反应是直接yolo train data=xxx.yaml,然后报一堆路径错误、类别错位、标签全 0 的玄学问题。

这个标题真正值钱的地方不是那 10000 张图,而是「同一批图同时给了 VOC、COCO、YOLO 三种格式标签」这件事。VOC 的 XML 是给人看的、方便改框;COCO 的 JSON 是给 mmdetection、detectron2 这类框架用的;YOLO 的 txt 是给 ultralytics 系直接吃的。三种格式并存意味着你可以一套数据跑通多个技术栈,也意味着你必须先搞清楚它们之间的坐标换算关系,否则划分脚本一跑,标签和图片就对不上了。

这篇笔记面向两类人:手里已经有类似压缩包、想把它跑起来的新手;以及想搞清楚「多格式标签共存时怎么组织目录、怎么校验、怎么避免训练时 mAP 异常」的熟手。下面按「先看懂结构 → 再动手转换和划分 → 再训练 → 再排坑 → 最后讲进阶校验」的顺序走一遍,每一步都给能直接抄的命令和脚本。

2. 三种标签格式到底差在哪:坐标、类别与目录约定

2.1 VOC、COCO、YOLO 的坐标定义差异

先把最容易翻车的地方说清楚:三种格式的框坐标定义完全不同,混用必错。

VOC 的 XML 里,<bndbox>存的是xmin, ymin, xmax, ymax,是绝对像素坐标,原点在左上角,且是包含边界的整数。一个典型文件长这样:

<annotation> <folder>JPEGImages</folder> <filename>000001.jpg</filename> <size> <width>640</width> <height>480</height> <depth>3</depth> </size> <object> <name>plastic</name> <bndbox> <xmin>112</xmin> <ymin>96</ymin> <xmax>308</xmax> <ymax>402</ymax> </bndbox> </object> </annotation>

COCO 的 JSON 里,bbox[x, y, width, height],同样是绝对像素,但注意是宽高而不是右下角坐标,而且category_id通常从 1 开始(0 一般留给 background)。这一点和 YOLO 从 0 开始编号直接冲突,是类别错位的高发区。

YOLO 的 txt 每行是class_id cx cy w h,全部是归一化到 0~1 的相对值cx cy是框中心点。换算公式:

cx = (xmin + xmax) / 2 / img_w cy = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h

提示:归一化时如果用了错误的 img_w/img_h(比如读的是缩略图尺寸而不是原图),框会整体偏移,训练时 loss 能降但 mAP 上不去,这是最隐蔽的坑之一。

2.2 目录组织:一套图喂三种格式的推荐结构

压缩包里常见的目录是扁平的,直接拿去训练会乱。我一般整理成下面这样,图片只存一份,标签分目录:

dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ ├── annotations_voc/ # 原始 XML,保留用于改框 ├── annotations_coco/ # 转换出的 COCO json ├── classes.txt # 类别名,一行一个 └── data.yaml # ultralytics 训练配置

关键点是images/train/000001.jpg必须对应labels/train/000001.txt文件名(不含扩展名)严格一致。YOLO 训练时是按文件名去 labels 目录找同名 txt 的,找不到就当作负样本(背景),于是你会看到「训练正常但什么都检测不到」。

classes.txt的顺序就是 class_id 的顺序,第 0 行对应 id 0。这个文件一旦定了,VOC 转 YOLO、COCO 转 YOLO 都必须按它来映射,不能各转各的。

2.3 类别映射表:多格式共存时最容易错位的一环

垃圾分类常见类别是 4 类:可回收物(recyclable)、厨余(kitchen)、有害(hazardous)、其他(other)。但不同来源的 VOC XML 里<name>可能写的是中文、英文、甚至拼音。COCO 的categories里 id 又可能是 1~4。所以转换前必须先建一张映射表:

统一 class_id统一名称VOC name 可能写法COCO category_id
0recyclablerecyclable / 可回收1
1kitchenkitchen / 厨余2
2hazardoushazardous / 有害3
3otherother / 其他4

这张表要落到代码里,而不是靠脑子记。下面转换脚本里会用到它。

3. 从 VOC 转 YOLO:转换脚本与四个边界坑

3.1 转换脚本:XML 批量转归一化 txt

假设你的 XML 在annotations_voc/,图片在images_all/,输出到labels_all/。脚本如下:

import os import xml.etree.ElementTree as ET from PIL import Image # 类别映射:VOC 里的 name -> 统一 class_id NAME2ID = { "recyclable": 0, "可回收": 0, "kitchen": 1, "厨余": 1, "hazardous": 2, "有害": 2, "other": 3, "其他": 3, } VOC_DIR = "annotations_voc" IMG_DIR = "images_all" OUT_DIR = "labels_all" os.makedirs(OUT_DIR, exist_ok=True) def convert(xml_path): tree = ET.parse(xml_path) root = tree.getroot() filename = root.find("filename").text # 优先用 XML 里记录的 size,避免重新读图 size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in NAME2ID: print(f"[skip] unknown class {name} in {xml_path}") continue cls_id = NAME2ID[name] 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) # 边界裁剪,防止越界 xmin = max(0, min(xmin, img_w - 1)) xmax = max(0, min(xmax, img_w - 1)) ymin = max(0, min(ymin, img_h - 1)) ymax = max(0, min(ymax, img_h - 1)) if xmax <= xmin or ymax <= ymin: 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"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") out_name = os.path.splitext(filename)[0] + ".txt" with open(os.path.join(OUT_DIR, out_name), "w") as f: f.write("\n".join(lines)) for f in os.listdir(VOC_DIR): if f.endswith(".xml"): convert(os.path.join(VOC_DIR, f)) print("done")

逻辑说明:脚本读 XML 的size拿原图宽高,而不是重新打开图片,这样即使图片被移动也能转。类别通过NAME2ID统一映射,遇到未知类别打印并跳过,避免静默丢数据。坐标做了裁剪和退化框过滤,防止xmax < xmin这种脏标注产生负宽高。

参数说明:NAME2ID必须和classes.txt顺序一致;OUT_DIR输出的 txt 文件名取自 XML 里的filename,所以 XML 里的 filename 必须和实际图片名一致,否则对不上。

3.2 坑一:XML 里的 filename 和实际图片名不一致

现象:转换后 labels 目录里有一堆 txt,但训练时提示找不到对应图片,或者图片被当负样本。

原因:很多数据集在整理时改过图片名(比如加了前缀),但 XML 里的<filename>没同步改。

解决:转换前先做一次一致性检查,用下面这段脚本列出不匹配项:

import os xml_names = {os.path.splitext(f)[0] for f in os.listdir("annotations_voc") if f.endswith(".xml")} img_names = {os.path.splitext(f)[0] for f in os.listdir("images_all") if f.lower().endswith((".jpg", ".png", ".jpeg"))} print("XML 有但图片没有:", xml_names - img_names) print("图片有但 XML 没有:", img_names - xml_names)

3.3 坑二:图片被缩放但 XML 记录的是原尺寸

现象:转出来的框整体偏小或偏大,训练 loss 正常但框位置系统性偏移。

原因:XML 的size是原图尺寸,但实际喂给训练的图片被预处理缩放过,或者反过来。

解决:以实际图片尺寸为准重算。把脚本里读size的部分改成用 PIL 打开图片取img.size,并加一句断言:

from PIL import Image img = Image.open(os.path.join(IMG_DIR, filename)) img_w, img_h = img.size assert img_w == int(size.find("width").text), f"size mismatch: {filename}"

3.4 坑三:类别名带空格或大小写不一致

现象:NAME2ID查不到,大量框被 skip,转出来的 txt 是空的。

原因:XML 里写的是"Plastic "(带尾空格)或"Recyclable"(首字母大写)。

解决:在name = obj.find("name").text.strip()之后统一.lower(),并把映射表的 key 也全部小写。中文类别不受影响,但英文类别必须做这一步。

3.5 坑四:空标签文件与负样本的取舍

现象:某些图片确实没有目标,转换后生成空 txt,训练时这些图被当作纯背景。

原因:YOLO 允许空 txt 表示负样本,但比例过高会拉低召回。

解决:统计空 txt 比例,超过 10% 就考虑剔除或补充标注:

find labels_all -name "*.txt" -empty | wc -l find labels_all -name "*.txt" | wc -l

4. 划分脚本怎么写:train/val 不能只按 8:2 随机切

4.1 为什么随机划分在垃圾分类上会翻车

垃圾分类数据集的采集往往有场景聚集性:同一批图可能来自同一个小区、同一次拍摄。如果纯随机按 8:2 切,训练集和验证集里会出现几乎相同的背景,验证 mAP 虚高,上线后一塌糊涂。常见做法是按「采集批次」或「图片前缀」分组后再切,保证验证集里的场景训练时没见过。

如果压缩包没给批次信息,退而求其次:先按文件名排序,再按固定间隔抽样做验证集,而不是random.shuffle。这样至少保证分布均匀。

4.2 划分脚本:图片和标签同步搬移

import os import shutil import random random.seed(42) # 固定种子,保证可复现 IMG_SRC = "images_all" LBL_SRC = "labels_all" OUT = "dataset" for split in ["train", "val"]: os.makedirs(f"{OUT}/images/{split}", exist_ok=True) os.makedirs(f"{OUT}/labels/{split}", exist_ok=True) names = sorted([os.path.splitext(f)[0] for f in os.listdir(IMG_SRC) if f.lower().endswith((".jpg", ".png", ".jpeg"))]) # 按间隔抽样做验证集,避免场景聚集 val_idx = set(range(0, len(names), 10)) # 每 10 张取 1 张做 val,约 10% for i, name in enumerate(names): split = "val" if i in val_idx else "train" # 找图片(扩展名可能不同) for ext in [".jpg", ".png", ".jpeg"]: src_img = os.path.join(IMG_SRC, name + ext) if os.path.exists(src_img): shutil.copy(src_img, f"{OUT}/images/{split}/{name}{ext}") break src_lbl = os.path.join(LBL_SRC, name + ".txt") if os.path.exists(src_lbl): shutil.copy(src_lbl, f"{OUT}/labels/{split}/{name}.txt") else: # 没有标签就写空文件,表示负样本 open(f"{OUT}/labels/{split}/{name}.txt", "w").close() print("train:", len(os.listdir(f"{OUT}/images/train"))) print("val:", len(os.listdir(f"{OUT}/images/val")))

逻辑说明:random.seed(42)保证每次划分结果一致,方便复现实验。用range(0, len(names), 10)做等间隔抽样,比随机抽样更能打散场景聚集。图片和标签同步搬移,缺标签的补空文件,避免训练时报「label missing」。

参数说明:间隔 10 约等于 9:1,想改成 8:2 就把步长改成 5。seed换掉会得到不同划分,但同一 seed 下结果稳定。

4.3 data.yaml 的写法与路径陷阱

ultralytics 系训练靠一个 yaml 描述数据位置和类别:

path: /abs/path/to/dataset train: images/train val: images/val nc: 4 names: 0: recyclable 1: kitchen 2: hazardous 3: other

注意:path建议写绝对路径。相对路径在不同工作目录下启动训练会解析成不同结果,是「本地能跑、换台机器就报找不到文件」的常见原因。names的顺序必须和classes.txt、转换脚本里的NAME2ID完全一致。

4.4 划分后必做的三项校验

划分完别急着训,先跑三个检查:

# 1. 图片和标签数量是否一致 ls dataset/images/train | wc -l ls dataset/labels/train | wc -l # 2. 有没有越界坐标(大于 1 或小于 0) awk '{if($2>1||$3>1||$4>1||$5>1||$2<0||$3<0||$4<0||$5<0) print FILENAME}' dataset/labels/train/*.txt | head # 3. 类别 id 是否超出 nc 范围 awk '{print $1}' dataset/labels/train/*.txt | sort -u

第 3 条输出的最大 id 必须小于nc,否则训练时直接报 index 越界。

5. 训练与排坑:从 yaml 到第一个能用的权重

5.1 最小训练命令与关键参数

环境按 ultralytics 官方方式装好后,最小训练命令:

yolo detect train \ data=/abs/path/to/dataset/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ workers=4 \ project=runs/garbage \ name=exp1

参数说明:model用预训练权重比从头训收敛快得多,垃圾分类这种中等规模数据强烈建议用预训练。imgsz=640是通用起点,如果小目标多(比如瓶盖、电池)可以提到 960,但显存和速度要权衡。batch根据显存调,OOM 就减半。workers在 Windows 上设 0 或 2,设太高容易卡死。

5.2 训练日志里该盯哪几个数

box_losscls_lossdfl_loss三条曲线要一起看。正常情况是前期快速下降后趋缓。如果cls_loss一直不降,多半是类别映射错了或者标签里类别 id 混乱。mAP50mAP50-95是验证指标,mAP50涨但mAP50-95不涨,说明框位置不够准,可能是归一化尺寸问题。

5.3 避坑与排查:五条血泪记录

现象一:训练启动就报No labels found原因:data.yamltrain路径指向的目录下没有 labels,或者图片和标签不在同一父目录结构下。 解决:确认dataset/images/traindataset/labels/train同级存在,且 yaml 里写的是images/train而不是train

现象二:训练正常但推理时框全在左上角。原因:标签坐标没归一化,或者归一化时除错了尺寸(比如除了 255)。 解决:抽查几个 txt,确认所有值都在 0~1 之间,且cx cy大致在 0.5 附近而不是几百。

现象三:mAP 一直是 0。原因:类别 id 从 1 开始编号(COCO 习惯),但ncnames从 0 开始,导致所有框的类别都越界被忽略。 解决:统一改成从 0 开始,或在转换时做id - 1

现象四:验证集 mAP 很高,实际用起来一塌糊涂。原因:划分时场景泄漏,验证集和训练集背景几乎一样。 解决:按采集批次分组划分,或加大验证集比例并人工检查验证集图片是否和训练集重复。

现象五:训练到一半 loss 变 NaN。原因:标签里有退化框(宽或高为 0)或坐标越界,导致除零。 解决:转换脚本里加退化框过滤(前面已给),训练前用 awk 检查越界。

5.4 COCO 格式什么时候用得上

如果你后面想换 mmdetection 或做实例分割,COCO json 就派上用场了。从 YOLO txt 转 COCO 的核心是把归一化坐标还原成绝对[x, y, w, h],并维护imagesannotationscategories三个数组。转换时注意category_id从 1 开始,image_id唯一,annotation id全局唯一。这块脚本较长,思路和 VOC 转 YOLO 对称,关键是别把bbox写成[xmin, ymin, xmax, ymax]——COCO 要的是宽高。

6. 进阶:用可视化校验把标签问题在上线前挖出来

训练前最值钱的一步不是调参,是把标签画到图上看一眼。我习惯写一个可视化脚本,把 YOLO txt 还原成框画在原图上,随机抽 20 张拼成网格。很多坐标错位、类别错标、框漂移的问题,肉眼看一遍就出来了,比盯着 loss 曲线猜快得多。

import os import random import cv2 NAMES = ["recyclable", "kitchen", "hazardous", "other"] COLORS = [(0,255,0), (255,0,0), (0,0,255), (255,255,0)] def draw(img_path, lbl_path): img = cv2.imread(img_path) h, w = img.shape[:2] if os.path.exists(lbl_path): for line in open(lbl_path): parts = line.strip().split() if len(parts) != 5: continue cid, cx, cy, bw, bh = int(parts[0]), *map(float, parts[1:]) x1 = int((cx - bw/2) * w) y1 = int((cy - bh/2) * h) x2 = int((cx + bw/2) * w) y2 = int((cy + bh/2) * h) cv2.rectangle(img, (x1,y1), (x2,y2), COLORS[cid % len(COLORS)], 2) cv2.putText(img, NAMES[cid], (x1, max(0,y1-5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, COLORS[cid % len(COLORS)], 2) return img names = [f for f in os.listdir("dataset/images/train") if f.endswith(".jpg")] random.seed(0) sample = random.sample(names, min(20, len(names))) rows = [] for n in sample: stem = os.path.splitext(n)[0] img = draw(f"dataset/images/train/{n}", f"dataset/labels/train/{stem}.txt") img = cv2.resize(img, (320, 320)) rows.append(img) # 拼成 4x5 网格 import numpy as np grid = np.vstack([np.hstack(rows[i*5:(i+1)*5]) for i in range(4)]) cv2.imwrite("check_grid.jpg", grid) print("saved check_grid.jpg")

逻辑说明:脚本把归一化坐标乘回原图宽高,还原成像素框,再按类别上色。拼成网格后一眼能看出框是否贴合目标、类别颜色是否和物体对得上。如果发现某类框系统性偏移,基本就是归一化尺寸或坐标定义错了。

参数说明:COLORS按类别数扩展,sample数量按需调。random.seed(0)保证每次抽同样的图,方便对比修改前后的效果。

除了可视化,还有两个我常做的验证:一是用yolo detect val在验证集上跑一遍,看每类的 P/R 是否均衡,某一类特别低通常是该类样本太少或标注质量差;二是把训练好的权重在几张训练集图片上推理,如果训练集都检不出来,说明标签或类别映射有硬伤,别急着调超参。

我自己的习惯是:拿到任何多格式数据集,先花半小时做「一致性检查 + 可视化抽查」,再开始训练。这半小时能省掉后面几小时的瞎调参。垃圾分类这类场景,类别边界本来就模糊(比如沾了油的纸盒算可回收还是其他),标注一致性比模型结构重要得多。希望帮到你。

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

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

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

立即咨询