
简介面向图像分类入门学习者的实战资料包基于TensorFlow与OpenCV实现智能垃圾分类覆盖数据集制作、网络DIY、训练与预测全流程可作为图像分类任务的通用模板。资源共1046个文件以1041张JPG图片为主另有Python脚本、说明txt、演示mp4和预训练h5模型压缩包整体约824.68MB。其中train.py用于训练数据集predict.py加载模型并输出预测结果能在图片上以中文显示干垃圾、湿垃圾、可回收垃圾、有害垃圾等类别方便直接复现与二次开发。已有1330人学习下载适合希望掌握图像分类完整工程流程的学生、开发者及爱好者参考学习。1. 图像分类落在垃圾分类上先想清楚要分什么一个摄像头对准小区垃圾桶要求自动识别纸箱、饮料瓶、剩饭和电池这就是“图像处理图像分类”最典型的落地场景。垃圾分类不是 ImageNet 那种一千类均衡分类它的难点在于类间相似度极高透明矿泉水瓶和透明玻璃瓶长得几乎一样沾了油的报纸和干净打印纸在视觉上只有纹理差异而一次识别错误被监控拍到整个系统的可信度就归零。这篇文章把这条链路拆成四段来讲数据怎么准备、传统特征怎么做基线、深度学习模型怎么训、训完怎么部署和修错。整套流程用 OpenCV 和 PyTorch 就能跑通不依赖特殊硬件适合正在做课程设计、工创赛垃圾分类垃圾桶、或者准备把垃圾分类做成产品原型的开发者。先不急着调模型先把“分什么、按什么标准分”定清楚否则后面所有指标都是自欺欺人。2. 图像处理先于图像分类数据集、类别体系与预处理策略2.1 垃圾分类图像分类的任务定义与类别体系分类任务的第一步不是选模型而是定义标签空间。国内多数城市采用“可回收、厨余、有害、其他”四分类但直接按四类训练往往效果差因为“其他”是一锅烩包含陶瓷、卫生纸、灰土视觉特征毫无共性。更合理的做法是采用两级体系第一级是材料类别比如塑料、玻璃、纸类、金属、织物、厨余、有害物第二级再映射到投放要求。材料类别在图像上是有视觉共性的模型学起来更容易也方便后续接入称重传感器或红外光谱做多模态融合。材料类别典型物品视觉特征四分类映射塑料矿泉水瓶、外卖盒半透明、高光、可变形可回收玻璃酒瓶、碎玻璃透光、反光强烈可回收纸类纸箱、报纸、纸杯表面粗糙、纹理明显可回收金属易拉罐、铁皮金属光泽、边缘锐利可回收厨余剩菜、果皮色彩杂乱、形态不规则厨余有害电池、灯管体积小、标签醒目有害标签体系一旦确定就不要轻易改后面所有数据清洗、模型评估都依赖这个定义。实操中建议把类别数控制在 615 类之间少于 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, interpolationcv2.INTER_AREA) # 自适应直方图均衡化缓解户外光照不均 lab cv2.cvtColor(img, cv2.COLOR_RGB2LAB) l, a, b cv2.split(lab) clahe cv2.createCLAHE(clipLimit2.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(fprocessed/{cls}, exist_okTrue) dst fprocessed/{cls}/{os.path.basename(raw_path).replace(.jpg, .npy)} preprocess_image(raw_path, dst)代码里两个关键点cv2.COLOR_BGR2RGB是把 OpenCV 读图后的 BGR 顺序转成训练框架默认的 RGBCLAHE是限制对比度的直方图均衡用在垃圾分类里能显著提升户外暗光样本的识别率但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(p0.5), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.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 大概率过拟合。这时先用传统视觉方法做一个基线有双重价值一是验证数据本身是否可分如果 HOGSVM 都能到 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, orientations9, pixels_per_cell(8, 8), cells_per_block(2, 2), block_normL2-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(fprocessed/{cls_name}/*.npy): feat extract_feature(feat_path) X.append(feat) y.append(cls_idx) X np.array(X) clf make_pipeline(StandardScaler(), SVC(kernelrbf, C10, gammascale)) scores cross_val_score(clf, X, y, cv5) print(f5-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 的C10是经验值C 太小欠拟合、太大容易把个别脏样本当成支持向量。3.3 传统方案的瓶颈和转深度学习的判别信号HOGSVM 在单一背景、受控光照下能做到 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, transformNone): 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(pretrainedTrue) 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()), lr1e-3, weight_decay1e-4) criterion nn.CrossEntropyLoss(label_smoothing0.1) train_loader DataLoader(GarbageDataset(processed, train_transform), batch_size32, shuffleTrue, num_workers4) 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(fEpoch {epoch1}: loss {running_loss / len(train_loader):.4f})这段代码里两个值得注意的细节。label_smoothing0.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(fprocessed/{c})) for c in classes]) weights class_counts.sum() / (class_counts * len(class_counts)) # 按样本数反比加权多数类权重小少数类权重大 class_weights torch.tensor(weights, dtypetorch.float32) criterion nn.CrossEntropyLoss(weightclass_weights, label_smoothing0.05)class_weights的计算方式是类别样本数的倒数再归一化。要注意的是加了weight参数后label_smoothing会与权重相互作用少数类的目标分布被拉得更平所以这里把平滑系数降到了 0.05。训练过程中监控每个类别的单独召回率而不仅仅是平均准确率有时候整体准确率上升了但“有害垃圾”的召回率反而在下降这说明权重设置过头了。5. 部署验证与难例修正混淆矩阵驱动的迭代优化模型训练完不要急着接摄像头。先在测试集上输出混淆矩阵按错误对数排序逐张看。垃圾分类里的错误往往集中在固定几对类别透明塑料瓶 vs 透明玻璃瓶白色塑料袋 vs 白色泡沫盒深色金属罐 vs 深色塑料瓶。这些错误的根源不在模型能力而在训练数据里这些类别各自的光照条件不一致。一个有效的技巧是单独为混淆对收集“困难样本”把它们从训练集里抽样出来做同一种增强策略后放进一个独立的 fine-tune 集合。具体操作用 PyTorch 的Subset接口从原始数据集里筛出两个类的所有样本额外训练 5 个 epochconfuse_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_size16, shuffleTrue) # 用当前模型权重继续训练学习率再降一半 for param in model.fc.parameters(): param.requires_grad True optimizer AdamW(model.fc.parameters(), lr3e-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图像处理项目”落地时最容易踩的坑。本文还有配套的精品资源点击获取