☰
智能垃圾分类系统实战:MobileNetV3端侧识别与树莓派部署
2026/9/26 16:42:04 网站建设 项目流程

简介:这是一份面向计算机、人工智能方向学生与开发者的智能垃圾分类系统项目资料,可作为毕业设计或课程作业的完整参考。项目围绕计算机视觉、机器学习与大数据分析展开,涵盖图像预处理、特征提取、CNN分类模型训练、数据集构建与标注、超参数调优及模型融合等关键环节,并延伸至垃圾分类大数据平台的趋势预测与资源分配思路。压缩包共20个文件,约35.12MB,以7个Python源码为主,另含6个数据集压缩包、3个UI界面文件、1个C++测试文件及图片、说明文档等,目录结构清晰,便于按模块查阅。目前已有121人学习。读者可从中获取系统设计文档、算法实现细节、训练与测试数据、界面代码及测试报告等资料,适合通过复现项目巩固计算机视觉与深度学习知识,提升编程与工程实践能力。

1. 智能垃圾分类系统:从一份课程作业到能跑通的识别流水线

很多同学拿到“智能垃圾分类系统”这个题目时,第一反应是找个开源模型套个界面就交差,结果答辩时被问“为什么不用轻量网络”“摄像头帧率掉到 5 帧怎么办”就卡住了。这个标题背后其实是一条完整的端侧识别链路:图像采集、预处理、分类模型推理、结果映射到四类垃圾桶、再通过界面或串口输出。它适合课程作业和毕设场景,因为硬件门槛低——一台带摄像头的笔记本或一块树莓派就能跑通全流程,同时又能延伸出模型量化、数据增强、误判分析等加分项。我见过太多人把精力花在 UI 动画上,却让核心分类器在真实光照下翻车,这篇就把我踩过的坑和能直接抄的代码一次讲清。

2. 先定技术路线:为什么选 MobileNetV3 而不是 ResNet50

2.1 课程作业的算力边界决定模型选型

大部分毕设环境是笔记本 CPU 或树莓派 4B,没有独立显卡。ResNet50 参数量 25M 左右,单张推理在树莓派上要 1.2 秒以上,加上摄像头读取和界面刷新,帧率直接掉到个位数。MobileNetV3-Small 参数量约 2.5M,输入 224×224 时树莓派 4B 单张推理能压到 80 毫秒以内,配合多线程可以做到 8~10 帧的准实时效果。这不是说 ResNet 不好,而是课程作业的验收场景通常只看演示流畅度和分类准确率,没必要为了“看起来高级”牺牲可运行性。

选型时还要看数据集规模。垃圾分类公开数据集常见的有 TrashNet(约 2500 张)和华为云垃圾分类数据集(约 1.5 万张)。TrashNet 类别少、背景干净,MobileNetV3 微调后验证集准确率能到 92% 以上;如果自己拍的数据背景杂乱,建议先用 MobileNetV3-Large 试,准确率不够再考虑换 backbone,而不是一上来就堆大模型。

2.2 四分类映射与数据集目录规范

垃圾分类系统最终要输出“可回收物、厨余垃圾、有害垃圾、其他垃圾”四类。但公开数据集往往按纸板、玻璃、金属、塑料等细类标注,需要做一层映射。我一般会在项目根目录建dataset/文件夹,按最终四类建子目录,再用脚本把细类图片复制或软链接过去。这样训练时ImageFolder直接读,不用改 DataLoader。

import os import shutil # 细类到四分类的映射表,按自己数据集调整 CATEGORY_MAP = { "cardboard": "recyclable", "glass": "recyclable", "metal": "recyclable", "paper": "recyclable", "plastic": "recyclable", "trash": "other", "battery": "hazardous", "food": "kitchen", } SRC_ROOT = "raw_dataset" DST_ROOT = "dataset" for split in ["train", "val"]: src_split = os.path.join(SRC_ROOT, split) for fine_cls in os.listdir(src_split): fine_path = os.path.join(src_split, fine_cls) if not os.path.isdir(fine_path): continue coarse_cls = CATEGORY_MAP.get(fine_cls.lower()) if coarse_cls is None: print(f"跳过未映射类别: {fine_cls}") continue dst_dir = os.path.join(DST_ROOT, split, coarse_cls) os.makedirs(dst_dir, exist_ok=True) for img_name in os.listdir(fine_path): shutil.copy( os.path.join(fine_path, img_name), os.path.join(dst_dir, img_name) ) print("映射完成,检查 dataset/train 下四个子目录是否都有图片")

这段脚本的关键是CATEGORY_MAP,你需要根据自己下载的数据集类别名逐条核对。常见坑是类别名大小写不一致,比如Plastic和plastic同时出现,所以统一用.lower()处理。复制完成后务必人工抽查每个子目录,避免某个类别只有几张图导致训练时类别不平衡。

2.3 训练脚本与三个必调参数

训练用 PyTorch 的 torchvision 即可,不需要额外框架。下面是最小可运行训练脚本,重点看BATCH_SIZE、LR、EPOCHS三个参数。

import torch import torch.nn as nn from torchvision import datasets, transforms, models from torch.utils.data import DataLoader # 数据增强:训练集用随机翻转和裁剪,验证集只做缩放 train_tf = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), transforms.RandomHorizontalFlip(), 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(256), transforms.CenterCrop(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) # BATCH_SIZE:树莓派训练建议 16,笔记本 GPU 可以 32 train_loader = DataLoader(train_ds, batch_size=16, shuffle=True, num_workers=2) val_loader = DataLoader(val_ds, batch_size=16, shuffle=False, num_workers=2) # 加载预训练 MobileNetV3-Small,替换最后一层为四分类 model = models.mobilenet_v3_small(weights=models.MobileNet_V3_Small_Weights.DEFAULT) model.classifier[3] = nn.Linear(model.classifier[3].in_features, 4) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) # LR:微调场景 1e-3 起步,太大容易震荡,太小收敛慢 criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) # EPOCHS:TrashNet 级别 15~20 轮足够,自建大数据集可到 30 for epoch in range(20): model.train() for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() loss = criterion(model(imgs), labels) loss.backward() optimizer.step() # 每轮验证,记录准确率 model.eval() correct = total = 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) pred = model(imgs).argmax(1) correct += (pred == labels).sum().item() total += labels.size(0) print(f"Epoch {epoch+1}, val_acc={correct/total:.4f}") torch.save(model.state_dict(), "garbage_mobilenetv3.pth")

BATCH_SIZE设 16 是因为树莓派内存有限,太大容易 OOM;笔记本有 GPU 可以翻倍。LR用 1e-3 是微调的常见起点,如果 loss 在前几轮就飙到 NaN,降到 1e-4。EPOCHS不是越多越好,验证准确率连续 3 轮不升就可以停,否则过拟合。训练完保存的.pth文件后面部署直接加载。

3. 推理部署:把模型塞进摄像头循环里

3.1 OpenCV 读取摄像头与预处理对齐

训练时的预处理必须和推理时完全一致,否则准确率会莫名其妙掉十几个点。常见错误是训练用了RandomResizedCrop,推理时直接Resize(224),尺度不一致导致模型“不认识”输入。推理阶段应该用和验证集一样的Resize(256) + CenterCrop(224)。

import cv2 import torch from torchvision import transforms, models import torch.nn as nn # 推理预处理,必须和训练时验证集一致 infer_tf = transforms.Compose([ transforms.ToPILImage(), transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) CLASS_NAMES = ["recyclable", "kitchen", "hazardous", "other"] 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() cap = cv2.VideoCapture(0) # 0 表示默认摄像头 if not cap.isOpened(): raise RuntimeError("摄像头打开失败,检查是否被其他程序占用") while True: ret, frame = cap.read() if not ret: break # BGR 转 RGB,因为训练用的是 RGB rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) tensor = infer_tf(rgb).unsqueeze(0) with torch.no_grad(): logits = model(tensor) prob = torch.softmax(logits, dim=1) conf, pred = prob.max(1) label = CLASS_NAMES[pred.item()] text = f"{label} {conf.item():.2f}" cv2.putText(frame, text, (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow("Garbage Classification", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()

这段代码里cv2.cvtColor那一步最容易被忽略。OpenCV 默认 BGR,而 torchvision 预训练模型期望 RGB,不转的话颜色通道错位,准确率直接崩。另外map_location="cpu"是为了在没有 GPU 的机器上也能加载。如果要做串口输出到 Arduino 控制舵机开盖,把label通过serial.write发出去即可,波特率一般 9600 或 115200。

3.2 帧率优化:多线程与跳帧策略

单线程读摄像头加推理,在树莓派上通常只有 3~5 帧,演示时肉眼可见卡顿。我一般用两个办法:一是把推理放到单独线程,主线程只负责显示;二是跳帧,每 3 帧做一次推理,中间帧沿用上次结果。这样显示帧率能回到 15 帧以上,观感流畅很多。

import threading import queue # 推理线程:从队列取帧,结果放回另一个队列 def infer_worker(in_q, out_q, model, tf): while True: frame = in_q.get() if frame is None: break rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) tensor = tf(rgb).unsqueeze(0) with torch.no_grad(): prob = torch.softmax(model(tensor), dim=1) conf, pred = prob.max(1) out_q.put((pred.item(), conf.item())) in_q = queue.Queue(maxsize=2) out_q = queue.Queue(maxsize=2) worker = threading.Thread(target=infer_worker, args=(in_q, out_q, model, infer_tf), daemon=True) worker.start() frame_count = 0 last_label, last_conf = 0, 0.0 while True: ret, frame = cap.read() if not ret: break frame_count += 1 # 每 3 帧送一次推理,队列满就跳过,避免堆积 if frame_count % 3 == 0 and not in_q.full(): in_q.put(frame.copy()) if not out_q.empty(): last_label, last_conf = out_q.get() text = f"{CLASS_NAMES[last_label]} {last_conf:.2f}" cv2.putText(frame, text, (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow("Garbage Classification", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break

队列设maxsize=2是关键,防止推理慢时帧堆积导致延迟越来越大。跳帧策略下,如果物体移动快,标签会滞后,所以演示时建议把物体在摄像头前停 1 秒再移开。

3.3 模型量化:让树莓派推理再快一倍

如果树莓派上还是觉得慢,可以做动态量化,把权重从 float32 转成 int8,模型体积缩小约 4 倍,推理速度提升 30%~50%,准确率通常只掉 1~2 个点。

import torch.quantization # 动态量化,只需在保存的模型上操作 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() quantized_model = torch.quantization.quantize_dynamic( model, {nn.Linear}, dtype=torch.qint8 ) torch.save(quantized_model.state_dict(), "garbage_quantized.pth") print("量化完成,加载时同样用 quantize_dynamic 包装后再 load_state_dict")

注意量化后的模型加载时也要先做同样的quantize_dynamic包装,不能直接load_state_dict到原始模型。另外卷积层动态量化支持有限,主要加速来自全连接层,MobileNetV3 的全连接层占比不大,所以提升没有想象中夸张,但体积缩小对存储紧张的树莓派很实用。

4. 避坑与排查:那些答辩前夜才发现的翻车点

4.1 现象:训练准确率 99%,摄像头前完全乱判

原因通常是训练集和推理输入分布不一致。检查三处:训练是否用了ColorJitter而推理没有对应归一化;摄像头是否自动白平衡导致色温偏移;数据集里同一类图片背景过于单一,模型学到了背景而不是物体。解决办法是采集一批摄像头实拍图加入训练集,或者在推理时固定摄像头曝光和白平衡。

4.2 现象:树莓派上运行报 “Illegal instruction”

原因多是 PyTorch 版本与树莓派 CPU 指令集不匹配,常见于在 x86 上 pip 下载的 wheel 直接拷到 ARM 板子。解决是在树莓派上重新安装对应架构的 PyTorch,或者用 ONNX Runtime 替代,先导出 ONNX 再在树莓派上推理。

4.3 现象:摄像头打开成功但画面全黑

原因可能是摄像头排线接触不良,或者cv2.VideoCapture(0)的索引不对。树莓派上有时 CSI 摄像头对应索引是 0,USB 摄像头是 1 或 2。解决是遍历索引 0~3 试,或者用v4l2-ctl --list-devices查看设备节点。

4.4 现象:验证集准确率震荡,一会 80% 一会 95%

原因通常是BATCH_SIZE太小导致 BatchNorm 统计量不稳定,或者学习率太大。解决是把BATCH_SIZE提到 32(内存允许的话),学习率降到 1e-4,并加torch.optim.lr_scheduler.StepLR每 5 轮衰减一次。

4.5 现象:量化后模型加载报错 “Missing key(s)”

原因是保存的是量化模型的state_dict,但加载时没有先构建量化结构。解决是加载前先执行quantize_dynamic得到空壳,再load_state_dict。如果还是报错,直接保存整个模型torch.save(quantized_model, ...)而不是只存 state_dict。

5. 进阶技巧:用混淆矩阵定位“最容易被误判的那一类”

课程作业答辩时,老师最爱问“哪两类最容易混”。与其临场编,不如训练完直接画混淆矩阵。下面这段代码在验证集上跑一遍,输出 4×4 矩阵,能清楚看到比如“厨余”被大量判成“其他”。

import numpy as np from sklearn.metrics import confusion_matrix, classification_report model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for imgs, labels in val_loader: preds = model(imgs).argmax(1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm = confusion_matrix(all_labels, all_preds) print("混淆矩阵(行=真实,列=预测):") print(cm) print(classification_report(all_labels, all_preds, target_names=CLASS_NAMES))

拿到矩阵后,如果发现“厨余→其他”误判多,就专门补这类难样本:去食堂拍一批剩饭剩菜,或者用数据增强模拟汤汁反光。我一般会针对混淆最严重的两类做 200~300 张补充采集,再微调 5 轮,准确率通常能拉回 3~5 个点。另一个技巧是把 softmax 置信度低于 0.6 的样本单独存下来,人工复核后加入训练集,这比盲目加数据有效得多。

最后说个血泪经验:答辩前一定要用真实摄像头在答辩现场的光照下跑一遍,别只在实验室白炽灯下测。我见过太多模型在实验室 95%,到教室投影灯下直接掉到 60%。提前准备一个“演示模式”,把置信度阈值调低,宁可多显示“其他”也别频繁跳错类。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询