news 2026/9/16 2:36:16

457张VOC数据训练垃圾箱目标检测:格式解析与YOLO实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
457张VOC数据训练垃圾箱目标检测:格式解析与YOLO实践

简介:面向目标检测学习者和开发者,这份数据集专门针对“垃圾箱”这一常见城市物体,提供一套经过人工标注的VOC格式样本,适用于需要识别垃圾箱的检测任务。包内共包含457张JPG图片与457个XML标注文件,总计914个文件,压缩包约26.79MB,体积小巧便于获取;图片尺寸在1-500KB之间,类别统一为dustbin,涵盖不同场景下的垃圾箱目标,标注信息完整。所有标注均使用labelImg工具逐张完成,并严格遵循精确框选目标边界、不漏标目标以及多标注者交叉一致性检查等规范,确保标注质量可靠,可直接用于模型训练与验证。目前已有94人学习,可作为目标检测算法对比实验的数据基础,也适合作为教学案例;解压后图片与标注文件分目录存放,无需解压密码,符合VOC标准组织方式,能够快速接入YOLO、Faster R-CNN等常见检测框架。

1. 垃圾箱目标检测为什么盯上VOC格式的457张小数据集

一个智慧环卫项目常见的数据形态是:巡检车在园区跑几圈,拍下几百张含垃圾箱的照片,标注员用 LabelImg 框出目标,导出就是一份 VOC 格式的 XML 标注。457 张这个量级在目标检测里不算大,但它恰好覆盖「一个场景、一类目标、固定视角」的专用识别任务,比如判断某个垃圾箱有没有满溢、有没有被移走、箱门有没有打开。很多人拿到这种数据的第一反应是「太少」,转头去混入公开数据集,结果引入外观差异极大的垃圾桶,反而把模型训崩。VO C格式的优势在于标注信息完整、目录规范,转换到 YOLO 或 COCO 都方便;小样本的难点则在于划分和增强。下面要讲的流程,就是围绕这 457 张 VOC 标注数据,把目录拆开、把标注查一遍、再把它喂进检测模型。

2. 拆开垃圾箱数据的VOC标注格式:三个目录和XML字段各管什么

2.1 典型目录结构:JPEGImages、Annotations、ImageSets 怎么组织457张样本

VOC 格式是从 Pascal VOC 挑战赛沿袭下来的标注规范,标注工具(LabelImg)默认就导出这种结构。拿到数据先看目录,标准布局通常是:

trash_bin_dataset/ ├── Annotations/ # 457 个 XML 标注文件 ├── JPEGImages/ # 457 张原始图像 └── ImageSets/ └── Main/ ├── train.txt ├── val.txt ├── trainval.txt └── test.txt

JPEGImages放原图,Annotations放同名 XML,ImageSets/Main下的 txt 记录训练和验证用的图像文件名(不带扩展名,每行一个)。后面做数据划分、写数据集类、转 YOLO 格式,都围绕这三个目录操作。注意一点:ImageSets有时不存在,有些标注工具只导出图片和 XML,那么 train/val 划分就需要你自己生成,后面第 4 章会讲。

2.2 一个垃圾箱XML里值得读的字段:bndbox、truncated、difficult

打开一个 Annotations 下的 XML,核心内容长这样:

<annotation> <folder>JPEGImages</folder> <filename>trash_001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>trash_bin</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>512</xmin> <ymin>300</ymin> <xmax>720</xmax> <ymax>610</ymax> </bndbox> </object> </annotation>

size字段记录图像实际宽高,这个值在转换坐标时要用;objectname是类别名,bndbox四个值分别表示目标的左上角 x、y 和右下角 x、y,单位是像素。truncated表示目标是否超出图像边界,difficult表示该目标是否属于难例,部分训练框架(如 MMDetection 的 VOC 数据集类)默认会过滤掉difficult=1的框,所以这两个字段会直接影响训练样本数,别忽略。

2.3 VOC、COCO、YOLO三种坐标语义的差异决定了转换方式

目标检测数据集标注格式里,VOC、COCO、YOLO 是三种最常见的坐标表示方式,差异集中在两处:边框表达方式和坐标是否归一化。

格式存储方式边框字段坐标特点
VOCXML 文件,每张图一个xmin, ymin, xmax, ymax绝对像素坐标
COCO单个 JSON 文件x, y, width, height绝对像素坐标,左上角+宽高
YOLOtxt 文件,每行一个目标class, x_center, y_center, width, height相对图像尺寸归一化到 0~1

这里有个新手容易踩的坑:VOC 的bndbox给的是两个对角点,转 YOLO 时要先算中心点再除以图像宽高;转 COCO 时要先算宽高再写进 JSON。如果直接把xmaxymax当宽高用,模型训练时边框会整体偏到右下角,而且验证 mAP 会异常高——因为模型学会了「预测一个右下的框就能命中一半区域」。后面第 3 章的转换代码会把这个逻辑完整走一遍。

3. 用Python解析垃圾箱VOC标注:类别统计、画框核对和批量转YOLO

3.1 用ElementTree统计457个XML里的目标类别和空标注

拿到 457 个 XML,先别急着训练,第一步是统计类别分布。垃圾箱这类数据容易出现类别名不统一的情况:有人标trash_bin,有人标garbage_can,甚至有的文件里混入了Trash_Bin。用 Python 标准库的xml.etree.ElementTree就能快速盘点,不需要额外装 lxml:

import os import xml.etree.ElementTree as ET from collections import Counter annotations_dir = "Annotations" name_counter = Counter() empty_files = [] for f in os.listdir(annotations_dir): if not f.endswith(".xml"): continue tree = ET.parse(os.path.join(annotations_dir, f)) root = tree.getroot() objects = root.findall("object") if not objects: empty_files.append(f) for obj in objects: name = obj.findtext("name").strip() name_counter[name] += 1 print("XML 文件总数:", len([f for f in os.listdir(annotations_dir) if f.endswith(".xml")])) print("类别统计:", dict(name_counter)) print("空标注文件:", empty_files)

findall("object")拿到一个 XML 里的所有目标框,findtext("name")读取类别名,.strip()去掉标注时可能带入的首尾空格。这里用 Counter 而不是手动维护字典,后续如果要按类别过滤或者做长尾分析会方便很多。空标注文件单独列出来,后面训练时要决定是剔除还是保留——如果一张图没有垃圾箱但出现在验证集里,它会影响模型的误报率评估,不能简单忽略。

3.2 把bndbox画回JPEGImages,肉眼核对标框位置

统计完类别,下一步是抽样可视化。标注文件是文本,边界框是否贴合目标,只有画回原图才能判断。用 OpenCV 读取图片,把 XML 里的bndbox画成矩形:

import cv2 import xml.etree.ElementTree as ET def draw_voc_boxes(image_path, xml_path, color_map): img = cv2.imread(image_path) if img is None: print("图像读取失败:", image_path) return None tree = ET.parse(xml_path) for obj in tree.getroot().findall("object"): name = obj.findtext("name").strip() bnd = obj.find("bndbox") xmin = int(float(bnd.findtext("xmin"))) ymin = int(float(bnd.findtext("ymin"))) xmax = int(float(bnd.findtext("xmax"))) ymax = int(float(bnd.findtext("ymax"))) color = color_map.get(name, (0, 255, 0)) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), color, 2) cv2.putText(img, name, (xmin, max(ymin - 6, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) return img img = draw_voc_boxes("JPEGImages/trash_001.jpg", "Annotations/trash_001.xml", {"trash_bin": (0, 255, 0), "trash_lid": (0, 200, 255)}) cv2.imwrite("check_trash_001.jpg", img)

int(float(...))是因为 XML 里读出的是字符串,有些标注工具还会写成带小数的数值,先转 float 再转 int 最稳。ymin - 6可能为负,max(ymin - 6, 0)保证文字不会画到图像外面。抽查时优先挑类别统计里出现次数少的文件名,以及空标注文件,这两类最容易发现问题。

3.3 批量把VOC格式转成YOLO txt:归一化坐标一行搞定

训练 YOLO 系列模型需要 txt 格式的标注,所以转换是刚需。转换逻辑一句话说清:把 VOC 的左上右下角点换算成中心点坐标和宽高,再分别除以图像宽高。其中图像宽高应该从 XML 的size字段读,而不是自己另读图片,这样可以顺带校验 XML 和图像是否匹配:

import os import xml.etree.ElementTree as ET CLASSES = ["trash_bin", "trash_lid"] # 按实际类别名调整 def voc_to_yolo(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") img_w = int(size.findtext("width")) img_h = int(size.findtext("height")) lines = [] for obj in root.findall("object"): name = obj.findtext("name").strip() if name not in CLASSES: continue cls_id = CLASSES.index(name) bnd = obj.find("bndbox") xmin = float(bnd.findtext("xmin")) ymin = float(bnd.findtext("ymin")) xmax = float(bnd.findtext("xmax")) ymax = float(bnd.findtext("ymax")) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") base = os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(out_dir, base + ".txt"), "w") as f: f.write("\n".join(lines)) os.makedirs("labels", exist_ok=True) for x in os.listdir("Annotations"): if x.endswith(".xml"): voc_to_yolo(os.path.join("Annotations", x), "labels")

归一化公式是(xmin + xmax) / 2 / img_w,不是xmax / 2,也不是(xmax - xmin) / 2。中心点取两个角点的平均值,宽高取差值,这是最基础的几何换算。输出cls_id是类别在CLASSES列表里的下标,这份列表在训练配置里要完全一致,顺序错一个,类别标签就全乱了。

3.3.1 转换代码里为什么要从XML的size读宽高

很多转换脚本会用cv2.imread重新读图片拿宽高,但这里选择 XML 的size字段,有两个原因。一是效率,457 张图少读一遍磁盘;二是校验,转换时如果发现某张图的 XML 尺寸和实际不一致,说明这批数据的标注在某个环节被改动过,应该先排查。转换后检查labels目录下的 txt 文件数量和 JPEGImages 里的 jpg 数量是否一致,不一致的文件名记录下来,通常是 XML 缺失或图片损坏。

4. 457张垃圾箱图的训练路线:按场景划分、增强同步bbox、参数怎么设

4.1 按场景划分train/val,别让同一地点的照片同时出现在两边

457 张小数据集最容易被忽视的问题是数据划分。垃圾箱照片往往是按拍摄时间连续采集的,同一地点可能拍了 5 到 10 张,画面里只有垃圾箱的角度、光照、遮挡略有不同。如果随机打乱再划分,同一个地点的照片会同时出现在训练集和验证集里,验证指标虚高,模型一上手新场景就露馅。我一般按文件名前缀或序列号把同一场景的照片分到同一侧。

假设文件名是spot01_001.jpgspot01_002.jpg这种带场景编号的命名,划分脚本这样写:

import os import random image_ids = [f[:-4] for f in os.listdir("JPEGImages") if f.endswith(".jpg")] # 按文件名前6位分组,按你的命名规则调整切片范围 groups = {} for img_id in image_ids: scene = img_id[:6] groups.setdefault(scene, []).append(img_id) scene_names = list(groups.keys()) random.Random(42).shuffle(scene_names) val_scenes = set(scene_names[: int(len(scene_names) * 0.2)]) train_ids = [img_id for scene, ids in groups.items() for img_id in ids if scene not in val_scenes] val_ids = [img_id for scene, ids in groups.items() for img_id in ids if scene in val_scenes] with open("ImageSets/Main/train.txt", "w") as f: f.write("\n".join(train_ids)) with open("ImageSets/Main/val.txt", "w") as f: f.write("\n".join(val_ids))

random.Random(42)固定随机种子,保证每次重跑划分结果一致。分组逻辑是全脚本的核心:先按场景分组,再对场景列表做 shuffle,而不是对每张图做 shuffle。这样同一地点的照片要么全进训练、要么全进验证,评估结果才接近真实部署环境。划分完成后,把图片和对应的 txt 标注按 YOLO 的目录习惯整理:

import shutil def copy_split(split_file, dst_img_dir, dst_label_dir): os.makedirs(dst_img_dir, exist_ok=True) os.makedirs(dst_label_dir, exist_ok=True) for name in open(split_file).read().split(): shutil.copy(f"JPEGImages/{name}.jpg", dst_img_dir) shutil.copy(f"labels/{name}.txt", dst_label_dir) copy_split("ImageSets/Main/train.txt", "datasets/trash_bin/images/train", "datasets/trash_bin/labels/train") copy_split("ImageSets/Main/val.txt", "datasets/trash_bin/images/val", "datasets/trash_bin/labels/val")

这一步把第 3 章的转换结果和训练框架的目录要求接上了。复制的开销对 457 张图可以忽略,但一劳永逸,后续换框架、换机器都不用再改数据路径。

4.2 增强策略:旋转、亮度、遮挡都要同步bbox,给出albumentations配置

小数据集全靠增强撑多样性。垃圾箱是刚性物体,适合的增强手段要围绕拍摄环境来选:白天晚上光照不同,加亮度对比度扰动;树影和雨水会带来模糊,加轻微运动模糊;巡检视角有轻微倾斜,加小角度旋转。动目标框坐标的增强(旋转、裁剪、缩放)必须同步更新 bbox,这也是很多自写增强代码出错的地方。

如果走离线增强或需要可视化增强效果,用 albumentations 是最省事的方案,它自动同步 bbox:

import albumentations as A train_transform = A.Compose([ A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.15, p=0.8), A.HueSaturationValue(hue_shift_limit=5, sat_shift_limit=15, val_shift_limit=10, p=0.5), A.Rotate(limit=10, border_mode=0, p=0.5), A.HorizontalFlip(p=0.5), A.RandomSizedBBoxSafeCrop(height=640, width=640, erosion_rate=0.2, p=0.3), A.MotionBlur(blur_limit=5, p=0.2) ], bbox_params=A.BboxParams(format="pascal_voc", label_fields=["class_labels"]))

format="pascal_voc"表示输入的 bbox 是[xmin, ymin, xmax, ymax],正好对应 VOC XML 里的四个值。label_fields指定和 bbox 绑定的类别列表,增强后类别标签会和框一起重排。RandomSizedBBoxSafeCrop保证裁剪窗口不会切掉任何 bbox,这是垃圾箱这类中等大小目标的关键——普通 RandomCrop 很容易把垃圾箱裁掉一半。

提示:如果直接用 Ultralytics YOLO 训练,内置增强已经支持坐标同步。上面这套 albumentations 配置更适合用来生成增强样本、人工检查增强是否合理,或者用于自定义训练管线。两条路选一条,不要同时开两套增强。

4.3 训练参数参考:小模型、低学习率、早停和data.yaml写法

457 张图训练目标检测,模型规模必须克制。我用 Ultralytics YOLO 时,首选yolo11s.pt或同级的yolov8s.pt,这类小模型参数量在千万级左右,抗过拟合能力强,单卡就能训。数据配置写成trash_bin.yaml

path: /data/trash_bin train: images/train val: images/val names: 0: trash_bin 1: trash_lid

path指到第 4.1 节整理的datasets/trash_bin目录,trainval用相对 path 的目录。names的类别顺序必须和第 3.3 节CLASSES列表完全一致。训练命令:

yolo detect train \ data=trash_bin.yaml \ model=yolo11s.pt \ epochs=200 \ batch=32 \ imgsz=640 \ lr0=0.005 \ lrf=0.01 \ patience=30 \ degrees=10.0 \ fliplr=0.5 \ hsv_h=0.02 hsv_s=0.6 hsv_v=0.4 \ mosaic=0.5

参数的选择逻辑:lr0=0.005比默认的 0.01 低一半,小数据集学习率太猛容易在头几个 epoch 就把特征学偏;patience=30开启早停,验证指标连续 30 个 epoch 不提升就自动终止,省时间;mosaic=0.5意味着有一半概率做马赛克增强,四个小图拼一张,能显著缓解小样本的过拟合,训练后期 Ultralytics 会自动关闭 mosaic 做最后的精调。degrees=10hsv_v=0.4分别对应小幅旋转和亮度变化,和 4.2 节 albumentations 配置的思路一致。

训练中间看两个指标:train/box_loss是否持续下降但val/box_loss不再动,这是典型的过拟合信号,此时应加大mosaic概率或提前停止;metrics/mAP50如果早期就冲到 0.9 以上,先怀疑数据划分有没有泄漏,回看 4.1 节的分组逻辑,而不是高兴太早。

5. 交付垃圾箱VOC数据前必查的五项和一键排出脚本

5.1 五项必查清单

VOC 格式的标注数据交付前,最怕的是问题藏在 457 个文件里,训练时才爆出来。我每次都会跑一遍五项检查,任何一项不过都先修数据再谈训练。

检查项判定标准常见问题
图像与XML一一对应Annotations 和 JPEGImages 文件名集合完全一致缺 XML、孤儿图片
size字段一致性XML 里的 width/height 等于图像实际像素图像被压缩过、XML 被复制改名
坐标合法性xmin <= xmax、ymin <= ymax,且都在图像范围内手滑标出界、坐标顺序填反
类别名统一所有 name 值在预设类别集合内,无大小写/空格差异trash_bin 和 Trash_Bin 混用
空标注与难例占比空标注文件列表明确,difficult=1 的数量已知漏标导致空文件、难例被误标

5.2 一段脚本跑完五项检查,错误逐条打印

下面的脚本把五项检查合到一起,输出格式是「文件名 + 具体问题」,排查时直接按文件名定位:

import os import cv2 import xml.etree.ElementTree as ET ann_dir, img_dir = "Annotations", "JPEGImages" ann_ids = {f[:-4] for f in os.listdir(ann_dir) if f.endswith(".xml")} img_ids = {os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.lower().endswith((".jpg", ".jpeg", ".png"))} print("缺XML的图片:", sorted(img_ids - ann_ids)) print("缺图片的XML:", sorted(ann_ids - img_ids)) allowed_names = {"trash_bin", "trash_lid"} for name in sorted(ann_ids & img_ids): tree = ET.parse(os.path.join(ann_dir, name + ".xml")) root = tree.getroot() size = root.find("size") xml_w, xml_h = int(size.findtext("width")), int(size.findtext("height")) img = cv2.imread(os.path.join(img_dir, name + ".jpg")) if img is None: print("图片损坏:", name) continue if img.shape[1] != xml_w or img.shape[0] != xml_h: print("尺寸不一致:", name, "XML:", xml_w, xml_h, "图像:", img.shape[1], img.shape[0]) for obj in root.findall("object"): obj_name = obj.findtext("name").strip() if obj_name not in allowed_names: print("未知类别:", name, obj_name) bnd = obj.find("bndbox") x1, y1 = int(float(bnd.findtext("xmin"))), int(float(bnd.findtext("ymin"))) x2, y2 = int(float(bnd.findtext("xmax"))), int(float(bnd.findtext("ymax"))) if not (0 <= x1 <= x2 <= xml_w and 0 <= y1 <= y2 <= xml_h): print("坐标越界:", name, obj_name, (x1, y1, x2, y2))

脚本运行完没有任何输出,说明五项检查全部通过。img.shape[1]是图像的宽度,img.shape[0]是高度,这个顺序经常有人写反,检查时会报出一堆不存在的尺寸错误。最后再随机挑 5 张图上一步的可视化脚本,确认框贴合程度,这份数据就可以放心交付或进入训练流程了。把这段检查脚本和训练流程放在一起,每次数据更新后先跑一遍,能省下后面排错的大量时间。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 2:33:45

从OSI分层到Ping命令:网络故障排查的实用方法论

1. 为什么我放弃了“万能重启法”先说实话&#xff0c;我以前也是那种一遇到网络卡顿就冲过去拔电源、等十秒、再插回去的人。十年前我刚接触网络运维的时候&#xff0c;前辈教我的第一句话就是“重启解决99%的问题”&#xff0c;当时觉得这话没毛病&#xff0c;光猫重启、路由…

作者头像 李华
网站建设 2026/9/16 2:33:37

Seata实战总结:AT模式原理、模式对比与生产排坑指南

七章写完&#xff0c;很多读者私下问我&#xff1a;Seata到底值不值得学&#xff0c;学了之后真正落地是什么感觉。我的回答一直是同一句话——分布式事务这块硬骨头&#xff0c;Seata是目前把“理解成本”和“接入成本”平衡得最好的开源方案&#xff0c;没有之一。尤其是AT模…

作者头像 李华
网站建设 2026/9/16 2:32:27

Spring Boot短视频推荐系统实战:从协同过滤到Redis缓存优化

Spring Boot 短视频推荐系统这个项目&#xff0c;我在不同阶段接触过好几次——一开始是给学弟学妹看毕设&#xff0c;后来自己也动手重构过一版。说实话&#xff0c;网上叫“短视频推荐系统”的项目不少&#xff0c;但很多就是 CRUD 包了一层推荐算法的壳&#xff0c;用户表、…

作者头像 李华
网站建设 2026/9/16 2:32:07

BIM GIS桥梁监管平台是什么?5 大核心功能与应用价值详解

BIM与GIS的相遇&#xff0c;正在为桥梁监管行业打开一扇全新的大门。从2002年BIM概念被正式提出&#xff0c;到2016年国家标准落地&#xff0c;再到如今与GIS、物联网等技术的深度融合&#xff0c;桥梁管理正从二维图纸上的静态记录&#xff0c;走向三维空间中的动态治理。本文…

作者头像 李华
网站建设 2026/9/16 2:31:08

Zemax+MATLAB联合仿真:微透镜阵列与光场相机完整流程

做微透镜阵列相关仿真这几年&#xff0c;我最大的感受是&#xff1a;Zemax和MATLAB单拎出来都不够用。Zemax能把光追得明明白白&#xff0c;但它不太擅长处理"这个像素属于哪个微透镜视角"这类计算成像问题&#xff1b;MATLAB做图像处理和算法验证很强&#xff0c;可…

作者头像 李华
网站建设 2026/9/16 2:31:04

三款主流智能电动车安全体系深度对比解析

1. 项目概述&#xff1a;为什么这三款车的安全解析值得花20分钟认真读完最近在几个新能源车主群和智能驾驶技术论坛里&#xff0c;频繁刷到“尚界Z7”“阿维塔12”“阿维塔06”这三个名字——不是因为谁又降价了&#xff0c;而是因为一批真实车主在高速实测后发的长帖&#xff…

作者头像 李华