城市场景下用 Faster R-CNN 识别打包垃圾:从数据集到部署的一次完整实践
最近终于把面向“打包垃圾”检测的这套 Faster R-CNN 项目跑通了。说“终于”,是因为这类任务远比想象中麻烦——垃圾不是一个规整的物体,它的外观、颜色、遮挡情况千变万化,同一袋垃圾在早上和傍晚的光线下看起来完全是两个东西。这篇文章把我从数据收集、标注、训练到部署的全过程梳理一遍,包括踩过的坑和最终采用的参数方案,给正在做类似城市视觉项目的朋友一个参考。
这套项目基于 Python 实现,核心算法采用 Faster R-CNN,目标是识别城市道路、小区、商业街区等场景中“打包放置”的垃圾(垃圾袋、成捆废弃物、堆放的杂物包等),输出检测框和置信度。它解决的痛点是:城市管理巡查中人工巡检效率低、盲区多,需要用摄像头自动发现乱堆乱放的垃圾包,及时上报处置。适合做智慧城市、环卫监管、社区治理这类方向的同学参考,也适合刚入门目标检测、想找一个非标准数据集练手的人阅读。
1. 项目背景:城市里“看不完”的垃圾
1.1 这个项目到底要解决什么问题
先说清楚“打包垃圾”和普通垃圾的区别。街面上的垃圾主要分两类:一类是散落的零碎垃圾,比如纸屑、烟头、落叶,这类目标太小太碎,检测难度极大,实战价值也低;另一类就是“打包垃圾”——居民用塑料袋装好的生活垃圾、商铺门口堆放的一捆纸箱、装修遗弃的编织袋、绿化带里成堆的落叶袋。这类垃圾通常体积不小、边界相对清晰,但问题是形状极其不固定:垃圾袋是软的,放在地上会坍成不规则形状;纸箱捆是硬的,形态相对稳定;编织袋可能是鼓着的也可能是瘪的。而且它们出现在城市各种角落:墙角、垃圾桶旁边、绿化带里、人行道上,背景杂乱,光线变化大。
这套项目选定的目标就是后者。识别出这些“成包的垃圾”后,系统可以给城市管理部门生成工单,提示某个点位出现了垃圾堆放,需要调度环卫人员处理。相比逐帧靠人眼看监控,这种自动检测能覆盖更多点位、全天候工作,而且积累了数据之后还能做趋势分析——比如哪个区域在什么时间段垃圾高发,倒推管理上的根源。
1.2 为什么是目标检测而不是图像分类
一开始有人建议用图像分类来做:把摄像头拍到的画面直接分“有没有垃圾”两类。这个方案在实验室看着可行,落地就崩。原因在于摄像头是固定视角的大场景画面,一帧 1920x1080 的图里,垃圾往往只占很小一块区域,整图分类根本定位不了垃圾在哪,后续也没法让环卫人员快速找到点位。
目标检测输出的是一个矩形框加上类别和置信度,恰好满足“定位”这个核心需求。有了检测框,就可以算出垃圾在画面中的像素坐标,再结合摄像头的安装角度做简单的映射,大致判断垃圾在现实世界中的方位。这就是选检测框架而不是分类网络的根本原因——业务要的不只是“有”或“没有”,而是“在哪”。
2. 数据与标注:决定模型上限的前置环节
2.1 数据采集策略
做检测项目,数据是最容易被低估的环节。我一开始天真地以为网上能找到现成的“垃圾检测数据集”,结果翻了半天,要么是垃圾分类数据集(区分可回收、厨余、有害那种),要么是无人机视角的河道漂浮物,跟“城市地面打包垃圾”完全不是一回事。最后只能自己采集和整理。
采集渠道主要用了三类:一是从某试点街道的监控摄像头导出了一批历史录像,覆盖晴天、阴天、雨天和不同时段;二是团队几个人拿着手机在城市里走街串巷,专门拍街角的垃圾堆放点;三是从公开的街景数据里筛了一部分存在垃圾堆放的截图。三种来源加起来一共攒了大约 4200 张有效图片,其中约 3000 张用于训练,600 张用于验证,600 张用于测试。
这里有个经验教训:监控录像导出的帧,画面质量差异极大。有的摄像头是老式模拟信号转数字,分辨率只有 704x576,垃圾在远处只有十几个像素;有的新摄像头是 400 万像素枪机,细节不错但视野太宽,垃圾目标依然偏小。我最后在训练集里故意混入了不同分辨率的图片,而不是统一 resize 到同样的清晰度,让模型尽量适应真实场景的多样性。如果你做的项目也依赖现场摄像头,一定不要只用高清手机拍的照片训练,否则上线后会被老旧摄像头的画面直接击穿。
2.2 打包垃圾的标注规范
标注是这套项目里最需要较真的地方。垃圾不是人、不是车,标注得很随意的话,模型学出来也很飘。我参考了多位做过环卫检测项目的同行的经验,定了几条硬性规则:
- 垃圾袋、堆放物出现完整的袋体轮廓时,按外接矩形框住,框要贴住目标边缘,不留太多背景。
- 多个垃圾袋紧挨在一起时,如果彼此重叠严重,框成一个大目标;如果之间有明显间隔,分开标。
- 垃圾被车辆、行人、栏杆部分遮挡时,仍按可见部分的最大范围标注,不要凭想象补齐遮挡部分。
- 垃圾桶本身不标注,但垃圾桶旁边散落的垃圾袋要标注。
- 不清晰的远距离目标(小于 40 像素)不标注,这些目标在训练中只会引入噪声。
标注工具用的是常见的开源标注软件,导出为 VOC 格式的 XML,再转换成 COCO 格式的 JSON,方便后续接入训练框架。前前后后标了大概两周,中间有一轮返工——第一批标注的框偏大,把不少地面背景圈进去了,导致模型训练时误把一些砖缝纹理当成垃圾特征。后面重新统一标准后效果明显改善。标注这件事没有捷径,如果自己一个人做,多检查多复盘比闷头标完再回头改要高效得多。
2.3 数据增强与样本扩充
4200 张图不算多,对 Faster R-CNN 这种较重的模型来说,不增强一下很容易过拟合。我主要用了以下几类增强手段:
- 随机水平翻转,概率 0.5,这是最基础也是最有效的增强,不改变目标语义。
- 随机亮度、对比度调整,模拟一天中不同时段的光照。垃圾检测对光照很敏感,这个增强实测提升明显。
- 随机裁剪和缩放,让模型看到不同尺度的垃圾。
- 对少样本类别做过采样,比如“成捆纸箱”这类样本在数据里偏少,就在训练时把包含这类目标的图片多喂几遍。
有一点要注意:像马赛克增强、混合增强这类手段,用在通用检测数据集上效果不错,但在垃圾检测这种目标不规整、背景又高度相似的数据上,未必是加成。我试过把两张图拼接做增强,模型反而把“画面边缘出现半截物体”的频率学高了,导致推理时对画面边缘的目标误检率上升。最终只保留了翻转、颜色扰动、随机裁剪这三类保守增强。
3. Faster R-CNN 选型分析与原理拆解
3.1 为什么不用 YOLO 系列
当前目标检测领域,YOLO 系列是热度最高的方案之一,速度快、工程生态完善,很多人一上来就默认选 YOLO。但我在这套项目里最终选了 Faster R-CNN,不是因为它更先进,而是因为这个任务的特点决定了它更合适。
第一,精度优先。城市管理场景对漏检的容忍度极低——漏掉一袋垃圾,工单系统就不会生成,问题就会一直积压。YOLO 走的是单阶段路线,速度快但小目标检测精度通常不如两阶段方法。垃圾袋在监控画面里经常属于中小目标,Faster R-CNN 的特征提取和区域建议机制对这种目标更友好。
第二,训练数据量有限。Faster R-CNN 的候选区域机制本质上是在做“先粗筛再细判”,它对标注噪声的鲁棒性比端到端回归的 YOLO 好一些,在小数据集上不容易跑偏。
第三,推理速度并非瓶颈。当前部署场景不需要实时处理每一帧,摄像头每 10 秒截一帧做检测,单帧推理 200 毫秒左右完全够用。如果项目要求在嵌入式设备上做到 30 FPS,那肯定要选 YOLO 或更轻量的模型,但我们的场景是云端或边缘服务器,速度要求远低于精度要求。
当然不是说 YOLO 不行。如果数据量能到上万张,且对帧率有硬性要求,YOLO 系列可能是更稳的选择。选型这件事,没有绝对的好坏,只有匹配不匹配。
3.2 网络结构与关键组件
Faster R-CNN 的结构分四块:骨干网络(Backbone)、区域建议网络(RPN)、ROI 池化(ROI Pooling/Align)、分类与回归头。简单理解就是:骨干网络负责从图中提取特征,RPN 负责“猜测哪些位置可能有目标”,ROI 池化把候选区域的特征统一成固定尺寸,最后分类头判断“是什么”和“框准不准”。
骨干网络选的是 ResNet-50。有人会问为什么不用 ResNet-101,后者理论上特征表达能力更强,但在这个数据集上实测 mAP 提升不到 1 个百分点,训练时间却增加了将近一半。ResNet-50 在精度和效率之间是最平衡的选择。RPN 部分需要重点调的是 Anchor 的设置。早期我在默认配置下训练,Anchor 是三种尺度(128、256、512)和三种宽高比(1:1、1:2、2:1),结果对“长条形的纸箱捆”检测很差。后来统计了标注框的尺寸分布,发现大量目标的宽高比集中在 1:1 到 1:1.5 之间,尺寸集中在 80 到 300 像素区间,就把 Anchor 调整为 (64、128、256、512) 配合 (1:1、1:1.5、1:2) 的组合,检测效果立刻好了不少。
ROI 池化这里,我直接用了 ROI Align。相比早期的 ROI Pooling,Align 用双线性插值解决了两次量化造成的像素偏移问题,对小目标坐标回归精度有明显帮助。在垃圾检测这种边界模糊的任务上,坐标精确多几个像素,后续的定位映射就能准确不少。
3.3 预训练与迁移学习
这部分是少走弯路的关键。我从头训练过一版,loss 降到一定程度就再也不动了,mAP 卡在 0.5 附近,效果很差。后来改用 ImageNet 上预训练好的 ResNet-50 权重作为初始化,冻结骨干网络前两个阶段,只训练后半部分和 RPN、检测头,效果立竿见影。迁移学习的本质是:ImageNet 上学到的边缘、纹理、形状等底层特征,对任何视觉任务都是通用的,垃圾虽然在类别上不属于 ImageNet 的一类,但“袋子的褶皱”“纸箱的棱边”这些视觉模式,底层特征都是相通的。
这里有个细节:加载预训练权重后,需要确认参数名称是否对齐。不同框架导出预训练权重的命名规则不完全一致,加载时经常出现“某层参数随机初始化”的警告。我当时就因为这个原因,骨干网络后半段实际上是随机初始化训练的,白白浪费了两天时间。检查方法是在加载后打印每一层的参数来源,或者对比加载前后同名层的权重数值是否变化。
4. 训练实操与参数调优
4.1 环境配置与依赖流程
环境方面我比较推荐直接用现成的开源检测框架,比如 MMDetection,这比手写 Faster R-CNN 全部代码靠谱得多。MMDetection 把骨干网络、RPN、检测头做成了标准组件,改配置即可跑通绝大多数训练流程,而且自带 COCO、VOC 数据集的加载器,省去大量数据预处理代码。
硬件方面,单卡 24GB 显存(RTX 3090 级别)即可比较舒服地训练,批大小可以开到 8,输入尺度设成 1333x800(短边 800、长边不超过 1333)。如果显存只有 8GB,批大小降到 4 或 2,其余配置保持不变也能训练,就是时间长一些。Python 环境建议用 3.8 以上,PyTorch 1.13 或 2.x 均可,CUDA 11.7 以上。这里提醒一下:MMDetection 不同版本对 PyTorch 版本有硬性要求,安装前先查好对应关系,不要直接 pip install 最新版然后期待它能跑。
4.2 训练参数设置
我最终采用的参数如下,可以直接参考:
# 训练周期与学习率 runner = dict(type='EpochBasedRunner', max_epochs=24) optimizer = dict(type='SGD', lr=0.005, momentum=0.9, weight_decay=0.0001) lr_config = dict( policy='step', warmup='linear', warmup_iters=500, warmup_ratio=0.1, step=[16, 22] ) # 批大小 data = dict( samples_per_gpu=8, workers_per_gpu=4 )学习率用的是 0.005 而非默认的 0.02,原因是垃圾检测数据集偏小且目标任务与通用目标不一致,过大的学习率极易震荡。SGD 搭配动量 0.9、权重衰减 0.0001 是检测任务的经典组合,Adam 在检测上并不总能胜过 SGD,我实测用 SGD 收敛更稳。step 策略在 16 轮和 22 轮分别将学习率降到原来的 1/10,这比余弦退火在这个数据集上效果好一点,后者在小数据集上容易提前收敛到平台期。
warmup 也不能省。前 500 次迭代用 1/10 的学习率预热,让 RPN 网络在早期不因为初始化的随意性而剧烈震荡。很多入门的朋友忽略这个参数,训练一开始 loss 就飞了,还以为是代码写错了。
4.3 训练过程监控与收敛判断
训练过程中我主要盯着三个曲线:总的 loss、RPN 的 loss、以及验证集上的 mAP。loss 曲线只用来判断训练是否稳定,真正的收敛标志是验证集 mAP 在连续若干 epoch 内不再上升,甚至开始下降——后者是过拟合信号,应该提前停止。
这里分享一个判断 mAP 是否可信的技巧:我不仅看平均 mAP,还单独统计了卡车遮挡、夜间、雨天这三类困难场景的子集 mAP。全局 mAP 可能看起来不错,比如 0.72,但夜间子集可能只有 0.4。如果只看平均值,上线后被夜间场景拖垮都不知道原因。按场景拆开统计,能精准定位模型的短板,再针对性地补充数据和调整策略。最终测试集上的平均 mAP 约为 0.73,其中晴天场景约 0.82,雨天约 0.63,夜间约 0.55——这个结果不算惊艳,但对于这类形态高度不稳定的目标,已经是能落地的水平了。
5. 推理部署与性能优化
5.1 推理流程与置信度阈值选择
训练完的模型在部署时,需要把推理代码从训练框架里抽出来。MMDetection 提供了现成的推理接口,但在正式环境我不会直接用原始接口,而是先做一轮整理:输入图片先做预处理(resize 到 1333x800,归一化),前向推理得到检测框、类别和分数,再做 NMS 去重,最后过滤掉置信度低于阈值的检测框。
置信度阈值是部署前必须反复调的参数。阈值调低(比如 0.3),召回率提高但误检增多;阈值调高(比如 0.7),误检减少但漏检增加。我的做法是结合业务成本来定:一个误检工单让环卫工人白跑一趟,成本是浪费人力;一个漏检让垃圾继续堆放,成本是城市管理考核扣分。两者权衡后,我选择了 0.45 作为默认阈值,又给夜间模式单独设了 0.35 的较低阈值,因为夜间光线差,模型整体置信度会偏低。
NMS 的 IoU 阈值用的是 0.5,这个值比较常规。有一个容易忽略的问题:NMS 只对同一类别内的框做抑制,如果你后续想同时检测“打包垃圾”和“散落垃圾”两个细分类别,且它们位置高度重叠时,跨类别的框不会被抑制,可能出现同一个垃圾袋被输出两个框的情况。目前的方案是只设了一个“打包垃圾”类别,绕过了这个问题,但后续如果扩类别,需要留意。
5.2 推理加速方案
虽然不需要实时处理,但推理速度依然影响成本——摄像头数量多了以后,单帧推理时间直接决定需要多少台服务器。我尝试了几种加速手段,整理成下面的对比:
| 方案 | 推理耗时(ms/帧, 1333x800) | 精度损失 | 实施难度 |
|---|---|---|---|
| 原始 PyTorch 推理 | 450 | 无 | 最低 |
| FP16 半精度推理 | 220 | 约 0.5% mAP 下降 | 低 |
| ONNX + ONNXRuntime | 180 | 无 | 中 |
| TensorRT FP16 | 110 | 约 1% mAP 下降 | 较高 |
最终生产环境用的是 ONNX + ONNXRuntime 方案。TensorRT 更快,但部署环境对 TensorRT 版本的依赖比较苛刻,而且导出过程中遇到过算子兼容问题,调试成本高。ONNXRuntime 的精度几乎没有损失,提速效果也够用。导出时需要注意输入张量的动态维度设置,因为真实图片尺寸不固定,ONNX 导出时要把输入维度设为动态,否则遇到不同尺寸的图片会直接报错。
快 2 倍多里有一个关键是把 GPU 上的预处理也做了。很多人只在 GPU 上跑网络,图片读取、resize、归一化都放在 CPU 上,CPU 和 GPU 的轮转会吃掉大量时间。我的做法是提前写好一个数据流水线,用 GPU 上的张量操作把整个预处理链都放到推理设备上,实测省掉了每帧约 80 到 100 毫秒的等待。
5.3 部署到实际场景的注意点
部署阶段有一个坑容易被忽视:摄像头画面和训练集画面的分布差异。训练集里大多是白天光线正常的图片,但实际监控画面里,有早晚太阳直射造成的过曝、路灯下的偏色、甚至摄像头镜头上有水渍导致的模糊。上线第一周,误检率比测试时高出不少,后来分析发现,很多误检是把地面上的深色水渍当成了垃圾袋。解决办法有两个:一是从现场反复抓拍,持续补充新场景数据做循环训练;二是在推理层加一道简单的后处理规则——如果检测框里面的区域纹理特征过于平滑(比如水面、阴影),则降低该框的置信度。
另外,部署的输入帧率也不要太高。摄像头 25 帧每秒,如果每一帧都做检测,服务器压力大且没必要。我按场景配置了两种采样策略:普通路段每 10 秒抽一帧,重点点位(垃圾高发区)每 3 秒抽一帧。这样既保证了发现时效,又控制了计算成本。后续还可以做基于运动检测的触发式采集——画面区域发生变化时才做识别,但这是后话了。
6. 踩坑记录与问题排查技巧
6.1 训练过程与结果中的典型问题速查表
做这个项目过程中反复遇到的问题不少,挑几个高频且有共性的整理成表格,方便大家直接对照排查:
| 现象 | 根本原因 | 解决方案 |
|---|---|---|
| 训练 loss 不下降 | 学习率过大或 backbone 未加载预训练权重 | 降低学习率至 0.005,检查权重加载日志 |
| 验证 mAP 很高但实际场景漏检严重 | 训练集过于“干净”,缺少真实监控图的噪声 | 混入低分辨率、雨天、逆光等真实图像 |
| 对画面边缘的垃圾几乎不检测 | Anchor 尺寸覆盖不够,或训练时数据增强裁剪过度 | 统计标注框尺寸分布,调整 Anchor;减少大尺度随机裁剪 |
| 同一袋垃圾输出多个重叠框 | NMS 阈值偏低或不同类别的框互相不抑制 | 调整 NMS 的 IoU 阈值,检查输出类别设置 |
| 夜间误检率显著升高 | 夜间样本太少或光照增强不足 | 单独补充夜间数据,对夜间样本做亮度、对比度增强 |
| 推理速度远低于预期 | CPU 预处理与 GPU 推理没有流水线化 | 预处理移到 GPU 上执行,或增大 batch 推理 |
6.2 印象最深的两个 Bug
第一个 Bug 是加载预训练权重时的“静默失败”。MMDetection 加载权重后默认只打印总加载数量,不报错。我一个阶段训练完发现骨干网络 B 部分的权重全是初始值,细查才发现配置里的 load_from 路径因为版本不匹配被判定为“只加载兼容层”,导致后半段骨干网络没有加载成功。排查方式很简单——在加载权重的代码里加几行,把模型加载前后某个中间层的权重数值打印出来,做一次数值对比确认真的加载了,再继续跑。
第二个 Bug 是验证集的“假阳性”误导。训练过程中验证集 mAP 一度到了 0.8,让我以为模型已经很强了。结果可视化推理时发现,验证集里有一批图片本身比较暗,垃圾袋和地面的对比度低,模型输出的是置信度极低的框,但因为验证集里这类图片占比不多,平均 mAP 还是被拉高了。后来按场景分层评估才暴露了夜间样本薄弱的真相。这件事提醒我:指标一定要拆开看,平均数是会骗人的。
6.3 后续可以扩展的方向
最后聊聊这套项目的后续扩展。当前只做单类别检测,实际业务中完全可以细分:可回收垃圾包、厨余垃圾包、装修废弃物包——对应的处置流程不同,分类输出能让工单系统直接分派到不同的处理渠道。不过细分类需要更精细的标注和更多的数据,尤其是厨余垃圾袋和可回收垃圾袋在外观上看不出明显差别时,需要结合其他信息(比如堆放位置、周围环境上下文)辅助判断。
另外一个方向是结合目标跟踪算法,对同一袋垃圾做持续跟踪。摄像头检测到垃圾后,不马上生成工单,而是连续观察一段时间,确认这袋垃圾是否被清理。如果跟踪到环卫人员把垃圾清走,就不产生重复工单;如果垃圾在原地停留超过预设时间(比如 2 小时),再触发告警。这样能大幅降低误报率,也符合城市管理的实际节奏。
从个人体会来说,这类“非标准物体检测”项目,难点从来不在模型结构有多新,而在于对数据和场景的理解。Faster R-CNN 这套方案在网络结构上不算前沿,甚至是五六年前的架构了,但它在精度和稳定性上的表现,配合上对城市垃圾场景的针对性调整,依然能做出可落地的效果。做类似的视觉项目,不妨先把数据和评估体系打磨扎实,再回头挑剔模型架构,顺序反了往往事倍功半。