3. 端侧图像分类
任务目标
在Raspberry Pi Pico(RP2040,264KB SRAM)上部署MobileNetV2,实现端侧图像分类。由于MobileNetV2原始模型约13MB,远超Pico存储与内存,本案例采用知识蒸馏后的1.0×MobileNetV2(输入96×96×3,量化至int8),模型大小约1.2MB,运行时峰值内存约180KB。最终在Pico上完成对96×96 RGB图像的实时分类推理。
环境与依赖
- 硬件:Raspberry Pi Pico(RP2040,2MB Flash)
- SDK:pico-sdk v1.5+,编译工具链arm-none-eabi-gcc
- 模型转换:TensorFlow 2.13 + TFLite Converter(量化int8)
- 推理框架:TFLite Micro(从tensorflow/tflite-micro仓库获取,版本对应TF 2.13)
- 数据集:ImageNet子集(蒸馏训练用,本案例不涉及训练细节)
模型准备与转换
使用TensorFlow将训练好的MobileNetV2浮点模型转换为int8量化TFLite模型。关键转换参数如下(完整训练代码不在本案例范围):
import tensorflow as tf
# 加载训练好的浮点模型(输入96x96x3)
converter = tf.lite.TFLiteConverter.from_saved_model('mobilenetv2_96_saved_model')
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset_gen # 校准数据集生成器
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
tflite_model = converter.convert()
# 保存模型
with open('mobilenetv2_96_int8.tflite', 'wb') as f:
f.write(tflite_model)
转换后模型约1.2MB,需通过xxd -i mobilenetv2_96_int8.tflite > model_data.cc生成C数组嵌入固件。
关键完整代码
以下为Pico上TFLite Micro推理核心实现(main.c):
#include <stdio.h>
#include <pico/stdlib.h>
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
#include "tensorflow/lite/schema/schema_generated.h"
#include "model_data.cc" // 包含模型数组
// 输入输出缓冲区(int8)
constexpr int kTensorArenaSize = 190 * 1024;
uint8_t tensor_arena[kTensorArenaSize];
// 图像数据(96x96x3,int8,范围-128~127)
extern const int8_t test_image[96*96*3]; // 由外部提供
int main() {
stdio_init_all();
sleep_ms(2000); // 等待串口就绪
// 加载模型
const tflite::Model* model = tflite::GetModel(g_model);
if (model->version() != TFLITE_SCHEMA_VERSION) {
printf("Model version mismatch!\n");
return -1;
}
// 注册算子(仅需MobileNetV2用到的)
static tflite::MicroMutableOpResolver<10> resolver;
resolver.AddConv2D();
resolver.AddDepthwiseConv2D();
resolver.AddAdd();
resolver.AddMul();
resolver.AddRelu();
resolver.AddSoftmax();
resolver.AddReshape();
resolver.AddMean();
resolver.AddPad();
resolver.AddQuantize();
// 构建解释器
static tflite::MicroInterpreter interpreter(
model, resolver, tensor_arena, kTensorArenaSize);
if (interpreter.AllocateTensors() != kTfLiteOk) {
printf("AllocateTensors failed!\n");
return -1;
}
// 获取输入输出张量
TfLiteTensor* input = interpreter.input(0);
TfLiteTensor* output = interpreter.output(0);
// 检查输入尺寸
if (input->dims->size != 4 || input->dims->data[1] != 96 ||
input->dims->data[2] != 96 || input->dims->data[3] != 3) {
printf("Unexpected input shape!\n");
return -1;
}
// 拷贝图像数据到输入张量
memcpy(input->data.int8, test_image, 96*96*3*sizeof(int8_t));
// 执行推理
if (interpreter.Invoke() != kTfLiteOk) {
printf("Invoke failed!\n");
return -1;
}
// 获取输出(1000类,int8)
int8_t* out_data = output->data.int8;
float scale = output->params.scale;
int zero_point = output->params.zero_point;
// 反量化并找Top-1
int max_idx = 0;
float max_score = -128.0f;
for (int i = 0; i < 1000; i++) {
float score = (out_data[i] - zero_point) * scale;
if (score > max_score) {
max_score = score;
max_idx = i;
}
}
printf("Top-1 class: %d, score: %.4f\n", max_idx, max_score);
return 0;
}
CMake配置
cmake_minimum_required(VERSION 3.13)
include(pico_sdk_import.cmake)
project(mobilenetv2_pico)
# TFLite Micro源码路径(需提前克隆)
set(TFLITE_MICRO_DIR ${CMAKE_CURRENT_SOURCE_DIR}/tflite-micro)
add_executable(mobilenetv2_pico
main.c
${TFLITE_MICRO_DIR}/tensorflow/lite/micro/all_ops_resolver.cc
# 此处需列出所有编译的.cc源文件(约30+个)
)
target_include_directories(mobilenetv2_pico PRIVATE
${TFLITE_MICRO_DIR}
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(mobilenetv2_pico PRIVATE pico_stdlib)
pico_add_extra_outputs(mobilenetv2_pico)
完整TFLite Micro源文件列表较长,建议使用其官方CMakeLists.txt中的target_sources定义,或直接引用micro_speech示例的构建方式。
编译与烧录
mkdir build && cd build
cmake ..
make -j4
# 生成mobilenetv2_pico.uf2,按住Pico BOOTSEL键后连接USB,拷贝uf2文件到弹出的U盘
验证方法
使用串口监视器(如minicom,波特率115200)观察输出。通过条件:
- 程序启动后2秒内打印
Top-1 class: xxx, score: x.xxxx,无错误信息 - 使用已知测试图像(如ImageNet中类别为"goldfish"的96×96图像),预期输出类别索引与真实标签一致(goldfish对应索引1)
- 推理耗时约1.5~2.5秒(可通过
time_us_32()测量,本代码未包含计时,如需验证性能可自行添加)
若输出类别与预期不符,检查:模型输入是否需归一化至[-128,127]、图像数据是否按RGB顺序排列、模型是否成功量化。