资讯动态

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

发布时间:2026/9/17 14:47:28 来源:尧图企业网站定制
简介面向目标检测学习者和开发者这份数据集专门针对“垃圾箱”这一常见城市物体提供一套经过人工标注的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.txtJPEGImages放原图Annotations放同名 XMLImageSets/Main下的 txt 记录训练和验证用的图像文件名不带扩展名每行一个。后面做数据划分、写数据集类、转 YOLO 格式都围绕这三个目录操作。注意一点ImageSets有时不存在有些标注工具只导出图片和 XML那么 train/val 划分就需要你自己生成后面第 4 章会讲。2.2 一个垃圾箱XML里值得读的字段bndbox、truncated、difficult打开一个 Annotations 下的 XML核心内容长这样annotation folderJPEGImages/folder filenametrash_001.jpg/filename size width1920/width height1080/height depth3/depth /size object nametrash_bin/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin512/xmin ymin300/ymin xmax720/xmax ymax610/ymax /bndbox /object /annotationsize字段记录图像实际宽高这个值在转换坐标时要用object里name是类别名bndbox四个值分别表示目标的左上角 x、y 和右下角 x、y单位是像素。truncated表示目标是否超出图像边界difficult表示该目标是否属于难例部分训练框架如 MMDetection 的 VOC 数据集类默认会过滤掉difficult1的框所以这两个字段会直接影响训练样本数别忽略。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。如果直接把xmax、ymax当宽高用模型训练时边框会整体偏到右下角而且验证 mAP 会异常高——因为模型学会了「预测一个右下的框就能命中一半区域」。后面第 3 章的转换代码会把这个逻辑完整走一遍。3. 用Python解析垃圾箱VOC标注类别统计、画框核对和批量转YOLO3.1 用ElementTree统计457个XML里的目标类别和空标注拿到 457 个 XML先别急着训练第一步是统计类别分布。垃圾箱这类数据容易出现类别名不统一的情况有人标trash_bin有人标garbage_can甚至有的文件里混入了Trash_Bin。用 Python 标准库的xml.etree.ElementTree就能快速盘点不需要额外装 lxmlimport 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_okTrue) 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.jpg、spot01_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_okTrue) os.makedirs(dst_label_dir, exist_okTrue) for name in open(split_file).read().split(): shutil.copy(fJPEGImages/{name}.jpg, dst_img_dir) shutil.copy(flabels/{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 是最省事的方案它自动同步 bboximport albumentations as A train_transform A.Compose([ A.RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.15, p0.8), A.HueSaturationValue(hue_shift_limit5, sat_shift_limit15, val_shift_limit10, p0.5), A.Rotate(limit10, border_mode0, p0.5), A.HorizontalFlip(p0.5), A.RandomSizedBBoxSafeCrop(height640, width640, erosion_rate0.2, p0.3), A.MotionBlur(blur_limit5, p0.2) ], bbox_paramsA.BboxParams(formatpascal_voc, label_fields[class_labels]))formatpascal_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.yamlpath: /data/trash_bin train: images/train val: images/val names: 0: trash_bin 1: trash_lidpath指到第 4.1 节整理的datasets/trash_bin目录train和val用相对 path 的目录。names的类别顺序必须和第 3.3 节CLASSES列表完全一致。训练命令yolo detect train \ datatrash_bin.yaml \ modelyolo11s.pt \ epochs200 \ batch32 \ imgsz640 \ lr00.005 \ lrf0.01 \ patience30 \ degrees10.0 \ fliplr0.5 \ hsv_h0.02 hsv_s0.6 hsv_v0.4 \ mosaic0.5参数的选择逻辑lr00.005比默认的 0.01 低一半小数据集学习率太猛容易在头几个 epoch 就把特征学偏patience30开启早停验证指标连续 30 个 epoch 不提升就自动终止省时间mosaic0.5意味着有一半概率做马赛克增强四个小图拼一张能显著缓解小样本的过拟合训练后期 Ultralytics 会自动关闭 mosaic 做最后的精调。degrees10和hsv_v0.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 混用空标注与难例占比空标注文件列表明确difficult1 的数量已知漏标导致空文件、难例被误标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 张图上一步的可视化脚本确认框贴合程度这份数据就可以放心交付或进入训练流程了。把这段检查脚本和训练流程放在一起每次数据更新后先跑一遍能省下后面排错的大量时间。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价