资讯动态

基于树莓派和CNN的果蔬识别系统:从模型训练到TFLite部署实践

发布时间:2026/9/13 9:02:35 来源:尧图企业网站定制
简介基于树莓派和卷积神经网络构建的果蔬图片分类工程面向需要完成毕业设计、课程设计、竞赛实训的嵌入式与人工智能方向学习者解决模型训练后部署到树莓派实现输入单张图片即返回分类结果的问题。压缩包内共38个文件大小约2.53MB以Python源码和图片素材为主涵盖数据划分、数据增强、模型训练与测试脚本同时包含图形界面及登录模块代码图片文件多为测试样本或结果可视化PDF与Markdown文档分别提供设计报告和使用说明目录结构清晰。已有323人学习浏览过。整套源码、工程文件与说明文档能帮助使用者快速跑通“数据准备—模型训练—部署验证”全流程也可自行扩展类别或接入摄像头适合作为项目开发、毕设课设及学科竞赛的复现与二次开发基础。1. 先立论为什么用树莓派跑 CNN 而不是直接上单片机或云端很多人第一反应是果蔬分类这种任务随便在 PC 上用 TensorFlow 训个模型就行为什么要绕一圈部署到树莓派上答案是课程设计、毕业设计、竞赛答辩时评委真正关心的是你是否理解“模型训练”和“模型部署”是两件完全不同的事。PC 上跑通的代码搬到 ARM 架构的树莓派上会遇到 TensorFlow 版本不兼容、内存不足、摄像头输入格式不一致等一系列问题。这套项目把整条链路走通了从果蔬图片数据集准备、CNN 模型训练到把训练好的模型放到树莓派上做实时推理最终还带了一个 PyQt 桌面窗口和登录模块完整度很高。适合三类人第一类是毕设选了嵌入式 深度学习方向的学生第二类是做大创或竞赛需要快速出成果的团队第三类是想搞明白“模型从 PC 到嵌入式设备”中间有哪些坑的从业者。它覆盖的不是某一个点而是整条技术链路。2. 果蔬数据集的目录规划与样本增强细节2.1 get_data.py从散落图片到结构化数据集拿到工程包之后先别急着跑训练先看数据是怎么组织的。get_data.py这个脚本做的事情非常简单但极其关键把images目录下按类别归档的果蔬图片读取出来划分成训练集和测试集并以 TFRecord 或未打包的目录形式导出供后续训练脚本直接使用。我打开源码后看到它用的是最朴素但最稳定的方案利用os.listdir()遍历类别目录再按比例随机切分。import os import random import shutil source_dir images # 原始图片目录按类别分子文件夹 train_dir data/train val_dir data/val split_ratio 0.8 # 80% 训练20% 验证 categories [d for d in os.listdir(source_dir) if os.path.isdir(os.path.join(source_dir, d))] for cat in categories: cat_path os.path.join(source_dir, cat) imgs [f for f in os.listdir(cat_path) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(imgs) split_idx int(len(imgs) * split_ratio) os.makedirs(os.path.join(train_dir, cat), exist_okTrue) os.makedirs(os.path.join(val_dir, cat), exist_okTrue) for img in imgs[:split_idx]: shutil.copy(os.path.join(cat_path, img), os.path.join(train_dir, cat, img)) for img in imgs[split_idx:]: shutil.copy(os.path.join(val_path : os.path.join(val_dir, cat), img), os.path.join(val_dir, cat, img))代码逻辑不复杂先扫描images下每个子目录每个子目录名就是类别标签然后把每个类别的图片按 8 : 2 的比例随机分配到训练集和验证集。关键点是随机打乱时固定random.seed()否则每次运行得到的数据划分都不一致后续对比实验就没有意义。我一般会在脚本开头加一行random.seed(42)。split_ratio这个值不建议设置为 0.9 以上因为果蔬图片的类间差异大但类内差异也不小比如不同品种的苹果颜色差异明显验证集太小会导致评估结果波动很大。2.2 Data_enhancement.py用数据增强把 500 张图变成 5000 张的效果数据增强这部分是整个项目里最值得细读的模块。Data_enhancement.py用的是 TensorFlow 内置的ImageDataGenerator这是 Keras 时代最经典的增强方案。很多人觉得增强就是翻转一下图片实际上这里面有三个参数直接决定模型能不能收敛。from tensorflow.keras.preprocessing.image import ImageDataGenerator datagen ImageDataGenerator( rotation_range40, # 随机旋转 0~40 度 width_shift_range0.2, # 水平平移 20% height_shift_range0.2, # 垂直平移 20% shear_range0.15, # 错切变换强度 zoom_range0.2, # 随机缩放 20% horizontal_flipTrue, # 随机水平翻转 fill_modenearest # 填充新像素的策略 ) train_generator datagen.flow_from_directory( data/train, target_size(128, 128), batch_size32, class_modecategorical )rotation_range40意味着每张图最多旋转 40 度这个值对果蔬场景是合理的因为实际拍摄时果蔬不会倒置。fill_modenearest处理的是旋转和平移后图片边缘产生的空白区域用最邻近像素填充比constant填 0更自然避免模型学到黑色边框这种伪特征。flow_from_directory会自动根据子目录名生成类别索引返回的train_generator是惰性的——它不是一次性把增强后的图片都加载进内存而是在每次迭代时实时生成这一点对树莓派这种内存吃紧的设备很重要。源项目的增强脚本写得很克制没有用brightness_range和channel_shift_range我建议训练时补上brightness_range[0.8,1.2]因为不同光照条件下拍摄的果蔬照片亮度差异很大这个增强手段对泛化能力的提升比旋转平移都明显。2.3 标签编码与类别数目的自动识别果蔬数据集的类别数量通常在 10 到 30 之间。flow_from_directory会在运行时自动扫描目录并编码标签因此train_cnn.py里不需要硬编码类别数量而是通过train_generator.num_classes动态获取。这是容易踩坑的点如果某人为了省事在代码里写死num_classes10而实际数据集有 12 类训练时最后一层全连接的输出维度就会和标签维度不匹配TensorFlow 会直接抛错。正确做法是在训练脚本中加一行num_classes train_generator.num_classes这个动态获取的方式也方便后续扩展——想加入更多果蔬类别时只需要往目录里加子文件夹其他代码不用动。3. CNN 主干网络设计从输入层到全连接输出的参数推演3.1 输入分辨率和卷积核的选择逻辑train_cnn.py里定义的 CNN 结构是典型的“VGG 风格”堆叠式网络多个卷积层加池化层最后接全连接层。输入图片尺寸设为 128×128×3这个选择是经过权衡的因为树莓派 4B 的推理端跑 224×224 的输入每秒只能处理 2 到 3 张图而 128×128 可以跑到每秒 8 到 10 张识别精度差别不超过 3 个百分点。如果用的是树莓派 5可以上调到 160×160推理速度和精度的平衡点更优。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout def build_cnn(input_shape(128, 128, 3), num_classes15): model Sequential([ Conv2D(32, (3, 3), activationrelu, paddingsame, input_shapeinput_shape), Conv2D(32, (3, 3), activationrelu, paddingsame), MaxPooling2D(pool_size(2, 2)), Conv2D(64, (3, 3), activationrelu, paddingsame), Conv2D(64, (3, 3), activationrelu, paddingsame), MaxPooling2D(pool_size(2, 2)), Conv2D(128, (3, 3), activationrelu, paddingsame), Conv2D(128, (3, 3), activationrelu, paddingsame), MaxPooling2D(pool_size(2, 2)), Flatten(), Dense(256, activationrelu), Dropout(0.5), Dense(128, activationrelu), Dropout(0.3), Dense(num_classes, activationsoftmax) ]) return model这个网络结构有三层卷积块每个块内是两个 3×3 卷积加一个最大池化通道数从 32 翻倍到 64 再翻倍到 128。两个 3×3 卷积堆叠的感受野等于一个 5×5 卷积但参数量更少、非线性表达能力更强这是 VGGNet 验证过的结论。paddingsame保持特征图尺寸不变让池化层负责降采样这种设计在反向传播时梯度更稳定。需要注意的是Dropout只在训练时生效推理时自动关闭dropout0.5放在第一个全连接层后面防止全连接层过拟合。果蔬分类的类别间相似度高比如番茄和辣椒都是红色长条形状过拟合风险大于普通物体分类所以 Dropout 的比例应保持在 0.5 左右不要降到 0.3 以下。3.2 模型参数量与 FLOPs 估算用 128×128 输入计算各层输出尺寸和参数量第一层卷积输出 128×128×32第二层输出 128×128×32池化后降到 64×64×32第三层输出 64×64×64第四层输出 64×64×64池化后 32×32×64第五层输出 32×32×128第六层输出 32×32×128池化后 16×16×128。展平后是16*16*128 32768维向量第一个全连接层参数约为 32768 × 256 ≈ 840 万。权重参数总量约在 900 到 1000 万之间模型文件保存为 HDF5 格式约为 35 到 40MB。这个体量在树莓派 4B 上可以接受但内存需要预留至少 1GB 给推理进程。如果设备的存储空间吃紧可以选择只在每层保留前几个卷积核但更推荐的方案是直接做权重量化——用model.save()保存 float32 权重再用 TensorFlow Lite 转换器转成 float16 精度参数量双减一半且精度几乎无损。import tensorflow as tf model build_cnn(num_classes15) model.summary() model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), losscategorical_crossentropy, metrics[accuracy] )优化器选择 Adam 而不是 SGD原因是果蔬识别任务中特征分布不均衡某些类别样本数明显少于其他类别Adam 的自适应学习率能更好地应对稀疏梯度。learning_rate0.001是 Adam 的默认值但如果训练集很小比如每类只有 100 张图建议降到0.0005配合ReduceLROnPlateau回调在验证损失停滞时自动降学习率。关于学习率我见过很多新手直接在model.compile里写lr0.1这在 Adam 上是完全跑不起来的因为 Adam 默认的步长设计假设初始学习率远小于 1。3.3 训练脚本中的回调函数配置from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint callbacks [ EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue), ModelCheckpoint(best_model.h5, monitorval_accuracy, save_best_onlyTrue, verbose1) ] history model.fit( train_generator, steps_per_epochtrain_generator.samples // 32, validation_dataval_generator, validation_stepsval_generator.samples // 32, epochs50, callbackscallbacks )EarlyStopping的patience10意味着连续 10 个 epoch 验证损失没有下降就停止训练这一策略能节省大量时间。validate_steps和steps_per_epoch需要设置为样本数除以 batch_size 后取整这是 TensorFlow 的惰性计算特性决定的如果设置过大会导致验证集被重复读取过小则会跳过部分数据。4. 树莓派部署TensorFlow 环境搭建与模型推理适配4.1 树莓派 4B 的 TensorFlow 安装方案对比树莓派部署是整个项目最核心的章节。这里有一个关键决策树莓派 4B 的 ARM 架构不支持 x86 的官方 pip 包从 2023 年后官方不再发布预编译的 tensorflow 包因此常见做法是安装基于 TensorFlow Lite 的运行环境或者通过第三方编译源安装。# 方式一安装 tf-nightly 兼容版本推荐先尝试 sudo apt update sudo apt install python3-pip libatlas-base-dev pip3 install tensorflow2.13.0 # 方式二使用 TensorFlow Lite Runtime轻量级部署 pip3 install tflite-runtime从 PC 训练完成后模型需要经过转换才能在树莓派上流畅运行。转换的核心逻辑如下import tensorflow as tf # 加载 PC 端训练好的模型 model tf.keras.models.load_model(best_model.h5) # 转成 TFLite 格式并做量化压缩 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_quantized.tflite, wb) as f: f.write(tflite_model)上述代码把大约 40MB 的 HDF5 模型压缩成约 20MB 的 TFLite 文件。tf.lite.Optimize.DEFAULT会自动对权重做量化tf.float16是在 ARM 设备上精度和速度的折中选项。如果追求极致压缩可以去掉supported_types改用 INT8 量化但 INT8 需要校准数据集且精度损失在果蔬分类这种细粒度任务上可能达到 5 个百分点以上。4.2 test_model.py 推理脚本拆解test_model.py是树莓派端的主推理脚本它做的事情是读取一张图片预处理成与训练时一致的格式送入 TFLite 模型输出分类结果。import numpy as np import tensorflow as tf from PIL import Image # 加载 TFLite 模型不用再打开 TensorFlow 完整图 interpreter tf.lite.Interpreter(model_pathmodel_quantized.tflite) interpreter.allocate_tensors() input_details interpreter.get_input_details() output_details interpreter.get_output_details() def predict_image(img_path, top_k3): img Image.open(img_path).convert(RGB) img img.resize((128, 128), Image.BILINEAR) # 归一化训练时是 [0,1]推理保持一致 input_data np.array(img, dtypenp.float32) / 255.0 input_data np.expand_dims(input_data, axis0) interpreter.set_tensor(input_details[0][index], input_data) interpreter.invoke() output interpreter.get_tensor(output_details[0][index])[0] top_indices np.argsort(output)[::-1][:top_k] return [(class_names[i], float(output[i])) for i in top_indices]这里最容易被忽视的是dtype的一致性训练时ImageDataGenerator默认把图片归一化到[0,1]浮点数那么推理时也必须除以 255.0否则模型的输入分布完全不同输出的置信度会失真。np.expand_dims增加 batch 维度因为 TFLite 模型期望输入形状是(1, 128, 128, 3)而单张图片只有(128, 128, 3)。4.3 摄像头实时推理用 OpenCV 替代静态图片工程里包含了摄像头场景的推理逻辑这是实际展示时最有冲击力的部分。实时推理需要解决一个关键问题TFLite 推理本身很快但树莓派的摄像头读帧、图片预处理会消耗大量 CPU导致整体帧率很低。import cv2 import numpy as np cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while True: ret, frame cap.read() if not ret: break # 裁剪中心区域为正方形再缩放到 128x128 h, w frame.shape[:2] side min(h, w) x (w - side) // 2 y (h - side) // 2 cropped frame[y:yside, x:xside] resized cv2.resize(cropped, (128, 128)) # BGR 转 RGB再归一化 rgb cv2.cvtColor(resized, cv2.COLOR_BGR2RGB) input_data np.array(rgb, dtypenp.float32) / 255.0 input_data np.expand_dims(input_data, axis0) # 推理 interpreter.set_tensor(input_details[0][index], input_data) interpreter.invoke() output interpreter.get_tensor(output_details[0][index])[0] label np.argmax(output) confidence float(output[label]) # 把结果画在画面上 cv2.putText(frame, f{class_names[label]} {confidence:.2%}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(Fruit Classification, frame) if cv2.waitKey(1) 0xFF ord(q): break注意色彩空间的坑OpenCV 默认读取为 BGR 通道顺序如果直接送进模型而不做cvtColor转换模型看到的图片颜色是反的果蔬分类准确率会降到随机水平。这是我在调试时最容易忽略的坑因为肉眼在屏幕上很难察觉 BGR 和 RGB 的差异但模型对像素通道顺序极其敏感。4.4 部署过程中的典型错误对照树莓派上跑项目大概率会碰到的错误和解决方案整理如下错误现象根因排查方法Illegal instruction (core dumped)TensorFlow 版本与 ARM CPU 指令集不兼容换装tensorflow2.13.0或更早版本Segmentation fault发生在导入模块时OpenCV 与 NumPy 版本冲突用pip3 install numpy1.24.4固定版本推理速度低于 2 FPS开启了图形加速但未启用XNNPACK禁用tf.lite的XNNPACK用interpreter.set_num_threads(4)摄像头画面是花屏CSI 摄像头驱动异常执行sudo modprobe bcm2835-v4l2内存不足自动卡死树莓派交换分区过小调整/etc/dphys-swapfile的CONF_SWAPSIZE为 2048 后重启树莓派 4B 同时跑 OpenCV 和 TFLite 时内存占用约为 800MB 到 1GB因此建议在启动参数中加入arm_64bit1并且不要同时打开桌面环境和浏览器。使用命令行模式没有图形界面能节省约 500MB 内存对推理稳定性帮助显著。5. window.py 与 login.py把分类器变成可交付的桌面工具5.1 登录模块的架构思路login.py和window.py构成了这套项目的用户交互层。很多毕设项目在模型推理之外只有命令行输出而这里用 PyQt5 搭建了一个带登录界面的桌面应用。从工程交付角度讲这很加分因为答辩现场不可能让评委去终端里敲命令。import json import hashlib from PyQt5.QtWidgets import QDialog, QLineEdit, QPushButton, QVBoxLayout, QMessageBox class LoginDialog(QDialog): def __init__(self): super().__init__() self.setWindowTitle(果蔬分类系统 - 登录) self.user_input QLineEdit(self) self.user_input.setPlaceholderText(用户名) self.pwd_input QLineEdit(self) self.pwd_input.setPlaceholderText(密码) self.pwd_input.setEchoMode(QLineEdit.Password) self.login_btn QPushButton(登录, self) self.login_btn.clicked.connect(self.check_login) layout QVBoxLayout() layout.addWidget(self.user_input) layout.addWidget(self.pwd_input) layout.addWidget(self.login_btn) self.setLayout(layout) def check_login(self): username self.user_input.text().strip() password self.pwd_input.text().strip() hashed hashlib.sha256(password.encode()).hexdigest() # users.json 存储的是 {username: sha256hash} try: with open(users.json, r) as f: users json.load(f) except FileNotFoundError: QMessageBox.warning(self, 错误, 未找到用户配置文件) return if username in users and users[username] hashed: self.accept() else: QMessageBox.warning(self, 错误, 用户名或密码错误)这里的核心设计是把密码做 SHA-256 哈希后存储而不是明文保存。虽然毕设级别的系统不需要上 HTTPS 或 OAuth但采用哈希存储能体现作者具备基本的安全意识。users.json的格式是一组键值对新增用户只需要手动添加一条记录。5.2 PyQt 窗口中嵌入推理结果展示window.py是主窗口模块它调用test_model.py里定义好的predict_image函数来做推理再用QLabel显示上传的图片和识别结果。from PyQt5.QtWidgets import QMainWindow, QLabel, QPushButton, QFileDialog, QVBoxLayout, QWidget from PyQt5.QtGui import QPixmap class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(果蔬图片分类系统) self.image_label QLabel(self) self.image_label.setText(请选择图片) self.result_label QLabel(self) self.result_label.setText(等待识别...) self.btn QPushButton(选择图片, self) self.btn.clicked.connect(self.select_image) layout QVBoxLayout() layout.addWidget(self.image_label) layout.addWidget(self.result_label) layout.addWidget(self.btn) container QWidget() container.setLayout(layout) self.setCentralWidget(container) # 预加载 TFLite 模型 from test_model import load_interpreter self.interpreter load_interpreter() def select_image(self): path, _ QFileDialog.getOpenFileName( self, 选择图片, , 图片文件 (*.jpg *.png *.jpeg)) if path: pixmap QPixmap(path).scaled(256, 256) self.image_label.setPixmap(pixmap) # 调用推理逻辑 result self.predict(path) self.result_label.setText(result)这个窗口的设计有两个值得注意的细节。第一TFLite 模型在__init__里只加载一次而不是每次点击按钮都重新加载否则每次推理都要重新读取 20MB 模型文件界面会卡顿两秒以上。第二推理过程如果直接放在 UI 主线程里图片一多界面会无响应更完整的做法是用QThread把推理放到子线程再通过信号把结果传回 UI 线程。5.3 自动预测脚本批量测试图片的分类效果test_images目录下放了一批待测试的图片用来验证模型的泛化能力。批量测试的推理脚本可以直接复用test_model.py中的predict_image函数但对多个图片文件做循环时要注意图片格式不一致的问题。项目里测试集可能出现.jpeg、.JPG、.png混存的情况PIL打开没有问题但如果用 OpenCV 的cv2.imread直接读某些.png的透明度通道会被读成 4 通道送入模型就会报维度错误。# 在树莓派上快速测试单张图片 python3 test_model.py --image test_images/apple_01.jpg5.4 模型评估的验证方法课程设计答辩时最常见的提问就是“你的模型准确率是多少”。工程里提供了针对验证集的评估脚本可通过计算混淆矩阵来查看哪些类别容易混淆。from sklearn.metrics import confusion_matrix, classification_report # 在 PC 端训练完成后执行 y_true [] y_pred [] for i in range(len(val_generator)): x_batch, y_batch val_generator[i] pred model.predict(x_batch) y_true.extend(np.argmax(y_batch, axis1)) y_pred.extend(np.argmax(pred, axis1)) if len(y_true) val_generator.samples: break print(classification_report(y_true, y_pred, target_namesclass_names)) cm confusion_matrix(y_true, y_pred)这部分数据应该保存下来并写入项目文档。果蔬分类中番茄和青椒、苹果和梨在颜色和纹理上高度相似通常混淆集中在这几类。可以通过classification_report中的 f1-score 找出低于 0.7 的类别然后针对性地补充该类别的数据增强策略——比如对番茄这类圆形且颜色单一的果蔬增加亮度扰动比增加旋转更有效。5.5 模型文件管理和发布最后需要注意的一件小事是模型文件的路径管理。源工程把best_model.h5放在根目录但实际部署到树莓派时我建议建立一个单独的models/目录存放 float32 版本用于 PC 端继续训练和 float16 TFLite 版本用于树莓派推理文件名标注日期和准确率例如model_fruit_v2_93.2acc.tflite。这样做一方面是为了复现实验时能够回溯到特定模型对应的数据划分和训练参数另一方面在竞赛验收时你可以在 README 里清晰说明哪个模型对应哪个版本——这一点在最终提交工程时远比优化那两个百分点的准确率更能打动评委。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价