ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

YOLOV5实战:垃圾桶满溢检测三类别数据集与部署调优

YOLOV5实战:垃圾桶满溢检测三类别数据集与部署调优

简介:本资源为基于YOLOV5的垃圾桶满溢检测实战项目,面向计算机视觉入门与进阶开发者、目标检测课程设计或竞赛参赛者,解决城市环卫场景中垃圾桶状态与散落垃圾的自动识别问题。项目包含完整代码、数据集与训练好的权重参数,经测试可直接运行。压缩包共约2000个文件,以txt标签、py脚本、yaml配置、sh训练脚本及md说明为主,整体约450MB。数据集含3类别:满溢垃圾桶、未满溢垃圾桶和垃圾,训练集2680张图片及对应标签,验证集669张图片及对应标签。项目迭代100个epoch,最优精度map0.5=0.91、map0.5:0.95=0.73,runs目录保存了混淆矩阵、PR曲线、F1曲线及推理结果,便于复盘训练过程与调参。已有366人学习,适合希望快速跑通检测流程、理解数据标注与评估指标的读者参考。

1. 垃圾桶满溢检测为什么总在傍晚翻车

做过环卫巡检项目的同行大概都有同感:垃圾桶满溢检测这个需求听起来简单,真跑起来却总在傍晚集中翻车。原因不玄学——傍晚光线斜射、桶体阴影拉长、垃圾袋反光,模型把阴影当成溢出物,把反光当成空桶,误报率直接翻倍。YOLOV5 实战项目:垃圾桶满溢检测数据集(3类别)这个标题,讲的正是用三分类目标检测把「空桶 / 半满 / 满溢」区分开,落到可部署的巡检方案上。它解决的不是「能不能检测到桶」,而是「能不能稳定判断桶的状态」——这直接决定清运车该不该出车、出几趟。适合谁?做智慧环卫、园区物业、边缘盒子落地的算法工程师和集成商,尤其是手里已经有摄像头、想低成本加一层状态识别的团队。三类别看似简单,但类别边界模糊、样本长尾严重,恰恰是练数据工程和调参手感的好靶子。

2. 三类别标注体系怎么定才不返工

2.1 为什么是「空桶 / 半满 / 满溢」而不是「满 / 不满」

很多人第一反应是二分类:满和不满。我一开始也这么干,结果标注到第三批数据就崩了。问题出在「半满」这个中间态——它既不是空也不是满,二分类会强迫标注员在边界上做主观判断,同一个桶两个人标出两个结果,训练集里全是噪声。三类别把中间态显式建模,标注员只需要回答「有没有明显垃圾露出桶口」和「垃圾是否接近桶口」,判断标准从连续量变成两个离散阈值,一致性立刻上来。

更关键的是业务侧:清运调度关心的不是「满没满」,而是「还能撑多久」。半满意味着这趟可以缓一缓,满溢意味着必须马上处理。三类别直接对应三档调度策略,比二分类多出来的那一类,恰恰是省成本的那一类。

类别定义建议写成可执行的判定规则,而不是形容词:

类别判定规则(满足其一即可)典型误标
空桶桶内可见底部,或垃圾高度低于桶深 1/3把深色桶底当垃圾
半满垃圾高度在桶深 1/3 到 4/5 之间,未超出桶口把桶沿阴影当垃圾
满溢垃圾超出桶口平面,或桶盖无法闭合把背景堆物算进桶内

这张表要贴在标注工具旁边,标注员每标一张对照一次。别嫌麻烦,返工一次的成本是首次标注的三倍。

2.2 采集清单:每个类别至少覆盖这几种场景

数据集质量决定上限,这话在满溢检测里尤其真。我一般按「类别 × 场景 × 光照」三维度列采集清单,避免某一类在某个场景下完全缺失。三类别各准备以下场景,每类每场景不少于 150 张:

  • 空桶:标准空桶、刚清运完、桶内有少量积水、桶盖半开
  • 半满:日常投放后、纸箱类大件、袋装垃圾堆叠、桶口有零星垃圾
  • 满溢:垃圾高出桶口、桶盖被顶开、桶边散落、多桶并排其中一桶溢出

光照维度至少覆盖顺光、逆光、阴天、夜间补光四种。傍晚斜射光单独归一类,因为它是误报重灾区,样本量要加倍。采集时用固定机位和移动机位各拍一轮,固定机位模拟已部署摄像头,移动机位补充角度多样性。

提示:采集阶段就按「场景_光照_序号」命名文件,后期切分数据集时能直接按前缀分层抽样,省掉大量人工核对。

2.3 标注规范:框怎么画、遮挡怎么处理

标注规范不统一,后面调参调到怀疑人生。三条硬规则:

第一,框只框桶体可见部分,不框散落在桶外的垃圾。满溢类别的框以桶口为基准向上扩展,包含超出桶口的垃圾,但不包含地面散落物。第二,遮挡超过 50% 的桶直接丢弃,不标。遮挡 20% 到 50% 的标,但在标签文件里加一个occluded标记位,训练时可以选择性加权。第三,多桶并排时每个桶单独成框,不允许一个大框套多个桶。

标签格式用 YOLO 标准:class_id x_center y_center width height,全部归一化到 0 到 1。类别 id 固定为 0 空桶、1 半满、2 满溢,顺序一旦定下就不要改,改一次所有历史标签全废。

3. 用 YOLOV5 在本地跑通三类别训练

3.1 环境与数据目录:一次配置到位

先确认硬件:单卡 8GB 显存起步,三类别数据量在 5000 张以内,YOLOV5s 足够,别一上来就上 x。环境用 conda 隔离,避免和系统里的其他框架打架。

# 创建环境,python 版本按 YOLOV5 官方要求选 3.8 以上 conda create -n trash_detect python=3.9 -y conda activate trash_detect # 安装依赖,torch 版本按自己 CUDA 版本去官网查对应命令 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install -r requirements.txt # 验证 GPU 可用 python -c "import torch; print(torch.cuda.is_available())"

数据目录按 YOLOV5 约定组织,images 和 labels 平行放置,文件名一一对应:

dataset/ images/ train/ val/ test/ labels/ train/ val/ test/ trash.yaml

trash.yaml是数据集描述文件,内容如下:

# 数据集根路径,建议写绝对路径避免相对路径踩坑 path: /data/trash_dataset train: images/train val: images/val test: images/test # 类别数必须和 names 长度一致,写错直接报错 nc: 3 names: ['empty', 'half', 'full']

参数说明:path写绝对路径,YOLOV5 对相对路径的解析依赖启动目录,换目录就找不到数据。nc和names必须严格对应,顺序就是标签里的 class_id 顺序。改完 yaml 先用官方脚本校验一遍标签,能提前揪出越界坐标和空标签。

3.2 训练命令与关键参数怎么设

启动训练前先跑一次数据校验,确认没有损坏图片和越界标签:

# 校验数据集,会输出每类样本数和标签分布 python utils/general.py --check dataset/trash.yaml

正式训练命令:

python train.py \ --data dataset/trash.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --hyp data/hyps/hyp.scratch-low.yaml \ --name trash_v1 \ --cache

逐参数说清楚:--img 640是输入分辨率,满溢检测里桶口细节重要,640 是精度和速度的平衡点,显存够可以上 768,但推理速度会掉三成。--batch 16按显存调,8GB 卡跑 640 分辨率 16 是安全值,爆显存就降到 8。--epochs 100是上限,实际看验证集 mAP 曲线,连续 20 轮不涨就可以停。--hyp用 low 增强配置,因为满溢场景本身样本少,强增强容易把满溢样本增广成半满,反而添乱。--cache把图片缓存到内存,数据量小于 8000 张时能明显加速,内存不够就去掉。

训练过程中重点盯三个指标:mAP@0.5看整体,mAP@0.5:0.95看定位精度,每类的precision和recall分开看。满溢类的 recall 比 precision 重要——漏报一个满溢桶,清运车白跑一趟;误报一个,顶多多派一次车。所以调参时优先保满溢类 recall。

3.3 推理与结果可视化:先看混淆再谈部署

训练完先别急着导出模型,用验证集跑一遍混淆矩阵,看清楚错在哪:

# 在验证集上推理,生成混淆矩阵和 PR 曲线 python val.py \ --data dataset/trash.yaml \ --weights runs/train/trash_v1/weights/best.pt \ --img 640 \ --conf 0.25 \ --iou 0.45 \ --task val

--conf 0.25是置信度阈值,低于这个值的框直接丢。满溢检测里建议先设 0.25 看召回,如果满溢类漏检多就降到 0.15,代价是误报上升。--iou 0.45是 NMS 的 IoU 阈值,多桶并排场景下这个值别设太高,否则相邻桶的框会被合并,0.45 是稳妥起点。

混淆矩阵出来后重点看两处:满溢被误判成半满的比例,以及半满被误判成空桶的比例。前者说明桶口特征学得不够,后者说明中间态样本太少。针对性地补样本,比盲目加 epoch 有效得多。

单张图片推理验证:

python detect.py \ --weights runs/train/trash_v1/weights/best.pt \ --source test_images/ \ --img 640 \ --conf 0.25 \ --save-txt

--save-txt会把检测框坐标存成 txt,方便后续和业务系统对接。输出目录在runs/detect/exp下,每跑一次序号递增,别搞混。

4. 满溢检测的避坑与排查清单

4.1 坑一:满溢类 recall 死活上不去

现象:训练到 60 轮,空桶和半满的 mAP 都过 0.8 了,满溢类还在 0.5 徘徊,混淆矩阵里大量满溢被标成半满。

原因:满溢和半满的视觉差异集中在桶口那一小块区域,640 分辨率下桶口可能只占几十个像素,特征被下采样吃掉。加上满溢样本通常比另外两类少,模型倾向于把边界样本判成多数类。

解决:三步走。第一,把满溢样本单独过采样,用--oversample或者在 yaml 里复制满溢图片路径,让三类样本量接近。第二,把输入分辨率提到 768,桶口细节能多保留一层特征图。第三,在hyp里调高box损失权重,让模型更关注定位精度。我一般先做第一步,八成情况能拉回来。

4.2 坑二:傍晚图片误报率飙升

现象:白天验证集表现正常,一换傍晚斜射光的测试集,空桶被大量误报成满溢,precision 掉到 0.4。

原因:斜射光在桶内形成长条阴影,纹理和垃圾袋高度相似,模型把阴影当成了溢出物。这是数据分布问题,不是模型问题。

解决:在采集阶段就补傍晚样本,每个类别至少 200 张,且要覆盖不同太阳角度。训练时开启 HSV 增强里的hsv_v参数,让模型对亮度变化更鲁棒。如果已经训练完才发现,用傍晚样本做一轮小学习率微调,--lr0设成初始值的十分之一,跑 20 轮就能明显改善。

4.3 坑三:多桶并排时框合并

现象:两个相邻的桶,一个满一个空,检测结果只出一个框,类别还是满溢。

原因:NMS 的 IoU 阈值设太高,两个桶的框重叠度超过阈值被合并。或者标注时就用了一个大框套两个桶,模型学到的就是合并模式。

解决:先查标注,确认每个桶单独成框。然后调低--iou到 0.35,让 NMS 更激进地保留独立框。如果还不行,在推理后处理里加一层按框中心点距离的分割逻辑,距离小于桶宽一半的框强制拆开。

4.4 坑四:小目标桶检测不到

现象:远景摄像头里桶只占画面很小一块,模型完全没输出。

原因:YOLOV5 默认的三层检测头最小 stride 是 8,对应 640 输入下最小可检测目标约 8 像素。桶小于这个尺寸就丢了。

解决:换用带 P2 检测头的配置,yolov5s-p2.yaml多一层 stride 4 的特征图,小目标召回明显提升,代价是推理慢 20% 左右。或者把输入分辨率提到 1280,但边缘设备基本扛不住。实际项目里我一般建议调整摄像头安装位置,让桶占画面高度至少 1/10,比改模型划算。

4.5 坑五:标签类别顺序写反

现象:训练 loss 正常下降,但推理结果全乱,空桶被标成满溢。

原因:yaml 里names顺序和标签文件里的 class_id 对不上。比如 names 写的是['full', 'half', 'empty'],标签里 0 却是空桶。

解决:定一个铁律——class_id 0 永远是空桶,1 半满,2 满溢,所有脚本、yaml、标注工具统一。改完 yaml 后跑一次val.py,看混淆矩阵对角线是否集中,不集中就是顺序错了。

5. 从能跑到好用:阈值调优与边缘部署的取舍

模型跑通只是起点,真正决定这套方案值不值得投入的,是推理阈值和部署形态的匹配。满溢检测的业务逻辑决定了它不能只追求 mAP 好看,得按调度成本反推阈值。

先做阈值扫描。写个小脚本,在验证集上遍历 conf 从 0.1 到 0.6,步长 0.05,记录每个阈值下满溢类的 recall 和误报数:

import numpy as np from pathlib import Path # 假设已经拿到每个检测框的 conf 和是否正确的标记 # confs: 模型输出的置信度数组 # is_correct: 该框是否与真值匹配(IoU>0.5 且类别正确) confs = np.load('val_confs.npy') is_correct = np.load('val_correct.npy') for thresh in np.arange(0.1, 0.65, 0.05): # 高于阈值的框才保留 kept = confs >= thresh tp = np.sum(kept & is_correct) fp = np.sum(kept & ~is_correct) fn = np.sum(~kept & is_correct) recall = tp / (tp + fn + 1e-6) precision = tp / (tp + fp + 1e-6) print(f"conf={thresh:.2f} recall={recall:.3f} precision={precision:.3f} fp={fp}")

这段逻辑很直白:遍历阈值,算每个阈值下的召回和精确率,重点看误报数 fp 随阈值的变化曲线。满溢检测里我一般选 recall 不低于 0.9 的前提下 fp 最小的那个阈值。实际项目里这个点通常在 0.2 到 0.3 之间,比默认 0.25 略低。

再说部署形态。边缘盒子跑 YOLOV5s 在 640 分辨率下,主流芯片能到 15 到 30 FPS,对垃圾桶巡检这种低频场景绰绰有余。但有两个取舍要注意:第一,别用 TensorRT 的 FP16 量化去压满溢类,量化误差在桶口细节上会被放大,我实测过 FP16 下满溢 recall 掉 5 个点,用 FP32 或者 INT8 校准更稳。第二,如果摄像头是 4G 回传,别在边缘做全量推理,改成运动触发——画面有变化才推理,能省七成流量。

最后说一个我踩过的坑:别在训练集上验证阈值。训练集 loss 低不代表阈值合适,一定要留一个完全独立的测试集,按场景分层抽样,傍晚样本占比和实际部署环境一致。我吃过一次亏,测试集全是白天图,阈值调到 0.3 看着很美,上线第一周傍晚误报把运维电话打爆。后来养成习惯,任何阈值调整都在「傍晚 + 阴天 + 夜间」三个子集上分别验证,取最差情况做决策。这套流程跑下来,三类别满溢检测的误报率能压到可接受范围,方案才真正值得投。希望帮到你。

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

RELATED READING

延伸阅读

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