做城市街道垃圾自动检测那段时间,我最大的体会是:YOLO 模型本身并不难跑通,真正决定效果上限的反而是训练数据。城市街道垃圾检测数据集(YOLO)解决的就是这块最让人头疼的环节——它把街景中常见的垃圾目标整理成标准的目标检测标注格式,图片、边界框、类别标签、训练集划分都按 YOLO 的胃口备好了。拿到这套数据,你可以直接喂给 YOLOv5、YOLOv8 等常见模型开始训练,也可以把它作为自己业务数据的起点和参考模板。
这篇文章我打算从一个从业者的角度,把这类数据集从数据组织、YOLO 标注格式、清洗标注、训练评估到问题排查完整串一遍。无论你是刚接触目标检测的学生,还是在做智慧城市、环卫巡检项目的算法工程师,按照里面的步骤都能逐步复现,并且避开我踩过的那些坑。
1. 项目整体设计:街道垃圾数据到底在解决什么问题
1.1 为什么必须单独做“街道垃圾”数据集
简单说,城市街道垃圾检测和通用目标检测有本质差别。通用数据集里常见的行人、车辆、猫狗,特征相对稳定,网上开箱即用的预训练权重也能识别;但街道垃圾是另一回事:烟头只有几十像素,塑料袋被风吹得形态百变,空的矿泉水瓶躺在树影里和地面颜色几乎融为一体。用通用目标检测模型的预训练权重直接跑,很容易出现漏检和误检并存的情况。
所以项目的第一步不是选模型,而是把数据的地基打好。城市街道垃圾检测数据集(YOLO)这类数据,核心价值在于它把所有久经考验的“脏活”提前做完了:收集了大量真实街景画面中的垃圾目标,按照统一的命名规范整理图片和标注文件,并且把数据划分成训练集、验证集和测试集。你拿过来可以立刻开始训练,不需要花几周时间从零采集和标注。
这里还要提一下“数据源”这个词。很多这类数据集不只是给你一堆图片和 txt,还会附带每条样本的来源信息,比如来自固定摄像头、车载巡检相机还是人工手持拍摄,以及当时的时段、天气等条件。这些信息看起来不起眼,实际用起来很关键,因为部署场景和训练数据来源越贴近,模型的落地效果就越好。你完全可以根据这些数据源信息做针对性筛选,比如单独评估夜间样本的检测能力。
1.2 垃圾类别怎么定:粒度决定了模型的天花板
这类数据集通常不会把所有垃圾都糊成一个“垃圾”类,那样模型学不到区分度。比较合理的做法是拆成若干形态差异大、实际治理工作中有意义的类别。下表是一种常见的类别设计方案:
| 类别ID | 类别名 | 典型形态 | 标注难点 |
|---|---|---|---|
| 0 | plastic_bottle | 矿泉水瓶、饮料瓶 | 透明瓶身与地面反光难分 |
| 1 | can | 易拉罐 | 小目标、反光强烈 |
| 2 | plastic_bag | 塑料袋、包装袋 | 形态高度可变 |
| 3 | paper | 纸屑、纸箱、纸杯 | 颜色接近路面 |
| 4 | lunch_box | 一次性餐盒 | 遮挡多、半透明 |
| 5 | cigarette_butt | 烟头 | 目标极小 |
| 6 | fruit_peel | 果皮、果核 | 腐烂变色,边缘模糊 |
| 7 | leaf_branch | 枯枝落叶 | 常被当作背景干扰 |
| 8 | glass | 玻璃碎片、酒瓶 | 透明、反光 |
| 9 | other_trash | 难以归类的杂物 | 形态杂乱 |
这里有个很值得讨论的设计决策:枯枝落叶算不算垃圾?不同项目答案不一样。有些数据集把它单独列出来,而不是混进“其他垃圾”,原因是落叶和真正的垃圾在环卫处理路径上不一样,而且它在街景里出现频率极高。如果模型把大量落叶当垃圾输出,误报率会非常难看。单独设一个类别,至少能让误检归到一个相对无害的类别里。
类别粒度并不是越细越好。你分得越细,每个类别能分到的训练样本就越少,标注难度也越高,模型更容易混淆相邻类别。如果是第一次做,我建议先按上表这种中等粒度跑通流程,后续根据实际误报再拆细或者合并。
1.3 数据来源多元化:让模型见过足够多的“街”
街道垃圾识别最怕的就是模型只认得训练集里的那几条路。数据采集时最好覆盖这几类来源:
- 固定高位摄像头:主要拍人行道、马路沿、绿化带边缘,视角固定,背景相对稳定,但目标通常比较小。
- 车载巡检相机:环卫车或巡检车顶的相机,行驶中拍摄,视角有透视变化,目标会出现尺度缩放和运动模糊。
- 人工手持拍摄:用手机或运动相机沿街拍摄,角度自由,能覆盖到机器够不到的死角,比如座椅底下、墙角、下水道口。
从场景维度看,最好把住宅区、商业街、公园步道、工业区周边都收集一些;从时间维度看,早晨逆光、正午强光、傍晚阴影、夜间路灯下的样本都要有。很多数据集会把每张样本的来源和拍摄条件记录在文件名或配套 json 里,这就是前面说的“数据源信息”,目的是让你能按条件做子集测试,从而知道自己模型的泛化边界到底在哪。
2. 读懂 YOLO 标注格式:一行文本背后的坐标逻辑
2.1 目录结构与 txt 文件长什么样
拿到数据集之后,第一件事不是急着跑代码,而是先打开目录结构看清楚。标准 YOLO 格式数据集通常长这样:
dataset/ ├── images/ │ ├── train/ │ │ ├── street_0001.jpg │ │ ├── street_0002.jpg │ │ └── ... │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ │ ├── street_0001.txt │ │ ├── street_0002.txt │ │ └── ... │ ├── val/ │ └── test/ └── dataset.yaml图片和标注文件是一一对应的:images/train/street_0001.jpg对应的标注文件一定是labels/train/street_0001.txt,文件名相同,只是后缀不同。任何一个标注文件丢失或者改名,训练时都会报错或者导致那张图被静默跳过。
打开一个 txt 标注文件,每行代表一个目标:
0 0.5123 0.4211 0.0832 0.1244 5 0.3887 0.7620 0.0511 0.0632这行的五个数字分别是:类别 ID、目标中心点的 x 坐标、目标中心点的 y 坐标、目标宽度 w、目标高度 h。注意最后四个数不是像素坐标,而是相对于图片宽度和高度的归一化比例,取值范围通常在 0 到 1 之间。
2.2 从像素框到归一化坐标的换算过程
如果你拿到的原始标注是左上角和右下角的像素坐标 (x1, y1, x2, y2),要转成 YOLO 格式,就按这个逻辑换算:
def convert_to_yolo(x1, y1, x2, y2, img_w, img_h): # 框的中心点 cx = (x1 + x2) / 2.0 cy = (y1 + y2) / 2.0 # 框的宽高 w = x2 - x1 h = y2 - y1 # 归一化到 [0, 1] cx = cx / img_w cy = cy / img_h w = w / img_w h = h / img_h return cx, cy, w, h我见过有人图省事,直接用 (x1, y1, w, h) 转,没做中心点换算,结果训练出来 loss 怎么都降不下去。因为 YOLO 的损失函数需要的是中心点坐标、宽高这种表达方式,你给它左上角坐标,等于让它用错误的信息去回归。
尤其要注意边界框的“合法性”。标注软件基本不会出错,但当你自己写脚本合并数据、抽帧、自动切图的时候,很容易出现以下几种脏数据:
- 框的右边界超出图片宽度,导致 w 大于 1;
- 框的左上角和右下角几乎重合,w 或 h 接近 0;
- 某些框被裁剪后变成了负数坐标,换算出的值落在 [0,1] 之外。
YOLO 训练时遇到这种非法标注,轻则忽略该框,重则 loss 直接变成 NaN。稳妥的做法是加一个校验逻辑:凡是中心点坐标落在 [0,1] 外、宽高小于 0.0001 的标注行,一律打印出来检查。
2.3 dataset.yaml 配置与划分注意事项
数据集的入口其实是一个 yaml 文件,YOLO 训练时靠它来定位数据。一个典型的配置长这样:
path: /path/to/dataset train: images/train val: images/val test: images/test names: 0: plastic_bottle 1: can 2: plastic_bag 3: paper 4: lunch_box 5: cigarette_butt 6: fruit_peel 7: leaf_branch 8: glass 9: other_trash这里有几个坑值得提醒。第一,names的索引顺序必须和标注文件里的类别 ID 严格一致,而且 ID 从 0 开始,不能跳跃,不能从 1 开始。第二,路径最好不要带中文,跨 Windows、Linux 训练时很容易因为编码问题读取失败。第三,如果你不需要 test 集,可以只写train和val,训练时程序会自动对 val 做验证,不要硬留一个空的 test 目录。
划分数据的时候还有一个隐蔽但致命的问题:数据泄漏。如果同一段街道视频里连续的三帧,分别被分到训练集、验证集和测试集,那么验证集和测试集就形同虚设,因为你测的其实是模型已经见过的场景,指标会虚高,一旦换到新街道马上现原形。正确做法是:按视频片段或者按街道分组来划分,保证同一个场景的帧只出现在一个集合里。
3. 数据从原始图片到合格训练集:清洗、标注、增强
3.1 采集之后的筛选与隐私处理
很多数据集的第一版原始图片并不适合直接标注,直接扔给标注工具只会浪费人力和算力。我建议先做一轮清洗,标准很简单:
- 去掉严重模糊的图片:对焦不准的照片,人眼都分不清,标出来也是噪声。
- 去掉重复度极高的帧:视频抽帧时如果每秒取 10 帧,相邻帧几乎一模一样,对训练几乎没有增量贡献。
- 去掉过暗或者过曝到完全无法辨认内容的图片,除非你确实要训练夜间模型且画面里有可见目标。
- 处理隐私信息:街景照片里出现的人脸、车牌要打码。这不是形式主义,很多数据发布和使用场景都会涉及肖像和隐私问题,最好在采集阶段就做好脱敏。
视频抽帧的时候,我习惯一分钟视频里每隔 0.5 到 1 秒取一帧,也就是每秒一到两帧。既保留了足够的动作变化和视角变化,又不会因为连续帧太像而让训练集“虚胖”。抽帧可以用 OpenCV 自己写个 20 行的小脚本,也可以用现成的视频处理工具,关键是抽完后要看一遍缩略图,确保没有整段黑屏或花屏的废素材。
3.2 标注规范:边界情况需要一条“死规则”
使用开源的标注工具(比如 LabelImg)可以很快完成画框和打标签,但真正决定数据集质量的是“边界情况规则”。多名标注员同时干活时,如果没有统一规则,很容易同一个物体一个人标一个框、另一个人标两个框,导致模型学得无所适从。
我常用的几条规则很简单,但也踩过不少坑才定下来:
- 目标被遮挡且肉眼能辨认出类别,仍然标注;超过 50% 被遮挡或只露出一角无法确认是什么垃圾,不标。
- 多个垃圾堆在一起,视觉上能分开就分开标;缠绕在一起无法分离的,按一个框标为“其他垃圾”。
- 小于 10 像素的烟头要不要标?我建议单独把烟头这类极小目标全部标注,因为这是街道垃圾检测的重要对象,漏掉会在实际场景中形成大量漏检。
- 人眼都看不清的背景干扰物不标。不要为了“丰富数据”硬标,那样只会让模型学到错误特征。
这些规则一定要写成简短的标注手册,发给每个参与标注的人。不要靠口头传达,标注这种枯燥工作做到一半,人会进入机械状态,没有白纸黑字的规则,最终返工的成本会让人崩溃。
3.3 数据增强与类别平衡的实操思路
标注完的数据通常存在严重的类别不平衡。烟头可能只有几百个样本,塑料瓶却有一万个框。这时候模型会倾向于把不确定的目标都预测成高频类别,烟头的召回率会非常难看。
解决类别不平衡,我推荐几个组合用法:
- 对样本少的类别做离线复制粘贴增强:把小目标物体裁剪出来,粘贴到不同的背景图上,同时生成对应的标注框。这样可以快速扩充稀缺类别。
- 使用 Mosaic 增强:把四张图拼接成一张,YOLO 系列训练时默认开启这个增强,能显著提升模型对小目标和遮挡情况的鲁棒性。
- 在线增强里把亮度扰动、随机翻转、缩放打开,让模型对光照和尺度变化不那么敏感。
- 不要在增强阶段对某些类别无脑做水平翻转。垃圾检测里大多数类别翻转后仍然是垃圾,问题不大,但如果数据里有带文字的包装袋,翻转会让文字方向变奇怪。好在垃圾检测更依赖形状和纹理,这个问题通常不严重。
另外,如果夜间样本太少,单纯靠亮度增强模拟是不够的,最好还是真的去夜间街头补拍一批。合成数据和真实数据的分布始终有差距,夜间路灯的色温、地面反光、阴影形态都是增强模拟不出来的细节。
3.4 划分数集与自动校验脚本
把增强后的数据按 8:1:1 或者 9:0.5:0.5 划分训练、验证和测试集后,还要跑一遍自动校验。我每次开始训练之前都强制自己执行一个几十行的检查脚本,内容包括:
- 每个 txt 文件是否对应一张存在的图片;
- 每个 txt 中的每一行是否能解析出 5 个浮点数;
- 类别 ID 是否在 0 到 nc-1 之间;
- 归一化坐标是否在合理范围内,比如中心点 x、y 在 [0,1],w、h 不大于 1.5(允许少量越界但超过要警报);
- 是否存在图片有虚拟文件(图片存在但没有 txt),以及 txt 存在但没有图片的情况。
这个脚本跑完,能拦下八成以上的低级错误。我曾经因为一次批量改名操作导致图片和标注文件名后缀对不上,训练过程一直正常,但 mAP 忽高忽低,排查了一整天才发现问题,后来靠这个校验脚本彻底杜绝了同类问题。
4. 用这个数据集训练 YOLO:参数、曲线、评估
4.1 模型规模选择与训练参数
数据集准备好之后,就到了训练环节。现在 YOLOv8 这类官方工程已经非常成熟,训练命令基本是模板化的。模型规模上,我一般按显存和精度预期选:
- YOLOv8n:适合边缘设备或快速验证,精度低一些,但推理速度快。
- YOLOv8s:平衡之选,如果目标是跑通流程、验证数据质量,我通常先用这个。
- YOLOv8m 及以上:适合对精度要求高且部署端算力宽裕的情况,训练时间也会相应变长。
训练命令大致如下:
yolo detect train \ data=dataset.yaml \ model=yolov8s.pt \ epochs=150 \ batch=16 \ imgsz=640 \ device=0一些参数经验:batch要根据显存调,显存不够时可以用更小的 batch 配合梯度累积,效果等于增大了等效 batch;imgsz默认 640 对大多数街道场景够用,但如果你的目标普遍很小,可以试着提高到 960,小目标的 mAP 会有肉眼可见的提升,代价是训练和推理变慢;epochs我建议先设 100 到 150,配合早停机制,不用死等训练结束。
如果要从零训练而不是加载预训练权重,只把model=yolov8s.pt换成model=yolov8s.yaml即可,但一般不建议这么干。用 COCO 预训练权重做迁移学习,收敛速度快得多,而且 COCO 里虽然没有“烟头”这类类别,但模型对边缘、纹理、物体边界的通用特征已经学习过了,迁移效率很高。
4.2 训练曲线怎么读:正常和异常的表现
训练开始后不要干等,要看曲线。用官方训练脚本跑完会输出一张 results 曲线图,重点关注四个量:box_loss、cls_loss、dfl_loss、mAP50。
- 正常情况:三个 loss 在前 20 个 epoch 快速下降,之后平缓波动;mAP50 和 mAP50-95 稳步上升,最后逐渐饱和。
- 过拟合迹象:训练 loss 继续下降,但验证 loss 在某个 epoch 之后开始反弹;mAP 在验证集上停滞甚至下降。此时可以减小模型的复杂度、加大增强强度或者增加数据量。
- 训练震荡:loss 曲线像锯齿一样剧烈跳动,且迟迟不收敛,大概率是学习率太高或者数据里有脏标注。先排查 label 再降学习率。
- 一直不下降:检查是否忘记加载预训练权重、yaml 的类别数是否和标注一致、数据目录是否真的被读取到,而不是看错了配置文件。
4.3 评估指标与部署验收标准
模型训练完,第一个要看的指标是 mAP50 和 mAP50-95。两者的区别在于 IoU 阈值:mAP50 只要求预测框和真实框的 IoU 大于 0.5 就算对;mAP50-95 则是在 0.5 到 0.95 之间按 0.05 步长取多个阈值求平均。后者更严格,也更反映定位精度。
但在真实街道场景里,指标只能给你一个大致参考。我见过 mAP50 达到 0.8 的模型,部署到实际街道上依然会在光影变化大的时候漏掉黑色塑料袋。所以在数据集带的测试集上跑完指标后,一定要再拿出几十张“没见过的街景照片”,甚至一段现场录制的视频,用模型跑一遍看看实际效果。
调优阶段,置信度阈值是个容易忽略的旋钮。检测场景如果更看重少漏检,可以把阈值从默认 0.25 调到 0.15 左右,但这会带来更多误报;如果更看重少误报,就调高到 0.4 以上。街道巡检这类应用一般偏向高召回,因为漏掉一个烟头问题不大,但要是在干净路面频繁报警,后台人员很快会失去耐心。
5. 训练与部署中常见的问题和解决实录
5.1 误检重灾区:地砖纹理、树影、水渍
街道垃圾检测项目最常见的误检并非“把瓶子认成餐盒”,而是把地面本身的东西当成垃圾。地砖碎裂的纹理、树影投在地面上的轮廓、雨后残留的水渍、沥青路面的色差,都会在模型眼里酷似垃圾。
这类问题本质上不是模型能力不够,而是训练数据的“负样本”不足。负样本就是背景图片,也就是不包含任何目标的纯街道照片,标注文件为空。很多数据集会忽略负样本,觉得空标注文件毫无意义,但对降低误报率来说,负样本的价值极高。
处理手段有三条:一是尽量在数据集中保留 5% 到 10% 的空标注图片,让模型学会“这条路是干净的”;二是对采集到的误检图片做“难例挖掘”,把部署时被误判为垃圾的图片收入训练集,并保持标注为空;三是把置信度阈值适当调高,过滤掉低置信度的边缘预测。
5.2 漏检重灾区:烟头、碎玻璃这类极小目标
小目标漏检几乎是必然的,除非数据里针对性做了处理。YOLO 的主干网络经过若干次下采样后,一个 16x16 像素的烟头在特征图上可能只剩一个点,信息量几乎丢失。解决思路需要从数据、模型、推理三个层面同时做:
- 数据层面:增加小目标样本的密度,可以把远景图片裁剪成多个小图训练,或者用“复制粘贴增强”把小目标放到不同背景下,让模型见到更多小目标正样本。
- 模型层面:提高输入分辨率,让原图中 16 像素的烟头在放大后的输入图里变成 32 像素甚至更大;或者使用带有 P2 小目标检测层的模型结构。
- 推理层面:使用切片推理(SAHI 这类方法),把大图切成若干重叠块分别检测,再把结果合并。这个方法对小目标效果非常明显,但推理耗时也会成倍增加。
实际项目里这三个手段可以叠加使用。先靠数据增强把小目标样本量拉起来,再决定要不要上切图推理,避免一开始就把系统搞复杂。
5.3 训练 loss 不降或震荡的排查顺序
如果训练过程出现 loss 偏高、震荡、甚至 NaN,按下面的顺序排查,大多数问题都能找到:
- 用自动校验脚本检查标注文件格式,重点看类别 ID 是否越界、坐标是否为合法浮点数。
- 打开几十张图片和标注叠加重绘,肉眼确认框的位置和类别对不对。这一步能发现标注工具导出格式错误、类别 ID 错位等问题。
- 确认数据划分没有泄漏,验证集图片和训练集来源完全分开。
- 检查 yaml 文件路径是否正确,classes 数量是否和标注实际类别数一致。
- 把学习率调低一个数量级再试,排除优化参数问题。
- 如果出现 NaN,优先怀疑数据里有 width 或 height 为 0 的框,或者在数据增强阶段产生了非法的尺度变换。
这条链路我基本是固定执行顺序,每次都能在五到十分钟内锁定位问题,比瞎调参数强得多。
5.4 多种问现象排查速查表
| 现象 | 可能原因 | 处理思路 |
|---|---|---|
| 训练 loss 在 10 个 epoch 后仍无明显下降 | 数据路径错误、预训练权重未加载 | 检查 yaml 路径和日志输出 |
| mAP50 很高但 mAP50-95 极低 | 定位框不够准,边框边缘贴得不紧 | 检查标注框是否过松,重标边界 |
| 验证集 mAP 明显低于训练集 | 过拟合或数据泄漏 | 加增强、加数据、重新按来源划分 |
| 部署时频繁把地面纹理当垃圾 | 负样本不足 | 增加空标注背景图,难例挖掘 |
| 小目标几乎全部漏检 | 下采样导致特征丢失 | 提高分辨率、小目标增强、切图推理 |
| 夜间效果远差于白天 | 夜间样本太少 | 补真实夜间数据,不要只用亮度增强 |
| 类别之间频繁混淆 | 类别定义不清晰、样本重叠 | 重新审视类别粒度,统一标注规范 |
6. 从数据集到真实场景的扩展方向
6.1 部署端的推理优化思路
训练好的模型如果只在服务器上跑评估,那还只是理论产物。真正落地到街道巡检,通常需要部署在边缘设备或者车载工控机上。这时要面对的是算力和内存的限制。
常见做法是把训练好的模型导出为 ONNX 格式,再转到具体的推理引擎上做加速。对于 YOLO 模型来说,导出后通常能支持 fp16 甚至 int8 量化,帧率能提升不少。但量化要谨慎,int8 量化后小目标和重叠目标的精度会明显下降,建议上线前在真实视频上做回归测试。
部署的另一个关键是把输出结果和业务逻辑对接。街道垃圾检测不是每个框报告一次就完了,更实用的做法是:对同一个目标在连续多帧中的检测结果做聚合,只有稳定出现的目标才产生一次告警,这样可以有效减少单帧误检带来的噪声。这些都是数据训练之后的工程活,但决定了一个检测系统好不好用。
6.2 夜间、雨天与遮挡场景的专项加强
白天的检测效果做得再漂亮,如果夜间一塌糊涂,环卫夜间巡检仍然用不上。专项加强的手段主要有三类:
- 补充真实夜间数据:夜间照片的噪声模式、路灯色温、车灯轨迹和白天完全不同,亮度增强模拟不出来。如果预算允许,夜间样本最好至少占到总数据的 15% 到 20%。
- 使用双模态数据:有些街拍摄像头带有红外补光或者同时输出可见光和红外图,红外图对光照不敏感。代价是需要额外的采集和数据对齐成本,但效果非常直接。
- 用伪标签做半监督扩展:拿训练好的白天模型去跑夜间未标注数据,挑出高置信度的预测结果作为伪标签,回灌到训练集里。这种方法可以快速扩大夜间样本量,但要注意用低置信度的伪标签反而会污染数据集,建议只收置信度很高的框。
雨天场景则是水渍和路面反光会把垃圾特征搅乱,可以靠加入雨雾增强、补充雨天真实样本来缓解。遮挡问题则更依赖数据层面的堆量,让模型见过足够多“半露在外”的垃圾。
6.3 从“检测”到“分割”再到“自动清理”
如果你的应用不止于“发现垃圾”,而是要让无人清扫车知道“垃圾在哪里、边界多大”,那么目标检测的矩形框就不够用了,矩形框会把周围的杂物也框进去,导致机械臂抓取或者扫地车避障误判。这时可以把标注数据从 YOLO 检测格式升级成实例分割格式,用 YOLOv8-seg 这类模型训练分割版本。
分割标注的工作量比检测大得多,需要沿着目标边缘打多边形,但训练流程和检测基本一致。更重要的是,检测、分割和实际清理可以形成闭环:无人清扫车在街道上运行,记录下所有检测和分割结果,把置信度低和明显漏掉的样本定期回传给标注团队,重新标注后进入下一轮训练。这个闭环跑起来之后,模型会随着时间推移越来越适应自己负责的那片街道。
街道垃圾检测这件事,做到“能用药”和做到“好用”之间隔着一条数据飞轮。一开始花在标注规范和数据清洗上的时间,后续都会以训练效率和部署效果的形式加倍还回来。如果只让我给一个建议,那就是:不要急着把模型训完,先花时间把数据的所有边界情况规则写清楚,这套数据集的价值才能真正发挥出来。