资讯详情 训练前必做:3000张垃圾分类数据集的清洗与校验全流程
📅 2026/10/21 23:13:05
简介包含3000张左右四类垃圾图片的标注数据集面向准备2023年生活垃圾智能分类装置赛项的团队及计算机视觉学习者。数据按有害垃圾、可回收垃圾、厨余垃圾、其他垃圾四类组织涵盖电池、过期药品、易拉罐、矿泉水瓶、小土豆、切块萝卜、瓷片、鹅卵石、砖块等典型物品贴合初赛与决赛赛题要求也便于复现多件垃圾同时投入的检测场景。包体共2000个文件以1998个XML标注文件为主体配套YOLO格式的训练/验证列表文件可直接用于YOLOv5等目标检测框架训练和验证压缩包整体约411.59MB。已有1626人学习下载该数据集能节省大量人工标注时间帮助使用者快速搭建比赛基准模型同时可根据赛题做数据增强、类别均衡等扩展适用于从入门熟悉到赛前冲刺的不同备赛阶段。1. 垃圾数据集拿到手先别急着训练3000张四分类图也要先过安检垃圾分类数据集这东西最容易翻车的不是模型结构而是数据本身。我拆过一套四类垃圾、标注好、约3000张图像的数据集单是训练前做目录级体检就花了将近一晚上。你以为压缩包解压完直接丢进YOLO就能开始跑实际上文件名后缀大小写不一致、空标签、标注越界、类别编号错位这些问题通常要到训练中段loss崩了才发现。这篇笔记就从约3000张四类垃圾标注数据出发把拿到手之后该做的盘目录、验标签、查坏图、划分训练集整套流程写透脚本基本可复现参数和边界也都标清楚。适合准备拿现成数据跑检测或分类任务的一线同事也适合刚入门的同学理解标注数据到底该怎么验。2. 四分类与标注格式先盘清这套数据的目录结构和标签内容2.1 四类垃圾的目录体系与数量分布核对拿到数据集第一步不是看图片而是看目录结构。规范一点的四类数据通常会按可回收物、有害垃圾、厨余垃圾、其他垃圾分成四个一级目录每类目录下再放同名前缀或连续编号的图片与标注文件。但也有人把两千张图平铺在一个目录里单纯用文件名前缀区分类别这种数据训练起来最麻烦因为你还得自己去拆目录。先跑一条命令看整体结构find . -maxdepth 2 -type d | sortmaxdepth设成2是为了只显示一级目录和二级目录如果资源里还有train/val这样的二级划分这里也能直接看出来。sort是为了让输出顺序稳定方便人工扫一眼有没有拼写错误的目录名。我曾经遇到过“可回收物”目录被命名成了“kehuishouwu”的情况排序后一眼就能发现问题。目录结构没问题后再统计每个类目下的图片数和标签文件数import os from pathlib import Path root Path(四类垃圾数据集) for cls_dir in sorted(root.iterdir()): if not cls_dir.is_dir(): continue imgs list(cls_dir.glob(*.jpg)) list(cls_dir.glob(*.png)) labels list(cls_dir.glob(*.txt)) print(f{cls_dir.name}: 图片 {len(imgs)} 张标签文件 {len(labels)} 个)这段脚本的逻辑是遍历一级目录用glob分别收集jpg和png后缀文件再收集txt标注文件。glob模式可以按需补全如果数据里有jpeg、bmp后缀加到列表里即可。脚本跑出来的数字要对照项目描述里“四类共计3000张左右”这个数量级四类加起来是否在3000附近如果某一类明显偏多或偏少大概率是采集阶段没有按最终类别配额来做后续就要考虑类别不平衡的问题。这一步还能顺带发现空目录。如果某些类只有几十张图后面训练出来的模型这类别的mAP大概率会很难看。早点发现早点决定是做增广还是合并类别。2.2 标注文件格式盘点VOC XML、YOLO txt与COCO JSON拿到标注数据后先别急着转格式。用过的标注工具不同输出格式基本分成三类labelImg类工具输出的VOC XMLYOLO训练用的txt以及labelme这类工具导出的COCO JSON。三者字段含义完全不一样混着用最容易出坐标错乱。先抽一个文件看前几行判断格式from pathlib import Path label_file Path(可回收物/img_0001.txt) raw label_file.read_text(encodingutf-8, errorsignore) print(raw[:500])开头是annotation说明是VOC XML每行五个数字用空格分隔说明是YOLO格式的txt如果是一串带images和annotations的JSON结构那就是COCO格式。先确认格式再写后面的校验脚本这一步省下来后面会走弯路。VOC格式的XML解析方式如下import xml.etree.ElementTree as ET tree ET.parse(img_0001.xml) root tree.getroot() for obj in root.findall(object): name obj.find(name).text box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) print(name, xmin, ymin, xmax, ymax)这里解析出的xmin、ymin、xmax、ymax都是像素绝对坐标坐标原点在图片左上角。用VOC做检测训练很多框架内部会自己做转换但如果要转成YOLO的归一化坐标就必须知道原图宽高不能直接用归一化后的比例值去套。YOLO txt格式更简洁每一行代表一个目标with open(img_0001.txt, encodingutf-8) as f: for line in f: parts line.split() if len(parts) ! 5: continue cls_id, x_c, y_c, w, h parts print(f类ID:{cls_id} 中心点:({x_c},{y_c}) 宽高:({w}x{h}))YOLO格式的前置条件很多人会忽略类别ID本身不携带语义信息具体ID对应哪一类黄颜色完全由你写的data.yaml或labels.txt决定。也就是说标签文件里写0不代表一定是可回收物只有配合类别映射文件才有意义。所以拿到数据后先找有没有类别映射文件没有的话就只能从样本图片里人工确认ID和类别的对应关系这一步偷懒后面会吃大亏。如果数据同时包含多个类且XML里name字段混用中英文比如“玻璃瓶”“bottle”“glass_bottle”同时出现那就说明标注时没统一命名。遇到这种要先做一轮字典映射把所有同义名称归到一个ID下再进训练流程。3. 目录级三文件对齐批量脚本查图片、标签、类别号3.1 图片与标签同名对齐找出有图无标和有标无图标注数据的最核心约束是一张图片对应一个同名标签文件。只要文件名对不上训练时就会报“找不到标签”或者隐性漏检。很多数据整合时是人工拷贝的图片和注释文件一旦分开拷贝就会出现一边多一边少的情况。批量检查同名对齐的脚本如下import os from pathlib import Path IMG_SUFFIX {.jpg, .jpeg, .png, .bmp} def check_alignment(root: Path): for cls_dir in root.iterdir(): if not cls_dir.is_dir(): continue imgs {p.stem.lower() for p in cls_dir.iterdir() if p.suffix.lower() in IMG_SUFFIX} txts {p.stem.lower() for p in cls_dir.iterdir() if p.suffix.lower() .txt} no_label sorted(imgs - txts) no_image sorted(txts - imgs) if no_label: print(f[缺标签] {cls_dir.name}: {no_label[:5]} ...) if no_image: print(f[缺图片] {cls_dir.name}: {no_image[:5]} ...)核心逻辑是把图片文件名集合和标签文件名集合做差集。p.stem取文件名去掉后缀的部分lower()是为了让大写的.JPG和小写的.jpg能对上。如果一个目录里缺标签的文件很多说明这批图片是被临时加进数据集做背景负样本的没有对应目标框如果缺图片但标签文件多说明拷贝时掉了图片这种数据训练时一定会因为找不到文件而中断。需要说明的是上面检查的是“同名文件存在”并不代表标签内容就有效。有的标签文件是0字节空文件这种情况在缺标签检查里不会被发现所以还要结合空标签检查一起做。3.2 标签坐标与类别ID合法性检查文件对上了不代表坐标就能直接喂给网络。我实际遇到过的两类问题一是类别ID越界比如只有四类却出现了ID5二是归一化坐标越界x_c或y_c超出[0,1]区间。这类问题在YOLO训练时往往不直接报错但loss会异常波动推理结果也完全不可信。检查脚本可以这样写import shutil from pathlib import Path def check_txt(root: Path, valid_ids(0, 1, 2, 3)): for txt in root.rglob(*.txt): for idx, line in enumerate(txt.read_text(encodingutf-8, errorsignore).splitlines()): parts line.split() if len(parts) ! 5: print(f{txt}: 第{idx1}行字段数不是5实际{len(parts)}) continue try: cls_id, x_c, y_c, w, h map(float, parts) except ValueError: print(f{txt}: 第{idx1}行包含非数字字符) continue if int(cls_id) not in valid_ids: print(f{txt}: 第{idx1}行类别ID越界值为{cls_id}) if not 0 x_c 1 or not 0 y_c 1: print(f{txt}: 第{idx1}行中心点越界坐标为({x_c},{y_c})) if w 0 or h 0 or w 1 or h 1: print(f{txt}: 第{idx1}行宽高异常值为({w}x{h}))脚本会逐行拆分标签文件把五个字段转成浮点数。转不了数字的行直接打印能转数字就检查类别ID和坐标范围。valid_ids参数要按数据集类别实际编号来传有些数据集的类别从0开始四类就是(0,1,2,3)也有的工具从1开始那就改成(1,2,3,4)千万别默认。中心点越界最常见的原因是VOC转YOLO时除以了目标框的宽度而不是原图宽度。比如一张图宽1920某个框xmin100、xmax300正确写法是(100300)/2/1920如果错写成(100300)/2/(300-100)坐标直接变成2.0这种问题不靠脚本检查肉眼看一两张图根本发现不了。处理越界文件时不要直接删除原文件我一般会建一个quarantine目录把问题文件挪进去等修好或者确认不影响训练再放回去。这样至少留了后悔药。3.3 坏图、空标签与后缀统一训练前的物理健康检查还有两类问题不涉及标注内容本身但同样致命图片文件损坏和文件后缀大小写混乱。图片损坏一张两张训练不太受影响但如果某目录里坏图占了上百张那个类别的学习直接缺了数据。用PIL做坏图检查from PIL import Image from pathlib import Path def check_images(root: Path): for p in root.rglob(*): if p.suffix.lower() not in {.jpg, .jpeg, .png}: continue if p.stat().st_size 0: print(f[空文件] {p}) continue try: with Image.open(p) as im: im.verify() except Exception: print(f[坏图] {p})这里im.verify()只读取文件头部并验证完整性不会真的把整张图加载到内存速度快且安全。st_size 0的零字节文件多半是下载中断产生的直接挪走。空标签文件的检查很简单看标签文件大小是否为0find . -name *.txt -size 0一条命令就能列出所有空标签。空标签意味着这张图被当成了纯背景如果没有特别标注背景这类图片训练时会被当作负样本。但检测模型对负样本的期待是“图片中没有目标”如果你标空标签的图片里其实有垃圾但没标模型会在推理时漏检这个目标。所以空标签要么确实没物体要么就该人工补标不能置之不理。最后统一图片后缀for f in $(find . -name *.JPG); do mv $f ${f%.JPG}.jpg; done改成小写后缀是为了避免在Windows压缩、Linux解压的场景下路径匹配出问题。很多加载脚本写死了.jpg一旦数据里有.JPG预处理阶段就会少加载一部分图。如果你用glob通配符方式加载大小写问题不致命但为了减少后续不必要的排错变量建议统一掉。4. 避坑标注数据集常见的五个翻车现场4.1 五个高发坑现象、原因与解决第一个坑是数量分配失衡。现象项目描述写四类约3000张统计之后发现某一类只有两三百张其余全集中在一类。原因是采集阶段按场景批次进行热门类拍了大量重复角度冷门类则很稀有。解决训练前先画各类别bbox数量和图片数量分布对样本量小于另外类别五分之一的类先考虑收集补标或者用该类做增广后再训练。硬拿不平衡数据去训模型会直接偏向多数类。第二个坑是某一类目标面积过小。现象训练日志里loss一直在降但某个类别的mAP几乎为0。原因是这类垃圾在图里占了极小像素面积标注框通常小于20×20对下采样后的检测层来说特征太弱。解决统计所有bbox的宽高分布把面积过小的框单独导出看是漏标还是真的太小。如果是后者可以对该类别单独做裁剪放大训练或调整anchor尺寸而不是硬调loss权重。第三个坑是VOC转YOLO坐标错乱。现象转换后部分行出现x_c或w大于1loss发散不收敛。原因是转换公式里分母用错了常见的是除以bbox的宽度而不是图片宽度。解决转换前打印原图尺寸核对公式(xminxmax)/2/width和(xmax-xmin)/width转换后随机抽查三张图把坐标画回原图对比位置。这个习惯能避免百分之九十的坐标类事故。第四个坑是后端加载图片路径失败。现象训练到某个epoch时报FileNotFoundError检查文件名完全一致但就是读不到。原因大多是后端加载代码用的是小写后缀匹配而实际文件是.JPG大写后缀或者有几张.jpeg混在里面。解决先统一后缀再重建索引同时排除掉.DS_Store、Thumbs.db这类隐藏文件。这两个文件在Windows/macOS解压后会混进目录别看它们不影响训练一旦跑批量处理脚本文件列表会被污染导致莫名其妙的统计误差。第五个坑是类别语义边界混乱。现象同一类物体在一批图里被标成可回收物在另一批里被标成其他垃圾。原因是批标注的人对“是否可回收”的理解不一致甚至同一个人标注到后期标准也会漂移。解决建立一份简短的类别判定表列几个容易混淆物品的归属然后针对这些类别抽20%的样本做人工复核。复核时不要只干活不看图快速浏览缩略图就能发现一批标错类别的样本。4.2 一套通用的数据排查顺序踩坑踩多了之后我给自己定了一个固定的排查顺序拿到任何标注数据都强制按这个顺序走基本不用返工先列目录树看类别组织和文件层级。跑一遍2.1的数量统计脚本确认类目规模与项目描述一致。跑3.1的同名对齐脚本找出图片与标签文件的不匹配项。跑3.2的坐标合法性和3.3的坏图/空标签检查。最后从每类随机挑5到10张图把标注框画出来人工过一眼。这套顺序里前三步都是纯脚本全量跑很快第四步会出若干问题文件需要分类处理第五步是唯一依赖人工的环节但量小控制在半小时内。从那以后我再也没有遇到过训练到一半才发现数据质量崩盘的情况。5. 类别平衡与数据集划分把3000张用明白5.1 按类统计bbox与图片数识别空转类别类目录下的图片数量均衡不代表目标框数量均衡。一张图里可能标了十个瓶子另一张图里只标了一个烟头后者对模型的贡献比前者小得多。所以划分数据集之前先按类别统计bbox数和图片数from collections import Counter from pathlib import Path def count_boxes(root: Path): box_counter Counter() # 每个类别的目标框总数 img_counter Counter() # 含该类别的图片总数 for txt in root.rglob(*.txt): content txt.read_text(encodingutf-8, errorsignore).splitlines() cls_in_img set() for line in content: cls_id line.split()[0] if cls_id.isdigit(): box_counter[cls_id] 1 cls_in_img.add(cls_id) for c in cls_in_img: img_counter[c] 1 return box_counter, img_counterrglob(*.txt)递归遍历所有子目录cls_in_img用集合去重是为了避免同一张图里多框同一类别时把图片数重复累加。如果某个类别的图片数很少但bbox数很高说明该类目标集中在少数几张图上模型学到的可能是图片背景而不是目标本身。这种情况比单纯少样本更隐蔽处理起来也更麻烦。如果发现某类bbox总数不足100我的建议是要么手动补采数据要么把这个类别从主模型里拿掉单独做一个小模型。指望增广能凭空变出有效样本最后得到的就是个“空转”的类别收益很低。5.2 训练/验证集划分脚本按目录抽样的三个细节训练集和验证集的划分关键不是代码多复杂而是不能把同一类图片全部丢进验证集。按文件全局随机看起来公平但小类别很容易被随机洗掉导致验证集里根本没有该类别的样本mAP报告出来严重失真。按类目录抽样是最稳妥的做法import random from pathlib import Path src Path(四类垃圾数据集) train_dir Path(data/train) val_dir Path(data/val) random.seed(2024) for cls_dir in src.iterdir(): if not cls_dir.is_dir(): continue files sorted(cls_dir.iterdir()) random.shuffle(files) val_n max(1, round(len(files) * 0.2)) val_files files[:val_n] for f in val_files: # 图片和标签同名移动保证成对进入验证集 f.rename(val_dir / cls_dir.name / f.name) # 剩余文件留在原处等价于进入训练集 print(f{cls_dir.name}: 验证集 {val_n} 张训练集 {len(files) - val_n} 张)这段代码有几个细节值得注意。random.seed(2024)固定随机种子保证别人复现时划分结果完全一致。val_n max(1, round(len(files) * 0.2))保证每个类目下至少抽出一张进验证集即使这个类目只有两张图。f.rename()直接移动文件比先复制再删除要少一步但移动前要确保data/train和data/val目录已经建好。这里最容易踩的坑是只移动图片不移动标签文件。移动图片后标签还在原目录训练时会发现标签文件数量不匹配然后各种报错。我一般会在移动后立刻跑一遍3.1的对齐检查验证data/val里每个图片都能找到对应标签这个习惯强烈建议保留。5.3 类别平衡的常见处理策略与副作用如果统计下来确实存在明显的类别不平衡我一般按下面的顺序处理而不是一开始就上权重。先做欠采样把多数类图片随机抽减一部分让四个类别的图片数差距缩小。代价是多出来的真实数据被浪费了但训练时间变短分布也更干净。再做增广对少数类使用flip、旋转、HSV扰动把数量补上来。但要注意YOLO的mosaic增强对坐标有联动处理如果你自己做离线增广图片翻转后标注框的坐标必须同步重算不能只翻转图片不翻转标签。最不推荐的做法是用类别权重硬压loss。权重过大时模型会对少数类过拟合多数类精度反而下降总体mAP不升反降。很多同事在这上面耗过很长时间最后还是调低了权重才恢复正常。6. 训练前最后一道工序格式转换与增广取舍6.1 YOLO 与 VOC 格式的双向转换校验YOLO的归一化坐标转回绝对坐标时偶尔会有人直接把中心点坐标当像素用。这里给一个稳妥的转换公式def yolo_to_abs(cls_id, x_c, y_c, w, h, img_w, img_h): x1 (x_c - w / 2.0) * img_w y1 (y_c - h / 2.0) * img_h x2 (x_c w / 2.0) * img_w y2 (y_c h / 2.0) * img_h return cls_id, round(x1), round(y1), round(x2), round(y2)参数说明x_c、y_c是中心点的归一化坐标w、h是目标框的归一化宽高img_w和img_h来自原图的像素尺寸必须用PIL或OpenCV读取不能靠猜。转换后把矩形画回图上人工核对两个目标位置对齐了再继续。6.2 增广开关与坐标联动最后说增广。垃圾分类场景下颜色本身是有判别力的玻璃和塑料在不同光泽下颜色差异很大HSV的色相扰动别调太猛hue偏移超过0.1就容易让同一物体在训练集里变成两个颜色。水平翻转对瓶罐类很安全但要注意标签x坐标要同步翻转对于烟头这类只有文字信息且不对称的样本翻转反而会引入不存在的情景可以把flip概率设低一点。用mosaic时尽量交给框架内置处理不要自己离线拼图坐标拼接很容易出错。增广本身救不了少样本只能缓解特征不足的问题。从那以后我拿到任何标注好的数据集第一件事都是固定走一遍目录统计、格式识别、同名对齐、坐标校验、坏图过滤这条链路全部跑完才允许自己碰模型。这套流程已经成了我的默认习惯毕竟离线数据体检花一小时远好过训练十个epoch后才发现问题推倒重来。希望帮到你。本文还有配套的精品资源点击获取