树莓派垃圾分类识别全流程:从模型训练到TFLite端侧部署实战

树莓派垃圾分类识别全流程:从模型训练到TFLite端侧部署实战 简介一套基于树莓派的垃圾分类识别项目方案覆盖硬件选型、模型训练、预测与服务部署全流程适合物联网课程期末作业、K12创客教学或智能硬件入门者解决从图像识别到机械控制联动的实际问题。压缩包共131个文件112.48MB以Python源码、训练好的模型文件和前端交互代码为主体包括py、h5、vue、js、json及md等多种类型结构清晰便于二次开发。已有245人学习适合需要快速复现智能识别场景的开发者参考。资源内附硬件材料清单、训练与命令启动方式、服务部署说明并配套视频教程train.py与predict_local.py可直接运行输出h5模型和tflite文件方便部署还预留ResNet50模型接入方式有助于深入理解卷积神经网络在嵌入式环境的应用。1. 期末作业选「树莓派垃圾分类识别」的人一半会卡在跑通这一步这门期末作业最难的从来不是写代码而是让模型真的在树莓派上转起来。从网上下载现成源码或者训练好的模型摄像头一开十有八九是黑屏、报错、推理卡成幻灯片。原因不复杂这个题目看起来像“一个视觉项目”实际是一条「摄像头采集 → 本地推理 → 结果上报」的完整端侧链路任何一环没对齐整个作业就塌了。硬件选型、系统配置、模型格式、推理框架、MQTT 上报每一步都有它自己的坑。这篇文章按“硬件准备 → 模型训练 → 树莓派端部署 → 工程化交付”的顺序走一遍适合还没定方案的也适合手里已有源码但跑不通、想补齐原理再动手的读者——攻项目、代做、毕业设计都适用。2. 树莓派硬件选型与系统准备先让摄像头出图再谈识别2.1 树莓派 4B 还是 Zero 2W决定后面所有工作量的选择垃圾分类识别本质是端侧轻量推理后端几乎没有渲染需求所以选型核心看两件事内存够不够放下 TFLite 解释器和 OpenCV以及 CSI 摄像头能不能稳定出图。常见选型是树莓派 4B2GB/4GB预算够就上 4GB。没有必要上树莓派 5这类作业的推理负载 4B 的 CPU 已经足够树莓派 5 的散热和高功耗在便携展示场景里反而是负担。型号CPU内存CSI 摄像头做这个作业的实际体验Raspberry Pi 4B 2GB/4GB四核 Cortex-A72 1.5GHz2/4GB LPDDR4原生 CSI 排线推荐。TFLite 推理单帧 0.3~0.5 秒余量充足Raspberry Pi Zero 2W四核 Cortex-A53 1GHz512MB LPDDR2Mini CSI 排线勉強能跑内存吃紧推理慢到 1 秒级别Raspberry Pi 3B四核 Cortex-A53 1.4GHz1GB LPDDR2原生 CSI 排线能跑但发热明显需要主动散热选 4B 的另一个理由是一体化演示方便HDMI 接屏幕、GPIO 接舵机、CSI 接摄像头一个板子组成完整展台。树莓派 Zero 2W 更适合嵌入式形态但课程验收往往要求现场演示交互4B 的接口丰富度会省掉很多转接麻烦。OV5647 和 IMX219 这两种常见摄像头传感器模块在官方 Bookworm 系统里都走 libcamera 驱动接线和配置不区分型号。2.2 烧录系统、换软件源、建虚拟环境十分钟进入可操作状态系统镜像选择 Raspberry Pi OSBookworm 64-bit的 Lite 版本。不要装 Desktop 版桌面环境会吃掉大量内存识别任务只需要命令行和摄像头驱动。烧录工具用官方 Raspberry Pi Imager写入后在 boot 分区放一个名为ssh的空文件即可开启 SSH方便后续远程调试。装完系统第一件事是固定时间树莓派没有 RTC联网后 NTP 会自动校准但如果验收现场没网就先把时间同步好。接着把 apt 源和 pip 源换成清华镜像否则一部份包下载会等到怀疑人生。Bookworm 之后的 apt 源文件改成 deb822 格式直接改/etc/apt/sources.list已经失效正确做法是改/etc/apt/sources.list.d/下的两个 sources 文件sudo sed -i s|http://deb.debian.org/debian|https://mirrors.tuna.tsinghua.edu.cn/debian|g /etc/apt/sources.list.d/debian.sources sudo sed -i s|http://archive.raspberrypi.com/debian|https://mirrors.tuna.tsinghua.edu.cn/raspberrypi|g /etc/apt/sources.list.d/raspi.sources sudo apt update sudo apt upgrade -y这段命令把 Debian 官方源和树莓派官方源同时替换为清华镜像。注意 Bookworm 的 apt 源文件后缀是.sources不是.list用旧的改法经常会漏掉 raspi 源导致摄像头固件和libcamera相关包升级失败。Python 环境用 venv 隔离严禁直接在系统环境里pip install。Bookworm 的 pip 装了externally-managed-environment保护裸装会直接报错。以下是完整的环境初始化流程sudo apt install -y python3-venv python3-pip git git config --global http.postBuffer 524288000 python3 -m venv ~/garbage-sorter/.venv source ~/garbage-sorter/.venv/bin/activate pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple pip install numpy opencv-python tflite-runtime paho-mqtt RPi.GPIO这里用tflite-runtime而不是完整的tensorflow包。TensorFlow 完整包在树莓派上体积大、依赖多且树莓派 4B 并不适合跑完整 TensorFlow 的模型训练或复杂图计算tflite-runtime只负责推理体积和内存占用小一个量级。RPi.GPIO用于后续舵机控制不做自动分类桶的话可以先不装。OpenCV 这里用的是opencv-python精简版覆盖imread、resize、cvtColor这些常用 API 完全够用。2.3 摄像头接线与验证先拍一张照片再谈识别摄像头接 CSI 排线是新手翻车重灾区。插法主板上的 CSI 接口有金属卡扣掀起卡扣排线金属触点朝向 PCB 板面插入压下卡扣锁住。树莓派 4B 的 CSI 接口朝 HDMI 方向金属触点朝下Zero 系列方向相反。如果插反启动日志会报Unsupported device或摄像头无法注册两根方向都试一次不会烧硬件。Bookworm 系统已经移除了raspistill摄像头验证统一用 libcamera 生态的命令行工具libcamera-hello -t 3 # 预览 3 秒 libcamera-jpeg -o test.jpg # 抓一帧 jpg libcamera-still -o full.jpg --width 1280 --height 960如果libcamera-hello能正常出画面说明驱动链路正常可以继续后续步骤。常见报错是Failed to enumerate camera排查顺序排线插紧没、板子是否过热降频、raspi-config里 camera 是否 enabled。Bookworm 默认 CSI 摄像头是自动启用的不需要再在raspi-config里手动开。3. 数据集准备与垃圾分类模型训练用自己的模型替换网上现成的3.1 数据集选择与目录结构Keras 直接读文件夹的坑垃圾分类公开数据集有国外社区整理的 Garbage Classification12 类以及国内几所高校开源的垃圾分类数据常见 40 类 / 6 类两种子集。选型标准类别数和你最终的硬件推理要匹配论文或演示里只展示 6 到 8 类就够因为类别越多树莓派端误判率越高答辩时现场翻车概率也越大。我一般用 6 类常见垃圾cardboard纸板、glass玻璃、metal金属、paper纸张、plastic塑料、trash其他垃圾。标注好的数据按 Keras 的image_dataset_from_directory目录约定组织data/train/ cardboard/ # 每类放 300~500 张图 glass/ metal/ paper/ plastic/ trash/ data/validation/ cardboard/ ...这个结构决定了label_modecategorical时 Keras 会把子文件夹名映射为类别索引类别名的字母顺序就是输出层的索引顺序后续推理端解析标签时必须保持一致。很多复现项目跑完训练、导出模型却在树莓派端输出张冠李戴就是因为训练时文件夹顺序和推理时labels.txt的写入顺序不一致。如果导入的数据是公开数据集自带的原始结构往往是单目录、文件名带前缀先写一段脚本重排目录import os, shutil from glob import glob src_dir raw/ for f in glob(src_dir *.jpg): fname os.path.basename(f) label fname.split(_)[0] # 假设文件名格式为 metal_001.jpg dst_dir fdata/train/{label} os.makedirs(dst_dir, exist_okTrue) shutil.copy(f, dst_dir / fname)3.2 用迁移学习训练MobileNetV2 的冻结与解冻节奏树莓派端适合跑轻量卷积网络MobileNetV2 是最稳妥的选择。它在 ImageNet 上预训练过的特征图对垃圾纹理、形状有足够强的泛化能力只需要替换分类头。以下是常见训练脚本结构运行环境建议在本地 GPU 或 Colab 这类云环境上完成树莓派 CPU 训练太慢没有任何意义import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers BATCH_SIZE 64 IMG_SIZE (224, 224) NUM_CLASSES 6 EPOCHS_FROZEN 10 EPOCHS_FINETUNE 5 train_ds keras.preprocessing.image_dataset_from_directory( data/train, label_modecategorical, image_sizeIMG_SIZE, batch_sizeBATCH_SIZE, shuffleTrue, ) val_ds keras.preprocessing.image_dataset_from_directory( data/validation, label_modecategorical, image_sizeIMG_SIZE, batch_sizeBATCH_SIZE, ) # 数据增强垃圾分类对旋转和亮度变化敏感度低但要防止过拟合 data_aug keras.Sequential([ layers.RandomFlip(horizontal), layers.RandomRotation(0.1), layers.RandomZoom(0.1), ]) base_model tf.keras.applications.MobileNetV2( input_shape(224, 224, 3), include_topFalse, weightsimagenet, ) base_model.trainable False inputs keras.Input(shape(224, 224, 3)) x data_aug(inputs) x tf.keras.applications.mobilenet_v2.preprocess_input(x) x base_model(x) x layers.GlobalAveragePooling2D()(x) x layers.Dense(128, activationrelu)(x) # 中间层宽度不要太大防过拟合 x layers.Dropout(0.3)(x) outputs layers.Dense(NUM_CLASSES, activationsoftmax)(x) model keras.Model(inputs, outputs) model.compile( optimizerkeras.optimizers.Adam(learning_rate1e-3), losscategorical_crossentropy, metrics[accuracy], ) model.fit(train_ds, validation_dataval_ds, epochsEPOCHS_FROZEN) # 解冻最后 20 层微调学习率调低一个量级 base_model.trainable True for layer in base_model.layers[:-20]: layer.trainable False model.compile( optimizerkeras.optimizers.Adam(learning_rate1e-5), losscategorical_crossentropy, metrics[accuracy], ) model.fit(train_ds, validation_dataval_ds, epochsEPOCHS_FINETUNE) model.save(model/garbage_mobilenetv2.h5)冻结阶段用 1e-3 的学习率训练 10 epoch让随机初始化的分类头快速收敛解冻后只训练最后 20 层学习率降到 1e-5。如果一开始就解冻整个 backbone预训练特征会被破坏树莓派端表现会明显恶化而训练日志里的终精度反而很好看——这是典型的“训练指标和端侧表现不一致”案例。Dropout 0.3 用于降低过拟合公开数据集规模不大不加增强的话验证集准确率可能虚高现场演示反而掉链子。3.3 导出 TFLite 模型float16 量化比 int8 更适合树莓派训练产物是 H5 格式树莓派端真要跑得动必须转成 TFLite。注意力不要放在model.save上而是要转出来的.tflite文件这才是交付物里那个“训练好的模型”。import tensorflow as tf model tf.keras.models.load_model(model/garbage_mobilenetv2.h5) converter tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations [tf.lite.Optimize.DEFAULT] converter.target_spec.supported_types [tf.float16] tflite_model converter.convert() with open(model/garbage_mobilenetv2_float16.tflite, wb) as f: f.write(tflite_model)float16 量化是课程作业场景下最稳妥的折中模型体积约是原来的 1/2推理速度提升 30%~50%精度基本无感下降。int8 全整数量化需要额外提供校准数据集且对训练时的算子支持有要求实现复杂只在内存极紧张的部署场景才值得做。转完后看两个指标模型文件大小和推理结果不要只看能加载就完事。3.4 训练指标与端侧表现的落差原因和处理方式训练好的模型准确率很高不代表树莓派端表现一样好。常见落差来源有两个。一是图片尺寸训练时用的是 224×224端侧摄像头拍出来的原始分辨率往往是 1280×960 以上直接缩放后再输入模型长宽比改变会导致被摄物体变形影响识别精度。处理方式是端侧resize时保持比例将边缘做 paddingdef resize_keep_ratio(image, target224): h, w image.shape[:2] scale target / max(h, w) new_w, new_h int(w * scale), int(h * scale) resized cv2.resize(image, (new_w, new_h)) canvas np.zeros((target, target, 3), dtypenp.uint8) x_off (target - new_w) // 2 y_off (target - new_h) // 2 canvas[y_off:y_off new_h, x_off:x_off new_w] resized return canvas二是光照分布的迁移。训练集大多是网上扒的清晰样本端侧现场光线复杂强烈建议训练时加入亮度扰动比如layers.RandomBrightness(0.2)让模型学会泛化。答辩时老师最经常做的事是拿手机手电筒照一下识别物看模型会不会罢工这个扰动能在多数情况下救命。4. 树莓派端推理从图像帧到分类结果上报4.1 加载本地 TFLite 模型的最小推理代码树莓派端安装的是tflite-runtimeAPI 和 TensorFlow 里的 LITE 解释器几乎一致但注意不要混着用tflite_runtime.interpreter和tensorflow.lite.python.interpreter是两个模块。项目里通过本地路径直接加载模型文件无需任何在线服务import cv2 import numpy as np import tflite_runtime.interpreter as tflite CLASS_NAMES [cardboard, glass, metal, paper, plastic, trash] MODEL_PATH model/garbage_mobilenetv2_float16.tflite interpreter tflite.Interpreter(model_pathMODEL_PATH) interpreter.allocate_tensors() input_detail interpreter.get_input_details()[0] output_detail interpreter.get_output_details()[0] print(input shape:, input_detail[shape], input_detail[dtype]) print(output shape:, output_detail[shape], output_detail[dtype]) def classify_frame(frame_bgr): rgb cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) resized resize_keep_ratio(rgb, 224) # 用上一节的等比例缩放 input_data np.expand_dims(resized.astype(np.float32) / 255.0, axis0) interpreter.set_tensor(input_detail[index], input_data) interpreter.invoke() probs interpreter.get_tensor(output_detail[index])[0] idx int(np.argmax(probs)) return CLASS_NAMES[idx], float(probs[idx])get_input_details()[0][index]拿到的不是固定 0 或 1解释器内部张量索引可能和模型输入顺序不同必须通过 runtime 查询拿到。另一个关键点是输入数据的归一化方式MobileNetV2 的 Keras 版本自带preprocess_input会把像素值映射到[-1, 1]但这里手动除以 255 是[0, 1]范围两者不能混用。如果你在树莓派端发现输出概率非常接近均匀分布、完全无判别力优先检查归一化范围。4.2 摄像头实时抓帧640 真香4K 纯属自找麻烦树莓派作文业演示核心诉求是“能看清 不卡顿”。实测 4B 跑 MobileNetV2 float16 TFLite单帧推理约 0.3~0.5 秒如果抓 3280×2464 原始图再缩放内存和预处理时间都会拖后腿。我一般直接用 picamera2 抓 640×480比 224×224 大一点既能看清物体表面纹理又不会让 CPU 做太多无效缩放。演示按“按帧识别”而不是“连续视频流”不追求实时率。from picamera2 import Picamera2 import time picam2 Picamera2() config picam2.create_still_configuration(main{size: (640, 480)}) picam2.configure(config) picam2.start() while True: frame picam2.capture_array() # 返回 RGB 格式 label, conf classify_frame(cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)) if conf 0.65: label unknown print(f{time.strftime(%H:%M:%S)} {label} {conf:.3f}) time.sleep(0.3)create_still_configuration看起来是拍照模式但配合capture_array()按帧读取非常好用CPU 占用比 video 模式低许多。这里要特别说明置信度阈值的作用垃圾桶打开状态下背景经常被误判成某种垃圾。0.65 是经验值具体要看验证集上错误样本的置信度分布调试方法就是打印一段时间的conf值观察正常样本和误检样本的分布区间。树莓派发热降频也可能导致推理时间不稳定最好加一个小散热片。4.3 MQTT 上报把识别结果发出去才是「物联网」项目垃圾分类识别本身是纯视觉任务加上 MQTT 上报项目才名副其实——摄像头识别发生在端侧结果上报到中心这正好构成“云边端”体系。这里用轻量级 MQTT 协议比 HTTP 省资源得多。import paho.mqtt.client as mqtt broker_host 192.168.1.100 # 运行 MQTT Broker 的主机或局域网服务器 broker_port 1883 topic garbage/result client mqtt.Client(client_idrpi-garbage-sorter) client.connect(broker_host, broker_port, keepalive60) client.loop_start() def publish_result(label, conf): payload { label: label, confidence: conf, timestamp: int(time.time()), } client.publish(topic, json.dumps(payload))参数说明client_id必须唯一多台树莓派同时上报时不要写死相同 ID否则 Broker 会把后者踢下线。keepalive60是心跳间隔树莓派在校园网这类 NAT 环境下长时间静默会被网络设备断开连接设置心跳比默认值小一些更稳。loop_start在后台开启网络线程可以一边循环识别一边发布不会阻塞主流程。Broker 端如果没有现成服务器直接在树莓派本地装 Mosquittosudo apt install -y mosquitto mosquitto-clients sudo systemctl enable mosquitto mosquitto_sub -h 127.0.0.1 -t garbage/result调试阶段分两步验证先用mosquitto_sub订阅能看到 JSON再在 PC 端用同一网段 IP 订阅确认跨设备通信正常。课程现场往往会遇到没有路由器的环境将树莓派和笔记本连同一个手机热点即可只要在同一个子网MQTT 不需要互联网。4.4 加分一项用 GPIO 输出 PWM 波控制分类舵机这个标题下“物联网”属性很强如果还希望演示有交互可以在树莓派 GPIO 上接一个舵机模拟垃圾分类翻板。对树莓派来说舵机控制本质就是输出 PWM 波代码并不复杂import RPi.GPIO as GPIO GPIO.setmode(GPIO.BCM) GPIO.setup(18, GPIO.OUT) pwm GPIO.PWM(18, 50) # 50Hz 是常规舵机要求的频率 pwm.start(2.5) # 2.5% 占空比对应 0 度 def open_flap_for(label): if label in (paper, glass, metal): pwm.ChangeDutyCycle(7.5) # 90 度 time.sleep(1) pwm.ChangeDutyCycle(2.5) # 回到 0 度这里的两个关键参数是 PWM 频率 50Hz 和占空比到角度的换算关系。常见舵机如 SG90脉宽 0.5ms 对应 0 度2.5ms 对应 180 度占空比就是(脉宽/20ms) x 100%。注意树莓派 GPIO 输出的 PWM 是软件模拟对舵机这类控制周期不敏感的设备够用不建议驱动大功率电机。当label为unknown时不要触发舵机否则空桶状态下舵机乱动会让老师观感不好。5. 期末作业工程化收尾源代码、模型与视频怎么配合5.1 项目骨架一份 10 秒能找到训练与推理两行的工程结构课程作业的源代码不要求架构级别但老师打开目录 10 秒内找不到开始入口印象分会直线下降。标准目录结构建议garbage_sorter/ ├── model/ # 训练好的模型 │ ├── garbage_mobilenetv2_float16.tflite │ └── labels.txt # 与 CLASS_NAMES 严格一致 ├── src/ # 树莓派端运行代码 │ ├── main.py # 主循环采集、识别、控制 │ ├── inference.py # TFLite 推理封装 │ └── mqtt_client.py # MQTT 发布 ├── tools/ # 训练与转换脚本 │ ├── train.py │ └── export_tflite.py ├── hardware/ # 接线文档与 GPIO 定义 │ └── PIN_OUT.md └── start.sh # 一键启动启动脚本start.sh的作用不是省一条命令而是明确运行环境。手动叠加.venv/bin/activate python src/main.py看似简单但课程现场经常有人忘了解释器路径直接用python导致 ImportError。写成如下形式#!/bin/bash cd $(dirname $0) source .venv/bin/activate exec python src/main.pyexec让 Python 进程接管脚本的 PIDCtrlC 时信号能正确传递给解释器。5.2 视频教程与演示视频的录制逻辑实拍优于录屏课程要求里的“视频教程”不是讲原理的教程而是演示项目功能并提交验收依据。最加分的做法是实拍视频内容顺序可以固定为拍摄设备整体外观树莓派、摄像头、舵机→ 在命令行窗口演示python src/main.py启动 → 把几件垃圾依次放到镜头前 → 展示终端输出的类别和置信度以及 PC 端 MQTT 订阅窗口收到 JSON。录屏只作为辅助视角不要整段录屏。录制时注意一个容易穿帮的细节终端日志里的类别顺序和标注文件labels.txt是否一致。先运行一次推理打印一行label paper, confidence 0.91再切到另一个终端执行cat model/labels.txt两边吻合这就是“可验证”的完整证据链。视频里出现的数字必须能在你自己的运行日志里复现课前提前录好不要临场写脚本。5.3 验证运行状态的自检日志验收前最后的检查在main.py开头加一段自检输出能让老师一眼看出模型加载是否正常、输入张量维度是否正确[INFO] loading model: model/garbage_mobilenetv2_float16.tflite (9.58 MB) [INFO] input shape: [1, 224, 224, 3] dtype: float32 [INFO] output shape: [1, 6] dtype: float32 [INFO] avg inference time over 10 frames: 0.31s这一段日志分别验证了模型文件存在且能加载、输入张量维度、输出类别数和推理耗时四项关键信息课程答辩时比数据还好用因为它直接证明“在本地树莓派上跑通了”。最后一招拔掉网线再跑一次推理——如果日志照常打印、识别照常工作说明整个流程完全不依赖远程服务模型是真正在本地加载的这正是“训练好的模型 源代码 树莓派”这个组合的核心价值所在。本文还有配套的精品资源点击获取