简介:面向深度学习初学者的智能垃圾分类图像分类项目包,以TensorFlow和OpenCV搭建DIY网络模型,完整覆盖数据集制作、模型训练和结果预测环节,可当作图像分类模板复用。压缩包共1046个文件、约824.68MB,主体为1041张JPG垃圾图片,可直接用于训练与测试;另有train.py训练脚本、predict.py预测脚本、my_model.h5已训练好的模型、txt说明文件和mp4演示视频,便于对照步骤运行和复现输出。项目将垃圾划分为干垃圾、湿垃圾、可回收垃圾、有害垃圾四类,预测时把中文类别直接显示在图片上,结果一目了然。资源内目录按数据、代码、模型、文档分区,包含数据集制作思路与训练参数设置说明;更换数据集后可迁移到其他图像分类任务,适合课程设计、毕业设计或竞赛入门。目前已有1332人学习下载,整体上手门槛低,适合快速实践。
1. 图像处理与图像分类在垃圾分类里的真实分量
摄像头拍下的塑料瓶和纸箱,在画面里通常是一团高光的白色和一坨发灰的纸色——这是垃圾分类识别真正要面对的输入。图像处理与图像分类的功夫,基本都花在把这种自然场景变成能稳定判别的信息上。我不建议一上来就去追最新的图像分类模型;更稳妥的序列是先拿OpenCV把图像清洗干净,再交给分类网络,最后才讨论部署。这篇笔记按我实际做课设和边缘项目的顺序来写:从形态学预处理讲到模型训练,再落到避坑和推理验证。适合正在做OpenCV图像处理项目、毕业设计,以及想从MATLAB图像处理大作业过渡到深度学习的读者。
2. 从形态学预处理到特征提取:先用OpenCV把垃圾图像洗干净
2.1 形态学图像处理:膨胀与腐蚀为什么比直接滤波更适合垃圾轮廓
垃圾分类的照片和ImageNet的“主体居中”风格完全不同:传送带纹理、阴影、透明塑料膜的反光会铺满整个画面。如果直接用高斯模糊,半透明塑料袋的边缘会被“抹”进背景;而形态学图像处理里的膨胀与腐蚀不猜纹理,它基于结构元素判断形状:闭运算(先膨胀后腐蚀)可以先用膨胀把高光造成的小孔和断裂边缘连起来,再用腐蚀把边缘拉回原尺寸。这样一来,塑料瓶身上的高光条不至于被误判成一个新物体,纸箱上的撕裂口也不会把整体轮廓断开。
下面是一段常见流程,把单张垃圾图转成更适合后续分类的灰度结构图:
import cv2 import numpy as np img = cv2.imread("trash.jpg") # OpenCV默认读成BGR,后面会解释这个坑 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blur = cv2.GaussianBlur(gray, (3, 3), 0) # 先去掉传感器噪声 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) closed = cv2.morphologyEx(blur, cv2.MORPH_CLOSE, kernel, iterations=2)参数的几个默认值:高斯核用3×3而不是5×5,因为5×5在模糊纹理较弱的纸板时会把纸纤维细节抹平;形态学核大小在3×3到7×7之间调,我一般从5×5起步;iterations不要超过3,超过3之后小件垃圾(比如瓶盖)会被当成背景“吃掉”。核形状用MORPH_ELLIPSE椭圆而不是矩形,原因是垃圾边缘几乎没有直角,椭圆核对曲线轮廓更友好。
如果你手头是明显偏模糊的图像,可以在高斯滤波前加一步直方图均衡或拉普拉斯锐化,但如果模糊来自运动,锐化反而放大噪声。这个判断没有标准答案,属于图像处理里比较“玄学”的部分。我的经验是:先看ROI区域直方图,若细节被压在一个很窄的灰度区间,再考虑均衡化;不要在整张背景图上做全局锐化。
2.2 一个可复用的OpenCV预处理流程:灰度、去噪、形态学、轮廓过滤
上面只是单帧实验。真实分拣线或者智能车图像处理场景里,往往需要对整帧视频做ROI提取,只把垃圾所在的前景区域放给分类网络。常见做法是“灰度→去噪→闭运算→Canny→轮廓过滤”,我把它封装成一个函数:
def preprocess_frame(frame, min_area=500): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) blur = cv2.GaussianBlur(gray, (3, 3), 0) kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) closed = cv2.morphologyEx(blur, cv2.MORPH_CLOSE, kernel, iterations=2) edged = cv2.Canny(closed, 50, 150) # 边缘检测,阈值按光照环境微调 contours, _ = cv2.findContours(edged, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) rois = [] for c in contours: if cv2.contourArea(c) < min_area: # 过滤掉背景纹理产生的小噪点 continue x, y, w, h = cv2.boundingRect(c) roi = frame[y:y+h, x:x+w] rois.append(roi) return rois这里两个参数你需要按摄像头分辨率重新标定:min_area=500基于720p画面,如果换到1080p或俯拍距离更远,同一块垃圾的面积像素会差几倍;Canny的低阈值50和高阈值150,在强光环境下可以试着压到40/120,太高的高阈值会把餐盒这种低对比度物体整个漏掉。轮廓检索用RETR_EXTERNAL,只取最外层轮廓,避免把瓶身上的标签或者纸箱上的胶带当成多个ROI,增加后面分类的网络调用次数。
需要注意一个边界:这套预处理对“物体边界比较干净”的场景很管用,比如分拣线、智能车拍到的单件垃圾。如果画面是密集堆叠的混合垃圾,形态学做完轮廓仍然连成一片,那就不要在这条路上硬抠ROI。此时更实际的做法是保留整帧图像,把位置的任务交给检测网络,分类网络只负责对抠出的区域判类别。预处理的作用是“把画面洗干净”,不是代替检测模型去切分密集目标。
2.3 传统特征提取的边界:什么时候该停手转向深度学习
很多从MATLAB图像处理大作业过渡过来的读者,第一反应是“用颜色直方图、纹理特征加SVM分类”。这类传统特征对特征强烈的类别确实好用:透明玻璃瓶能靠灰度直方图形状区分,金属罐的HSV色相分布和塑料差别很大。但垃圾分类的难点恰恰是那些“同色不同类、同类不同色”的样本——一个白色纸盒和一个白色塑料盒在颜色直方图里几乎完全重叠;皱成一团的纸袋和泡沫塑料在纹理特征上也分不开。我的判断标准很简单:把经过2.2节预处理的ROI截图发给自己看,如果人眼都犹豫,传统特征基本救不回来。
所以这个阶段我会果断转向图像分类模型。不是说传统图像处理没用了——在边缘设备上,FPGA图像处理或嵌入式方案仍然大量用形态学和轮廓做目标定位,因为它的延迟和功耗比跑神经网络低一个量级;但垃圾分类的分类精度,还是得靠深度网络来扛。真正合理的分工是:OpenCV负责从画面里找“哪一坨是垃圾”,分类模型负责回答“这一坨是什么”,两者各干各擅长的部分。
3. 图像分类算法选型与训练流水线:从ResNet到MobileNet
3.1 图像分类算法选型:按数据量决定考试范围
说到图像分类算法,最新的图像分类模型榜单年年换:ConvNeXt、EfficientNet、MobileNetV4这些名字轮番出现。但对垃圾分类这个任务,我建议先别看榜单,先数一数手里的标注量。我自己一般这样分档:样本少于1000张,不要微调大模型,冻结主干只训练最后的分类头;1000到1万张,微调ResNet18或者MobileNetV3的后半段;超过1万张,再考虑放开全网络微调,或者上ResNet34、EfficientNet-B3。很多人忽略了一个事实:垃圾图片不是ImageNet那种清晰的“中心物体”,它的噪声更多来自光照、遮挡和拍摄角度,模型再新也扛不住脏数据。
| 数据量 | 推荐做法 | 常见模型 |
|---|---|---|
| 小于1000 | 冻结主干,只训分类头 | MobileNetV3-Small / ResNet18 |
| 1000 ~ 1万 | 微调后段或全微调小网络 | ResNet18 / MobileNetV3 |
| 1万以上 | 全微调,可加正则 | ResNet34 / EfficientNet-B3 |
这张表不是精确的数学边界,但很实用:小数据时用轻量模型是为了减少过拟合,大数据时用更深的模型是为了吃满信息。另一个考虑点是部署环境。如果是手机或树莓派这类边缘设备,MobileNetV3的推理延迟会比ResNet低一倍不止;如果只是服务端离线分拣,ResNet34的稳定性更好。我一般同时准备好ResNet18和MobileNetV3两个配置,用验证集挑一个。
3.2 数据集划分与加载:随机种子和分组泄漏是要命的
垃圾分类数据集的常见来源是公开比赛或自己拍。目录我习惯按ImageFolder结构组织:data/train/cardboard、data/train/glass这样的类别目录,val目录结构相同。torchvision的ImageFolder可以直接吃,非常省事。但数据划分有个特别容易翻车的细节:如果按“每张图随机归入train或val”,同一批拍摄的照片会因为曝光、背景几乎相同而同时出现在训练集和验证集里,验证集分数虚高,真实场景现原形。我见过的实际案例,Validation的94%准确率拿到新场地只剩70%上下,原因基本都在这里。
import os, random, shutil random.seed(42) # 固定随机种子,保证结果可复现 # files: {类别: [(批次号, 文件路径), ...]},批次号来自拍摄会话 for cls, items in files.items(): batches = sorted(set(bid for bid, _ in items)) random.shuffle(batches) val_bids = set(batches[: max(1, int(len(batches) * 0.15))]) for bid, path in items: dst_dir = "data/val/"+cls if bid in val_bids else "data/train/"+cls os.makedirs(dst_dir, exist_ok=True) shutil.copy(path, os.path.join(dst_dir, os.path.basename(path)))这个脚本的关键是“按批次划分而不是按单张划分”:同一个拍摄会话、同一条传送带、同一个光照条件产生的图片,要进就一起进train,要进就一起进val。val_ratio我固定用0.15,数据量小于500张时建议提到0.2;随机种子42是我习惯用的,你也可以换其他值,但一旦定下来就不要在调参途中改,否则很难说清精度波动是模型变化还是数据划分变化。
3.3 训练ResNet18做六类垃圾分类:完整脚本与参数调法
下面是一个能跑通的最小训练脚本,按TrashNet常见的六类(glass、paper、cardboard、plastic、metal、trash)来设计,你换自己的类名也完全适用:
from torchvision import datasets, models, transforms import torch.nn as nn import torch.optim as optim transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_ds = datasets.ImageFolder("data/train", transform=transform) val_ds = datasets.ImageFolder("data/val", transform=transform) loader = torch.utils.data.DataLoader(train_ds, batch_size=64, shuffle=True, num_workers=4) model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) model.fc = nn.Linear(512, len(train_ds.classes)) # 换成自己的类别数 criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9) for epoch in range(30): model.train() for x, y in loader: pred = model(x) loss = criterion(pred, y) optimizer.zero_grad() loss.backward() optimizer.step()这里有几个参数值得展开。batch_size=64在单张消费级显卡上很舒服,太小(8以下)会让BN统计不稳定,太大则要配合调高学习率;lr=0.001是微调ResNet的常用起点,如果是从零训练要降到0.01以下;SGD的momentum=0.9几乎是标配,但换Adam的话lr要再降一个量级。ImageFolder会自动读取类别子目录名作为标签,所以你的目录名必须和类别语义一一对应。训练中如果发现loss在30个epoch内下不去,先别怀疑网络结构,先检查预处理里有没有用ImageNet均值方差归一化,再用一个batch的输入做前向,确认标签和图像没有错位。
提示:torchvision第一次加载预训练权重时,会在有网络的机器上下载权重文件。离线训练的机器请用weights=None把权重参数留空,再手动把下载好的权重load进state_dict,避免训练一开始就因下载失败而中断。
4. 数据增强与类别平衡:垃圾分类拉开差距的真正变量
4.1 增强策略:亮度、噪声与遮挡优先于旋转裁剪
垃圾分类图像和你熟悉的ImageNet增强有个不一样的地方:垃圾在采集时基本都是俯拍,角度相对固定,RandomRotation旋转30度反而可能造出不符合真实分布的角度,让模型学到“瓶盖横着”这种现实中不存在的样子。真正影响自然图像处理效果的是三类差异:光照强度、镜头脏污带来的模糊、以及局部遮挡。所以我的增强管线里,ColorJitter和GaussianBlur的地位比旋转高:
train_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.2), transforms.RandomApply([transforms.GaussianBlur(3)], p=0.3), transforms.ToTensor(), transforms.RandomErasing(p=0.2, scale=(0.02, 0.1)), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ])参数解释:先Resize到256再RandomCrop到224,等于给模型提供小幅平移的鲁棒性,比整体缩放更自然;brightness=0.3能模拟白天不同时段的自然光照,对比度0.3重点对付阴影;GaussianBlur的p=0.3,对应的是传送带振动或镜头指纹造成的模糊图像处理场景,p太高会把纸板纹理抹掉,导致“纸箱分不清纸”;RandomErasing的scale取0.02到0.1,模拟标签贴纸、手握遮挡这类小面积遮挡,它只擦掉2%~10%的区域,不会把整个物体盖住。
这样一整套增强跑下来,验证集精度不一定暴涨,但模型换场地后的泛化会明显更稳。我遇到过不少案例:不开增强时训练精度能到99%,验证集只有88%;开增强后训练精度降到96%,验证集反而上到93%。这属于正常的“把记住换成学会”,不用慌。
4.2 类别不均衡处理:加权采样器加针对性增强
垃圾分类数据集里,透明玻璃和高价值可回收金属往往占比小,而纸类和餐厨垃圾占比很大。如果直接用CrossEntropyLoss,模型会“学会”把一切都倾向大头类,因为这样loss下降最快。现象就是验证集总精度90%以上,但玻璃的召回率只有40%。这时候单看总accuracy会被骗,必须做类别加权。
from sklearn.utils.class_weight import compute_class_weight import numpy as np from torch.utils.data import WeightedRandomSampler labels = [s for _, s in train_ds.samples] classes = np.unique(labels) weights = compute_class_weight("balanced", classes=classes, y=labels) sample_weights = [weights[s] for s in labels] sampler = WeightedRandomSampler(sample_weights, num_samples=len(labels), replacement=True) balanced_loader = torch.utils.data.DataLoader( train_ds, batch_size=64, sampler=sampler)用这个sampler替换3.3里的loader之后,每个batch里少样本类别的出现频率会显著上升。compute_class_weight("balanced")计算的是各类样本数的倒数再归一化,是最省事的基线;想让少数类再突出一点,可以手动把对应类的权重乘上一个1.5到2的系数。要注意replacement=True表示每次抽样允许重复取同一张图,这对样本少于50张的类别尤其重要;如果某类只有十几张,单靠加权容易过拟合,更有效的做法是回到现场补拍。数据上的欠账,增强算法很难完全还清。
5. 垃圾分类图像分类避坑:数据、通道和预处理不一致
这一章的每一条都是实际踩过的坑,按“现象→原因→解决”来写。它们看着基础,但“基础”意味着出现频率高,我见过不少项目最后几天全部耗在查这类问题上。
5.1 数据层面的坑:分组泄漏、类别失衡与采集污染
坑一:分组泄漏造成验证集虚高。现象是训练时验证精度94%,放到新场地只有70%出头,模型看起来“考试全对、上班全废”。原因是同一批照片被逐张随机切进train和val,同一串曝光相近的画面同时出现在两边,模型其实记住了画面背景而不是垃圾本身。解决方法是按拍摄批次/会话划分数据,像3.2那样先按batch聚合再切分,并且固定随机种子;看指标时以“新采集地点”的测试为准,不以原val为准。
坑二:类别不均衡下的高精度低召回。现象是总准确率到了92%,但点开混淆矩阵发现玻璃类几乎全被分到“其他”。原因是CrossEntropy在样本占比悬殊时被少数类放弃,总损失仍然很低。解决方法是第四章的WeightedRandomSampler,或把Loss换成带class weights的CrossEntropyLoss;同时把汇报指标从accuracy改成“各类召回率+总正确率”。我一般要求可回收类(玻璃、金属、纸板)的召回率不得低于0.9,否则流水线分拣会漏掉有价值的资源。
坑三:脏标签污染训练。现象是训练loss到了后期反复震荡,某两个类之间总在互猜。原因是垃圾标注本身很容易错:透明碗和玻璃瓶壁厚不同但外形接近,皱纸和塑料袋颜色相近时标注员也会犹豫。解决方法是先把模型在原始标签上跑20个epoch,再取验证集里高置信度但预测错误的样本人工复核;这类样本往往就是标签画错的。把标签修正一遍再训,训练曲线会立刻变稳,这一招比换模型有效得多。
5.2 推理层面的坑:BGR/RGB与预处理不一致
坑四:OpenCV读图和PyTorch训练时的颜色通道不一致。现象是训练集上模型表现得很好,把同一张图用cv2.imread读进来预测,结果莫名偏向蓝色;原因很简单,cv2.imread返回的是BGR排列,而torchvision预训练模型在RGB图像上训练,通道顺序错位后颜色语义全乱。解决方法是任何从OpenCV读入的帧,都先加一行rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB),再做transform。这条排在我自己的“上线前检查清单”第一位。
坑五:推理预处理和训练预处理不一致。现象是同一张图在训练脚本里能预测正确,换到部署脚本或ONNX后置信度全面漂移。原因是很多人训练时的Normalize一直在,推理脚本里却忘了做ToTensor加Normalize,或者用了不同的resize尺寸和不同的均值方差。解决办法是把“Resize→ToTensor→Normalize”封装成一个固定函数,训练和推理共用这同一个函数,不要在两处各写一遍。如果已经上线,排查时先对同一张图逐层对比两边的张量,差别通常就出现在像素值范围上——训练端做了除以255,推理端没做。
此前做过一个智能车图像处理相关的边缘项目,最后排查出来的问题不是模型,而是推理端没有把归一化参数从float转成半精度时对齐,指标掉了5个点。后来我把所有预处理状态写进一个配置字典,训练、验证、导出、推理全部从同一个字典读,这类问题就再没出现。
6. 推理与验证技巧:从单张图片到真实分拣的闭环
6.1 推理脚本与置信度阈值:宁可拒判不可乱扔
训练和验证都跑通之后,单张推理脚本要写得和训练预处理完全一致。我习惯把推理函数做成只接受RGB的ndarray,避免调用方把BGR图直接喂进来:
from PIL import Image def predict(image_rgb): x = valid_transform(Image.fromarray(image_rgb)).unsqueeze(0) with torch.no_grad(): logits = model(x) prob = torch.softmax(logits, dim=1)[0] cls_id = torch.argmax(prob).item() conf = prob[cls_id].item() return cls_id, conf, prob返回三个值比只返回一个类别有用:conf可以直接用于阈值判定。在真实分拣线里,我的经验是把置信度阈值设在0.5到0.7之间,低于阈值的样本不强行分类,送入人工复查通道或另一台相机再拍一次。宁可拒判,不要把一瓶未清洗的酸奶盒硬塞进可回收类。
6.2 验证方法:混淆矩阵决定补拍方向
最终验收不要只看总准确率。用一段很短的脚本把验证集predict一遍,再打印混淆矩阵:
from sklearn.metrics import confusion_matrix for x, y in val_loader: with torch.no_grad(): preds = model(x).argmax(dim=1).numpy() y_true.extend(y.numpy()) y_pred.extend(preds) print(confusion_matrix(y_true, y_pred))重点关注哪些类被“漏”到哪一类。比如金属罐常被分到塑料类,说明问题出在颜色直方图接近;纸板常被分到纸张类,说明模型没学到厚度特征,需要补充侧拍或纹理增强。混淆矩阵能直接指出下一次数据采集应该补什么,这是总准确率给不了的信息。
如果目标是嵌入式设备或智能车,验证完的PyTorch模型不要直接拿过去用。我一般先导出ONNX,再用onnxruntime或厂商的RKNN/TensorRT工具链做转换:
model.eval() dummy = torch.randn(1, 3, 224, 224) torch.onnx.export(model.cpu(), dummy, "trash.onnx", input_names=["img"], output_names=["cls"], opset_version=13)导出前务必model.eval(),否则BN的统计量还在随batch更新;dummy张量尺寸必须和训练输入一致。复盘这个项目,我最后说点自己的教训:图像处理负责把画面交给模型,图像分类负责把类别交回给分拣机构,两者的接口边界必须用固定预处理函数来锁死。我早期一半以上的返工都发生在“训练时一套预处理、部署时又写了一套”这种低级不一致上。希望这篇笔记能帮你绕开这些坑,把精力花在看混淆矩阵和补数据上,那才是垃圾分类图像分类真正的决胜点。
本文还有配套的精品资源,点击获取