简介:面向目标检测学习者的YOLOv5实战资源,提供垃圾桶满溢检测三类别数据集及完整训练方案,适用于智慧环卫、城市精细化治理等场景的算法验证与项目实训。压缩包共2000个文件,主要包含1921个txt标注文件、40个Python脚本、23个yaml配置及9个sh脚本,另有少量说明文档和json配置,整体约450MB,代码与数据经测试可直接运行。已有366人学习,项目迭代100轮,最优精度mAP@0.5为0.91、mAP@0.5:0.95为0.73,并保留验证集混淆矩阵、PR曲线、F1曲线及推理结果等训练产物。资源内含2680张训练图与669张验证图,均配套YOLO格式标签,并提供训练好的权重参数,可直接用于检测推理、迁移学习,也适合在此基础上继续改进YOLOv5结构。
1. 垃圾桶满溢检测是什么:一张监控截图背后的 3 分类问题
凌晨四点,环卫调度中心的大屏上弹出一条告警:某街道中转站 3 号工位垃圾桶满溢,系统自动派单给最近的清运车。这套流程听起来不复杂,但背后那个能分清「这个桶到底满没满」的模型,就是标题里的 YOLOv5 实战项目要解决的事。它用一份 3 类别的垃圾桶状态数据集,训练一个基于 YOLOv5 的目标检测网络,让摄像头能实时判断每一个垃圾桶的满溢程度。这个任务最反直觉的地方在于:它不是一个简单的二分类问题,不同角度、不同光照下「满」的定义完全不同,所以真正决定模型上限的不是网络结构,而是数据集怎么标、怎么划分、怎么喂给训练脚本。这篇文章就是把你从「下载了个数据集」带到「能独立训出可用模型、知道参数为什么这么设、部署时哪里会翻车」的状态。适合正在做环卫智能化、边缘设备部署,或者第一次用 YOLOv5 训练自己的数据集、想避开入门暗坑的开发者。
2. 把原始图片变成 YOLO 能吃的标注:3 类别定义、目录结构与校验脚本
拿到一批垃圾桶照片后,第一件事不是急着训练,而是把数据整理成 YOLOv5 要求的格式。很多人在这里栽跟头:直接用现成数据集目录开训,报错一个接一个,其实 80% 的问题都出在标注文件格式、类别 ID 对不上、标签和图片文件名不匹配这三件事上。这一章我们把每一步拆开,做完之后,你的数据目录会是干干净净、可以反复使用的状态。
2.1 满溢检测的 3 个类别怎么定才不会被模型「带偏」
标题里写明是 3 类别,最常见的落地分法是按垃圾桶状态来分:normal(正常)、half(半满)、overflow(满溢)。为什么不用「有垃圾 / 没垃圾」这种二分类?因为清运调度需要的不是「有没有」,而是「该不该去」——只有 overflow 状态的桶才需要马上派车。但如果你把 half 和 overflow 之外的都归为 normal,模型很快就会被带偏:半满的桶在各种角度下和满溢长得太像,边界一模糊,误检率直接拉高。
我一般会再给标注定三条硬性规则,写下来贴给标注员看:
提示:宁可漏标,不要错标。对边界拿不准的图片直接删掉,不放进数据集。
- 桶内垃圾超过桶口平面视为 overflow。
- 垃圾高度在桶体 1/3 到 2/3 之间视为 half。
- 桶内基本为空或只有少量底部垃圾,且桶盖能正常盖上,视为 normal。
另一个常见做法是按垃圾材质分 3 类——厨余、可回收、其他。这在分类任务里没问题,但用在满溢检测上就会跑偏:同样是厨余桶,有的满溢有的半满,模型学的是「桶的种类」而不是「桶的状态」,部署时看到个干净的新桶反而懵了。所以凡标题强调「满溢检测」,类别必须先选状态维度,再考虑材质。
标注工具我常用 labelImg 或者开源的 X-AnyLabeling,导出成 YOLO 格式后,每张图片对应一个同名 txt 文件,格式是:
class_id x_center y_center width height注意这里 x_center、y_center、width、height 全部是归一化到 0~1 的小数。很多第一次上手的人会误填成像素坐标,模型训练直接崩,loss 直接 NaN。
2.2 标注格式转换与标签校验脚本,半小时排查 500 张图的毛病
如果你拿到的标注是 COCO 格式(一个 JSON 文件存全部标注),需要先转成 YOLO 的每图一个 txt。这里给一个我常用的转换脚本骨架,用 Python 跑,能顺带把越界框和类别 ID 错误一次查出来:
import json import os import cv2 def coco_to_yolo(json_path, img_dir, out_dir): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) os.makedirs(out_dir, exist_ok=True) img_info_map = {img["id"]: img for img in data["images"]} cat_map = {cat["id"]: idx for idx, cat in enumerate(data["categories"])} for ann in data["annotations"]: img_info = img_info_map[ann["image_id"]] img_path = os.path.join(img_dir, img_info["file_name"]) img = cv2.imread(img_path) if img is None: print(f"图片无法读取: {img_path}") continue h, w = img.shape[:2] x, y, bw, bh = ann["bbox"] # COCO bbox 是左上角 + 宽高 x_center = (x + bw / 2) / w y_center = (y + bh / 2) / h bw_norm = bw / w bh_norm = bh / h cat_id = cat_map[ann["category_id"]] txt_name = os.path.splitext(img_info["file_name"])[0] + ".txt" with open(os.path.join(out_dir, txt_name), "a") as tf: tf.write(f"{cat_id} {x_center:.6f} {y_center:.6f} {bw_norm:.6f} {bh_norm:.6f}\n") if __name__ == "__main__": # 用法: python coco_to_yolo.py <标注json> <图片目录> <输出目录> import sys coco_to_yolo(sys.argv[1], sys.argv[2], sys.argv[3])这段代码做的事情是把 COCO 的 bbox 换算成 YOLO 的归一化中心点格式。关键点有两个:一是cv2.imread读不到图时立即打印,防止图片缺失导致训练时 txt 和 jpg 对不上;二是类别 ID 要重新映射,因为 COCO 的 category_id 往往不是从 0 开始连续排列,而 YOLOv5 要求类别 ID 必须从 0 开始且不能跳号,否则训练日志里类别数和实际标签对不上。
如果你手里的数据本身就是 YOLO 格式,那也要做一次校验。我见过最多的坑是某个 txt 里写着5 0.5 0.5 0.2 0.2,但数据集只定义了 3 个类,类别 ID 5 直接让训练脚本报错。校验脚本也很短:
# 查出哪些标注文件的类别 ID 超范围 for f in labels/*.txt; do awk '{ if ($1 < 0 || $1 > 2) print FILENAME": "$0 }' "$f" done这一行 awk 能把你数据集里所有非法类别标出来。跑完发现有几个文件有问题,不要手改——直接回到标注工具里修正对应图片,再重新导出。手工改 txt 的数字很容易把框的中心点坐标一起改坏,这是血泪经验。
2.3 目录划分:train、val、test 怎么分才不「串味」
YOLOv5 对数据目录的默认要求很简单,但很多人会在划分逻辑上犯错。正确结构是这样的:
dataset/ ├── images/ │ ├── train/ # 训练图片,jpg/png 均可 │ ├── val/ # 验证图片 │ └── test/ # 测试图片,可选但建议留 ├── labels/ │ ├── train/ # 每张图片同名的 txt │ ├── val/ │ └── test/注意 images 和 labels 下的子目录名称必须完全一致,YOLOv5 会在训练时按文件名自动去找对应标注。如果你把 val 的图片放在images/val但标注放在labels/val2,就会看到 Loss 正常但 mAP 一直是 0 的诡异现象,最后查半天发现是路径没对上。
划分比例我一般用 8:1:1。关键是划分时要以「场景」为单位,而不是以「单张图」为单位。什么意思?同一个垃圾桶在不同时间拍的连续帧,如果一部分进了 train、一部分进了 val,val 的精度会虚高,因为模型在 train 里已经见过同一个桶了。部署后换个视角的桶,精度立刻掉一大截。所以先按摄像头点位或者桶的编号分组,再整组划分。
一个按文件名前缀分组的 Python 脚本片段:
import os import random import shutil from collections import defaultdict src_img = "images_all" src_lbl = "labels_all" groups = defaultdict(list) for f in os.listdir(src_img): prefix = f.split("_")[0] # 假设文件名是 cam01_0001.jpg 这样的格式 groups[prefix].append(f) all_files = list(groups.keys()) random.seed(42) random.shuffle(all_files) n = len(all_files) train_groups = set(all_files[:int(n * 0.8)]) val_groups = set(all_files[int(n * 0.8):int(n * 0.9)]) for f in os.listdir(src_img): prefix = f.split("_")[0] ext = os.path.splitext(f)[0] lbl = ext + ".txt" if prefix in train_groups: shutil.copy(os.path.join(src_img, f), "dataset/images/train/") shutil.copy(os.path.join(src_lbl, lbl), "dataset/labels/train/") elif prefix in val_groups: shutil.copy(os.path.join(src_img, f), "dataset/images/val/") shutil.copy(os.path.join(src_lbl, lbl), "dataset/labels/val/")这里random.seed(42)保证可复现——同一个数据集每次划分的结果一样,你和同事讨论精度时基准才统一。如果哪天调参后效果变好,你能确认是参数起的作用而不是数据划分变了。这是很多人忽略的玄学点。
3. 用 YOLOv5 训练自己的满溢数据集:yaml 配置、训练命令与三个关键超参数
数据准备好了,接下来就是训练。这一章直接给可抄的配置和命令。YOLOv5 的训练入口是仓库根目录下的train.py,但所有行为都由一个数据集 yaml 文件和一个训练命令决定。理解这两个文件里的每一项,比你盲目调十次参都有用。
3.1 数据集 yaml 配置文件:路径、类别名与一个容易忽略的坑
在 YOLOv5 仓库的data目录下新建一个trash_overflow.yaml,内容如下:
# 垃圾桶满溢检测数据集配置 path: /home/user/dataset # 数据集根目录的绝对路径 train: images/train # 相对 path 的训练图片目录 val: images/val # 相对 path 的验证图片目录 test: images/test # 相对 path 的测试图片目录 nc: 3 # 类别数量,必须和标注里的 class_id 最大值 + 1 相等 names: ['normal', 'half', 'overflow'] # 类别名,顺序就是 class_id 的顺序这里有个非常容易翻车的点:path写相对路径还是绝对路径。YOLOv5 官方代码里支持相对路径,但它会相对于你执行train.py时所在的目录解析,而不是相对于 yaml 文件所在目录。很多人把 yaml 放到data/下,然后path: dataset/images/train,结果训练报错说找不到图片。我一般直接写绝对路径,一劳永逸。
第二个坑是names列表的顺序必须和标注 txt 里的 class_id 一一对应。比如你的 txt 里0代表的是 overflow,但 yaml 里names[0]写的是 normal,那训练不会报错,但混淆矩阵和后续部署输出全乱套,模型推理出来标着 normal 的框其实是满溢桶。
3.2 最小训练命令与每个参数的含义
配置好 yaml 后,在 YOLOv5 仓库根目录执行:
python train.py \ --data trash_overflow.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --workers 4 \ --device 0这是一个适合单张消费级显卡的最小命令,比如 RTX 3060 或 4060 级别。逐项说明:
--weights yolov5s.pt:用 COCO 预训练权重做迁移学习。s 是 small,速度最快,内存占用最小。如果你的场景里有大量小目标(比如远处的垃圾桶),可以换yolov5m.pt或yolov5l.pt,但第一次跑建议先用 s 打通流程。--img 640:输入图像缩放尺寸。640 是速度和精度的常用平衡点。如果你的监控画面分辨率是 1920x1080,训练时 YOLOv5 会把图缩到 640,这没问题;但如果桶在画面里占的像素极少,建议用 896 或 1280,代价是训练时间和显存翻倍。--batch 16:每次迭代输入 16 张图。batch 越大,梯度越稳定,但显存占用越高。显存不够时先减 batch,不要先减 img,因为图像尺寸对精度影响更大。--epochs 100:训练轮数。满溢检测这类背景相对固定的任务,一般 60~100 轮就能收敛。如果你发现 50 轮后 loss 还在明显下降,就继续加。--workers 4:数据加载线程数。Windows 上有时会报错,调成 0 是最省事的办法;Linux 上设成 CPU 核心数的一半左右。
训练启动后不要干等着。盯住终端输出的几行指标:初始 loss 应该在 0.1 以下(因为有预训练权重),如果第一轮就是 NaN,说明标注里有空 txt 或者类别 ID 越界,立刻停下回去查数据。
3.3 三个直接影响收敛的超参数:学习率、图像尺寸与锚框
很多时候模型训出来 mAP 不低但实际效果差,问题不在网络结构,而在超参数。YOLOv5 默认的--lr0 0.01适合大多数情况,但满溢检测任务有个特点:类别高度不平衡。如果你的数据里 overflow 只占 5%,half 占 15%,normal 占 80%,模型会倾向于把所有框都预测成 normal,mAP 看着还行,真正满溢的桶一个都检不出来。
这种时候我一般做两件事。第一,给训练命令加类别权重文件,让模型对少数类别更敏感。方法是在 yaml 文件同目录下写一个类别权重列表传给--class-weights,但 YOLOv5 对这块支持不如 YOLOv8 完善,更实用的做法是直接在数据层面过采样。简单说就是把 overflow 图片复制几份放进 train 目录,让三类数量别差得太悬殊。经验值是:最少类样本数不少于最多类的 30%,否则很难训好。
第二个参数是--multi-scale。这个开关会在每次迭代时随机把输入图像缩放在img * 0.5到img * 1.5之间。满溢检测里桶的大小变化极大——近处的桶占了半屏,远处的桶只有几十个像素,加上这个参数可以有效提升小目标的召回率。代价是训练时间增加约 20%,但值得。
第三个是--save-period。默认每轮都保存权重,但浪费磁盘。我习惯设置为 10,每 10 轮存一个 pt 文件,方便回滚。训练到一半断电或 loss 发散,你可以从最近的一个 checkpoint 继续,不需要从头再来,这个习惯给我省过不少时间。
4. 训练结果分析:mAP、混淆矩阵与五个高频踩坑点
训练结束不是看results.jpg里 mAP 0.9 就能欢呼的。满溢检测的失败模式很隐蔽:mAP 高但现场误报多、白天准晚上崩、近处准远处漏。这一章先讲怎么看结果文件,再讲五条我用真金白银换来的排错经验。
4.1 从 runs 目录里读出模型真实水平:confusion_matrix.png 是最诚实的文件
训练完成后,YOLOv5 会在runs/train/exp/目录下生成一堆文件。大部分人只看results.png里的曲线,但最有价值的是confusion_matrix.png和F1_curve.png。
混淆矩阵里,对角线越亮越好,但如果看到溢出类别的行被大量预测成 normal,说明类别不平衡的老毛病还在。这种情况下你再调 mAP 都没有意义,回到数据集做过采样或者重新清洗误标样本。
F1 曲线能告诉你置信度阈值该设多少。满溢检测部署时,你不能直接用默认的 0.25 置信度——环卫场景里漏检一次满溢桶的投诉成本,远高于多跑一次空车。所以我一般会把阈值调到能接受的最大误检率下的最高置信度。看 F1_curve 上哪个置信度位置曲线的值最高,就在那个附近选。
另一个常见操作是拿一批没进过训练集的现场图片做批量推理,输出每张图的检测结果和置信度。用自带的detect.py:
python detect.py \ --weights runs/train/exp/weights/best.pt \ --source test_images/ \ --conf 0.4 \ --save-txt \ --project runs/detect/--save-txt会把每张图的检测结果写到 txt 里,方便脚本统计误检率。我会写一个简单的 Python 脚本统计这批现场图里,overflow 类别的检出率(召回率)和把 normal 错检成 overflow 的数量(误检率)。
4.2 避开这些坑:类别不平衡、过拟合、部署环境差异、空标签文件、置信度阈值
第一条,现象是训练 loss 下降但 val 的 mAP 波动大,最后 best.pt 反而出现在前几轮。原因是验证集里有大量和训练集几乎一样的连续帧,模型在训练集见过的场景太多,val 失去代表性。解决方法是按 2.3 节按场景分组重新划分,不要让同一个桶的连续帧同时出现在两个集合里。
第二条,类别不平衡导致 overflow 的召回率极低。现象是混淆矩阵里 overflow 行大部分落在 normal 列。解决方法是过采样,并检查是不是标注员把大量 overflow 标成了 half——因为「满到什么程度算溢」的主观判断差异比想象中大得多。
第三条,Windows 下训练时报BrokenPipeError。原因是--workers大于 0 时 Windows 的数据加载器和主进程之间有兼容性问题。解决方法是--workers 0,损失一点加载速度,但能稳定跑完。
第四条,训练一切正常,导出 ONNX 后在树莓派或者 RK3568 这样的边缘设备上部署,精度掉了 5~8 个点。原因是训练时图片经过的归一化和推理时的预处理不一致。YOLOv5 官方仓库导出 ONNX 时会内置预处理信息,但你自己写推理脚本时忘了做letterbox(等比例缩放加灰边),图直接 resize 导致几何变形。解决方法是导出时用python export.py --weights best.pt --include onnx --img 640,然后推理时严格按导出的预处理来。
第五条,val 里一个垃圾桶都检不出来,但训练 loss 正常。原因 90% 是 labels 目录下对应的 txt 文件是空的,或者文件名没对上。空的 txt 文件不会让训练报错,但会让这一张图的 loss 计算异常。排查方法很简单:
# 找出所有没有对应标签的图片 find images/val -name "*.jpg" | while read f; do name=$(basename "$f" .jpg) if [ ! -f "labels/val/$name.txt" ]; then echo "缺少标签: $f" fi done这三条排查思路能覆盖大多数「训练完了但效果不对」的怪问题。记住一个原则:先怀疑数据,再怀疑参数,最后才怀疑网络结构。YOLOv5 本身非常稳定,绝大多数翻车都发生在数据链路里。
5. 让满溢检测更贴近现场:三个改进方向与我的例行验证步骤
模型已经能跑,但离「让人放心部署」还差几步。这一章不展开讲新框架,只讲我在垃圾桶满溢检测场景里反复验证过有效的三个方向,以及每次改动后我固定要做的一套验证流程。
第一个方向是针对性数据增强。满溢桶的核心视觉特征是「桶盖上鼓起来、边缘变形」,但现场照片里这些特征很容易被阴影和反光干扰。我自己的做法是给训练命令加--hyp指向一个调过的超参文件,把hsv_h、hsv_s等颜色增强参数稍微调低,同时把translate和scale调高,因为摄像头安装角度变化比颜色变化更常见。具体数值我一般设置为hsv_h: 0.01、hsv_s: 0.5、translate: 0.2、scale: 0.8。这组参数对室内外混合的垃圾桶场景比默认值更稳。
第二个方向是时间维度的复用。单个摄像头单帧偶尔会误检,但同一个桶连续 10 帧里有 8 帧都显示 overflow,那基本就是真溢了。这个用部署侧的简单时序滤波就能解决,不需要改模型。我在边缘盒子上用 OpenCV 读帧,维护一个简单计数器:检测到 overflow 连续超过 N 帧才触发告警,N 一般取 5~8 帧。这样能把偶发的误检压到几乎为零,成本几乎可以忽略。
第三个方向是小目标优化。如果你发现远处小尺寸的桶经常漏检,除了调大--img,还可以尝试在训练时启用--rect参数。这个参数会让 YOLOv5 按图片宽高比分 batch,相同长宽比的图放在一起缩放,减少拉伸变形,对竖屏监控画面尤其友好。代价是每个 batch 的 shape 不一致,需要显存稍微大一点。
每次调整完数据或参数,我例行跑三条验证:
# 第一条:在未见过的现场图片上批量推理,统计每个类别的置信度分布 python detect.py --weights best.pt --source val_new/ --conf 0.25 --save-txt # 第二条:导出 ONNX 并用 onnxruntime 推理同一张图,对比和 PyTorch 原版的结果 python export.py --weights best.pt --include onnx --img 640 python onnx_infer.py --weights best.onnx --image test.jpg # 第三条:用 video 模拟连续帧,验证时序滤波逻辑 python detect.py --weights best.pt --source test_video.mp4 --conf 0.4第二条尤其重要。我遇到过 PyTorch 里检测正常、导出 ONNX 后同一个框置信度突变的案例,最后查到是 ONNX 的 NMS 参数和原版不一致。所以我的习惯是:任何模型改动之后,不只在 PyTorch 里自嗨,一定把导出 + onnxruntime 推理 + 和原版对比这一步走完,再交给部署。
这套组合拳打完,你的满溢检测模型才算真正从「训练集里 mAP 高」变成「现场能用」。我也曾在第三类上偷懒跳过 ONNX 对比,结果在 RK3568 上白排了一天错,最后还是回到这步流程。希望帮到你。
本文还有配套的精品资源,点击获取