
简介面向深度学习初学者的智能垃圾分类图像分类项目包以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, iterations2)参数的几个默认值高斯核用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_area500): 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, iterations2) 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:yh, x:xw] rois.append(roi) return rois这里两个参数你需要按摄像头分辨率重新标定min_area500基于720p画面如果换到1080p或俯拍距离更远同一块垃圾的面积像素会差几倍Canny的低阈值50和高阈值150在强光环境下可以试着压到40/120太高的高阈值会把餐盒这种低对比度物体整个漏掉。轮廓检索用RETR_EXTERNAL只取最外层轮廓避免把瓶身上的标签或者纸箱上的胶带当成多个ROI增加后面分类的网络调用次数。需要注意一个边界这套预处理对“物体边界比较干净”的场景很管用比如分拣线、智能车拍到的单件垃圾。如果画面是密集堆叠的混合垃圾形态学做完轮廓仍然连成一片那就不要在这条路上硬抠ROI。此时更实际的做法是保留整帧图像把位置的任务交给检测网络分类网络只负责对抠出的区域判类别。预处理的作用是“把画面洗干净”不是代替检测模型去切分密集目标。2.3 传统特征提取的边界什么时候该停手转向深度学习很多从MATLAB图像处理大作业过渡过来的读者第一反应是“用颜色直方图、纹理特征加SVM分类”。这类传统特征对特征强烈的类别确实好用透明玻璃瓶能靠灰度直方图形状区分金属罐的HSV色相分布和塑料差别很大。但垃圾分类的难点恰恰是那些“同色不同类、同类不同色”的样本——一个白色纸盒和一个白色塑料盒在颜色直方图里几乎完全重叠皱成一团的纸袋和泡沫塑料在纹理特征上也分不开。我的判断标准很简单把经过2.2节预处理的ROI截图发给自己看如果人眼都犹豫传统特征基本救不回来。所以这个阶段我会果断转向图像分类模型。不是说传统图像处理没用了——在边缘设备上FPGA图像处理或嵌入式方案仍然大量用形态学和轮廓做目标定位因为它的延迟和功耗比跑神经网络低一个量级但垃圾分类的分类精度还是得靠深度网络来扛。真正合理的分工是OpenCV负责从画面里找“哪一坨是垃圾”分类模型负责回答“这一坨是什么”两者各干各擅长的部分。3. 图像分类算法选型与训练流水线从ResNet到MobileNet3.1 图像分类算法选型按数据量决定考试范围说到图像分类算法最新的图像分类模型榜单年年换ConvNeXt、EfficientNet、MobileNetV4这些名字轮番出现。但对垃圾分类这个任务我建议先别看榜单先数一数手里的标注量。我自己一般这样分档样本少于1000张不要微调大模型冻结主干只训练最后的分类头1000到1万张微调ResNet18或者MobileNetV3的后半段超过1万张再考虑放开全网络微调或者上ResNet34、EfficientNet-B3。很多人忽略了一个事实垃圾图片不是ImageNet那种清晰的“中心物体”它的噪声更多来自光照、遮挡和拍摄角度模型再新也扛不住脏数据。数据量推荐做法常见模型小于1000冻结主干只训分类头MobileNetV3-Small / ResNet181000 ~ 1万微调后段或全微调小网络ResNet18 / MobileNetV31万以上全微调可加正则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_okTrue) 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, transformtransform) val_ds datasets.ImageFolder(data/val, transformtransform) loader torch.utils.data.DataLoader(train_ds, batch_size64, shuffleTrue, num_workers4) model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(512, len(train_ds.classes)) # 换成自己的类别数 criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.001, momentum0.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_size64在单张消费级显卡上很舒服太小8以下会让BN统计不稳定太大则要配合调高学习率lr0.001是微调ResNet的常用起点如果是从零训练要降到0.01以下SGD的momentum0.9几乎是标配但换Adam的话lr要再降一个量级。ImageFolder会自动读取类别子目录名作为标签所以你的目录名必须和类别语义一一对应。训练中如果发现loss在30个epoch内下不去先别怀疑网络结构先检查预处理里有没有用ImageNet均值方差归一化再用一个batch的输入做前向确认标签和图像没有错位。提示torchvision第一次加载预训练权重时会在有网络的机器上下载权重文件。离线训练的机器请用weightsNone把权重参数留空再手动把下载好的权重load进state_dict避免训练一开始就因下载失败而中断。4. 数据增强与类别平衡垃圾分类拉开差距的真正变量4.1 增强策略亮度、噪声与遮挡优先于旋转裁剪垃圾分类图像和你熟悉的ImageNet增强有个不一样的地方垃圾在采集时基本都是俯拍角度相对固定RandomRotation旋转30度反而可能造出不符合真实分布的角度让模型学到“瓶盖横着”这种现实中不存在的样子。真正影响自然图像处理效果的是三类差异光照强度、镜头脏污带来的模糊、以及局部遮挡。所以我的增强管线里ColorJitter和GaussianBlur的地位比旋转高train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.2), transforms.RandomApply([transforms.GaussianBlur(3)], p0.3), transforms.ToTensor(), transforms.RandomErasing(p0.2, scale(0.02, 0.1)), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ])参数解释先Resize到256再RandomCrop到224等于给模型提供小幅平移的鲁棒性比整体缩放更自然brightness0.3能模拟白天不同时段的自然光照对比度0.3重点对付阴影GaussianBlur的p0.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, classesclasses, ylabels) sample_weights [weights[s] for s in labels] sampler WeightedRandomSampler(sample_weights, num_sampleslen(labels), replacementTrue) balanced_loader torch.utils.data.DataLoader( train_ds, batch_size64, samplersampler)用这个sampler替换3.3里的loader之后每个batch里少样本类别的出现频率会显著上升。compute_class_weight(balanced)计算的是各类样本数的倒数再归一化是最省事的基线想让少数类再突出一点可以手动把对应类的权重乘上一个1.5到2的系数。要注意replacementTrue表示每次抽样允许重复取同一张图这对样本少于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, dim1)[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(dim1).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_version13)导出前务必model.eval()否则BN的统计量还在随batch更新dummy张量尺寸必须和训练输入一致。复盘这个项目我最后说点自己的教训图像处理负责把画面交给模型图像分类负责把类别交回给分拣机构两者的接口边界必须用固定预处理函数来锁死。我早期一半以上的返工都发生在“训练时一套预处理、部署时又写了一套”这种低级不一致上。希望这篇笔记能帮你绕开这些坑把精力花在看混淆矩阵和补数据上那才是垃圾分类图像分类真正的决胜点。本文还有配套的精品资源点击获取