3. ESP32-CAM图像分类部署

任务目标

在ESP32-CAM开发板上部署MobileNetV1图像分类模型,实现至少10类物体的实时识别。ESP32-CAM以ESP32-S芯片为核心,配备OV2640摄像头和MicroSD卡槽,算力有限(240MHz单核、520KB SRAM),因此采用TFLite Micro推理引擎加载量化后的MobileNetV1模型。

环境与依赖

  • 硬件:ESP32-CAM开发板(AI-Thinker)、FTDI串口模块(用于烧录)、5V电源
  • 软件:Arduino IDE 2.x、ESP32开发板包(v2.0.14+)
  • 库:EloquentTinyML(v1.0.0+)、EloquentTinyML::Tflite(自动拉取TFLite Micro)、esp32-camera(Arduino ESP32包内置)
  • 模型:MobileNetV1量化模型(输入96×96×3,10类输出),文件model.h(C数组格式)

模型准备

使用TensorFlow训练或转换一个10类分类模型(如CIFAR-10子集或自采集数据集),经量化感知训练后转换为TFLite格式,再通过xxd -i model.tflite > model.h生成C头文件。模型输入需为96×96 RGB图像(ESP32-CAM可承受的最大推理尺寸),输出为10个类别的置信度分数。本案例假设已生成model.h,其中包含unsigned char model_tflite[]数组和长度变量。

完整实现代码

#include <esp_camera.h>
#include <EloquentTinyML.h>
#include "model.h"

#define CAMERA_MODEL_AI_THINKER
#include "camera_pins.h"

// 输入/输出维度需与模型匹配
#define NUM_INPUTS 96 * 96 * 3
#define NUM_OUTPUTS 10
#define NUM_CLASSES 10

Eloquent::TinyML::Tflite<NUM_INPUTS, NUM_OUTPUTS> ml;

// 10个类别名称(按训练顺序)
const char* class_labels[NUM_CLASSES] = {
  "airplane", "automobile", "bird", "cat", "deer",
  "dog", "frog", "horse", "ship", "truck"
};

// 摄像头初始化(AI-Thinker引脚配置)
void setupCamera() {
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sccb_sda = SIOD_GPIO_NUM;
  config.pin_sccb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_RGB888;  // 必须RGB888
  config.frame_size = FRAMESIZE_96X96;     // 与模型输入一致
  config.jpeg_quality = 12;
  config.fb_count = 1;

  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("摄像头初始化失败: 0x%x\n", err);
    while (1) delay(1000);
  }
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  
  setupCamera();
  
  // 初始化TFLite模型
  ml.begin(model_tflite);
  Serial.println("模型加载完成");
}

void loop() {
  camera_fb_t* fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("图像采集失败");
    delay(1000);
    return;
  }

  // 将RGB888像素转为float数组(0~1归一化)
  float input[NUM_INPUTS];
  for (int i = 0; i < NUM_INPUTS; i++) {
    input[i] = fb->buf[i] / 255.0f;
  }

  // 推理
  uint32_t start = millis();
  float* output = ml.predict(input);
  uint32_t elapsed = millis() - start;

  // 找最高置信度类别
  int max_idx = 0;
  float max_score = output[0];
  for (int i = 1; i < NUM_OUTPUTS; i++) {
    if (output[i] > max_score) {
      max_score = output[i];
      max_idx = i;
    }
  }

  // 串口输出结果
  Serial.printf("类别: %s, 置信度: %.2f%%, 推理耗时: %d ms\n",
                class_labels[max_idx], max_score * 100, elapsed);

  esp_camera_fb_return(fb);
  delay(2000);  // 每2秒识别一次
}

运行与烧录

  1. 将model.h放在与.ino文件相同目录下
  2. Arduino IDE中选择开发板:AI Thinker ESP32-CAM
  3. 烧录设置:Flash Mode QIO,Flash Frequency 80MHz,Partition Scheme Huge APP (3MB No OTA)
  4. 连接FTDI模块(TX→U0R,RX→U0T,GND→GND,5V→5V),将GPIO0拉低进入下载模式
  5. 点击上传,烧录完成后断开GPIO0并复位

验证方法

打开串口监视器(115200波特率),将摄像头对准待识别物体。若模型加载成功且推理正常,应看到类似输出:

模型加载完成
类别: cat, 置信度: 87.50%, 推理耗时: 850 ms
类别: dog, 置信度: 92.10%, 推理耗时: 832 ms

通过条件:连续识别10次不同物体,至少8次分类正确;推理耗时在1秒以内(量化模型+96×96输入下典型值)。若输出NaN或置信度始终接近10%(随机猜测),说明输入预处理或模型张量维度不匹配,需检查NUM_INPUTS是否与模型实际输入一致。

常见问题

  • 编译错误“模型太大”:确认使用Huge APP分区,且模型文件小于3MB
  • 摄像头无法初始化:检查PSRAM是否启用(Tools→PSRAM→Enabled),OV2640需要PSRAM存储帧缓冲
  • 推理结果全为同一类:检查RGB通道顺序(ESP32-CAM输出为RGB888,若模型训练时使用BGR需交换通道)
  • 帧率过低:将FRAMESIZE_96X96改为FRAMESIZE_QVGA后在代码中裁剪中心96×96区域,可提升采集速度