ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

垃圾分类图像识别实战:从预处理到模型选型与部署的完整指南

垃圾分类图像识别实战:从预处理到模型选型与部署的完整指南 简介面向图像分类入门者与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×224EfficientNet-B0 是 224B4 到 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, interpolationcv2.INTER_AREA) img img.astype(np.float32) / 255.0 return img这段代码干了三件事把 BGR 转成 RGB因为 PyTorch 预训练模型是按 RGB 喂的用 INTER_AREA 做缩小它对高频纹理保留比双线性好瓶子表面的褶皱、纸张纤维不容易糊掉把像素压到 01统一数值范围。注意astype(np.float32)不能省uint8 数组直接除 255 在 Python 里会得到 0整段数据直接报废这个我踩过一次。更规范的做法是再做一次标准化用 ImageNet 的均值方差而不是简单除 255from 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(p0.5), A.RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.2, p0.7), A.HueSaturationValue(hue_shift_limit10, sat_shift_limit20, val_shift_limit20, p0.5), A.CoarseDropout(max_holes4, max_height32, max_width32, fill_value0, p0.3), A.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ToTensorV2(), ])每个增强都有它的用途。随机亮度对比度模拟早中晚不同光照色相饱和度偏移模拟白炽灯、日光灯、户外阴影的色温差异CoarseDropout 随机挖掉小块像素模拟垃圾被其他物品遮挡的情况。p 是概率不用每个都开满0.30.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, iterations2) 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_OPENOPEN 是用来去掉白色噪点的对垃圾这类主体占比较高的画面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( weightsmodels.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_features4)最后一层 in_features1280 这一组参数在 torchvision 里是固定的改 class 层之前必须取出来否则维度对不上。换成 ResNet50 时入口是model.fc.in_features换成 EfficientNet 时入口是model.classifier[1].in_features每个模型的分类头结构都不同最稳的做法是 print 一下模型结构再动手。迁移学习有两个阶段先冻结 backbone 只训练分类头跑 35 个 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, transformNone): 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], dtypetorch.long) dataset GarbageDataset(data/garbage, transformtrain_transform) loader DataLoader(dataset, batch_size32, shuffleTrue, num_workers4)这里有个关键点label 按文件夹名排序生成不要用os.listdir默认顺序因为不同操作系统的排序规则不一样同一份代码在 Windows 和 Linux 上跑出的标签顺序可能不同label 和类别对不上是灾难。我用sorted显式排序来保证顺序稳定。num_workers4在 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(weightclass_weights) # optimizer 和 scheduler 的配置 optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max30, eta_min1e-6)类别不平衡问题在垃圾分类里非常突出。一次试点采集的数据可回收物可能占 70%有害垃圾只有 5%。如果直接跑模型会学习“永远预测可回收物”准确率看起来还有 70%但有害垃圾全是误判。上面的代码在损失函数维度给少样本类更高的权重缓解这个问题。注意weight必须与 label 顺序对应类别 0 的权重放位置 0类别 1 放在位置 1不能乱。学习率这块迁移学习微调阶段 1e-4 到 3e-4 是一个常见安全区间。从lr1e-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 上跑 ResNet50800ms 一帧并不意外。很多人第一反应是换更强的硬件但换硬件意味着成本翻倍。对垃圾分类来说模型剪枝、量化和推理框架优化三板斧都上了速度通常能改善 35 倍。# 用 OpenVINO 做推理加速模型转换命令示例 mo --input_model model.onnx \ --input_shape [1,3,224,224] \ --data_type FP16 \ --output_dir ./optimized_modelOpenVINO 的模型转换工具会把 ONNX 模型编译成中间表示同时做算子融合。--data_type FP16是半精度精度损失通常很小但推理速度提升明显。部署前必须做一次精度对比分别用原始 PyTorch 模型和转换后的模型在同一个测试集上跑一遍确认准确率差异在 1 个百分点以内。量化到 INT8 会再快一截但如果数据分布和校准集偏差大准确率可能跌 35 个百分点属于“有损优化”要评估业务容忍度。5.4 有害垃圾这个类别基本没出现过训练时数据没做类别均匀采样模型对有害垃圾的预测概率永远压得很低。原因有两层数量和语义。有害垃圾在真实收集中占比本来就低加上电池、药品这些物品体积小、外观差异大模型很难学到统一的模式。解决思路不局限于训练。先在数据上做最基础的平衡WeightedRandomSampler或者直接对少样本类做过采样都可以试试。如果数据量实在不够采集上补充特写镜头让小体积有害垃圾占据画面更大比例给模型更清晰的纹理信息。类别权重放置在损失函数里也能起作用但不能作为唯一手段因为损失权重只是放大了少样本类的梯度信号如果该类样本量只有几十张权重再大也学不出来。最终手段是把有害垃圾的检测提到分类前面用目标检测模型单独检电池和药品剩余物品再走四分类。5.5 目标检测裁剪出来的分类输入连人眼都看不懂如果分类模型之前还有一个目标检测器最隐蔽的坑出现在“裁剪框”上。检测框把塑料瓶框得满满当当瓶子边缘顶到图像边界分类模型只看到瓶身中间的一截关键外观信息瓶盖、标签、瓶口的形状全被切掉了。人眼都很难判断模型更是强人所难。解决方法是给检测框加外扩比例。常见做法是 bbox 向外扩展 10%15%让瓶子周边环境也进到分类模型视野里。代码层面就是在x, y, w, h上做膨胀def expand_bbox(x, y, w, h, img_w, img_h, ratio0.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.20.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), keepdimTrue) cam (weights * feature_map[value]).sum(dim1, keepdimTrue) cam F.relu(cam) cam F.interpolate(cam, size(224, 224), modebilinear, align_cornersFalse) 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只保留正向刺激。热力图叠加到原图上后我见过最典型的两种情况热区在瓶子中部、清晰聚焦说明模型学对了热区散落在背景、手部或阴影区域说明模型学的是环境偏置越往后训练越容易过拟合到背景。这个自检勾要我建议在每个训练版本都做一次挑 2030 张错误样本批量输出热力图对比。如果热区位置不对优先怀疑数据采集时背景单一、特定类别总是和某种背景同时出现。先解决数据问题再考虑改网络结构。另外在把模型交给下一位维护者或者部署团队时附上这套热力图比写一份参数说明更有价值——它让人一眼看到模型的决策依据。我做垃圾分类项目最吃亏的一次就是训练时只看 loss 曲线和准确率没去关心模型到底在看什么。上线后收到反馈说“有时候对着矿泉水瓶能识别但把旁边蓝色抹布也框进去了”一查热力图模型果然盯着蓝色高光区域。从那以后 Grad-CAM 成了我每个版本的固定动作希望能帮到你少走这一段弯路。本文还有配套的精品资源点击获取
返回列表