这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 有奖活动 » [ESP-IDF]使用OV3660作为输出,到ESP32-S3中进行手写数字识别

共1条 1/1 1 跳转至

[ESP-IDF]使用OV3660作为输出,到ESP32-S3中进行手写数字识别的推理

高工
2026-05-19 14:43:55     打赏
简介

ESP32-S3 结合tf-lite的教程我已经发了很多了,但是之前的有些文章只是单纯的将输出固定到代码中进行推理。最近翻出来了我之前的ESP32S3-XIao开发板于是便想着将摄像头作为输出并且进行推理。关于摄像头的驱动可以参考这篇文章。【ESP-IDF】使用ESP32-S3-Xiao拍摄图片

以往发表过的关于机器学习的文章如下

使用Edgeimpulse训练图像识别的模型,并且部署到ESP32S3-Sense开发板

【M5STACKTAB5W/OBATTERY】【八】卷积神经网络-手写数字识别

[机器学习]使用TensorFlow+卷积神经网络训练宝可梦分类识别

【ESP-IDF系列】全流程打通,使用Tensorflow训练模型并且部署到ESP32S3进行推理

这个本文章实际上是沿用了【M5STACKTAB5W/OBATTERY】【八】卷积神经网络-手写数字识别内的部分代码,【ESP-IDF】使用ESP32-S3-Xiao拍摄图片。中的代码作为上述手写数字识别的组件管理器那的example的出现的。因此这里不再阐述如何将模型部署到ESP32中。


项目结构如下

image.png

其中红色框选的为【ESP-IDF】使用ESP32-S3-Xiao拍摄图片的组件代码。众所周知,Mnist的图像的输入的尺寸为28 * 28 ,这个摄像头的输出尺寸如下所示。image.png

经过我的测试这个输入尺寸最小也就只能到128 * 128 了,所以为了适配模型的输入这里一共有两种办法。 第一种办法是直接训练一个输入层为128 * 128 通道数为1的模型。 这一点在tensorflow上是非常好实现的。但是最主要的问题是输入图像的大小的增加会造成模型负责度的增加。 在增加到128 * 128 之后对一个转换成model.cc 的数组的大小已经超过了50MB,对于这个开发板只有8MB psram的情况可以说是完全不可取的。 第二种办法就是说按照28 * 28 的尺寸部署到开发板上,但是在拍摄照片之后再将尺寸缩放到28 * 28 进行输入。我这里采用的是第二种办法。

这里的处理办法主要是 :

1- 首先将摄像头的输入转换为灰度图像

  uint8_t GetGrayPixel(const camera_fb_t *frame, int x, int y)
  {
    // 如果帧已经是灰度格式,直接索引返回
    if (frame->format == PIXFORMAT_GRAYSCALE)
    {
      return frame->buf[static_cast<size_t>(y) * frame->width + x];
    }

    if (frame->format == PIXFORMAT_RGB565)
    {
      size_t index = (static_cast<size_t>(y) * frame->width + x) * 2;
      uint16_t pixel = (static_cast<uint16_t>(frame->buf[index]) << 8) |
                       frame->buf[index + 1];
      // RGB565 解码(位扩展)
      uint8_t red = static_cast<uint8_t>(((pixel >> 11) & 0x1F) << 3);
      uint8_t green = static_cast<uint8_t>(((pixel >> 5) & 0x3F) << 2);
      uint8_t blue = static_cast<uint8_t>((pixel & 0x1F) << 3);
      // 使用常见的加权平均法将彩色转为灰度(Y = 0.30R + 0.59G + 0.11B)
      return static_cast<uint8_t>((red * 30 + green * 59 + blue * 11) / 100);
    }

    // 其它格式暂不支持,返回 0
    return 0;
  }

2- 然后在图像缩放的函数中调用通道转换的函数。

  void ResizeToMnistInput(const camera_fb_t *frame, uint8_t *dest)
  {
    for (int y = 0; y < kInputHeight; ++y)
    {
      // 对应原图中的 y 坐标(向下取整)
      const int src_y = (y * static_cast<int>(frame->height)) / kInputHeight;
      for (int x = 0; x < kInputWidth; ++x)
      {
        const int src_x = (x * static_cast<int>(frame->width)) / kInputWidth;
        uint8_t gray = GetGrayPixel(frame, src_x, src_y);
        // 根据配置决定是否要反转灰度值(白/黑背景转换)
        if (kInvertInput)
        {
          gray = static_cast<uint8_t>(255 - gray);
        }
        dest[y * kInputWidth + x] = gray;
      }
    }
  }

那么此时处理好的数据即为 28 * 28 的灰度输入图像。

3- 然后增加一个打印矩阵的函数用于调试的目的。

  void PrintMnistInputMatrix(const uint8_t *input_frame)
  {
    ESP_LOGI(kTag, "Resized 28x28 input matrix (0-255):");
    for (int y = 0; y < kInputHeight; ++y)
    {
      char row_buffer[256];
      int offset = 0;
      for (int x = 0; x < kInputWidth; ++x)
      {
        // 使用 snprintf 拼接每一行的像素值,保持输出可读性
        offset += std::snprintf(
            row_buffer + offset,
            sizeof(row_buffer) - static_cast<size_t>(offset),
            "%3u%s",
            static_cast<unsigned>(input_frame[y * kInputWidth + x]),
            (x == kInputWidth - 1) ? "" : " ");
        if (offset >= static_cast<int>(sizeof(row_buffer)))
        {
          break;
        }
      }
      row_buffer[sizeof(row_buffer) - 1] = '\0';
      ESP_LOGI(kTag, "%s", row_buffer);
    }
  }

4- 主函数中,首先调用调试函数,然后调用像素转换函数,并且作为输入到Mnist的模型中。完成一次推理。

void setup()
{
  // 从链接进来的模型数组 g_model 中获取模型指针(由 model.h 提供)
  model = tflite::GetModel(g_model);
  if (model->version() != TFLITE_SCHEMA_VERSION)
  {
    // 模型版本不匹配时输出错误并返回
    MicroPrintf("Model schema version mismatch: %d != %d", model->version(), TFLITE_SCHEMA_VERSION);
    return;
  }

  // 注册推理过程中会用到的算子(根据模型生成时所用的算子)
  resolver.AddConv2D();
  resolver.AddFullyConnected();
  resolver.AddSoftmax();
  resolver.AddMaxPool2D();
  resolver.AddReshape();

  // 在静态内存中构造解释器,避免堆分配
  static tflite::MicroInterpreter static_interpreter(
      model, resolver, tensor_arena, kTensorArenaSize);
  interpreter = &static_interpreter;

  // 为解释器分配所需张量内存
  if (interpreter->AllocateTensors() != kTfLiteOk)
  {
    MicroPrintf("AllocateTensors() failed");
    return;
  }

  // 获取输入输出张量的句柄,后续用于填充输入与读取输出
  input = interpreter->input(0);
  output = interpreter->output(0);

  // 初始化摄像头,若失败则退出
  if (!InitCamera())
  {
    return;
  }

  initialized = true; // 标记已初始化,loop 可开始运行推理
  MicroPrintf("Setup complete. Ready for inference.");
}

// ================= Loop =================
// loop() 周期性运行:采集一帧、预处理、量化、推理并输出结果
void loop()
{
  if (!initialized)
  {
    // 若未完成初始化,直接返回
    return;
  }

  // 从驱动获取一帧图像(可能阻塞或返回 nullptr)
  camera_fb_t *frame = esp_camera_fb_get();
  if (frame == nullptr)
  {
    ESP_LOGE(kTag, "Failed to get camera frame");
    return;
  }

  // 打印帧格式与尺寸信息,便于调试
  ESP_LOGI(kTag, "Camera frame: format=%d size=%dx%d len=%u",
           static_cast<int>(frame->format),
           static_cast<int>(frame->width),
           static_cast<int>(frame->height),
           static_cast<unsigned>(frame->len));

  // 将摄像头帧缩放/裁剪为 28x28 并写入 resized_frame
  ResizeToMnistInput(frame, resized_frame);
  // 可选:将 28x28 矩阵打印到日志,方便观察输入图片(调试用)
  PrintMnistInputMatrix(resized_frame);
  // 处理完成后归还帧缓冲
  esp_camera_fb_return(frame);

  // 获取模型输入的量化参数
  const float input_scale = input->params.scale;
  const int input_zero_point = input->params.zero_point;

  // 将每个像素量化并写入输入张量(按模型期望的格式写入 int8 数据)
  for (int i = 0; i < kInputWidth * kInputHeight; ++i)
  {
    input->data.int8[i] = QuantizePixel(resized_frame[i], input_scale, input_zero_point);
  }

  // 调用解释器执行推理
  if (interpreter->Invoke() != kTfLiteOk)
  {
    MicroPrintf("Inference failed");
    return;
  }

  // 从输出张量中找到得分最高的类别(手写数字 0-9)
  int8_t max_score = output->data.int8[0];
  int predicted = 0;
  for (int i = 1; i < 10; ++i)
  {
    if (output->data.int8[i] > max_score)
    {
      max_score = output->data.int8[i];
      predicted = i;
    }
  }

  // 打印预测结果与置信度(量化分数)
  MicroPrintf("Predicted digit: %d (confidence score: %d)", predicted, max_score);

  // 打印所有类别的分数,便于分析模型输出分布
  MicroPrintf("All scores:");
  for (int i = 0; i < 10; ++i)
  {
    MicroPrintf("  Digit %d: %d", i, output->data.int8[i]);
  }
}

5- 烧录代码和实验现象。image.png

此时为手按压摄像头,完全黑的情况下的灰度图像输出。基本上都是240 以上证明摄像头的输入是正确的。

数字3 

image.png

实际上效果并不是太好,因为这个摄像头缩放的太多了。是很难说直接读取到28 * 28 这种图像的信息。

image.png


完整代码如下供大家学习参考

mnist_esp32.zip




关键词: ESP-IDF     OV3660     ESP32-S3     M    

共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]