☰
垃圾分类图像识别实战:从预处理到模型选型与部署的完整指南
2026/10/5 13:44:42 网站建设 项目流程

简介:面向图像分类入门者与DIY神经网络爱好者,这是一份以垃圾分类为落地场景的完整模板,基于TensorFlow与OpenCV实现从数据集制作、模型训练到预测输出的全流程,可直接复用于图像分类项目参考,帮助快速解决生活中的垃圾分类识别需求。资源包采用RAR压缩,共1046个文件,主要由1041张JPG样本图片、2个Python脚本(train.py训练、predict.py预测)、1个训练好的H5模型、1个演示MP4视频和1个说明TXT组成,压缩包整体约825MB,目录结构便于按功能模块查找,数据集、代码、模型一目了然。目前已有1332人学习参考。借助这套模板,读者可了解如何利用简单数据集构建轻量级图像分类神经网络,熟悉数据预处理、模型保存与加载、图像中文标注等关键环节;predict.py能将干垃圾、湿垃圾、可回收垃圾、有害垃圾四类结果直接显示在图片上,便于直观验证模型效果。整个项目结构清晰、可复用性强,适合作为图像分类任务的入门范本,也便于在此基础上扩展自己的分类模型,从数据到训练再到预测均有清晰脚本支撑。

1. 垃圾分类为什么不是“给图片打个标签”那么简单

小区垃圾亭边竖一个摄像头,垃圾袋放上台面,屏幕直接报出“可回收物 / 厨余垃圾”,这是垃圾分类的图像识别最常见落地形态。它背后是两条线:图像处理负责把环境干扰剥掉,图像分类负责判断垃圾类别。真正做过的人会告诉你,难的不是模型结构,而是现实垃圾的不确定性——瓶子压扁、纸张揉团、灯光冷暖混杂、塑料袋反光,这些干扰项会让分类准确率肉眼可见地往下掉。这篇笔记按一条从零跑通的路线梳理:预处理怎么做、模型怎么选、训练参数怎么配、上线前哪些坑必须提前填。适合正在做毕业设计、智能回收箱或园区试点项目的从业者。

2. 图像处理三件事:先让画面能进模型,再让画面“讲人话”

很多朋友是从 matlab图像处理大作业 起步的,拿一张图做灰度、二值化、边缘检测,出一张对比图就完事。但垃圾分类这类真实项目里,图像处理要回答的问题非常现实:你手里的照片来自监控截图、手机拍摄、网络爬图,分辨率、亮度、拍摄角度完全不一样,模型不能直接吃这种“野图”。预处理做得好不好,决定你后面分类准确率的上限。

2.1 尺寸归一化与像素归一化:让不同来源的图站到同一条起跑线

先说出镜率最高的矛盾:监控画面是 480p 甚至更低,志愿者手机拍的是 4K,网上爬来的图可能是 500×500 的缩略图。图像分类模型通常要求固定输入尺寸,ResNet 和 MobileNet 一般是 224×224,EfficientNet-B0 是 224,B4 到 B7 更大。第一步就是把所有图统一缩放到这个尺寸。

import cv2 import numpy as np def load_and_resize(path, size=(224, 224)): img = cv2.imread(path) if img is None: raise ValueError(f"图片读不出来: {path}") img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, size, interpolation=cv2.INTER_AREA) img = img.astype(np.float32) / 255.0 return img

这段代码干了三件事:把 BGR 转成 RGB,因为 PyTorch 预训练模型是按 RGB 喂的;用 INTER_AREA 做缩小,它对高频纹理保留比双线性好,瓶子表面的褶皱、纸张纤维不容易糊掉;把像素压到 0~1,统一数值范围。注意astype(np.float32)不能省,uint8 数组直接除 255 在 Python 里会得到 0,整段数据直接报废,这个我踩过一次。

更规范的做法是再做一次标准化,用 ImageNet 的均值方差,而不是简单除 255:

from torchvision import transforms transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])

很多人不理解为什么要用 ImageNet 的均值和方差去减。因为你用的是在 ImageNet 上预训练过的权重,它学到的颜色分布是以这套均值为中心的,输入分布不一致,微调阶段梯度更新就会不稳定,收敛变慢。如果你是从零训练你自己的模型,那可以不用这套参数,但迁移学习场景下不建议改。

2.2 数据增强:把“摆拍的垃圾”变成“现实垃圾”

垃圾分类训练集里最常见的照片是“摆拍”的:干净背景、正中放置、光线充足。真实场景是垃圾混在一起、局部遮挡、灯光发黄、有人手/阴影进入画面。不做数据增强,模型在验证集上漂亮,一上线就露馅。我一般用 albumentations 做增强,它比 torchvision 自带 transform 灵活很多:

import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform = A.Compose([ A.Resize(224, 224), A.HorizontalFlip(p=0.5), A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.7), A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=20, val_shift_limit=20, p=0.5), A.CoarseDropout(max_holes=4, max_height=32, max_width=32, fill_value=0, p=0.3), A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ToTensorV2(), ])

每个增强都有它的用途。随机亮度对比度模拟早中晚不同光照;色相饱和度偏移模拟白炽灯、日光灯、户外阴影的色温差异;CoarseDropout 随机挖掉小块像素,模拟垃圾被其他物品遮挡的情况。p 是概率,不用每个都开满,0.3~0.7 就够。要注意增强参数不要拉太狠,比如 brightness_limit 超过 0.3,图片会发灰失真,反而让模型学到错误的颜色分布。

这套增强在普通分类任务上是“锦上添花”,在垃圾分类上属于“刚需”。因为垃圾的物理形态是高度随机的,同一个塑料瓶,捏扁了、踩瘪了、标签撕一半,视觉特征差距巨大,增强正是用来模拟这些变化的。

2.3 OpenCV 形态学与背景抑制:膨胀腐蚀在分类管线里的真实位置

你要做过 opencv图像处理项目,对膨胀和腐蚀肯定不陌生。很多教程把它放在“预处理”里,但在深度学习分类管线中,它的定位要重新想清楚。直接对整图做膨胀腐蚀,会把塑料瓶表面的文字、纸箱上的印刷图案抹掉,反而损失关键特征。我现在的用法是,只把它用在两处:一是数据清洗时做背景分离,二是目标检测给分类模型提供“干净裁剪框”之后。

比如从监控视频里抓帧,先用传统方式快速判断当前位置有没有垃圾出现,再触发分类模型,这时候形态学就派上用场:

import cv2 import numpy as np img = cv2.imread("frame_001.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blur = cv2.GaussianBlur(gray, (5, 5), 0) _, thresh = cv2.threshold(blur, 120, 255, cv2.THRESH_BINARY) kernel = np.ones((3, 3), np.uint8) # 先腐蚀去掉细小噪点,再膨胀还原主体 clean = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=2) contours, _ = cv2.findContours(clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for c in contours: area = cv2.contourArea(c) if area < 5000: # 过滤掉桌台纹理造成的小块区域 continue x, y, w, h = cv2.boundingRect(c) # 这里拿到的 bbox 就是后续裁剪分类的输入

MORPH_CLOSE 是“先膨胀再腐蚀”,用来填平垃圾主体内部的细小黑洞,同时保留边缘轮廓。为什么不用 MORPH_OPEN?OPEN 是用来去掉白色噪点的,对垃圾这类主体占比较高的画面,CLOSE 更合适。面积阈值 5000 是真实验证出来的经验值,按你的摄像头分辨率和机位距离要重调。这套传统视觉流程不参与分类推理,它只是给分类模型当“哨兵”,避免每一帧都跑一次大模型。

3. 图像分类模型选型:从迁移学习到 MobileNetV3 落地

模型选型在整个项目里的权重很高,但很多人一上来就盯最新的图像分类模型排行榜,把 ViT、Swin Transformer 搬出来试,训完发现推理速度不达标,又推倒重来。这里先说清楚选型逻辑。

3.1 为什么自己是做的,别从零训练

垃圾分类不是 ImageNet 级别的通用视觉任务,公开数据集的规模通常是几千到几万张,这个量级从零训练一个深度卷积网络,效果非常难看。迁移学习的价值在工业落地场景是实打实的经验:在 ImageNet 上学到的边缘、纹理、颜色组合特征,对垃圾材质识别同样有效。

import torch import torch.nn as nn import torchvision.models as models # 加载 ImageNet 预训练权重,而不是随机初始化 model = models.mobilenet_v3_large( weights=models.MobileNet_V3_Large_Weights.IMAGENET1K_V1 ) # MobileNetV3-Large 的 classifier 是 [Linear, Hardswish, Dropout, Linear] num_features = model.classifier[3].in_features model.classifier[3] = nn.Linear(num_features, out_features=4)

最后一层 in_features=1280 这一组参数在 torchvision 里是固定的,改 class 层之前必须取出来,否则维度对不上。换成 ResNet50 时,入口是model.fc.in_features;换成 EfficientNet 时,入口是model.classifier[1].in_features,每个模型的分类头结构都不同,最稳的做法是 print 一下模型结构再动手。迁移学习有两个阶段:先冻结 backbone 只训练分类头,跑 3~5 个 epoch 观察 loss 是否降下来,确认数据加载和标签没问题;再解冻全部层,用更小的学习率微调。很多人一上来就全量微调,导致预训练特征被破坏,训练集过拟合,验证集漂移。

3.2 模型对比:ResNet50、MobileNetV3、EfficientNet-B0 怎么选

把三个主流模型放在一张表里看,选型符合“先定部署端,再定模型”的原则:

模型参数量设备适配垃圾分类场景评价
ResNet50约 25M服务器 GPU、边缘盒子(勉强)稳妥,微调对数据量要求低,但推理偏慢
MobileNetV3-Large约 4.2M树莓派、RK3399、手机速度快,内存占用小,塑料/玻璃这类细分类要多加数据
EfficientNet-B0约 5.3MCPU/GPU 均可用精度参数比好,但对输入尺寸缩放敏感

如果项目只在云端跑,带宽和延时都允许,ResNet50 是最小风险方案,资料多,调参坑少。如果要做成智能回收箱或者嵌入式的实时识别,MobileNetV3 是更现实的选择。EfficientNet-B0 卡在中间,精度确实香,但它对图片缩放比例极其敏感,务必要在预处理里用Resize((224, 224))而不是Resize(256)后CenterCrop,否则精度会明显掉。

3.3 用 MobileNetV3 跑通最小的垃圾分类分类器

这里给一个可以完整跑最小验证的流程,包含数据读取、训练和评估的骨架:

import torch from torch.utils.data import Dataset, DataLoader from PIL import Image import os class GarbageDataset(Dataset): def __init__(self, root_dir, transform=None): self.samples = [] self.labels = [] for label, cls_name in enumerate(sorted(os.listdir(root_dir))): cls_dir = os.path.join(root_dir, cls_name) for fname in os.listdir(cls_dir): if fname.lower().endswith(('.jpg', '.jpeg', '.png')): self.samples.append(os.path.join(cls_dir, fname)) self.labels.append(label) self.transform = transform def __len__(self): return len(self.samples) def __getitem__(self, idx): img = Image.open(self.samples[idx]).convert('RGB') if self.transform: img = self.transform(img) return img, torch.tensor(self.labels[idx], dtype=torch.long) dataset = GarbageDataset("data/garbage", transform=train_transform) loader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=4)

这里有个关键点:label 按文件夹名排序生成,不要用os.listdir默认顺序,因为不同操作系统的排序规则不一样,同一份代码在 Windows 和 Linux 上跑出的标签顺序可能不同,label 和类别对不上是灾难。我用sorted显式排序来保证顺序稳定。num_workers=4在 Windows 上偶尔有问题,如果报 DataLoader worker 错误,改成 0 或 2 即可。

4. 垃圾分类训练实战:数据集、标签体系与超参设置

数据是一切的上限。模型结构再合理,数据集本身脏、标签体系混乱,训练过程就一定不会稳定。这一章讲清楚数据层面和训练配置层面的完整决策过程。

4.1 数据集准备与标签体系设计:按“最终投放点”来定,不按“学术惯例”来定

公开数据集里最常用的是 TrashNet,包含玻璃、纸张、金属、塑料、纸板、其他垃圾六类,图片大多是实验室背景,比较“干净”。用它做学术 demo 没问题,但直接搬去做园区项目,效果会打折扣。常见做法是拿公开数据集做预训练或者说做 baseline,然后自己拍一批目标场景的照片做微调。

标签体系的设计是一个经常翻车的点。垃圾分类目前的实用口径是四分类:可回收物、有害垃圾、厨余垃圾、其他垃圾。有些项目会直接按材质细分成塑料、纸张、玻璃、金属,这跟用户的实际投放习惯是一致的,但对视觉模型来说是两个难度级别——厨余垃圾里可能有塑料袋,可回收物里可能有脏纸盒。我一般建议:第一版只做四分类,颗粒度太细会导致类别间视觉差异变小,模型很难收敛;等四分类在真实场景稳定了,再考虑细化到八类或十二类。先从业务需求倒推分类口径,再按口径整理数据集,这个顺序不能反。

4.2 训练超参配置:从学习率到类别不平衡,哪些参数值得花时间

超参配置是玄学,但有几个参数是经过大量验证有共识的。先看几个最关键的数字:

from torchsampler import WeightedRandomSampler # 第三方库,可选 # 计算各类别权重,做类别不平衡处理 labels = [item[1] for item in dataset.samples] counts = torch.bincount(torch.tensor(labels)).float() class_weights = counts.sum() / counts class_weights = class_weights / class_weights.sum() * len(class_weights) # 损失函数层面加权,训练时更稳健 criterion = torch.nn.CrossEntropyLoss(weight=class_weights) # optimizer 和 scheduler 的配置 optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max=30, eta_min=1e-6)

类别不平衡问题在垃圾分类里非常突出。一次试点采集的数据,可回收物可能占 70%,有害垃圾只有 5%。如果直接跑,模型会学习“永远预测可回收物”,准确率看起来还有 70%,但有害垃圾全是误判。上面的代码在损失函数维度给少样本类更高的权重,缓解这个问题。注意weight必须与 label 顺序对应,类别 0 的权重放位置 0,类别 1 放在位置 1,不能乱。

学习率这块,迁移学习微调阶段 1e-4 到 3e-4 是一个常见安全区间。从lr=1e-4起步,观察第一个 epoch 的 loss 变化,如果 loss 直接震荡或升高,优先把学习率降到 3e-5,而不是去调网络结构。CosineAnnealing 在训练后期会把学习率平滑拉低,帮助模型在大后期做细微修正,一般 T_max 设置成总 epoch 数即可。batch size 在 224×224 输入下,32 是一个兼顾显存和 BN 统计量的默认值;如果你显存吃紧,16 也可以,但 BatchNorm 的统计会变抖,建议多跑几个 epoch 稳定。

4.3 验证策略:Top-1 准确率不能只看一次,要分桶看

很多人训练完只看验证集整体准确率。垃圾分类场景里,这个指标会骗人。正确做法是按类别、按时间段、按摄像头位置分别统计。我曾经遇到一个项目,验证集准确率 93%,看似很理想,但把错误样本抽出来一看,90% 的错误发生在“纸张”和“塑料”之间。整体准确率掩盖了细分类问题。更可执行的做法是:每次验证都输出混淆矩阵、每类的 precision/recall、单条错误样本的预测置信度,这三个维度比 Top-1 准确率有用得多。

混合策略上,我习惯保留一个“现场测试集”。它不是在训练数据中随机抽的,而是单独留一批同机位、不同光照条件的照片,训练过程完全看不到。每次训练结束,在这个现场测试集上跑一次,看准确率与验证集的差距。如果现场准确率比验证集低 10 个百分点以上,说明数据增强没有模拟出真实环境差异,这时候回头调整增强策略比换模型更有效。

5. 垃圾分类落地的 5 个踩坑与排查记录:现象、原因、怎么解决

下面是几条有共性的踩坑记录,按“现象 → 原因 → 解决”的口径写。每一条都是我或合作团队真实踩过的,希望对你有直接参考价值。

5.1 训练准确率 98%,一上线就崩

训练集准确率 98%,验证集 95%,部署到现场之后直接“薛定谔式识别”:上午还行,下午光线一变就崩;刮风树叶影子一挡,什么都被判成厨余垃圾。这不是模型本身的问题,是训练集和真实环境之间的分布差异太大。志愿者摆拍的照片太规整,而现场是低照度、运动模糊、色偏、阴影,四类干扰叠加,模型根本没有见过。

解决思路是围绕 “采集即训练” 的思路:部署后第一周不急着正式验收,把所有误判样本保存下来,人工挑出典型难例补进训练集,做一轮增量微调。同时增强参数往大调,把RandomBrightnessContrast的幅度加大到 0.3,加RandomGamma,甚至可以用GaussianBlur模拟运动模糊。这类 ops 在 albumentations 里都有,按需组合即可。

5.2 塑料和纸张始终分不清,怎么调都不行

这个现象很典型。塑料瓶可能是透明或浅色,纸张也是浅色的,揉成团后瞳孔反光、纹理杂乱,视觉特征极其接近。模型学到的最强特征可能就是“反光”,但纸张有哑光的、也有亮面的,塑料也有磨砂的。数据层面单纯把塑料样本加多,效果有限。

原因在于这两个类别在视觉上确实边界模糊,需要额外的物理特征来区分。实际项目里我会加两个解决角度:一是拍摄时增加侧光照角度,让塑料反光高光更一致地暴露,这属于采集规范;二是在模型层面引入第二个分支,用光谱特征或材质数据做多模态融合,但这会大幅提升复杂度。短期更便宜的做法是把标签口径调整为“可能混淆的类别统一归到‘其他垃圾’”,在业务上不做强行区分,把准确率保在 90% 以上更重要。至于误判反馈系统,这类容易混淆的类别就要刻意抓取,积累成专门的小数据集。

5.3 推理速度不够,单帧处理要 800ms

在树莓派或 RK3399 上跑 ResNet50,800ms 一帧并不意外。很多人第一反应是换更强的硬件,但换硬件意味着成本翻倍。对垃圾分类来说,模型剪枝、量化和推理框架优化三板斧都上了,速度通常能改善 3~5 倍。

# 用 OpenVINO 做推理加速,模型转换命令示例 mo --input_model model.onnx \ --input_shape [1,3,224,224] \ --data_type FP16 \ --output_dir ./optimized_model

OpenVINO 的模型转换工具会把 ONNX 模型编译成中间表示,同时做算子融合。--data_type FP16是半精度,精度损失通常很小,但推理速度提升明显。部署前必须做一次精度对比:分别用原始 PyTorch 模型和转换后的模型在同一个测试集上跑一遍,确认准确率差异在 1 个百分点以内。量化到 INT8 会再快一截,但如果数据分布和校准集偏差大,准确率可能跌 3~5 个百分点,属于“有损优化”,要评估业务容忍度。

5.4 有害垃圾这个类别基本没出现过

训练时数据没做类别均匀采样,模型对有害垃圾的预测概率永远压得很低。原因有两层:数量和语义。有害垃圾在真实收集中占比本来就低,加上电池、药品这些物品体积小、外观差异大,模型很难学到统一的模式。

解决思路不局限于训练。先在数据上做最基础的平衡,WeightedRandomSampler或者直接对少样本类做过采样都可以试试。如果数据量实在不够,采集上补充特写镜头,让小体积有害垃圾占据画面更大比例,给模型更清晰的纹理信息。类别权重放置在损失函数里也能起作用,但不能作为唯一手段,因为损失权重只是放大了少样本类的梯度信号,如果该类样本量只有几十张,权重再大也学不出来。最终手段是把有害垃圾的检测提到分类前面,用目标检测模型单独检电池和药品,剩余物品再走四分类。

5.5 目标检测裁剪出来的分类输入,连人眼都看不懂

如果分类模型之前还有一个目标检测器,最隐蔽的坑出现在“裁剪框”上。检测框把塑料瓶框得满满当当,瓶子边缘顶到图像边界,分类模型只看到瓶身中间的一截,关键外观信息(瓶盖、标签、瓶口的形状)全被切掉了。人眼都很难判断,模型更是强人所难。

解决方法是给检测框加外扩比例。常见做法是 bbox 向外扩展 10%~15%,让瓶子周边环境也进到分类模型视野里。代码层面,就是在x, y, w, h上做膨胀:

def expand_bbox(x, y, w, h, img_w, img_h, ratio=0.15): ex = int(w * ratio) ey = int(h * ratio) x1 = max(0, x - ex) y1 = max(0, y - ey) x2 = min(img_w, x + w + ex) y2 = min(img_h, y + h + ey) return x1, y1, x2, y2

外扩比例不是越大越好。拉太远,背景占比过大,模型又会被背景信息带偏。我从 0.1 调到 0.2,0.15 是多数场景下比较稳的中间值。另外要注意裁剪后的长宽比变化,如果外扩导致 bbox 变形严重,再统一 Resize 到 224×224 时会产生畸变,这一点也要同步考虑。

6. 用 Grad-CAM 给模型做一次“视力检查”,补上最后的自检闭环

当你说不清模型到底在看什么时,Grad-CAM 是目前最直接的定位工具。它用最后一层卷积特征图的梯度做加权,生成一个热力图,告诉我们模型在某张图上更关注哪个区域。这对垃圾分类极其重要——判断“塑料瓶”时,模型应该盯着瓶身和标签,而不是台面背景或旁边的手。

import torch import torch.nn.functional as F import cv2 import numpy as np target_layer = model.features[-1] # MobileNetV3 最后一个特征块 feature_map = {} gradient_map = {} def forward_hook(module, input, output): feature_map['value'] = output def backward_hook(module, grad_input, grad_output): gradient_map['value'] = grad_output[0] fh = target_layer.register_forward_hook(forward_hook) bh = target_layer.register_full_backward_hook(backward_hook) model.eval() out = model(x) # x 是经过预处理的单张图片 model.zero_grad() out[0, out.argmax()].backward() with torch.no_grad(): weights = gradient_map['value'].mean(dim=(2, 3), keepdim=True) cam = (weights * feature_map['value']).sum(dim=1, keepdim=True) cam = F.relu(cam) cam = F.interpolate(cam, size=(224, 224), mode='bilinear', align_corners=False) cam = (cam - cam.min()) / (cam.max() - cam.min()) heatmap = cv2.applyColorMap( (cam[0, 0].numpy() * 255).astype(np.uint8), cv2.COLORMAP_JET)

这段代码的核心是前向 hook 保存特征图,反向 hook 保存梯度。grad_output[0]就是当前层输出特征的梯度,对空间维度取平均得到每个通道的权重,再乘回特征图做加权求和,最后relu只保留正向刺激。热力图叠加到原图上后,我见过最典型的两种情况:热区在瓶子中部、清晰聚焦,说明模型学对了;热区散落在背景、手部或阴影区域,说明模型学的是环境偏置,越往后训练越容易过拟合到背景。

这个自检勾要我建议在每个训练版本都做一次,挑 20~30 张错误样本,批量输出热力图对比。如果热区位置不对,优先怀疑数据采集时背景单一、特定类别总是和某种背景同时出现。先解决数据问题,再考虑改网络结构。另外,在把模型交给下一位维护者或者部署团队时,附上这套热力图,比写一份参数说明更有价值——它让人一眼看到模型的决策依据。

我做垃圾分类项目最吃亏的一次,就是训练时只看 loss 曲线和准确率,没去关心模型到底在看什么。上线后收到反馈说“有时候对着矿泉水瓶能识别,但把旁边蓝色抹布也框进去了”,一查热力图,模型果然盯着蓝色高光区域。从那以后 Grad-CAM 成了我每个版本的固定动作,希望能帮到你少走这一段弯路。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询