ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

457张VOC标注垃圾箱检测:小样本训练与半自动标注实战

457张VOC标注垃圾箱检测:小样本训练与半自动标注实战 简介这份垃圾箱数据集面向从事目标检测训练与验证的开发者、学生及算法爱好者提供VOC格式的标注样本可用于训练或微调垃圾箱识别模型解决特定场景下目标检测数据不足的问题。资源包共914个文件包含457张jpg图片与457个对应的xml标注文件压缩包约26.79MB采用rar格式打包无需解压密码解压后图片与标注分文件夹存放便于直接导入labelImg等工具查看标注情况。标注类别统一为dustbin全部由labelImg完成遵循准确框选目标边界、尽量覆盖图像中所有目标、并通过一致性检查确保标注正确性的原则。目前已有94人学习下载适合作为目标检测入门练手或小规模实验的数据补充帮助读者快速搭建训练集、验证标注质量并推进模型迭代。1. 垃圾箱数据集 VOC 格式目标标注 457 张小样本检测到底能不能落地手上只有 457 张标注图能不能训出一个能用的垃圾箱检测模型这是我在做环卫场景智能化时被问得最多的问题。答案不是「能」或「不能」而是取决于你怎么用这批 VOC 格式的目标标注数据。VOC 格式意味着每张图对应一个 XML 文件里面记录了垃圾箱的边界框坐标、类别名和图像尺寸这套格式从 PASCAL VOC 时代沿用至今几乎所有主流检测框架都能直接读。457 张这个量级放在 COCO 或 CrowdHuman 那种万级数据集面前确实寒酸但在垂直场景里如果标注质量过关、场景单一、目标类别少它完全可以作为冷启动的种子数据。这篇文章面向的是手上有类似规模标注数据、想跑通垃圾箱检测的工程师和研究生我会把从数据检查、格式转换、增强策略到训练调参的完整路径拆开讲重点说清楚小样本条件下哪些操作是有效的、哪些是白费力气。2. 拿到 457 张 VOC 标注先别急着训练数据体检与格式转换2.1 为什么 VOC 格式在小样本场景下反而省事VOC 格式的核心是一个 XML 文件对应一张图结构清晰size记录宽高object记录每个目标的类别和bndbox坐标。相比 COCO 的 JSON 大文件VOC 的分散式存储在小数据集上有个实际好处——你改一张图的标注不会影响其他文件版本管理也方便。457 张图的 XML 总量不大用脚本批量处理几秒钟就跑完。但 VOC 有个容易翻车的地方坐标是 1-based 的而很多训练框架内部用 0-based。如果你直接拿 VOC 的 xmin/ymin 去算 YOLO 格式的归一化中心点会整体偏移一个像素。单看一个像素无所谓但在小目标密集的场景里一个像素的偏移可能让框和真实目标错位。我一般会在转换脚本里统一减 1再算归一化。另一个现实问题是标注一致性。457 张图如果经过多人之手类别名可能写成trash_bin、trashbin、TrashBin三种。VOC 的name字段是纯文本没有 schema 约束这种不一致在训练时会被当成三个不同类别直接导致模型学废。所以第一步不是转格式是先把所有 XML 里的类别名拉出来做一次去重统计。2.2 用脚本做数据体检类别分布、框尺寸、异常标注下面这段脚本做三件事统计类别名、统计每类目标数量、检查有没有宽高为 0 或超出图像边界的框。import os import xml.etree.ElementTree as ET from collections import Counter ann_dir Annotations img_dir JPEGImages class_counter Counter() size_list [] bad_boxes [] for xml_file in os.listdir(ann_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(ann_dir, xml_file)) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) for obj in root.findall(object): name obj.find(name).text.strip() class_counter[name] 1 bbox obj.find(bndbox) xmin int(float(bbox.find(xmin).text)) ymin int(float(bbox.find(ymin).text)) xmax int(float(bbox.find(xmax).text)) ymax int(float(bbox.find(ymax).text)) w xmax - xmin h ymax - ymin size_list.append((w, h)) # 检查异常框 if w 0 or h 0 or xmin 0 or ymin 0 or xmax img_w or ymax img_h: bad_boxes.append((xml_file, name, xmin, ymin, xmax, ymax)) print(类别分布:, class_counter) print(异常框数量:, len(bad_boxes)) for b in bad_boxes[:10]: print(异常:, b) # 框尺寸统计 if size_list: ws [s[0] for s in size_list] hs [s[1] for s in size_list] print(f框宽 min/mean/max: {min(ws)}/{sum(ws)/len(ws):.1f}/{max(ws)}) print(f框高 min/mean/max: {min(hs)}/{sum(hs)/len(hs):.1f}/{max(hs)})这段脚本的逻辑很直白遍历所有 XML累加类别计数同时把每个框的宽高和边界合法性检查一遍。参数上唯一需要注意的是int(float(...))这个写法——有些标注工具会写出123.0这种浮点字符串直接int()会报错。跑完之后你会拿到三个关键信息类别是否统一、每类样本量是否均衡、有没有需要手动修的脏标注。如果发现某个类别只有个位数样本要么补标要么在训练时直接合并到相近类别。2.3 VOC 转 YOLO 格式转换脚本与四个边界坑YOLO 系列训练需要的是每张图一个 txt每行class_id cx cy w h全部归一化到 0-1。转换脚本本身不长但边界情况多。import os import xml.etree.ElementTree as ET ann_dir Annotations label_out labels classes [trash_bin] # 按体检结果确定类别列表 os.makedirs(label_out, exist_okTrue) for xml_file in os.listdir(ann_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(ann_dir, xml_file)) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in classes: continue cls_id classes.index(name) bbox obj.find(bndbox) # VOC 坐标是 1-based减 1 转 0-based xmin int(float(bbox.find(xmin).text)) - 1 ymin int(float(bbox.find(ymin).text)) - 1 xmax int(float(bbox.find(xmax).text)) - 1 ymax int(float(bbox.find(ymax).text)) - 1 # 裁剪到图像边界内 xmin max(0, min(xmin, img_w - 1)) ymin max(0, min(ymin, img_h - 1)) xmax max(0, min(xmax, img_w - 1)) ymax max(0, min(ymax, img_h - 1)) # 归一化中心点和宽高 cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h if w 0 or h 0: continue lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) txt_name xml_file.replace(.xml, .txt) with open(os.path.join(label_out, txt_name), w) as f: f.write(\n.join(lines))四个边界坑分别是第一坐标减 1 不能忘否则整体偏移第二裁剪到边界内有些标注框会超出图像尺寸一两个像素第三宽高为 0 的框直接跳过不然归一化后是 0训练时算 loss 会出 NaN第四类别名映射要用体检后确定的统一列表不要用set()动态生成否则每次跑出来的 class_id 顺序可能不一样。转换完成后建议随机抽 5 张图用可视化脚本画框确认一遍这一步花两分钟能省掉后面几小时的排查。3. 457 张图怎么训增强策略、模型选型与训练参数3.1 小样本下哪些增强真正有用457 张图做检测数据增强不是可选项是必选项。但增强手段不能乱加有些增强在通用数据集上有效在垃圾箱这种场景反而有害。我按实际效果排个序。Mosaic 增强是 YOLO 系列自带的把四张图拼成一张等效于增加了场景多样性。对垃圾箱检测来说这个增强几乎必开因为它能让模型见到更多背景组合。但要注意Mosaic 会让小目标变得更小如果你的垃圾箱在图中占比本来就小Mosaic 后可能缩到十几个像素反而增加学习难度。我一般会把 Mosaic 的概率设在 0.5 到 0.8 之间而不是默认的 1.0。HSV 色彩抖动是第二有用的。垃圾箱场景的光照变化大白天、傍晚、阴天的色温差异明显HSV 增强能提升模型对光照的鲁棒性。参数上色调偏移控制在 0.015 左右饱和度 0.7明度 0.4这个范围不会把颜色改得面目全非。随机缩放和随机平移也有用但翻转要谨慎。水平翻转对垃圾箱通常没问题但垂直翻转要看你场景里垃圾箱是否总是正立。如果数据里有倒置的垃圾箱比如翻倒的垂直翻转可以开如果没有开了反而引入噪声。不适合的增强Cutout 和 Random Erasing。这两种随机遮挡在样本充足时能提升泛化但 457 张图的条件下遮挡会让本就不多的目标信息进一步丢失模型容易学不到完整特征。旋转增强也要小心大角度旋转会让边界框变大引入大量背景对小样本不友好。3.2 模型选型YOLOv8n 还是更大模型在 457 张图的量级上模型参数量不是越大越好。我做过对比YOLOv8nnano和 YOLOv8ssmall在这个数据量上的最终 mAP 差距通常在 1 到 2 个点以内但 nano 的训练速度快一倍以上显存占用也低得多。如果你只有一张消费级显卡nano 是更务实的选择。选 nano 的另一个理由是过拟合风险。457 张图如果模型参数量到几十 M训练不到 100 轮就会在训练集上接近完美、验证集上开始抖动。nano 的容量有限反而起到了一种隐式正则化的作用。当然如果你能用更强的增强和更长的训练周期s 版本也可能追上来但投入产出比不划算。预训练权重一定要用。COCO 上预训练的 YOLOv8n 已经学到了通用的边缘、纹理特征这些特征对垃圾箱检测同样有效。从零开始训 457 张图收敛会慢很多而且容易卡在局部最优。加载预训练权重后通常 50 到 100 轮就能看到验证集指标趋于稳定。3.3 训练参数怎么设一份可直接抄的配置下面这份配置基于 Ultralytics 的 YOLOv8 训练接口针对 457 张 VOC 转 YOLO 的数据集调过。from ultralytics import YOLO model YOLO(yolov8n.pt) # 加载 COCO 预训练权重 results model.train( datatrash_bin.yaml, # 数据集配置文件路径 epochs150, # 小样本建议 100-200 轮 imgsz640, # 输入尺寸与标注时分辨率匹配 batch16, # 根据显存调整nano 模型 16 通常够 lr00.01, # 初始学习率 lrf0.01, # 最终学习率系数 momentum0.937, weight_decay0.0005, warmup_epochs3.0, # 预热轮数小样本建议 3-5 mosaic0.7, # Mosaic 概率不设 1.0 hsv_h0.015, hsv_s0.7, hsv_v0.4, degrees0.0, # 不开启旋转 translate0.1, scale0.5, fliplr0.5, # 水平翻转概率 flipud0.0, # 垂直翻转关闭 patience30, # 30 轮无提升则早停 save_period10, # 每 10 轮存一次权重 device0 # 显卡编号 )参数说明几个关键点。lr00.01是 SGD 的初始学习率如果你换成 AdamW这个值要降到 0.001 左右。warmup_epochs3.0在小样本上很重要前几轮学习率从很低慢慢升上来避免一开始就把预训练权重带偏。patience30是早停457 张图的验证集波动可能比较大30 轮不提升再停比默认的 50 轮更节省时间。mosaic0.7而不是 1.0原因前面说过给模型留一部分原始图像分布。数据集配置文件trash_bin.yaml的写法path: ./dataset train: images/train val: images/val nc: 1 names: [trash_bin]这里nc是类别数names要和转换脚本里的classes列表完全一致。训练集和验证集的划分457 张图我一般按 8:2 分也就是 365 张训练、92 张验证。如果类别极不均衡要用分层抽样保证验证集里每类都有足够样本。4. 训练过程中最容易翻车的五个地方4.1 现象loss 降到很低但验证集 mAP 一直是 0原因通常有两个。一是验证集的标注路径写错了YOLO 找不到验证集的 label 文件算 mAP 时全是背景自然为 0。二是类别名映射不一致训练时 class_id 是 0验证时因为 names 列表顺序不同变成了别的 id。排查方法先确认val路径下的 images 和 labels 一一对应再打印一次训练集和验证集的类别分布看是否一致。4.2 现象训练到一半 loss 突然变成 NaN小样本训练里 NaN 最常见的原因是学习率过大加上某个 batch 里有异常框。异常框的来源可能是转换时没过滤掉的宽高为 0 的框也可能是标注里 xmax 小于 xmin。解决办法在转换脚本里加断言确保每个框的 xmax xmin 且 ymax ymin训练时把lr0降到 0.005 再试。如果还出 NaN检查输入图像有没有损坏文件用 PIL 打开每张图验证一遍。4.3 现象模型只检测大垃圾箱小目标全漏这是小样本检测的典型问题。457 张图里如果小目标占比低模型会倾向于忽略小目标。解决手段有三个一是提高输入分辨率从 640 提到 800 或 1024让小目标占据更多像素二是在增强里降低 Mosaic 概率避免小目标被进一步缩小三是检查 anchor 配置YOLOv8 是 anchor-free 的但如果你用的是 YOLOv5需要重新聚类 anchor。另外验证集里小目标的 mAP 要单独看不要只看总体 mAP。4.4 现象验证集 mAP 波动很大每次跑结果差好几个点457 张图的验证集只有 90 多张统计噪声本来就大。如果每次训练结果差异超过 5 个点说明验证集划分可能不合理。检查方法看验证集里是否包含了训练集里没有的场景或光照条件。如果是随机划分导致的可以固定随机种子或者做 5 折交叉验证取平均。另一个原因是 batch size 太小梯度估计噪声大把 batch 从 8 提到 16 或 32 能缓解。4.5 现象推理时框的位置整体偏移如果训练时指标正常推理时框偏移大概率是预处理不一致。训练时 YOLO 会把图像 resize 到 640x640 并做 padding推理时如果你自己写的预处理没有做同样的 padding坐标映射就会错。用 Ultralytics 的model.predict()接口不会出这个问题但如果你把模型导出成 ONNX 再自己写后处理就要手动复现 letterbox 的缩放和 padding 逻辑。检查方法拿一张训练集里的图做推理看框是否和标注重合如果不重合就是预处理问题。5. 457 张之后用半自动标注把数据滚到 2000 张457 张能训出一个 baseline但要让模型真正稳定数据量还得往上走。我的习惯是训完第一版之后用模型去推理未标注的图片把置信度高的预测框转成 VOC 格式人工只做修正和删减。这个流程能把标注效率提升三到五倍。具体操作先用训好的模型对一批新图做推理导出置信度大于 0.5 的框写成 XML。然后人工过一遍删掉误检、补上漏检、修正偏移的框。修正后的数据加入训练集重新训一版再用新模型去推理下一批。两到三轮之后数据量通常能从 457 涨到 1500 到 2000 张mAP 会有明显提升。这里有个技巧推理时把conf阈值设低一点比如 0.25让模型多输出一些候选框人工删比人工画快得多。但conf太低会引入大量误检增加人工负担0.25 到 0.35 之间是个平衡点。另外半自动标注的框质量参差不齐加入训练集前要用前面说的体检脚本再跑一遍确保没有异常框混进去。验证半自动标注是否有效的方法留出一批完全人工标注的图作为测试集不参与训练。每轮半自动标注后在这个测试集上评估看 mAP 是否持续上升。如果某一轮之后 mAP 反而下降说明自动标注引入的噪声超过了新增数据的收益需要提高conf阈值或增加人工修正的比例。我自己踩过的坑是贪快把conf设到 0.15结果自动标注里一半是误检人工修正花的时间比从头标还多。后来固定在 0.3每轮只加 200 到 300 张虽然慢一点但数据质量可控。小样本检测这件事数据质量永远比数量重要457 张干净标注比 2000 张脏标注管用得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表