ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

垃圾箱数据集VOC格式目标标注457张:VOC转YOLO训练与避坑指南

垃圾箱数据集VOC格式目标标注457张:VOC转YOLO训练与避坑指南 简介这份垃圾箱数据集面向计算机视觉初学者与目标检测开发者用于训练和验证垃圾箱识别模型可直接接入YOLO、Faster R-CNN等主流框架。资源包共914个文件包含457张jpg图片与457个对应的xml标注文件压缩包约26.79MB采用rar格式且无需解压密码解压后图片与标注分文件夹存放便于直接查看与调用。所有图片均由labelImg人工标注类别统一为dustbin标注过程遵循准确框选目标边界、尽量覆盖全部目标、完成后交叉检查一致性等原则标注质量较为可靠。目前已有94人学习下载适合作为小规模检测任务的训练集或数据增强素材也可用于验证标注流程与模型效果帮助读者快速搭建垃圾箱检测实验环境。1. 垃圾箱数据集 VOC 格式目标标注 457 张一份能直接喂给检测模型的现成料手里有一批园区或街道的垃圾箱监控图想训一个 dustbin 单类检测器最耗时的往往不是调模型而是从零标 457 张图。这份「垃圾箱数据集 VOC 格式目标标注 457 张」解决的正是这个前置环节它已经用 labelImg 把每张图里的垃圾箱框好导出成 VOC 的 jpg xml 配对结构解压就能看、能改、能直接转 YOLO 或 COCO 训练。适合两类人——一类是想快速验证单类检测 pipeline 的算法同学另一类是手头有垃圾满溢识别、环卫巡检需求、却卡在数据标注的工程同学。457 张不算大但作为冷启动的第一批种子数据够你把训练、评估、部署这条链路先跑通再决定要不要扩标。下面按「这份数据长什么样 → 怎么接进训练 → 坑在哪」的顺序拆开讲。2. VOC 结构与 labelImg 标注逻辑先看懂 xml 里到底存了什么2.1 目录结构与文件配对关系拿到压缩包解压后常见做法是得到两个平级文件夹一个放 jpg 图片一个放同名 xml 标注。VOC 的核心约定就是「图片名与 xml 名一一对应」比如IMG_621.jpg对应IMG_621.xml。正文里列出的IMG_621.jpg、IMG_966.jpg、o_1difg7ogo7sn1v9n14991b3f1i2qaq.jpg这些就是典型样本名命名不统一是正常的因为来源可能是手机拍摄加网络图混编只要配对不错就行。先做一次配对自检这是后面所有转换的前提# 统计图片与标注数量确认是否各 457 ls JPEGImages/*.jpg | wc -l ls Annotations/*.xml | wc -l # 找出有图无标注、有标注无图的孤儿文件 comm -3 (ls JPEGImages | sed s/\.jpg$// | sort) \ (ls Annotations | sed s/\.xml$// | sort)逻辑说明comm -3会输出两个已排序列表的差集第一列是有图无标注第二列是有标注无图。参数上sed s/\.jpg$//去掉扩展名是为了让两边能按纯文件名比对。如果这里输出为空说明 457 对完整如果有输出先别急着训练缺标注的图要么补标要么剔除否则训练时读不到对应 xml 会直接报错。2.2 xml 字段逐项拆解VOC 的 xml 结构固定关键字段就几个。打开任意一个 xml你会看到folder、filename、size、object这几块。size里的width、height、depth决定坐标是否越界object里的name就是类别名这份数据统一是dustbinbndbox里的xmin/ymin/xmax/ymax是左上角和右下角像素坐标。import xml.etree.ElementTree as ET from pathlib import Path def parse_voc(xml_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w, h int(size.find(width).text), int(size.find(height).text) boxes [] for obj in root.findall(object): name obj.find(name).text bb obj.find(bndbox) xmin int(float(bb.find(xmin).text)) ymin int(float(bb.find(ymin).text)) xmax int(float(bb.find(xmax).text)) ymax int(float(bb.find(ymax).text)) boxes.append((name, xmin, ymin, xmax, ymax)) return w, h, boxes # 抽查一张看坐标是否落在图像范围内 w, h, boxes parse_voc(Annotations/IMG_621.xml) print(w, h, boxes) for name, x1, y1, x2, y2 in boxes: assert 0 x1 x2 w, fx 越界: {x1},{x2} assert 0 y1 y2 h, fy 越界: {y1},{y2}逻辑说明int(float(...))是为了兼容个别工具导出成12.0这种浮点字符串的情况直接int()会抛异常。断言部分是在做边界校验VOC 里坐标越界是训练崩溃和 mAP 异常的常见根因。参数上xmin xmax、ymin ymax必须成立如果出现相等或反向说明标注时框拉反了得回 labelImg 修。2.3 为什么这份数据选 VOC 而不是直接给 YOLO txtVOC 是中间格式里的「通用货币」。labelImg 原生导出 VOC而 YOLO 训练要的是class x_center y_center w h的归一化 txtCOCO 要的是 json。拿到 VOC 后往哪个框架转都行反过来只有 txt 想补字段就麻烦。所以这份数据保留 VOC 是合理的转换成本一次性付出后面复用性最高。常见做法是写一个转换脚本把 VOC 一次性转成 YOLO训练时直接读 txt不再碰 xml。3. 从 VOC 转 YOLO 训练格式转换脚本与归一化参数3.1 坐标归一化的数学关系YOLO 的标注是相对值公式固定x_center (xmin xmax) / 2 / widthy_center (ymin ymax) / 2 / heightw (xmax - xmin) / widthh (ymax - ymin) / height。四个值都落在 0~1。这里最容易翻车的是把width/height用错——必须用该图自己的尺寸不能用统一值因为这份数据图片大小是 1–500KB 不等分辨率并不一致。import xml.etree.ElementTree as ET from pathlib import Path CLASSES [dustbin] # 单类顺序即类别 id IMG_DIR Path(JPEGImages) XML_DIR Path(Annotations) OUT_LABEL_DIR Path(labels) OUT_LABEL_DIR.mkdir(exist_okTrue) def voc_to_yolo(xml_path, out_path): root ET.parse(xml_path).getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in CLASSES: continue # 类别不在白名单直接跳过避免 id 错位 cls_id CLASSES.index(name) bb obj.find(bndbox) x1 float(bb.find(xmin).text) y1 float(bb.find(ymin).text) x2 float(bb.find(xmax).text) y2 float(bb.find(ymax).text) # 裁剪到图像边界防止个别越界框污染训练 x1, y1 max(0, x1), max(0, y1) x2, y2 min(w, x2), min(h, y2) xc (x1 x2) / 2 / w yc (y1 y2) / 2 / h bw (x2 - x1) / w bh (y2 - y1) / h lines.append(f{cls_id} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}) out_path.write_text(\n.join(lines)) for xml_file in XML_DIR.glob(*.xml): voc_to_yolo(xml_file, OUT_LABEL_DIR / (xml_file.stem .txt))逻辑说明CLASSES的顺序决定类别 id单类时就是 0千万别在训练配置里又改成 1 起始否则类别对不上。max(0, x1)和min(w, x2)是边界裁剪属于防御性写法。:.6f保留六位小数YOLO 官方推荐精度太少会丢框。转换完记得抽查一个 txt确认每行五个值、且都在 0~1。3.2 划分 train/val 与生成数据配置文件457 张按 8:2 划分约 365 训练、92 验证。划分要固定随机种子保证可复现。import random from pathlib import Path random.seed(42) imgs sorted(p.stem for p in Path(JPEGImages).glob(*.jpg)) random.shuffle(imgs) split int(len(imgs) * 0.8) train, val imgs[:split], imgs[split:] for name, subset in [(train, train), (val, val)]: with open(f{name}.txt, w) as f: for stem in subset: f.write(f./images/{stem}.jpg\n) print(len(train), len(val))逻辑说明random.seed(42)固定划分避免每次跑结果不同导致指标无法对比。写出的train.txt/val.txt是图片路径清单YOLOv5/v8 都认这种格式。参数上路径前缀./images/要和你实际的数据根目录一致放错会导致训练时找不到图。对应的data.yamlpath: ./dustbin_dataset train: train.txt val: val.txt nc: 1 names: [dustbin]nc是类别数单类填 1names顺序必须和转换脚本里的CLASSES完全一致这是类别 id 映射的唯一依据。3.3 用 YOLOv8 起一次训练验证数据可用数据转好后最直接的验证就是跑几个 epoch 看 loss 是否正常下降。yolo detect train \ data./dustbin_dataset/data.yaml \ modelyolov8n.pt \ epochs50 \ imgsz640 \ batch16 \ projectruns/dustbin \ nameexp1逻辑说明modelyolov8n.pt用 nano 版先验证链路数据量小、训练快。imgsz640是通用输入尺寸若原图普遍偏小可降到 416 提速。batch16视显存调整爆显存就减半。跑完看runs/dustbin/exp1下的results.csv如果train/box_loss在前几个 epoch 就发散成 nan八成是标注坐标有问题回到 3.1 的边界校验重查。4. 标注质量与数据分布457 张够不够、偏不偏4.1 单类 457 张的可用边界单类检测457 张属于「能跑通、别指望高精度」的量级。经验上单类 500 张左右配合迁移学习mAP0.5 做到 0.7 以上是现实的但前提是场景别太杂。如果这 457 张里既有白天又有夜间、既有近景又有远景、既有完整箱体又有被遮挡的那模型要覆盖的分布就宽457 张会被摊薄。判断方法很简单先按亮度、尺寸做聚类看样本是否集中在某几类场景。import cv2 import numpy as np from pathlib import Path stats [] for p in Path(JPEGImages).glob(*.jpg): img cv2.imread(str(p)) h, w img.shape[:2] gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) stats.append((w, h, gray.mean())) arr np.array(stats) print(分辨率范围:, arr[:, :2].min(0), arr[:, :2].max(0)) print(亮度均值分布:, np.percentile(arr[:, 2], [10, 50, 90]))逻辑说明gray.mean()粗略反映明暗np.percentile看 10/50/90 分位如果 10 分位和 90 分位差距很大说明明暗跨度大模型需要更强的数据增强。分辨率范围能告诉你是否要统一 resize差异过大时建议训练时开 mosaic 增强。4.2 一致性检查怎么做标注一致性是这份数据说明里明确提到的要求。单标注者的情况下一致性检查靠「二次抽检」随机抽 30~50 张重新看框是否贴边、是否漏标。漏标是单类检测里最隐蔽的问题——模型会把没框的垃圾箱学成背景直接拉低召回。import random from pathlib import Path random.seed(7) xmls list(Path(Annotations).glob(*.xml)) sample random.sample(xmls, 40) for x in sample: print(x.name)把抽出的文件名列出来逐张在 labelImg 里过一遍重点看三类问题框是否包含整个箱体、多个箱体是否都标了、边界是否明显偏移。这一步没有捷径但 40 张的抽检成本可控能挡掉大部分系统性错误。4.3 数据增强补足样本量457 张直接训容易过拟合常见做法是开增强。YOLO 系列默认带 mosaic、HSV 抖动、翻转。对垃圾箱这种目标水平翻转安全垂直翻转要谨慎——现实里垃圾箱不会倒过来垂直翻转可能引入不真实样本。HSV 的hsv_v可以调高一点因为这份数据明暗跨度可能较大。# 训练时在命令行追加增强参数 hsv_h: 0.015 hsv_s: 0.7 hsv_v: 0.4 flipud: 0.0 # 关闭上下翻转 fliplr: 0.5 mosaic: 1.0参数说明hsv_v: 0.4增强亮度扰动应对明暗差异flipud: 0.0关闭垂直翻转避免不真实样本mosaic: 1.0四图拼接小数据集提效明显但训练后期可降到 0.5 让模型收敛更稳。5. 避坑与排查VOC 转 YOLO 最容易翻车的五个点5.1 现象训练报「找不到标签文件」→ 原因txt 与图片不同目录或不同名 → 解决核对路径与 stemYOLO 默认按图片路径把.jpg替换成.txt去找标签。如果你的图片在images/、标签在labels/那data.yaml里的路径结构必须让框架能推导出对应关系。最稳的做法是保持images/train/与labels/train/平行或者用清单文件显式指定。报错时先ls一下对应目录确认同名 txt 真的存在。5.2 现象mAP 一直是 0 → 原因类别 id 或 names 顺序错位 → 解决对齐 CLASSES 与 data.yaml单类最容易犯的错是把nc写成 0 或者names写成别的字符串。转换脚本里dustbin映射到 0data.yaml里names: [dustbin]也是 0两边必须一致。如果转换时用了CLASSES [bin, dustbin]而 yaml 只写一个id 就全错。排查方法打印一个 txt 的首列应该全是 0。5.3 现象框位置整体偏移 → 原因归一化用了错误的宽高 → 解决逐图读取 size有人图省事用固定 640×640 去除结果原图不是这个尺寸框全偏。VOC 的 xml 里size字段就是该图真实宽高必须逐图读取。验证方法把某个 txt 反算回像素坐标和 xml 里的bndbox对比应该完全吻合。5.4 现象训练 loss 正常但验证集崩 → 原因train/val 划分有重叠或分布不均 → 解决固定种子并检查清单如果划分时没固定种子或者同一张图同时进了 train 和 val验证指标会虚高或异常。检查train.txt和val.txt是否有交集用comm -12一比就知道。另外如果 val 里全是夜间图而 train 全是白天指标也会崩这时要按场景分层抽样。5.5 现象个别图训练时报坐标越界 → 原因标注框超出图像边界 → 解决转换时裁剪并记录labelImg 里手滑把框拉出图像边界是常事。转换脚本里的max(0, x1)、min(w, x2)能兜住但更好的做法是把被裁剪的样本名打印出来回头修标注。裁剪只是让训练不崩不代表标注正确。6. 进阶把这份数据接进 COCO 评估与半自动预标注6.1 VOC 转 COCO 做标准评估YOLO 自带的评估够用但如果你要和别的模型横向比COCO 的 mAP 口径更通用。VOC 转 COCO 的核心是把bndbox转成[x, y, w, h]加category_id并生成images、annotations、categories三段结构。import json import xml.etree.ElementTree as ET from pathlib import Path coco {images: [], annotations: [], categories: [{id: 1, name: dustbin}]} ann_id 1 for img_id, xml_path in enumerate(sorted(Path(Annotations).glob(*.xml)), start1): root ET.parse(xml_path).getroot() size root.find(size) w, h int(size.find(width).text), int(size.find(height).text) fname root.find(filename).text coco[images].append({id: img_id, file_name: fname, width: w, height: h}) for obj in root.findall(object): bb obj.find(bndbox) x1 float(bb.find(xmin).text) y1 float(bb.find(ymin).text) x2 float(bb.find(xmax).text) y2 float(bb.find(ymax).text) coco[annotations].append({ id: ann_id, image_id: img_id, category_id: 1, bbox: [x1, y1, x2 - x1, y2 - y1], area: (x2 - x1) * (y2 - y1), iscrowd: 0 }) ann_id 1 Path(instances.json).write_text(json.dumps(coco))逻辑说明COCO 的bbox是[左上x, 左上y, 宽, 高]和 VOC 的[xmin, ymin, xmax, ymax]差一步减法这是转换里最容易写错的地方。category_id从 1 开始和 YOLO 的 0 起始不同别混用。area用于评估时区分大小目标。6.2 用训好的模型做半自动预标注457 张标完只是起点。当你有了第一批模型可以用它给新图打预标注再人工修正效率比纯手标高数倍。流程是模型推理出框 → 写成 VOC xml → 导入 labelImg 微调。from ultralytics import YOLO import xml.etree.ElementTree as ET from pathlib import Path model YOLO(runs/dustbin/exp1/weights/best.pt) results model.predict(new_images, conf0.4, saveFalse) for r in results: h, w r.orig_shape root ET.Element(annotation) ET.SubElement(root, filename).text Path(r.path).name size ET.SubElement(root, size) ET.SubElement(size, width).text str(w) ET.SubElement(size, height).text str(h) ET.SubElement(size, depth).text 3 for box in r.boxes: x1, y1, x2, y2 box.xyxy[0].tolist() obj ET.SubElement(root, object) ET.SubElement(obj, name).text dustbin bb ET.SubElement(obj, bndbox) ET.SubElement(bb, xmin).text str(int(x1)) ET.SubElement(bb, ymin).text str(int(y1)) ET.SubElement(bb, xmax).text str(int(x2)) ET.SubElement(bb, ymax).text str(int(y2)) ET.ElementTree(root).write(fpre_annotations/{Path(r.path).stem}.xml)逻辑说明conf0.4是预标注的置信度阈值调低会多出误检框、调高会漏框0.4 是常用折中。生成的 xml 结构和原始 VOC 完全一致可以直接丢进 labelImg 改。注意预标注只是省力不能省掉人工复核模型漏掉的框它自己不会补。6.3 一个我踩过的坑早期我图快预标注后没复核就直接拿去训练结果模型把上一版的误检当成了真值越训越偏指标看着还行但实际漏检严重。从那以后我每次做半自动标注都强制走一遍「抽检 20 张 对比原图」的流程宁可慢一点。这份 457 张的垃圾箱数据集本身标注是完整的但你要拿它当种子去扩标时这个习惯能帮你省下大量返工。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进