简介:面向图像分类入门学习者的实战资料包,基于TensorFlow与OpenCV实现智能垃圾分类,覆盖数据集制作、网络DIY、训练与预测全流程,可作为图像分类任务的通用模板。资源共1046个文件,以1041张JPG图片为主,另有Python脚本、说明txt、演示mp4和预训练h5模型,压缩包整体约824.68MB。其中train.py用于训练数据集,predict.py加载模型并输出预测结果,能在图片上以中文显示干垃圾、湿垃圾、可回收垃圾、有害垃圾等类别,方便直接复现与二次开发。已有1330人学习下载,适合希望掌握图像分类完整工程流程的学生、开发者及爱好者参考学习。
1. 图像分类落在垃圾分类上,先想清楚要分什么
一个摄像头对准小区垃圾桶,要求自动识别纸箱、饮料瓶、剩饭和电池,这就是“图像处理+图像分类”最典型的落地场景。垃圾分类不是 ImageNet 那种一千类均衡分类,它的难点在于类间相似度极高:透明矿泉水瓶和透明玻璃瓶长得几乎一样,沾了油的报纸和干净打印纸在视觉上只有纹理差异,而一次识别错误被监控拍到,整个系统的可信度就归零。
这篇文章把这条链路拆成四段来讲:数据怎么准备、传统特征怎么做基线、深度学习模型怎么训、训完怎么部署和修错。整套流程用 OpenCV 和 PyTorch 就能跑通,不依赖特殊硬件,适合正在做课程设计、工创赛垃圾分类垃圾桶、或者准备把垃圾分类做成产品原型的开发者。先不急着调模型,先把“分什么、按什么标准分”定清楚,否则后面所有指标都是自欺欺人。
2. 图像处理先于图像分类:数据集、类别体系与预处理策略
2.1 垃圾分类图像分类的任务定义与类别体系
分类任务的第一步不是选模型,而是定义标签空间。国内多数城市采用“可回收、厨余、有害、其他”四分类,但直接按四类训练往往效果差,因为“其他”是一锅烩,包含陶瓷、卫生纸、灰土,视觉特征毫无共性。
更合理的做法是采用两级体系:第一级是材料类别,比如塑料、玻璃、纸类、金属、织物、厨余、有害物;第二级再映射到投放要求。材料类别在图像上是有视觉共性的,模型学起来更容易,也方便后续接入称重传感器或红外光谱做多模态融合。
| 材料类别 | 典型物品 | 视觉特征 | 四分类映射 |
|---|---|---|---|
| 塑料 | 矿泉水瓶、外卖盒 | 半透明、高光、可变形 | 可回收 |
| 玻璃 | 酒瓶、碎玻璃 | 透光、反光强烈 | 可回收 |
| 纸类 | 纸箱、报纸、纸杯 | 表面粗糙、纹理明显 | 可回收 |
| 金属 | 易拉罐、铁皮 | 金属光泽、边缘锐利 | 可回收 |
| 厨余 | 剩菜、果皮 | 色彩杂乱、形态不规则 | 厨余 |
| 有害 | 电池、灯管 | 体积小、标签醒目 | 有害 |
标签体系一旦确定就不要轻易改,后面所有数据清洗、模型评估都依赖这个定义。实操中建议把类别数控制在 6~15 类之间,少于 6 类会让“其他”类负担过重,多于 15 类则采集成本急剧上升,且类间混淆难以收敛。
2.2 图像预处理:尺寸统一、色彩空间与增强策略
图像处理环节直接决定分类上限。常见做法是先把输入统一到 224×224 或 256×256,这一步不是为了迎合某个模型,而是让后续的特征提取和批训练有稳定的输入维度。色彩空间方面,RGB 是默认选择,但垃圾分类场景里亮度变化极大——户外垃圾桶在中午和傍晚的光照差 3 倍以上,建议在训练时同时生成 HSV 或 Lab 色彩空间的副本,让模型学到不受亮度干扰的颜色表征。
import cv2 import numpy as np from glob import glob import os def preprocess_image(src_path, dst_path, size=(256, 256)): img = cv2.imread(src_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # OpenCV 默认 BGR,训练前转回 RGB img = cv2.resize(img, size, interpolation=cv2.INTER_AREA) # 自适应直方图均衡化:缓解户外光照不均 lab = cv2.cvtColor(img, cv2.COLOR_RGB2LAB) l, a, b = cv2.split(lab) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) l_eq = clahe.apply(l) lab_eq = cv2.merge([l_eq, a, b]) img = cv2.cvtColor(lab_eq, cv2.COLOR_LAB2RGB) # 归一化到 [0,1],PyTorch 训练时通常再按 ImageNet 统计做标准化 img = img.astype(np.float32) / 255.0 np.save(dst_path, img) raw_images = glob("raw_data/*/*.jpg") for raw_path in raw_images: cls = raw_path.split(os.sep)[-2] os.makedirs(f"processed/{cls}", exist_ok=True) dst = f"processed/{cls}/{os.path.basename(raw_path).replace('.jpg', '.npy')}" preprocess_image(raw_path, dst)代码里两个关键点:cv2.COLOR_BGR2RGB是把 OpenCV 读图后的 BGR 顺序转成训练框架默认的 RGB;CLAHE是限制对比度的直方图均衡,用在垃圾分类里能显著提升户外暗光样本的识别率,但clipLimit不要超过 3.0,否则塑料瓶的高光区会被过度增强,产生伪纹理。
2.3 数据增强的边界:什么能变,什么不能变
垃圾分类的增强策略和花卉分类、森林图像分类不同,最大的区别在于:类别语义可能与空间方向强相关。一个竖着的酱油瓶和一个倒着的酱油瓶都是可回收物,方向不变类;但“未拆封的外卖盒”和“剩余饭菜”在视觉上可能只差一个打开的角度,此时过度旋转反而会破坏判别信息。
推荐的增强组合是:随机水平翻转、±15° 小角度旋转、随机亮度对比度调整、随机裁剪缩放。不要用垂直翻转,因为真实场景中摄像头不会倒挂;不要用极端颜色扰动,因为垃圾分类中颜色是判别性的。增强应该在训练循环内动态做,而不是提前存盘,否则每个 epoch 看到的是同一批增强结果。
from torchvision import transforms train_transform = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(degrees=15), transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])RandomResizedCrop的scale参数从 0.7 起而不是 0.08,因为垃圾分类中的物体通常占画面主体,裁剪过狠会丢失关键纹理。ColorJitter只调亮度和对比度,不动色相,保证塑料的蓝色和纸箱的棕色不被漂移。
3. 传统图像分类路线:HOG 特征与 SVM 做基线
3.1 为什么深度模型之前要先跑特征工程基线
数据集刚建好、只有几百张图时,直接训练 CNN 大概率过拟合。这时先用传统视觉方法做一个基线,有双重价值:一是验证数据本身是否可分,如果 HOG+SVM 都能到 85%,说明类别定义没问题,瓶颈在模型复杂度;二是给后续深度学习提供一个参照系,方便判断深度模型到底值不值得那几小时训练时间。
图像处理里常用的手工特征包括颜色直方图、LBP 纹理特征、HOG 形状特征。垃圾分类场景中,HOG 对塑料瓶的圆润边缘、纸箱的棱角结构、金属罐的高光区域都有不错的响应,配合 HSV 颜色直方图,能覆盖大多数静态场景。
3.2 HOG 特征 + SVM 的最小可运行流程
import cv2 import numpy as np from skimage.feature import hog from sklearn.svm import SVC from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.model_selection import cross_val_score def extract_feature(img_path): img = cv2.imread(img_path) img = cv2.resize(img, (128, 128)) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # HOG:捕获边缘和形状结构,对光照变化有较好的鲁棒性 hog_feat = hog(gray, orientations=9, pixels_per_cell=(8, 8), cells_per_block=(2, 2), block_norm="L2-Hys") # HSV 颜色直方图:补充 HOG 缺失的颜色信息 hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) hist = cv2.calcHist([hsv], [0, 1], None, [18, 32], [0, 180, 0, 256]) hist = cv2.normalize(hist, hist).flatten() return np.concatenate([hog_feat, hist]) X = [] y = [] for cls_idx, cls_name in enumerate(sorted(os.listdir("processed"))): for feat_path in glob(f"processed/{cls_name}/*.npy"): feat = extract_feature(feat_path) X.append(feat) y.append(cls_idx) X = np.array(X) clf = make_pipeline(StandardScaler(), SVC(kernel="rbf", C=10, gamma="scale")) scores = cross_val_score(clf, X, y, cv=5) print(f"5-fold CV accuracy: {scores.mean():.3f} ± {scores.std():.3f}")HOG 参数里两个最关键的量:pixels_per_cell决定了特征的最小粒度,8×8 意味着在 128×128 图上每 16×16 像素块才有一个描述子,适合捕捉瓶身和纸箱的中等尺度边缘;orientations取 9 是默认值,它不是拍脑袋定的,而是 9 个方向桶正好覆盖 0°~180°,再多对刚性物体边际收益很小。SVM 的C=10是经验值,C 太小欠拟合、太大容易把个别脏样本当成支持向量。
3.3 传统方案的瓶颈和转深度学习的判别信号
HOG+SVM 在单一背景、受控光照下能做到 88%~94% 的准确率,但遇到三类情况会明显崩溃:一是重叠物体,两个瓶子叠在一起时 HOG 特征互相干扰;二是非刚性变形,揉成一团的塑料袋纹理和报纸没有稳定的梯度方向;三是阴影和反光,金属罐的高光区域会产生假边缘,被 HOG 当作结构特征。
当你观察到验证集上错误样本集中在“透明容器”和“皱缩物体”上,并且 HOG 特征维度加到 4000 维以上准确率仍然不涨,就应该切换到 CNN 了。这个切换时机的判断,比直接无脑上 ResNet 更考验工程判断力。在嵌入式硬件比如 FPGA 上跑图像分类时,传统方案仍有部署价值,因为 HOG 特征提取没有卷积层的访存量压力,量化到 8bit 整数也不需要特殊指令集,这也是“fpga图像处理”方向在实际项目中仍然活跃的原因。
4. 深度图像分类模型:从 CNN 到 Transformer 的迁移学习实践
4.1 垃圾分类用什么网络架构:ResNet 打底,ViT 做增量
模型选型不是越新越好,而是看训练数据量和部署环境。垃圾分类数据集通常在几千到几万张量级,这个量级下从头训练 ViT 会严重欠拟合,因为 Transformer 缺少卷积的归纳偏置,需要大量数据才能学会像素之间的局部相关性。最稳妥的路线是 ResNet50 做主力,数据量过万后再试 EfficientNet 或 Swin Transformer。
最新的图像分类模型确实已经在 ImageNet 上超过了 ResNet,但那些收益来自更复杂的训练策略和更大的输入分辨率,在小数据集上无法复现。Flowers 102、CIFAR-100 这些基准任务上,ResNet50 微调后仍然是最稳定的 baseline。垃圾分类和花卉分类有个共同点:类别由局部纹理决定,而不是全局结构。塑料和玻璃的区别在一圈瓶口的螺纹,纸类和织物的区别在一个角落的纤维走向。ResNet 的多阶段下采样恰好保留了这个多尺度信息。
4.2 PyTorch 迁移学习训练脚本:冻结与解冻的策略
import torch import torch.nn as nn from torch.utils.data import DataLoader, Dataset from torchvision import models, transforms from torch.optim import AdamW class GarbageDataset(Dataset): def __init__(self, root, transform=None): self.images = glob(f"{root}/*/*.jpg") self.classes = sorted(os.listdir(root)) self.cls2idx = {c: i for i, c in enumerate(self.classes)} self.transform = transform def __len__(self): return len(self.images) def __getitem__(self, idx): path = self.images[idx] cls = path.split(os.sep)[-2] img = Image.open(path).convert("RGB") if self.transform: img = self.transform(img) return img, self.cls2idx[cls] model = models.resnet50(pretrained=True) num_classes = len(os.listdir("processed")) # 冻结卷积基座,只训练分类头 for param in model.parameters(): param.requires_grad = False model.fc = nn.Linear(model.fc.in_features, num_classes) # 只对 fc 层的参数做优化 optimizer = AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3, weight_decay=1e-4) criterion = nn.CrossEntropyLoss(label_smoothing=0.1) train_loader = DataLoader(GarbageDataset("processed", train_transform), batch_size=32, shuffle=True, num_workers=4) for epoch in range(10): model.train() running_loss = 0.0 for images, labels in train_loader: optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() print(f"Epoch {epoch+1}: loss = {running_loss / len(train_loader):.4f}")这段代码里两个值得注意的细节。label_smoothing=0.1在垃圾分类里非常实用,因为不同材料的边界是模糊的——纸盒外面贴了一层塑料膜,你说它是纸还是塑料?标签平滑不追求训练集上 100% 的置信度,给模型留出容错空间,会直接反映在验证集准确率上。freeze-resnet后只训 10 个 epoch,对几千张的垃圾分类数据集已经足够,再训就会开始记住训练集里的背景。
4.3 调参重点:学习率、批大小与类别不平衡
第一阶段解冻分类头,学习率用 1e-3 没问题;第二阶段解冻最后两个残差块,学习率要降到原来的 1/10。这个顺序模仿了迁移学习里的“渐进解冻”策略。批大小方面,32 在 224×224 输入下是 GTX 1080 的舒适区间,显存不够就降到 16,并同步把学习率降到 7e-4,否则梯度噪声会变大导致 loss 震荡。
垃圾分类里几乎没有平衡的数据集:工作日的生活垃圾里厨余占比过半,电池可能一周才有一小块。直接用 CrossEntropyLoss 会把“有害垃圾”类压到近乎不可见。
class_counts = np.array([len(os.listdir(f"processed/{c}")) for c in classes]) weights = class_counts.sum() / (class_counts * len(class_counts)) # 按样本数反比加权,多数类权重小,少数类权重大 class_weights = torch.tensor(weights, dtype=torch.float32) criterion = nn.CrossEntropyLoss(weight=class_weights, label_smoothing=0.05)class_weights的计算方式是类别样本数的倒数再归一化。要注意的是,加了weight参数后label_smoothing会与权重相互作用,少数类的目标分布被拉得更平,所以这里把平滑系数降到了 0.05。训练过程中监控每个类别的单独召回率,而不仅仅是平均准确率,有时候整体准确率上升了,但“有害垃圾”的召回率反而在下降,这说明权重设置过头了。
5. 部署验证与难例修正:混淆矩阵驱动的迭代优化
模型训练完,不要急着接摄像头。先在测试集上输出混淆矩阵,按错误对数排序,逐张看。垃圾分类里的错误往往集中在固定几对类别:透明塑料瓶 vs 透明玻璃瓶,白色塑料袋 vs 白色泡沫盒,深色金属罐 vs 深色塑料瓶。这些错误的根源不在模型能力,而在训练数据里这些类别各自的光照条件不一致。
一个有效的技巧是:单独为混淆对收集“困难样本”,把它们从训练集里抽样出来,做同一种增强策略后放进一个独立的 fine-tune 集合。具体操作用 PyTorch 的Subset接口,从原始数据集里筛出两个类的所有样本,额外训练 5 个 epoch:
confuse_pairs = [(idx_plastic, idx_glass), (idx_bag, idx_foam)] for a_idx, b_idx in confuse_pairs: sample_idx = np.where(np.isin(all_labels, [a_idx, b_idx]))[0] subset = Subset(full_dataset, sample_idx) loader = DataLoader(subset, batch_size=16, shuffle=True) # 用当前模型权重继续训练,学习率再降一半 for param in model.fc.parameters(): param.requires_grad = True optimizer = AdamW(model.fc.parameters(), lr=3e-4) for epoch in range(5): for images, labels in loader: optimizer.zero_grad() out = model(images) loss = nn.functional.cross_entropy(out, labels) loss.backward() optimizer.step()这相当于对易混淆类别做了一次局部判别力加强,每次只动分类头,不动特征提取器。推理部署时还有一个低成本优化:把输入分辨率从 224 提升到 320,对瓶口螺纹和纸张纹理这类细节有显著帮助,代价只是推理速度从 8ms 涨到 15ms 左右——在 CPU 上推进分类任务时这仍然是可接受的范围。
最后验证时,不要只看测试集准确率。拿一个真实场景的短视频按帧跑一遍,把每帧的预测类别和置信度写到 CSV 里,观察连续帧之间的类别跳变。如果一帧是塑料、下一帧变成玻璃、再下一帧又变回塑料,说明模型没有真正学到区分性特征,只是在碰运气。这时候回去检查训练数据里这两个类别的背景是否过于单一——很多项目在实验室白墙背景下训练效果很好,一到小区垃圾桶的绿皮表面就崩盘,原因就是训练集里缺少真实场景背景,这也是“opencv图像处理项目”落地时最容易踩的坑。
本文还有配套的精品资源,点击获取