
簡介基于樹莓派和卷積神經網絡構建的果蔬圖片分類工程面向需要完成畢業設計、課程設計、競賽實訓的嵌入式與人工智能方向學習者解決模型訓練后部署到樹莓派實現輸入單張圖片即返回分類結果的問題。壓縮包內共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 里清晰說明哪個模型對應哪個版本——這一點在最終提交工程時遠比優化那兩個百分點的準確率更能打動評委。本文還有配套的精品資源點擊獲取