基于行空板的嵌入式AI实践:从零构建端侧水果识别系统

发布时间:2026/7/28 8:24:26
基于行空板的嵌入式AI实践:从零构建端侧水果识别系统 1. 项目缘起当AI遇上嵌入式一个水果识别项目的诞生最近在捣鼓行空板这块国产的嵌入式开发板性能不错还自带屏幕和摄像头总琢磨着用它做点有意思的AI项目。正好手边有一堆水果苹果、香蕉、橙子、梨就想着能不能让这块板子自己“认识”它们。这不只是一个简单的图像识别更是一个完整的端侧机器学习Machine Learning项目实践从数据采集、模型训练到最终的部署推理全部在行空板这个资源有限的边缘设备上跑通。对于想入门嵌入式AI或者机器学习落地的朋友来说这个“水果识别”项目是个绝佳的起点。它麻雀虽小五脏俱全涵盖了物体分类Object Classification的核心流程而且结果直观有趣能让你快速获得成就感同时深刻理解AI模型从PC训练到端侧运行的完整链路。很多人一提到AI、机器学习就觉得门槛高需要强大的GPU服务器和复杂的算法知识。其实不然像物体分类这样的基础任务借助现有的成熟框架和像行空板这样集成度高的硬件完全可以在一个下午就搭建出可运行的Demo。这个项目的核心价值在于“闭环”你自己拍照片、自己标注、自己训练一个小模型然后把它塞进一块巴掌大的板子里让它实时识别你手里的水果。这个过程会让你对数据、模型、算力、部署这些概念有最直接的体感远比看十篇理论文章来得深刻。接下来我就把这次“水果识别”项目的完整过程、踩过的坑以及一些实用的技巧毫无保留地分享出来。2. 行空板开发环境搭建与核心工具链选型工欲善其事必先利其器。在开始写代码和训练模型之前得先把行空板的环境给配置好。行空板本质上是一块运行着定制化Linux系统的ARM开发板它最大的优点是开箱即用预装了Python、Jupyter Lab以及一些常用的库对于教育和个人原型开发非常友好。2.1 行空板基础连接与配置首先你需要通过USB线或者Wi-Fi将行空板连接到你的电脑。我强烈推荐使用Wi-Fi连接这样你可以通过浏览器访问行空板内置的Jupyter Lab获得一个完整的网页版编程环境操作起来和在本机写代码几乎没区别。上电与网络配置给行空板上电通过板载的屏幕操作连接到和你电脑同一个局域网的Wi-Fi。记下行空板屏幕上显示的IP地址。访问Jupyter Lab在你的电脑浏览器中输入http://[行空板IP]:8888就能打开Jupyter Lab界面。默认密码通常是dfrobot。这个环境就是我们后续所有操作的主战场。环境检查在Jupyter Lab中新建一个Python笔记本运行几句简单的命令检查关键库是否就位。例如import cv2OpenCV、import numpy、import PIL。行空板通常已经预装了这些基础库。注意行空板的存储空间和算力都有限。避免安装过于庞大或不必要的库。我们的原则是用到什么安装什么并且优先寻找ARM架构兼容的版本。2.2 机器学习框架的抉择为什么是TensorFlow Lite这是本项目第一个关键决策点。在资源受限的边缘设备上运行AI模型我们几乎不会直接使用训练时的原生框架如完整的TensorFlow或PyTorch而是使用其针对移动和嵌入式设备优化的版本或转换后的格式。主要候选者有TensorFlow Lite (TFLite)Google官方推出的轻量级解决方案工具链成熟部署文档丰富与行空板预装环境的兼容性好。PyTorch MobilePyTorch的移动端版本生态发展迅速但对于行空板这类特定ARM平台社区支持可能不如TFLite稳定。ONNX Runtime支持多框架模型但需要额外的转换步骤和运行时安装。我选择TFLite的理由很实际工具链完整从模型训练使用tensorflow库到模型转换使用tflite_converter再到端侧推理使用tflite_runtime库有一条龙的工具。tflite_runtime是一个极小的推理专用包非常适合行空板。社区支持度高行空板社区和许多嵌入式AI教程都优先采用TFLite作为案例遇到问题更容易找到解决方案。性能有保障TFLite针对ARM CPU进行了大量优化并且支持利用NNAPIAndroid Neural Networks API调用更底层的硬件加速虽然行空板当前可能用不上但为未来留了可能。因此我们的技术栈就明确了在PC端使用TensorFlowKeras来构建和训练模型然后将模型转换为.tflite格式最后在行空板上使用tflite_runtime进行推理。2.3 在行空板上安装TFLite Runtime行空板预装的Python环境可能没有tflite_runtime。我们需要通过pip来安装。由于行空板是ARM架构不能直接安装x86的包需要找对应的版本。最稳妥的方法是使用预编译的wheel文件。在行空板的Jupyter Lab中打开一个终端执行以下命令# 首先更新pip pip install --upgrade pip # 安装TFLite Runtime。版本号需要根据你的Python版本和系统架构选择。 # 对于行空板基于Debian的ARM系统可以尝试以下命令安装一个通用版本。 # 如果失败可能需要去TensorFlow官网查找对应版本的ARM wheel文件。 pip install https://github.com/google-coral/pycoral/releases/download/v2.0.0/tflite_runtime-2.5.0.post1-cp39-cp39-linux_aarch64.whl安装成功后在Python中运行import tflite_runtime.interpreter as tflite不报错即可。3. 从零开始构建水果图像数据集模型训练的第一步也是决定模型上限的关键一步就是数据。对于“水果识别”这个项目我们需要自己创建一个小型数据集。3.1 数据采集用行空板摄像头拍照我们完全利用行空板来完成数据采集。编写一个简单的Python脚本调用板载摄像头拍照并按照类别保存。这样做的好处是数据来源和最终应用场景完全一致减少了因摄像头差异、光线环境不同带来的偏差。import cv2 import os import time # 定义水果类别 classes [apple, banana, orange, pear] # 每类水果打算采集的图片数量 num_per_class 100 # 创建保存图片的目录 for cls in classes: os.makedirs(f./dataset/train/{cls}, exist_okTrue) os.makedirs(f./dataset/val/{cls}, exist_okTrue) # 预留验证集目录 # 初始化摄像头行空板摄像头设备号通常是0 cap cv2.VideoCapture(0) if not cap.isOpened(): print(无法打开摄像头) exit() print(数据采集开始按‘s’键保存当前图片按‘q’键退出当前类别。) for cls in classes: print(f\n准备采集 {cls} 的图片...) count 0 while count num_per_class: ret, frame cap.read() if not ret: print(获取帧失败) break # 显示实时画面 cv2.imshow(Data Collection - Press \s\ to save, \q\ to next class, frame) key cv2.waitKey(1) 0xFF if key ord(s): # 保存图片以类别和序号命名 filename f./dataset/train/{cls}/{cls}_{count:03d}.jpg cv2.imwrite(filename, frame) print(f已保存: {filename}) count 1 time.sleep(0.3) # 防止连续保存过快 elif key ord(q): print(f提前结束 {cls} 的采集) break print(\n数据采集完成) cap.release() cv2.destroyAllWindows()这个脚本会为每类水果创建一个文件夹并通过一个简单的GUI窗口让你控制拍照。记得在拍照时尽量让水果在画面中占据主要位置并且变换不同的角度、光照和背景增加数据的多样性。比如香蕉可以拍直的、弯的、带斑点的苹果可以拍红的、青的、带叶子的。3.2 数据预处理与增强让小数据集发挥大作用我们只采集了每类100张图对于深度学习来说是非常小的数据量。直接训练很容易导致模型过拟合即只记住了训练集无法识别新图片。因此数据增强Data Augmentation是必不可少的步骤。我们使用TensorFlow/Keras的ImageDataGenerator来在训练时实时进行数据增强。这相当于在每次训练时都对原始图片进行随机的、合理的变换生成“新”的图片从而极大地扩充了有效数据量。from tensorflow.keras.preprocessing.image import ImageDataGenerator # 定义训练数据生成器并配置多种增强参数 train_datagen ImageDataGenerator( rescale1./255, # 归一化将像素值缩放到[0,1]区间加速模型收敛 rotation_range20, # 随机旋转角度范围 width_shift_range0.1, # 随机水平平移范围 height_shift_range0.1, # 随机垂直平移范围 shear_range0.1, # 随机错切变换范围 zoom_range0.1, # 随机缩放范围 horizontal_flipTrue, # 随机水平翻转对于水果识别翻转通常是合理的 fill_modenearest, # 填充新像素的策略 validation_split0.2 # 划分20%的数据作为验证集 ) # 定义验证/测试数据生成器只进行归一化不进行增强 val_datagen ImageDataGenerator(rescale1./255, validation_split0.2) # 从目录生成数据流 train_generator train_datagen.flow_from_directory( ./dataset/train, target_size(224, 224), # 将所有图片统一缩放到224x224这是后面模型输入的尺寸 batch_size16, # 每个批次的图片数量 class_modecategorical, # 多分类任务 subsettraining # 指定这是训练集部分 ) val_generator val_datagen.flow_from_directory( ./dataset/train, # 注意和训练集来自同一个总目录 target_size(224, 224), batch_size16, class_modecategorical, subsetvalidation # 指定这是验证集部分 )这里有个关键点我们把总数据集的20%留作验证集。验证集不参与训练只用来在每轮训练后评估模型的泛化能力防止过拟合。flow_from_directory会自动根据子文件夹名来确定类别标签非常方便。4. 模型构建、训练与轻量化转换有了数据接下来就是模型的设计和训练。我们的目标是在行空板上运行因此模型必须在保证精度的前提下尽可能小、尽可能快。4.1 选择与微调预训练模型迁移学习从头开始训练一个卷积神经网络CNN来分类图片需要海量的数据和强大的算力。对于我们这个小数据集最佳实践是使用迁移学习。即利用一个在超大规模数据集如ImageNet上预训练好的模型将其特征提取能力迁移到我们的水果分类任务上。我选择了MobileNetV2作为基础模型。为什么是它轻量级专为移动和嵌入式设备设计参数量少计算量小。性能好在ImageNet上准确率不错证明了其强大的特征提取能力。与TFLite兼容性极佳Google自家出品转换和部署的优化支持最好。具体做法是保留MobileNetV2的卷积基用来提取通用图像特征去掉顶部的全连接分类层然后接上我们自己的、针对4类水果的小型分类头。from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras import layers, models # 加载预训练的MobileNetV2模型不包括顶部分类层输入尺寸定为224x224 base_model MobileNetV2(weightsimagenet, include_topFalse, input_shape(224, 224, 3)) # 冻结基础模型的权重在初始训练阶段不更新它们 base_model.trainable False # 构建我们自己的模型 model models.Sequential([ base_model, layers.GlobalAveragePooling2D(), # 将卷积层输出的特征图进行全局平均池化得到一个一维向量 layers.Dropout(0.2), # 丢弃层随机丢弃20%的神经元防止过拟合 layers.Dense(128, activationrelu), # 全连接层128个神经元 layers.Dropout(0.2), layers.Dense(4, activationsoftmax) # 输出层4个神经元对应4类水果softmax激活输出概率 ]) # 编译模型 model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy]) model.summary() # 打印模型结构查看参数量通过model.summary()你会看到可训练的参数主要集中在我们自己添加的几层而MobileNetV2的千万级参数被冻结了。这大大减少了需要训练的参数加快了训练速度并降低了对数据量的需求。4.2 模型训练与调参实战现在用我们准备好的数据生成器来训练模型。# 训练模型 history model.fit( train_generator, steps_per_epochtrain_generator.samples // train_generator.batch_size, epochs15, # 先训练15轮看看 validation_dataval_generator, validation_stepsval_generator.samples // val_generator.batch_size )训练过程通常在PC或云端进行。你需要观察训练损失和验证损失的变化。理想情况训练损失和验证损失都稳步下降验证准确率逐步上升。过拟合迹象训练损失持续下降但验证损失在某个点后开始上升或停滞不前。这意味着模型开始“死记硬背”训练数据了。应对过拟合如果出现过拟合可以尝试1) 增加数据增强的强度2) 在模型中增加Dropout比例3) 减少模型复杂度如减少自定义全连接层的神经元数4) 使用更早的停止点早停法。训练大概15-20轮后验证准确率通常能达到95%以上。此时我们可以进行微调以进一步提升性能解冻基础模型的部分顶层用很小的学习率进行联合训练。# 解冻基础模型的最后一些层比如最后10层 base_model.trainable True fine_tune_at len(base_model.layers) - 10 for layer in base_model.layers[:fine_tune_at]: layer.trainable False # 重新编译模型使用更小的学习率 model.compile(optimizertf.keras.optimizers.Adam(learning_rate1e-5), losscategorical_crossentropy, metrics[accuracy]) # 继续训练几轮 history_fine model.fit(...)4.3 模型转换从Keras到TFLite训练完成后我们得到了一个.h5格式的Keras模型。下一步是将其转换为TFLite格式并进行可能的优化。import tensorflow as tf # 加载训练好的模型 model tf.keras.models.load_model(fruit_classifier.h5) # 创建TFLite转换器 converter tf.lite.TFLiteConverter.from_keras_model(model) # 可选进行优化以减小模型大小和提升推理速度 converter.optimizations [tf.lite.Optimize.DEFAULT] # 如果需要进一步量化到INT8精度可能略有下降速度提升显著需要代表性数据集 # def representative_dataset(): # for _ in range(100): # data ... # 从验证集中取一些数据 # yield [data.astype(np.float32)] # converter.representative_dataset representative_dataset # converter.target_spec.supported_ops [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] # converter.inference_input_type tf.uint8 # converter.inference_output_type tf.uint8 # 转换模型 tflite_model converter.convert() # 保存TFLite模型 with open(fruit_classifier.tflite, wb) as f: f.write(tflite_model) print(模型已转换为TFLite格式大小, len(tflite_model) / 1024, KB)经过DEFAULT优化后模型大小通常会缩小到原来的1/3到1/4。对于我们的水果分类模型最终.tflite文件可能只有几MB非常适合在行空板上部署。5. 在行空板上部署与实时推理最后一步也是最有成就感的一步就是把转换好的模型部署到行空板上并编写一个实时识别程序。5.1 部署模型与编写推理脚本将生成的fruit_classifier.tflite文件上传到行空板的某个目录例如/home/pi/projects。同时我们需要一个标签文件labels.txt里面按顺序写上类别名称apple banana orange pear接下来在行空板的Jupyter Lab中编写推理脚本inference.pyimport numpy as np import cv2 import tflite_runtime.interpreter as tflite from PIL import Image import time # 1. 加载TFLite模型并分配张量 model_path fruit_classifier.tflite interpreter tflite.Interpreter(model_pathmodel_path) interpreter.allocate_tensors() # 2. 获取输入输出详情 input_details interpreter.get_input_details() output_details interpreter.get_output_details() input_shape input_details[0][shape] # 应该是 [1, 224, 224, 3] height, width input_shape[1], input_shape[2] # 3. 加载标签 with open(labels.txt, r) as f: labels [line.strip() for line in f.readlines()] # 4. 初始化摄像头 cap cv2.VideoCapture(0) if not cap.isOpened(): print(无法打开摄像头) exit() print(开始实时识别按‘q’键退出。) while True: start_time time.time() # 读取一帧 ret, frame cap.read() if not ret: break # 预处理图像调整大小、归一化、扩展维度 img cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # OpenCV是BGR转为RGB img Image.fromarray(img) img img.resize((width, height)) img_array np.array(img, dtypenp.float32) / 255.0 # 归一化 img_array np.expand_dims(img_array, axis0) # 增加批次维度 - [1,224,224,3] # 5. 将数据送入模型 interpreter.set_tensor(input_details[0][index], img_array) # 6. 运行推理 interpreter.invoke() # 7. 获取输出结果 output_data interpreter.get_tensor(output_details[0][index]) results np.squeeze(output_data) # 去掉批次维度 # 8. 解析结果 predicted_class_idx np.argmax(results) confidence results[predicted_class_idx] label labels[predicted_class_idx] # 计算推理耗时 inference_time (time.time() - start_time) * 1000 # 毫秒 # 9. 在图像上绘制结果 cv2.putText(frame, f{label}: {confidence:.2%}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.putText(frame, fTime: {inference_time:.1f}ms, (10, 70), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2) # 显示实时画面 cv2.imshow(Fruit Classification - Live, frame) # 按‘q’退出 if cv2.waitKey(1) 0xFF ord(q): break # 释放资源 cap.release() cv2.destroyAllWindows()5.2 性能优化与实测心得运行这个脚本你应该能看到一个实时视频窗口当你把水果放到摄像头前屏幕上会显示出识别出的水果类别和置信度。实测中的几点关键发现和优化技巧推理速度在行空板上使用浮点模型FP32进行一次推理大约需要100-200毫秒帧率在5-10 FPS左右对于演示来说完全够用。如果想追求更快的速度可以尝试之前提到的INT8量化这可能会将推理时间缩短到50毫秒以内但需要仔细评估量化带来的精度损失。内存占用tflite_runtime本身内存占用很小。主要内存消耗在于图像预处理存储原始帧和预处理后的张量。确保及时释放不再需要的大变量。预处理一致性这是最容易出错的地方模型训练时的预处理流程必须与推理时的预处理流程完全一致。我们训练时用了rescale1./255推理时也做了/255.0。如果训练时用了其他预处理如减去均值推理时也必须照做。摄像头延迟cv2.VideoCapture的read()函数有时会有缓冲导致画面不是真正的“实时”。可以在循环开始前加一句cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)来尝试减少缓冲区。错误处理在实际部署中要增加健壮性代码。比如检查模型和标签文件是否存在捕获并处理推理过程中的异常防止程序因单帧识别错误而崩溃。6. 项目总结与扩展思考这个项目走完了一个完整的嵌入式AI应用闭环数据采集 - 模型训练PC端- 模型转换与优化 - 端侧部署与推理行空板。它虽然以“水果识别”为例但其方法论可以迁移到任何类似的端侧图像分类任务上比如识别手势、区分不同类型的零件、识别简单的交通标志等。几个可以深入探索的扩展方向模型轻量化进阶尝试使用更小的基础模型如MobileNetV3-Small或者使用模型剪枝、知识蒸馏等技术进一步压缩模型追求极致的速度和体积。数据集质量提升数据的质量决定模型的天花板。可以尝试收集更多样、更困难的数据例如部分遮挡的水果、不同成熟度、多个水果同框并研究数据清洗和自动标注的方法。集成到实际应用将识别结果与其他硬件联动。例如识别到“香蕉”后通过行空板的GPIO控制一个舵机转动或者通过语音模块播报结果打造一个智能水果分拣装置的雏形。探索其他模型格式除了TFLite可以尝试将模型转换为ONNX格式并用ONNX Runtime在行空板上运行对比两者的性能和易用性。我个人最大的体会是嵌入式AI项目的核心挑战往往不在于算法本身而在于如何在资源算力、内存、功耗和性能精度、速度之间找到最佳平衡点。这个“水果识别”项目就像一块敲门砖它让你亲身体验了从数据到落地产品的每一个环节尤其是“部署”这个在理论教程中常常被一笔带过、实则充满细节的步骤。当你看到自己训练的模型在一块小小的板子上流畅运行起来时那种亲手创造智能的满足感是单纯调参跑分无法比拟的。希望这个详细的流程能帮你少走弯路更快地开启你自己的边缘AI项目。