简介:面向计算机视觉学习者和智能环卫应用场景,一份基于YOLOV5的垃圾桶满溢检测实战项目提供了一套可直接运行的目标检测方案。资源共2000个文件,其中1921个txt文件是每张图片对应的标签文件,另含Python训练脚本、yaml模型配置、shell辅助脚本和说明文档,压缩包整体约450MB。数据集包含训练集2680张图片与对应标签、验证集669张图片与对应标签,共3个类别:满溢垃圾桶、未满溢垃圾桶和垃圾。项目已训练100个epoch,最优map0.5=0.91、map0.5:0.95=0.73,runs目录保存了混淆矩阵、PR曲线、F1曲线等完整评估结果,同时提供训练好的权重参数,经测试可直接用于推理。已有366人学习下载,对希望掌握YOLOV5数据组织方式、训练调参流程及模型部署验证的读者都很有帮助,也适合用作毕业设计或竞赛项目的实用参考。
1. 垃圾桶满溢检测在解决什么问题:3 类状态与 YOLOV5 数据集的真实边界
“垃圾桶满溢检测”听起来像再简单不过的目标检测任务——把垃圾桶识别出来就行。但真正做过环卫巡检项目的人会告诉你:难的地方从来不是让 YOLOV5 认出垃圾桶,而是让模型把“满没满、溢没溢”这件事说准。摄像头正上方架着,桶内反光看不清;斜着架,桶口一半被桶壁挡掉;夜里补光灯一开,黑色垃圾袋和塑料桶壁叠在一起完全没轮廓。这篇实战笔记要解决的问题是:把一个满溢检测数据集按空桶、半满、满溢三类整理成 YOLOV5 能直接训练的结构,调好超参数,避开常见的坑,并让它部署到真实监控画面上。适合正在做智慧环卫、垃圾分类督导、市政巡检的算法工程师,以及手头只有几十张图、想跑通整个流程的毕设学生。
2. 从零做一个垃圾桶满溢检测数据集:3 类怎么分、怎么采、怎么转
2.1 3 类边界先定死:空桶、半满、满溢的定义不能靠感觉
先给类别定义。常见做法是把垃圾桶状态分成三类:empty表示空桶或桶内垃圾很少,half表示垃圾占到桶身一半以上,overflow表示垃圾已经高于桶口、桶盖盖不上或垃圾散落到桶外。这个定义听起来清楚,但落到标注工位上问题就来了:不同人眼里的“半满”可以差出 20% 的体积,有的人把垃圾压平了拍一张说这是 half,另一个人遇到垃圾高出桶口一截却因为侧面拍不到散落物标成 half。
我一般会要求项目先写一份标注意见书,篇幅可以只有一页,但必须写清楚两条量化规则:第一,按桶内垃圾体积占桶身高度比例来划,超过桶口下沿才算 overflow;第二,如果桶上压着没合上的盖子,以“盖子能否自然闭合”作为满溢的辅助判据,垃圾把盖子顶起来就算 overflow。标注前把这个意见书发给每一个标注员,每个人先标 20 张,我逐张抽查,不合格的返工。这一步看起来和训练无关,但实际决定模型的边界在哪里,而且直接影响后续所有迭代成本。
正因如此,我不建议直接拿网上现成的垃圾桶数据集来训练。开放数据集的问题不在“能不能下载”,而在它的桶型、拍摄角度和判定标准和你的现场完全两回事;车牌检测有 CCPD、船舶检测有 HRSC2016,这些数据集在自己的场景里很规范,但迁移到垃圾桶场景时只能当作预训练语义的参考,不能当作训练主体。自己采集的现场数据哪怕只有几百张,对功效的贡献也远大于几千张不相干的开源图。
2.2 采集与标注:按现场机位拍,不要只拍好看的角度
采集方式里,视频抽帧比单张拍照效率高很多。把一段监控视频每 3 秒抽一帧就能拿到一批连续但不过于重复的画面;间隔太短会让相邻帧几乎一样,数据集看起来大,实际信息量很低。抽完帧之后按场景分组,同一路摄像头同一个角度归到一组,然后按组来切 train 和 val。如果按时间顺序硬切,前面全是白天后面全是傍晚,验证集指标会虚高,模型记住的是光线而不是状态。
摄像头角度要按真实部署机位来拍。垃圾桶满溢检测最常见的部署角度是斜上方 45° 到 60°,这时候桶口和桶身都能看到;如果训练数据全是水平视角,部署时俯视画面会漏检。反过来,正上方视角虽然能看清桶内,但没有桶壁轮廓作为上下文,模型很容易把不同桶当成同一个目标。我一般会保证俯视、斜视、平视三类角度都有,但斜上方视角占大头,和最终现场保持一致。
标注框的规则也要在动手前定死:垃圾桶只框桶体,不框旁边的扫帚、纸箱和路面;满溢时暴露在桶口外的那堆垃圾算进满溢框里,不单独拆一个“垃圾类”。一句话原则,一物一框,框贴物体,框的边界不超过目标最外沿。用 LabelImg 存 VOC 格式 XML 在多人标注流程里最不容易出错,比 Labelme 导出后再转矩形少一次坐标变换风险。
2.3 VOC 转 YOLO 格式:一张图对应一个 txt,转换脚本与四个边界坑
LabelImg 存下来的是 VOC XML,YOLOV5 训练要的是 txt 加对应图片,所以转换脚本是绕不开的第一个代码块。常见做法是解析 XML 里的对象框,把绝对坐标转成归一化的中心点坐标和宽高,每个对象写成一行class_id cx cy w h:
import os import xml.etree.ElementTree as ET # class 顺序必须和之后 data.yaml 里的 names 保持一致 class_names = ["empty", "half", "overflow"] def voc_to_yolo(xml_path, out_txt_path, img_w, img_h): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in class_names: continue # 非三类目标直接跳过,不写进 txt box = obj.find("bndbox") x1 = float(box.find("xmin").text) y1 = float(box.find("ymin").text) x2 = float(box.find("xmax").text) y2 = float(box.find("ymax").text) w = x2 - x1 h = y2 - y1 if w <= 0 or h <= 0: continue # 反向框脏数据,直接丢掉 cx = ((x1 + x2) / 2.0) / img_w cy = ((y1 + y2) / 2.0) / img_h nw = w / img_w nh = h / img_h lines.append(f"{class_names.index(name)} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}") with open(out_txt_path, "w", encoding="utf-8") as f: f.write("\n".join(lines))这个脚本里最容易踩的坑是img_w和img_h。它们必须是原图的实际宽高,不能取标注工具预览窗口里的显示尺寸。很多人转完跑训练发现 mAP 突然崩掉,排查到最后就是这里:框的坐标全都按一个缩放过的画布归一化,位置全错。还有一种情况是 XML 里有横纵坐标反了的框,也就是xmax比xmin还小,脚本里的w <= 0 or h <= 0就是为这种脏数据准备的,我第一版没加这句,一个反向框就让 loss 曲线始终抖,后面花了半个下午才定位到源头。
转换完成后,检查 txt 和 jpg 是否同名同前缀、不同后缀。YOLOV5 加载数据时按文件名自动配对 label,找不到 txt 的图片会被当成背景图参与训练;如果 labels 目录里的文件名对不上,训练不会报错,但模型会学到“这张图里没有目标”,实际预期完全被带偏。另一个容易被忽略的点是类别索引:txt 第一列写的是class_names.index(name),它和 data.yaml 中 names 的排列顺序必须在训练时完全一致。中途改过类别名就全部重新转换,不要手动改号,文本里肉眼不容易发现错位,而一个错位会让某个类别全部变成脏标签。
3. 用 YOLOV5 训练自己的数据集:目录结构、YAML 与 4 个要先动的超参数
3.1 数据集目录结构:train/val 与 labels 一一对应
YOLOV5 官方仓库对数据目录有一套约定,也是我推荐直接照搬的结构:
dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── overflow.yamlimages/train和labels/train下的文件同名不同后缀,images/val和labels/val同理。第一次做这个项目的人最容易犯的错,是把 VOC 的 JPEGImages 和 Annotations 原封不动丢进来,没有建 labels 目录,或者 txt 名对不上。YOLOV5 遇到没有 label 的图片不会报错,只会把这张图当成背景,于是训练 loss 在一个低水平上落不下去,看起来像收敛了,实际模型什么都没学会。
train/val 的划分方式比划分比例更重要。简单的随机划分会让同一条视频抽出来的连续帧同时出现在两边,模型在验证时等于见到了训练帧的“邻居”,指标虚高得厉害。我一般会在抽帧后按场景和机位先分组,每组整体落入 train 或 val,而不是逐帧随机;val 占总量 15% 到 20%,并且保证 empty、half、overflow 三类在验证集里都出现。对垃圾桶满溢检测来说,宁可 val 只有 100 张但覆盖不同时段、不同角度,也不要 500 张全是同一路摄像头同一光线的图。
3.2 data.yaml 怎么填:nc、names 和路径这 3 个地方别写错
在 dataset 根目录放一个 overflow.yaml,内容如下:
path: /home/user/data/dataset # 改成本机绝对路径 train: images/train val: images/val nc: 3 names: 0: empty 1: half 2: overflowpath字段会被 YOLOV5 当作根路径,与前后的train、val拼接成实际图片目录。这里最常翻车的是写相对路径:终端当前目录一换,路径就飘了,训练直接找不到图片。推荐直接写绝对路径,省事也省排查时间。注意 yaml 里的字母、冒号、空格都不能乱动,在 Windows 上用记事本改容易混入全角冒号,加载时报错第一眼还看不出问题,建议用 VS Code 之类能显示空格的编辑器。
names的顺序就是标签索引,必须和上一章转换脚本里的class_names保持一致。0、1、2 分别对应 empty、half、overflow,不能因为“看着顺眼”把 overflow 排到前面。这个顺序错了,训练不会报错,但输出结果里 class_id 全部错位,部署时满溢会被当成空桶解析,这种错误在逻辑上极难排查。yaml 里尽量只写英文和数字,中文字段在某些环境下会出现编码问题。
3.3 训练命令与第一次该动的 4 个超参数
基础训练命令如下,以 YOLOV5 官方仓库根目录为当前目录执行:
python train.py \ --data overflow.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --hyp hyp.scratch-low.yaml \ --device 0--weights填 COCO 预训练权重,这是迁移学习的起点。YOLOV5 从 COCO 预训练权重开始,backbone 已经具备通用视觉特征,能显著减少垃圾桶数据量需求。--img 640是输入分辨率,垃圾桶在画面里偏小的话可以先试 640,显存有余量再考虑 960;batch按显存调到 16 或 8,尽量不要为 1,BatchNorm 在小 batch 下统计量抖得厉害。第一次训练不要改网络结构相关参数,YOLOV5 的模型结构由yolov5s.yaml决定,backbone 改起来风险大、收益不确定,先把数据管好、超参调顺,远比动结构有价值。
超参文件建议先用hyp.scratch-low.yaml,因为它比默认的hyp.scratch.yaml更克制,结构化数据量不足时不容易过拟合。第一次跑不要像调参玄学那样一次性改七八个参数,先动下面 4 个就够了:
| 参数 | 默认值 | 推荐值 | 作用与注意事项 |
|---|---|---|---|
lr0 | 0.01 | 0.005 | 初始学习率。数据量不到 2000 张时,0.01 容易早期震荡 |
lrf | 0.01 | 保持默认 | 最终学习率比例,初学不建议动 |
mosaic | 1.0 | 0.3~0.5 | 马赛克增强概率。满溢样本会被切碎,必须调低 |
mixup | 0.0 | 0.0 | 混合增强。垃圾桶纹理混杂,开高会让边界更糊 |
调法是在仓库data/目录下复制一份 hyp 文件,手动改lr0和mosaic,再传入--hyp。训练结束后,用runs/train/exp/weights/best.pt而不是last.pt去评估和部署;best 是按验证集指标挑选的。训练中断续跑时用--resume last.pt,别重新从头开始,否则前几十个 epoch 白跑一遍。
4. 评估与导出:从 mAP、混淆矩阵到 ONNX/TorchScript
4.1 跑 val.py:三类各自的指标比总 mAP 重要
训练完成后先评估,再谈导出:
python val.py \ --data overflow.yaml \ --weights runs/train/exp/weights/best.pt \ --img 640输出里会有每个类别以及平均的 Precision、Recall、mAP50、mAP50-95。对垃圾桶满溢检测这种 3 类模型,不要只盯总 mAP。我一般先看 overflow 这一行的 Recall,因为漏报一台满溢桶的直接后果就是垃圾溢出几小时没人处理,代价远大于把 half 误判成 overflow。如果 overflow 的 Recall 明显低于其他两类,基本可以判断满溢样本量不足、标注边界含糊,或者数据增强把正样本毁掉了。
4.2 读混淆矩阵:two 个最容易互认的类
val.py 会在runs/val/exp下输出confusion_matrix.png,这是比 mAP 更有价值的排障工具。满溢检测项目里最常见的两种混淆模式是 empty 和 half 互认、half 和 overflow 互认。前者说明评价基线定得偏高,桶里有一点垃圾就被当成了 half;后者是真正的业务风险——现场已经满溢了,模型却只给出 half。
如果混淆矩阵显示 half 和 overflow 大面积互认,不要急着加深网络,先回去看标注。很多团队在标注阶段只给了文字定义,没有给参考图,导致同一张图在不同人手里标的类不一样。可以把混淆矩阵里那些互相认错的样本单独挑出来,打印成贴图,逐张确认是漏标、错标还是边界本身就模糊。边界模糊的目标,比如垃圾刚好平桶口但没高出桶沿,这类图要么统一归入 half 作为安全侧,要么直接丢掉不要进训练集。
4.3 export.py 导出 ONNX/TorchScript:做好 yolov5 后处理才能部署
评估通过后导出部署格式:
python export.py \ --weights runs/train/exp/weights/best.pt \ --include onnx torchscript \ --img 640 \ --opset 12导出的 ONNX 可以给 ONNX Runtime、TensorRT、RKNN 等工具链使用,TorchScript 适合在 PyTorch 生态里做快速原型验证。导出后不要直接拿去上线,先用一个最小脚本验证输出:
import onnxruntime as ort import numpy as np sess = ort.InferenceSession("best.onnx") input_name = sess.get_inputs()[0].name # blob 是 letterbox 后按 1/255 归一化、维度为 [1, 3, 640, 640] 的 RGB 数组 outputs = sess.run(None, {input_name: blob}) # 常见输出形状为 [1, 25200, 7],最后要接解码和 NMS在 640×640 输入下,YOLOV5 的预测输出通常对应 80×80、40×40、20×20 三个尺度的 anchor 网格,共 25200 个候选框;解析这些输出、按置信度过滤、做 NMS,就是在做 yolov5 后处理。很多部署端为了省事会把 NMS 留在模型里,但在 RK3568 这类 NPU 设备上,我一般建议把 NMS 放到 CPU 侧做,量化后再导出,否则工具链经常不支持模型内部的非极大值抑制。INT8 量化的精度损失只能用真实场景数据来测,不能拿训练集或验证集代替。如果目标设备是树莓派 4B 这类资源紧张的板子,TorchScript 加 s 模型是成本最低的组合,内存占用可控,帧率也够用。
5. 五个翻车现场与排查清单:标注意见、夜间漏检到预处理不一致
5.1 验证集 mAP 很高,现场满溢却漏检
训练时 mAP50 到了 0.9,部署到现场第一天就漏掉一个明显满溢的桶。这是垃圾桶满溢检测项目最常见的翻车方式。原因基本不是模型,而是标注边界和现场定义不一致:训练数据里满溢框只框了桶口以上那一点,部署画面里垃圾从桶身侧面冒出来,框住的位置和训练样本差得太远。
解决方法是把标注意见书细化,明确“overflow 框必须包含桶口外的散落垃圾,但不包含桶体以外的无关物”,并在训练集里补充不同方向冒出的正样本。如果甲方定义满溢就是“盖子合不上”,那训练样本就要覆盖合不上盖子的桶,不要把没有盖子的桶混进 overflow 类。规则定死、样本匹配,比提高阈值管用得多。
5.2 满溢类别 recall 上不去:mosaic 把样本切没了
训练日志里每类的 recall 单独看,overflow 明显低于其他类。我排查时发现是数据增强的问题:YOLOV5 默认 mosaic 概率是 1.0,每张训练图都由四张图拼成,小目标被切碎的概率很大。满溢框本来只是桶口那一小块,四宫格一切,垃圾袋和桶壁被切成两半,模型学到的是一个不完整的形状。
解决方法是改 hyp 文件里的mosaic概率,满溢是主目标时调到 0.3~0.5;如果满溢样本本身很少,甚至可以考虑关到 0.1。另一种做法是把满溢样本单独放一个子目录,对这批数据关闭 mosaic,但这会让管线复杂化,我一般直接全局调低就够用了。
5.3 夜间补光场景检测直接消失:缺的是夜间样本,不是亮度增强
白天训练的模型拿到晚上,检测框数量骤降,满溢桶完全不出现。很多人第一反应是调 hsv 增强,把亮度调高让模型“见过更亮的图”。但这只对白天样本的光照变化有效,解决不了红外补光下垃圾桶的真实纹理:夜里垃圾桶在红外画面里是灰色调,塑料袋轮廓和桶壁几乎融为一体,和白天彩色照片差异巨大。
解决方法是采集夜间样本加入训练集,至少保证 val 里也有夜间片段。固定摄像头机位后,连续录一段傍晚到夜间的视频,抽帧后和白天数据一起参与训练。hsv 参数可以保留,但要清楚它的作用是补充光照多样性,替代不了真实拍摄条件下的色彩分布。
5.4 远距离小垃圾桶漏检:分辨率与锚框的问题
监控画面里远处一排垃圾桶,近处的能检出来,远处的小目标始终漏。画面里目标只有十几个像素高,问题通常出在两个地方:一是 640×640 输入下小目标经过下采样后特征信息几乎丢失,二是预训练锚框是按 COCO 数据集分布聚出来的,对满溢桶这种“小但细长”的目标不太匹配。
先试--img 960重新训练,输入分辨率变大之后小目标特征会明显改善,但显存占用上升,batch 要相应调小。如果显存不够,另一种做法是限制检测区域,在部署端把垃圾桶所在 ROI 划出来,缩小搜索范围,把注意力让给小目标。autoanchor 会训练前自动重聚锚框,训练日志里能看到新锚框的分布和默认值的差异,明显偏离时不要强行手动填回去,给模型一点信任。
5.5 部署后坐标错位:letterbox 前后不一致
验证集有框有准,部署到本地用 Python 推理或 RKNN 加载后,框要么整体偏移,要么锁定在图片角落。这类问题几乎都出在前处理不一致:YOLOV5 训练时用的是 letterbox,把图片等比缩放后填充灰色边;部署代码如果直接 resize 到 640×640,宽高比一变,坐标自然全偏。
解决方法是部署端必须复用官方仓库里的 letterbox 逻辑,并在后处理解码时按原始图尺寸还原坐标。导出 ONNX 时如果带动态尺寸,推理端要传入同样的输入尺寸,避免和模型预期不一致。经验是:先拿单张训练图走导出模型推理,把结果框画出来和原图比对,坐标对得上再谈上线,这一步能省掉大量现场排查时间。
6. 部署后反而靠这个技巧:连续帧裁定满溢并回流数据
单帧推理结果直接上报,会出现一个很讨厌的现象:垃圾袋从镜头前飘过、树叶挡了一下桶口,模型就报一次满溢。监控画面是每秒 10 帧以上的视频流,单帧误报被放大之后,后台投诉比漏报还难处理。针对固定机位场景,我一般会建议在模型后面加一个连续帧裁定逻辑:同一个跟踪目标连续 N 帧都被判定为 overflow,才真正输出一次满溢事件。
class OverflowJudge: def __init__(self, need_frames=3): self.counter = {} # track_id -> 连续满溢帧数 self.need_frames = need_frames def update(self, track_id, is_overflow): c = self.counter.get(track_id, 0) c = c + 1 if is_overflow else 0 self.counter[track_id] = c return c >= self.need_frames这段逻辑的要点是“连续”两个字:只要中间有一帧不是满溢,计数就清零,这样树叶遮挡、塑料袋飘过造成的单帧抖动会被压掉。固定摄像头场景里 track_id 用简单的 IOU 匹配就行,不需要上重跟踪模型。need_frames取 3 到 5,画面帧率越高取值越大;这不是模型训练能替代的部分,是部署侧最便宜的误报抑制手段。
除了连续帧裁定,还要让项目形成数据回流闭环。部署后每天把模型预测的漏检帧和误报帧存下来,每周挑几十张重新标注,修正后并回训练集;那些让模型犯错的图,就是下一轮训练最该加进去的样本。我见过太多项目把模型当成一次性交付物,上线跑通就结束了,但垃圾桶满溢检测的场景每天都可能变,桶型会换、光线会变、投放习惯会改,只有把误报样本持续喂给模型,指标才会越跑越稳。第一次做这个项目时,我也只盯着验证集 mAP,结果现场误报不断,加班折腾了两周才发现问题不在模型,而在“单帧判定”这个设计决策;后来把连续帧裁定和数据回流写进流程,误报率才降到可接受范围。希望这些经验能帮你的项目少走一段弯路。
本文还有配套的精品资源,点击获取