Skip to content

About

ESP32-S3 端侧猫狗图像分类:MobileNetV2 INT8 量化 + TFLite Micro,从训练到部署全流程

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

ESP32-S3 猫狗分类器

ESP32-S3 + TFLite Micro + int8 量化模型,从零搭建边缘 AI 图像分类项目

项目概述

在 ESP32-S3 上运行 MobileNetV2 (alpha=0.5) int8 量化模型,实现猫狗图片分类。图片内嵌 Flash,推理结果通过串口打印,无需 Wi-Fi / SD 卡。

性能指标(ESP32-S3 @ 240MHz, 8MB PSRAM):

环节 耗时
JPEG 解码 115~204ms
缩放 + 量化 ~7ms
推理 205ms
总计 330~420ms

分类准确率:6/6 正确(3 猫 + 3 狗测试图片)


从零搭建指南

以下是从拿到 ESP32-S3 芯片到完成分类器的完整步骤。

第一阶段:环境准备

1. 安装 ESP-IDF

git clone -b v6.0.1 --recursive https://github.com/espressif/esp-idf.git
cd esp-idf
./install.sh esp32s3
# 每次开终端都需要 source
. ./export.sh

2. 创建工程

mkdir esp32-catdog-classifier && cd esp32-catdog-classifier

创建以下目录结构:

esp32-catdog-classifier/
├── CMakeLists.txt           # 顶层 CMake
├── sdkconfig.defaults       # SDK 默认配置
├── partitions.csv           # 自定义分区表
├── main/
│   ├── CMakeLists.txt       # 组件 CMake
│   ├── idf_component.yml    # 组件依赖
│   ├── main.c               # 主程序
│   ├── image_preprocess.h   # 图像预处理头文件
│   ├── image_preprocess.c   # 图像预处理实现
│   ├── tflite_inference.h   # 推理引擎头文件
│   ├── tflite_inference.cpp # 推理引擎实现 (C++)
│   ├── model_data.cc        # 模型数据 (C 数组)
│   └── test_images.h        # 内嵌测试图片
├── model/
│   └── model_int8.tflite    # 量化模型文件
└── train_catdog_model.py    # 训练脚本 (Colab)

第二阶段:训练模型

1. 在 Google Colab 运行训练脚本

将 train_catdog_model.py 上传到 Colab 运行,或直接复制代码到 Colab 单元格执行。

训练脚本的核心流程:

  1. 下载 Kaggle 猫狗数据集
  2. 用 MobileNetV2 (alpha=0.5) 迁移学习,第一阶段冻结训练,第二阶段微调
  3. 构建推理模型(去掉数据增强层,直接复用训练好的层对象)
  4. 全整数量化 (int8) 导出 model_int8.tflite
  5. 验证量化后精度

关键代码(推理模型构建,解决 TFLite 兼容性问题):

# 数据增强层 (RandomFlip/RandomRotation/RandomZoom) 用的算子
# TFLite Micro 不支持,所以构建推理模型时跳过这些层
inference_model = tf.keras.Sequential([
    inputs,                    # InputLayer
    rescaling,                 # Rescaling(1./255)
    base_model_layers,         # MobileNetV2 特征提取
    global_pool,               # GlobalAveragePooling2D
    dropout,                   # Dropout (推理时无效果)
    dense,                     # Dense(1, sigmoid)
])
# 关键:直接复用训练好的层对象,不是创建新层再 set_weights

int8 量化导出:

def representative_dataset():
    for i in range(100):
        img = val_ds.unbatch().batch(1).skip(i).take(1)
        yield [next(iter(img))[0]]

converter = tf.lite.TFLiteConverter.from_keras_model(inference_model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
model_int8 = converter.convert()

2. 下载模型

训练完成后,从 Colab 下载 model_int8.tflite,放到 model/ 目录。


第三阶段:编写工程配置

1. 顶层 CMakeLists.txt

cmake_minimum_required(VERSION 3.16)

set(IDF_TARGET "esp32s3")

# 只编译需要的组件,排除 Wi-Fi/BLE 等大组件
set(COMPONENTS main esp_timer esp_psram espressif__esp-tflite-micro espressif__esp-nn espressif__esp_jpeg CACHE STRING "")

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(esp32-catdog-classifier)

要点:

  • IDF_TARGET 设为 esp32s3(不是 esp32s31,没有这个芯片型号)
  • COMPONENTS 显式列出,避免编译不需要的 Wi-Fi/BLE 组件

2. sdkconfig.defaults

# 自定义分区表
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"

# Flash 16MB
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y

# CPU 240MHz (默认 160MHz,提速 50%)
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ=240

# PSRAM (8MB OCT, 80MHz)
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_SPEED_80M=y
CONFIG_SPIRAM_USE_MALLOC=y

# 将 rodata/instructions/BSS 搬到 PSRAM,释放 DRAM
CONFIG_SPIRAM_RODATA=y
CONFIG_SPIRAM_FETCH_INSTRUCTIONS=y
CONFIG_SPIRAM_XIP_FROM_PSRAM=y
CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y

# 禁用 Wi-Fi / BLE(省 DRAM)
CONFIG_ESP_WIFI_ENABLED=n
CONFIG_BT_ENABLED=n

# 编译优化 -O2
CONFIG_COMPILER_OPTIMIZATION_PERF=y

要点:

  • PSRAM 配置让大数组(模型数据、tensor arena)可以放在 PSRAM
  • SPIRAM_RODATA + SPIRAM_FETCH_INSTRUCTIONS 让 Flash 中的模型数据和代码映射到 PSRAM
  • CPU 频率从默认 160MHz 提到 240MHz,整体提速 50%

3. partitions.csv

# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x6000,
phy_init, data, phy,     0xf000,  0x1000,
factory,  app,  factory, 0x10000, 0x700000,

要点:

  • 16MB Flash 默认分区只有 1MB factory,模型 + 图片需要更多
  • 0x700000 = 7MB,足够放下 ~966KB 模型 + 测试图片 + 代码

4. main/CMakeLists.txt

idf_component_register(
    SRCS
        "main.c"
        "image_preprocess.c"
        "tflite_inference.cpp"
        "model_data.cc"
    INCLUDE_DIRS
        "."
    REQUIRES
        esp_timer
)

要点:

  • tflite_inference.cpp 是 C++ 文件,model_data.cc 也是 C++ 编译
  • image_preprocess.c 是 C 文件,通过 extern "C" 头文件调用 C++ 函数
  • 不要把 .cc 文件同时放入 SRCS 和 #include,会导致重复符号

5. main/idf_component.yml

dependencies:
  espressif/esp-tflite-micro: "^1"
  espressif/esp_jpeg: "^1"

要点:

  • esp-tflite-micro 提供 TFLite Micro 库
  • esp_jpeg 提供软件 JPEG 解码器(ESP32-S3 没有硬件 JPEG)
  • esp-nn 是 TFLite Micro 的依赖,会自动拉取

第四阶段:编写代码

1. model_data.cc — 模型数据

将 .tflite 文件转为 C 数组:

xxd -i model/model_int8.tflite | \
  sed 's/model_model_int8_tflite/g_model_data/g' > main/model_data.cc

然后在 model_data.cc 的数组声明前加 extern 和确认有 const:

// 生成的文件内容大致如下:
// DO NOT EDIT - regenerate with: xxd -i model_int8.tflite > model_data.cc
extern const unsigned char g_model_data[] = {
  0x18, 0x00, 0x00, 0x00, ...
};
extern const unsigned int g_model_data_len = 966864;

要点:

  • 必须有 const:没有 const 的话,966KB 数据会被放在 DRAM(只有几百 KB),启动时崩溃
  • const 让数据放在 Flash 的 .rodata 段,不占 DRAM
  • 在 C++ 中 const 默认内部链接,需要 extern const 才能跨文件引用

2. test_images.h — 内嵌测试图片

将 JPEG 图片转为 C 数组:

xxd -i cat.4003.jpg | sed 's/cat_4003_jpg/cat_4003_jpg/g' > main/test_images.h

对每张图片重复,最终 test_images.h 包含:

extern const uint8_t cat_4003_jpg[];
extern const uint32_t cat_4003_jpg_len;
// ... 其他图片

3. image_preprocess.h — 预处理接口

#ifndef IMAGE_PREPROCESS_H
#define IMAGE_PREPROCESS_H

#include <stdint.h>
#include <stddef.h>

#ifdef __cplusplus
extern "C" {
#endif

// 模型输入尺寸 — 必须和训练时一致
#define MODEL_INPUT_WIDTH  96
#define MODEL_INPUT_HEIGHT 96
#define MODEL_INPUT_CHANNELS 3

// float32 路径(用于 float32 模型)
int image_preprocess_from_jpeg(const uint8_t *jpeg_buf, size_t jpeg_len,
                                float *out_buf);

// int8 路径(用于 int8 量化模型)
int image_preprocess_from_jpeg_int8(const uint8_t *jpeg_buf, size_t jpeg_len,
                                     int8_t *out_buf);

#ifdef __cplusplus
}
#endif

#endif

要点:

  • MODEL_INPUT_WIDTH/HEIGHT 必须和训练时的输入尺寸一致(96x96)
  • extern "C" 让 C++ 文件可以 include 这个头文件调用 C 函数

4. image_preprocess.c — 预处理实现

核心流程:JPEG 解码 → 缩放 → int8 量化

int image_preprocess_from_jpeg_int8(const uint8_t *jpeg_buf, size_t jpeg_len,
                                     int8_t *out_buf)
{
    // 1. 获取 JPEG 尺寸
    esp_jpeg_get_image_info(&jpeg_cfg, &jpeg_out);

    // 2. 如果图片 > 192px,用 1/2 缩放解码(减少 4x 解码像素量)
    if (src_w > 192 || src_h > 192)
        jpeg_cfg.out_scale = JPEG_IMAGE_SCALE_1_2;

    // 3. 解码为 RGB888
    esp_jpeg_decode(&jpeg_cfg, &jpeg_out);

    // 4. 双线性缩放 + int8 量化(合并为一步,省中间缓冲区)
    // 定点运算,不用 float
    for (dy, dx) {
        // 双线性插值
        val = (p00 * w00 + p10 * w10 + p01 * w01 + p11 * w11) >> 16;
        // int8 量化: [0,255] → [-128,127]
        out_buf[...] = (int8_t)(val - 128);
    }
}

要点:

  • ESP32-S3 没有硬件 JPEG,软件解码是性能瓶颈(占预处理 95% 时间)
  • JPEG_IMAGE_SCALE_1_2 让解码器在解 JPEG 时就做 1/2 缩放,像素量减少 4x
  • 缩放和量化合并为一步,避免分配额外的中间缓冲区
  • int8 量化公式取决于模型的输入 quantization 参数:
    • scale=1.0, zero_point=-128(Rescaling(1./255) 模型):int8 = pixel - 128
    • 其他情况:int8 = round(pixel / scale) + zero_point
  • 千万不要对 [0,255] 像素先除以 255 再量化,这会把所有值压缩到 -128 附近,破坏信息

5. tflite_inference.h — 推理引擎接口

#ifndef TFLITE_INFERENCE_H
#define TFLITE_INFERENCE_H

#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

typedef struct {
    float confidence;    // sigmoid 输出 (0~1)
    int   is_dog;        // >0.5 → 狗
    float cat_prob;
    float dog_prob;
    int   inference_ms;
} inference_result_t;

int  tflite_inference_init(void);
int  tflite_inference_run_int8(const int8_t *input_data, inference_result_t *result);
int  tflite_inference_get_input_quant(float *scale, int *zero_point);
void tflite_inference_deinit(void);

#ifdef __cplusplus
}
#endif

#endif

6. tflite_inference.cpp — 推理引擎实现

这是最关键的文件,要点如下:

为什么是 .cpp 而不是 .c?

TFLite Micro 的头文件(micro_interpreter.h 等)是 C++ 代码,不能在 .c 文件中 include。所以推理引擎必须用 C++ 编写,再通过 extern "C" 暴露 C 接口给 main.c 调用。

OpResolver — 注册算子

TFLite Micro 不会自动注册算子,必须手动注册模型用到的每一个:

typedef tflite::MicroMutableOpResolver<14> OpResolver;

static int register_ops(OpResolver *resolver)
{
    resolver->AddConv2D();           // 卷积
    resolver->AddDepthwiseConv2D();  // 深度可分离卷积 (MobileNet 核心)
    resolver->AddAdd();              // 残差连接
    resolver->AddMul();              // 逐元素乘法
    resolver->AddRelu();             // 激活函数
    resolver->AddAveragePool2D();    // 平均池化
    resolver->AddFullyConnected();   // 全连接分类层
    resolver->AddReshape();          // 张量形状变换
    resolver->AddQuantize();         // float → int8 量化
    resolver->AddDequantize();       // int8 → float 反量化
    resolver->AddPad();              // 零填充
    resolver->AddSoftmax();          // Softmax
    resolver->AddMean();             // Reduce Mean (Global Average Pooling)
    resolver->AddLogistic();         // Sigmoid
}

要点:

  • MicroMutableOpResolver<14> 的模板参数 14 是算子数量,必须和实际注册数一致
  • 如果报 "Didn't find op for builtin opcode 'XXX'",就是缺少算子,需要添加对应的 AddXxx()
  • 如何查看模型用了哪些算子:用 Netron (https://netron.app) 打开 .tflite 文件

Tensor Arena

#define TENSOR_ARENA_SIZE (1024 * 1024)  // 1MB

g_tensor_arena = (uint8_t *)heap_caps_malloc(TENSOR_ARENA_SIZE, MALLOC_CAP_SPIRAM);

要点:

  • Arena 必须分配在 PSRAM(MALLOC_CAP_SPIRAM),DRAM 不够
  • 如果 AllocateTensors() 失败,逐步增大 TENSOR_ARENA_SIZE
  • 当前模型实际使用 ~305KB,1MB 预留了充足余量
  • g_tensor_arena 类型是 uint8_t*(不是 float*),匹配新版 TFLite Micro API

int8 推理流程

int tflite_inference_run_int8(const int8_t *input_data, inference_result_t *result)
{
    // 1. 填入 int8 输入
    memcpy(g_input_tensor->data.int8, input_data, input_size);

    // 2. 运行推理
    g_interpreter->Invoke();

    // 3. 反量化输出: float = (int8 - zero_point) * scale
    float scale = g_output_tensor->params.scale;
    int zero_point = g_output_tensor->params.zero_point;
    float confidence = (float)(g_output_tensor->data.int8[0] - zero_point) * scale;

    // 4. sigmoid: >0.5 = 狗, <0.5 = 猫
    result->is_dog = (confidence > 0.5f) ? 1 : 0;
}

7. main.c — 主程序

static int8_t *g_input_buf = NULL;  // PSRAM 分配

void app_main(void)
{
    // 1. 分配 int8 输入缓冲区 (PSRAM)
    g_input_buf = heap_caps_malloc(96*96*3, MALLOC_CAP_SPIRAM);

    // 2. 初始化推理引擎
    tflite_inference_init();

    // 3. 逐张推理
    classify_image("cat.jpg", cat_jpg, cat_jpg_len);
    classify_image("dog.jpg", dog_jpg, dog_jpg_len);

    // 4. 释放资源
    tflite_inference_deinit();
    heap_caps_free(g_input_buf);
}

第五阶段:编译和烧录

# 设置目标芯片
idf.py set-target esp32s3

# 编译
idf.py build

# 烧录 + 串口监视
idf.py -p /dev/cu.usbmodem1301 flash monitor

踩坑记录

以下是开发过程中遇到的所有坑,按严重程度排列:

1. 模型数据占满 DRAM(致命)

现象:启动时崩溃,或 heap_caps_malloc 分配失败

原因:model_data.cc 中数组声明没有 const,966KB 数据被放在 DRAM(只有 ~400KB)

解决:extern const unsigned char g_model_data[],const 让数据放在 Flash 的 .rodata 段

2. 混合量化模型不兼容(致命)

现象:运行时报 "Hybrid models are not supported on TFLite Micro"

原因:TFLiteConverter 默认做混合量化(权重 int8,激活 float),TFLite Micro 只支持全整数量化

解决:转换时设置 converter.target_spec.supported_ops = [TFLITE_BUILTINS_INT8]

3. 数据增强层无法转换 TFLite(致命)

现象:转换时报 ERROR_NEEDS_FLEX_OPS

原因:RandomFlip/RandomRotation/RandomZoom 用了 TFLite 不支持的算子

解决:构建推理模型时跳过数据增强层,直接复用训练好的层对象(不是新建层再 set_weights)

4. C/C++ 混编(编译错误)

现象:.c 文件 include TFLite 头文件报 cstddef 等错误

原因:TFLite Micro 是 C++ 库,不能在 C 文件中使用

解决:

  • 推理引擎用 .cpp 文件编写
  • 头文件加 extern "C" 包装
  • C 文件(main.c, image_preprocess.c)通过 extern "C" 声明调用 C++ 函数

5. 缺少算子注册(运行时报错)

现象:"Didn't find op for builtin opcode 'MEAN'" 或 'LOGISTIC'

解决:在 MicroMutableOpResolver 中注册对应的 AddMean() / AddLogistic()

6. int8 量化预处理错误(推理结果全错)

现象:所有图片都分到同一类,置信度异常

原因:对 [0,255] 像素先除以 255 变成 [0,1],再用 scale=1.0 量化,所有值变成 -127~-128

解决:根据模型的输入 quantization 参数(scale, zero_point)做量化。Rescaling(1./255) 模型的 int8 输入参数是 scale=1.0, zero_point=-128,量化公式就是 int8 = pixel - 128

7. MicroInterpreter 构造函数参数类型不匹配

现象:编译报错 cannot convert float* to uint8_t*

原因:新版 TFLite Micro 的 MicroInterpreter 构造函数要求 uint8_t* 类型的 arena

解决:g_tensor_arena 声明为 uint8_t* 而非 float*


更换模型

如果重新训练了模型,需要以下步骤:

  1. 替换 model/model_int8.tflite
  2. 重新生成 main/model_data.cc:
    xxd -i model/model_int8.tflite | sed 's/model_model_int8_tflite/g_model_data/g' > main/model_data.cc
  3. 确认 model_data.cc 中数组声明有 extern const
  4. 如果模型输入尺寸变了,修改 image_preprocess.h 中的 MODEL_INPUT_WIDTH/HEIGHT
  5. 如果模型用了新算子,在 tflite_inference.cpp 的 register_ops() 中添加
  6. 如果模型更大,可能需要增大 TENSOR_ARENA_SIZE
  7. 重新编译烧录

工程文件说明

文件 用途
CMakeLists.txt 顶层 CMake,指定目标芯片和组件
sdkconfig.defaults PSRAM、CPU 频率、编译优化等配置
partitions.csv 7MB factory 分区(16MB Flash)
main/main.c 入口,分配内存,调用预处理和推理
main/image_preprocess.c/h JPEG 解码 + 缩放 + int8 量化
main/tflite_inference.cpp/h TFLite Micro 推理引擎封装
main/model_data.cc int8 模型的 C 数组(Flash 中)
main/test_images.h 内嵌 JPEG 测试图片
main/idf_component.yml esp-tflite-micro、esp_jpeg 依赖
train_catdog_model.py Colab 训练脚本
model/model_int8.tflite int8 量化模型文件
sdcard_images/ 测试图片(仅备份,不参与编译)

About

ESP32-S3 端侧猫狗图像分类:MobileNetV2 INT8 量化 + TFLite Micro,从训练到部署全流程

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages