简介:这份资源是一套基于Python开发的智能垃圾分类系统完整源码与部署指南,源自本科毕业设计课题,适合计算机视觉入门者、毕业设计选题学生及需要期末大作业参考的学习者。系统通过卷积神经网络与迁移学习实现可回收物、厨余垃圾、有害垃圾及其他垃圾四大类别的识别分类,涵盖图像采集、数据预处理、模型推理与可视化界面等模块。压缩包共24个文件,约55.76MB,包含7个py源码文件、7个zip数据集与备份包、3个ui界面文件、3个zbak备份文件,以及cpp、jpg、md、png等辅助资料,代码采用模块化设计并配有详尽注释。资源内附环境配置、依赖库清单、模型训练与部署流程说明,便于快速复现与二次开发。目前已有57人学习,可为教学研究和技术开发提供完整参考。
1. 从一堆垃圾到一条数据:智能垃圾分类系统到底在做什么
小区楼下四个桶,厨余、可回收、有害、其他,督导员站在旁边盯着你手里的塑料袋。这个场景催生了一个很实际的需求:能不能让摄像头或一张照片自动判断垃圾类别,把结果推给投放口或者小程序?基于 Python 的智能垃圾分类系统,本质就是一套「图像输入 → 类别输出 → 部署上线」的完整链路,核心是图像分类模型,外围是数据采集、推理服务、前端交互和部署运维。它适合两类人:一类是想拿一个完整项目练手 Python 工程化的开发者,另一类是想在社区、园区、学校做小规模试点的实施者。源码和部署指南这两个词之所以被反复搜,是因为大多数人卡在「模型跑通了但服务起不来」这一步。下面按数据、模型、服务、部署、排错的顺序,把这条链路拆开讲清楚。
2. 数据从哪来:垃圾分类数据集的采集与清洗
2.1 公开数据集与自采数据的取舍
做垃圾分类,第一道坎不是模型,是数据。公开数据集里常见的是 TrashNet 这类,大约两千多张,分六类:纸板、玻璃、金属、纸、塑料、其他。它的优点是干净、标注规范,缺点是背景单一,全是白底摆拍,直接拿去识别真实场景里的垃圾袋会翻车。我的做法是公开数据集打底,再自采补充。自采渠道有三个:手机在小区投放点拍、从监控视频抽帧、网上爬取带标注的图片。自采数据一定要覆盖真实光照和遮挡,否则模型上线后准确率会掉得很难看。
数据量上,每类至少准备 500 张起步,四分类(厨余、可回收、有害、其他)建议每类 800 到 1500 张。类别不均衡是常态,厨余和其他往往最多,有害垃圾最少。处理不均衡有两个办法:一是对少数类做数据增强,二是训练时用类别权重。我一般两个一起上。
2.2 用 Python 做数据清洗和增强的实操
清洗的核心是去重、去模糊、修正错标。下面这段脚本做三件事:用感知哈希找重复图、用拉普拉斯方差筛模糊图、按类别统计数量。
import os import cv2 import imagehash from PIL import Image from collections import defaultdict DATA_DIR = "dataset" # 结构:dataset/类别名/图片 def find_duplicates(root, hash_size=8): """用感知哈希找出重复或高度相似的图片""" hashes = defaultdict(list) for cls in os.listdir(root): cls_dir = os.path.join(root, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): path = os.path.join(cls_dir, fname) try: h = imagehash.phash(Image.open(path), hash_size=hash_size) hashes[str(h)].append(path) except Exception as e: print(f"读取失败 {path}: {e}") # 同一哈希下多于一张,视为重复 return {k: v for k, v in hashes.items() if len(v) > 1} def find_blurry(root, threshold=100.0): """拉普拉斯方差低于阈值判为模糊""" blurry = [] for cls in os.listdir(root): cls_dir = os.path.join(root, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): path = os.path.join(cls_dir, fname) img = cv2.imread(path) if img is None: continue gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) score = cv2.Laplacian(gray, cv2.CV_64F).var() if score < threshold: blurry.append((path, round(score, 2))) return blurry def count_by_class(root): stat = {} for cls in os.listdir(root): cls_dir = os.path.join(root, cls) if os.path.isdir(cls_dir): stat[cls] = len(os.listdir(cls_dir)) return stat if __name__ == "__main__": print("重复图:", find_duplicates(DATA_DIR)) print("模糊图:", find_blurry(DATA_DIR)) print("类别分布:", count_by_class(DATA_DIR))逻辑说明:imagehash.phash把图片压成一个 64 位指纹,内容相近的图指纹接近,适合找翻拍和重复。cv2.Laplacian的方差反映图像边缘强度,方差越低越模糊,阈值 100 是经验值,手机拍摄的垃圾图可以放宽到 60。参数说明:hash_size越大越敏感,8 是通用值;threshold要根据你的拍摄设备调,先用一批人工确认的清晰图跑一遍,取最低分的 80% 作为阈值。
增强用torchvision.transforms或albumentations都行。垃圾分类场景我推荐albumentations,因为它对随机裁剪、旋转、亮度调整的组合更灵活。注意增强只加在训练集,验证集和测试集保持原样,否则评估结果会虚高。
提示:清洗完一定要人工抽检 50 张,机器筛出来的模糊图里往往混着正常图,直接删会丢样本。
3. 模型怎么选:从 MobileNet 到迁移学习的落地路径
3.1 为什么垃圾分类不适合从零训练
垃圾分类的类别少、类间差异大(玻璃瓶和纸板差别明显),但类内差异也大(不同形状的塑料瓶)。从零训练一个 CNN 需要几十万张图才能收敛,个人和小团队没有这个数据量。迁移学习是标准答案:拿 ImageNet 上预训练好的骨干网络,换掉最后的全连接层,用你的数据微调。骨干网络选型看部署环境:服务器端可以用 ResNet50 或 EfficientNet-B3,边缘设备或手机端用 MobileNetV3 或 ShuffleNetV2。我一般先用 MobileNetV3-Small 跑通全流程,准确率不够再换大的。
3.2 用 PyTorch 微调 MobileNetV3 的完整代码
下面这段是训练脚本的核心部分,包含数据加载、模型改造、训练循环和验证。
import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms, models DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") BATCH_SIZE = 32 EPOCHS = 20 LR = 1e-3 NUM_CLASSES = 4 # 厨余、可回收、有害、其他 # 训练集增强,验证集只做缩放和归一化 train_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) val_tf = 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("dataset/train", transform=train_tf) val_ds = datasets.ImageFolder("dataset/val", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=4) # 加载预训练模型,替换分类头 model = models.mobilenet_v3_small(weights=models.MobileNet_V3_Small_Weights.DEFAULT) model.classifier[3] = nn.Linear(model.classifier[3].in_features, NUM_CLASSES) model = model.to(DEVICE) # 类别不均衡时给少数类更高权重 class_weights = torch.tensor([1.0, 1.0, 2.0, 1.0]).to(DEVICE) criterion = nn.CrossEntropyLoss(weight=class_weights) optimizer = torch.optim.AdamW(model.parameters(), lr=LR, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS) for epoch in range(EPOCHS): model.train() running_loss = 0.0 for imgs, labels in train_loader: imgs, labels = imgs.to(DEVICE), labels.to(DEVICE) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() scheduler.step() # 验证 model.eval() correct, total = 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(DEVICE), labels.to(DEVICE) outputs = model(imgs) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) acc = correct / total print(f"Epoch {epoch+1}/{EPOCHS} loss={running_loss/len(train_loader):.4f} val_acc={acc:.4f}") torch.save(model.state_dict(), "garbage_mobilenetv3.pth")逻辑说明:models.mobilenet_v3_small的classifier是一个 Sequential,索引 3 是最后的 Linear 层,替换成你的类别数。class_weights里给有害垃圾(假设索引 2)加了 2 倍权重,缓解样本少的问题。CosineAnnealingLR让学习率按余弦曲线下降,比固定学习率更容易收敛到好的局部最优。参数说明:BATCH_SIZE在显存够的情况下越大越稳,8G 显存跑 224 分辨率可以到 64;LR用 1e-3 是微调的常用起点,如果 loss 震荡就降到 3e-4;EPOCHS看验证准确率,一般 15 到 30 轮就饱和。
训练完看混淆矩阵,如果厨余和其他经常混,说明这两类在视觉上确实接近,需要补充更多区分度高的样本,或者考虑加一个二级分类器。
4. 服务怎么起:用 FastAPI 把模型包成接口
4.1 推理服务的接口设计
模型训练完只是半成品,要能用起来必须包成 HTTP 接口。选 FastAPI 的理由是异步支持好、自带文档、部署简单。接口设计上,一个/predict接收图片返回类别和置信度,一个/health做健康检查。图片传输用 multipart 表单,返回 JSON。注意推理时要加torch.no_grad(),并且把模型设为 eval 模式,否则 BatchNorm 和 Dropout 会导致结果不稳定。
4.2 FastAPI 推理服务代码与启动命令
import io import torch import torch.nn as nn from fastapi import FastAPI, File, UploadFile from PIL import Image from torchvision import transforms, models app = FastAPI(title="垃圾分类推理服务") DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") CLASSES = ["厨余垃圾", "可回收物", "有害垃圾", "其他垃圾"] # 与训练时保持一致的预处理 tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) def load_model(): model = models.mobilenet_v3_small(weights=None) model.classifier[3] = nn.Linear(model.classifier[3].in_features, len(CLASSES)) model.load_state_dict(torch.load("garbage_mobilenetv3.pth", map_location=DEVICE)) model.to(DEVICE) model.eval() return model model = load_model() @app.get("/health") def health(): return {"status": "ok"} @app.post("/predict") async def predict(file: UploadFile = File(...)): raw = await file.read() img = Image.open(io.BytesIO(raw)).convert("RGB") tensor = tf(img).unsqueeze(0).to(DEVICE) with torch.no_grad(): logits = model(tensor) probs = torch.softmax(logits, dim=1)[0] idx = int(probs.argmax()) return { "class": CLASSES[idx], "confidence": round(float(probs[idx]), 4), "all_probs": {CLASSES[i]: round(float(probs[i]), 4) for i in range(len(CLASSES))} }逻辑说明:load_model里weights=None是因为我们要加载自己训练好的权重,不需要再下载预训练参数。map_location=DEVICE保证在 CPU 和 GPU 之间切换时不会报错。unsqueeze(0)把单张图变成 batch 维度为 1 的张量。返回all_probs是为了前端能做置信度展示,也方便排查误判。
启动命令:
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2参数说明:--workers 2表示起两个进程,适合多核 CPU;如果模型在 GPU 上,多 worker 会各自加载一份模型,显存要够。生产环境建议用gunicorn配合uvicorn.workers.UvicornWorker,并加--timeout 60防止大图推理超时。
注意:预处理必须和训练时完全一致,包括归一化的均值和方差。我见过有人推理时忘了 Normalize,准确率直接掉 30 个点,排查了半天。
5. 部署怎么落地:Docker 打包与边缘设备适配
5.1 用 Docker 把服务打包成镜像
部署最省心的方式是 Docker。基础镜像选python:3.10-slim,把依赖、模型文件、代码一起打进去。注意 PyTorch 的 CPU 版本和 GPU 版本镜像不同,边缘设备用 CPU 版,服务器用 GPU 版。下面是一个可用的 Dockerfile。
FROM python:3.10-slim WORKDIR /app # 先装系统依赖,opencv 需要 libgl RUN apt-get update && apt-get install -y --no-install-recommends \ libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY main.py garbage_mobilenetv3.pth ./ EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]requirements.txt里写:
fastapi==0.110.0 uvicorn[standard]==0.29.0 torch==2.2.0 torchvision==0.17.0 pillow==10.2.0 python-multipart==0.0.9构建和运行:
docker build -t garbage-cls:1.0 . docker run -d --name garbage -p 8000:8000 garbage-cls:1.0参数说明:--no-install-recommends减少镜像体积;python-multipart是 FastAPI 接收文件上传的必需依赖,漏了会报 422。镜像大小 CPU 版大约 1.5G,GPU 版 5G 以上,边缘设备建议用 ONNX Runtime 替代 PyTorch,能把体积压到 200M 以内。
5.2 边缘设备上的模型转换与性能取舍
如果部署在树莓派、Jetson 或安卓设备上,PyTorch 太重,转 ONNX 或 NCNN 是常见做法。转 ONNX 的代码:
import torch import torch.nn as nn from torchvision import models model = models.mobilenet_v3_small(weights=None) model.classifier[3] = nn.Linear(model.classifier[3].in_features, 4) model.load_state_dict(torch.load("garbage_mobilenetv3.pth", map_location="cpu")) model.eval() dummy = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy, "garbage.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, opset_version=12 )逻辑说明:dynamic_axes让 batch 维度可变,方便批量推理。opset_version=12兼容性较好,Jetson 上的 TensorRT 支持这个版本。转完后用onnxruntime验证输出和 PyTorch 一致,误差在 1e-4 以内算正常。
性能取舍上,MobileNetV3-Small 在树莓派 4B 上单张推理约 80 到 120 毫秒,够用;如果要更快,可以量化成 INT8,速度提升 2 到 3 倍,准确率掉 1 到 2 个点。量化用onnxruntime.quantization的quantize_dynamic就行。
6. 避坑与排查:垃圾分类系统上线前后的 5 个血泪教训
现象一:本地准确率 95%,部署后用户反馈一半识别错。原因:训练数据是白底摆拍,真实场景是复杂背景和遮挡,域偏移导致模型失效。 解决:自采至少 30% 的真实场景图加入训练集,推理前加一个简单的目标检测或裁剪,把垃圾主体从背景里抠出来再分类。
现象二:服务跑一段时间后内存持续上涨,最后 OOM。原因:FastAPI 默认不限制上传大小,有人传了 10M 的大图,PIL 解码后占大量内存;另外模型没有做单例,每次请求都重新加载。 解决:在load_model外用模块级变量保证只加载一次;加UploadFile大小校验,超过 5M 直接返回 413;用--limit-max-requests让 worker 定期重启。
现象三:GPU 显存够但推理报 CUDA out of memory。原因:多个 uvicorn worker 各自加载一份模型,显存翻倍。 解决:GPU 部署时--workers 1,用批处理提高吞吐;或者用torch.cuda.empty_cache()在请求间隙释放缓存,但更推荐单 worker 加异步队列。
现象四:同一张图两次请求返回不同类别。原因:模型忘了eval(),Dropout 和 BatchNorm 在训练模式下随机性导致输出波动。 解决:加载模型后立刻model.eval(),推理包在torch.no_grad()里。这个坑很隐蔽,本地测试时如果只跑一次不会发现。
现象五:Docker 镜像构建成功但容器启动报找不到模型文件。原因:.dockerignore里把.pth排除了,或者COPY路径写错。 解决:检查.dockerignore,模型文件单独COPY并确认路径;用docker run --rm -it garbage-cls:1.0 ls /app进容器看文件在不在。
提示:上线前一定要做一轮「脏数据测试」,故意传纯色图、文字截图、超大图,看服务会不会崩。这些边界情况在真实使用中一定会遇到。
7. 把准确率再往上推:置信度阈值与二级分类的实战技巧
模型上线后,你会发现大部分误判集中在置信度 0.5 到 0.7 这个区间。我的做法是设一个阈值,低于 0.7 的结果不直接返回,而是走「人工确认」或「二级分类」。二级分类针对容易混的类别对,比如厨余和其他,单独训一个小模型,只在这两类之间做区分。这个小模型可以用更细粒度的特征,比如颜色直方图和纹理,甚至不需要深度学习,用 SVM 加 HOG 特征就能到 85% 以上。
另一个技巧是测试时增强(TTA)。对同一张图做水平翻转和轻微裁剪,各推理一次,把 softmax 概率平均。这个操作能让准确率涨 1 到 2 个点,代价是推理时间翻倍。边缘设备上不划算,服务器端可以开。
def predict_with_tta(model, img_tensor, n_aug=3): """TTA:原图 + 水平翻转 + 轻微裁剪,概率平均""" model.eval() probs = torch.zeros(1, len(CLASSES)).to(DEVICE) with torch.no_grad(): probs += torch.softmax(model(img_tensor), dim=1) probs += torch.softmax(model(torch.flip(img_tensor, dims=[3])), dim=1) # 中心裁剪再缩放 crop = transforms.CenterCrop(200)(img_tensor) crop = transforms.Resize((224, 224))(crop) probs += torch.softmax(model(crop), dim=1) return (probs / n_aug).argmax(dim=1)参数说明:n_aug是增强次数,3 次是精度和耗时的平衡点;CenterCrop(200)的 200 要根据你的主体占比调,主体小就裁大一点。TTA 对旋转不变的类别(比如玻璃瓶)提升明显,对方向敏感的类别要慎用。
最后说一个我自己的习惯:每次改完模型或预处理,一定用同一批 100 张固定测试图跑一遍,记录准确率和混淆矩阵,和上一版对比。没有这个基线,你根本不知道改动是变好还是变坏。垃圾分类这个方向,数据质量比模型结构重要得多,把清洗和自采做扎实,比换十个骨干网络都管用。希望帮到你。
本文还有配套的精品资源,点击获取