这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 板卡试用 » 【M5STACKTAB5W/OBATTERY】【八】卷积神经网络-手写数字识别

共2条 1/1 1 跳转至

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

工程师
2026-02-01 00:49:26     打赏

简介

还记得我们之前在论坛中发布了很多关于如何使用tf-lite在ESP32下环境的使用的教程,申请这个开发板的其中的一个目的就是为了来在开发板上以图形的方式来运行TinyML。如下附上之前发布过的机器学习帖子的链接。【ESP-IDF系列】全流程打通,使用Tensorflow训练模型并且部署到ESP32S3进行推理。和【PocketBeagle2】过程贴(五)-使用scikit-learn对环境数据进行回归分类 和 【分享评测,赢取加热台】+记录一次使用树莓派结合机器学习框架SKlearn实现随机森林对TOF传感器进行手势分类

上述的这些帖子都是关于机器学习的相关帖子,我记得好像还有一个宝可梦识别的帖子,但是暂时没有找到在哪里。废话不多说,开始我们今天的主题,在M5 Stack Tab5 上部署手写数字识别的TF-lite模型,并且实现从输入到输出的推理。


效果如下所示


436c8ce12513594afcc017082e07e14f.jpg

模型训练

模型的训练采用的是TensorFlow,教程则来自TensorFlow的官方入门示例如下所示。

image.png

需要注意的是模型的输入的形状是 28 * 28 , 这也就是说在M5STACK tab5 上的输入形状也必须是28 * 28 , 如果不是28 * 28 的话则需要缩放到28 * 28. 这也就是我为什么没有采用摄像头的原因,因为在ESP32上对图形进行缩放处理是一件比较麻烦的事情。而且还不好控制。 还有一点最重要的是,摄像头的输入没办法缩放到28 * 28 (据我这两天的研究是这样的,也有可能通过其他的算法可以缩放1200 * xxx 的摄像头像素)。


主要代码逻辑

当模型训练成功之后将模型转换成tflite的方式然后将其转换成C语言的数组,用于和官方TF-lite一样的c语言数组model的形式从而被TF加载。下述为核心的转换代码,其中使用的是int8的精度。

# 创建代表性数据集,用于量化
def representative_dataset():
    for i in range(100):
        image = x_train[i:i+1]
        yield [image.astype('float32')]

# 转换为 TFLite int8
converter = tf.lite.TFLiteConverter.from_keras_model(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   # 输入类型 int8
converter.inference_output_type = tf.int8  # 输出类型 int8

tflite_model = converter.convert()

# 保存文件
with open('mnist_model.tflite', 'wb') as f:
    f.write(tflite_model)
print("TFLite model saved!")


然后使用XDD将其转换成C语言数组。

image.png

按照tensorflow的demo的格式准备好所需的必要文件(demo中有,model为我们转换好的model)。

image.png

注意、上述的model.cc 和h 以及输出的句柄和主函数都需要根据你自己的模型进行修改。


项目的IDF组件依赖如下

## IDF Component Manager Manifest File
dependencies:
## Required IDF version
idf:
version: '>=4.1.0'
espressif/m5stack_tab5: ^1.0.0
espressif/esp_io_expander: '*'
espressif/esp-tflite-micro: '*'

在CmakeList中增加对其文件的引用

idf_component_register(SRCS "main.c" "main_functions.cc" "model.cc" "output_handler.cc"
INCLUDE_DIRS "." "../"
PRIV_REQUIRES spi_flash esp_timer unity m5stack_tab5 esp_driver_ppa)

# 使用自定义的 lv_conf.h
target_compile_definitions(${COMPONENT_TARGET} PUBLIC LV_CONF_INCLUDE_SIMPLE)

由于ESP32-P4的PSRAM不够LVGL使用创建28 * 28 的 格子,因此需要设置PSRAM来将所有的LVGL的RAM都保存在PSRAM中,如下为LVGL的配置文件中对PSRAM的配置。

/* 1 is: use custom malloc/free, 0 is: use the built-in `lv_mem_alloc()` and `lv_mem_free()` */
#define LV_MEM_CUSTOM      0  // 改为0,使用内置内存管理
#if LV_MEM_CUSTOM == 0
    /* Size of the memory available for `lv_mem_alloc()` in bytes (>= 2kB)*/
#define LV_MEM_SIZE    (1024U * 1024U * 2)          /* 512KB */
    /* Set an address for the memory pool instead of allocating it as a normal array. Can be in external SRAM too. */
#define LV_MEM_ADR          0     /*0: unused*/
    /* Instead of an address give a memory allocator that will be called to get a memory pool for LVGL. E.g. my_malloc */
#if LV_MEM_ADR == 0
        /* 使用 PSRAM 作为 LVGL 内存池 */
#define LV_MEM_POOL_INCLUDE "esp_heap_caps.h"
#define LV_MEM_POOL_ALLOC(size) heap_caps_malloc(size, MALLOC_CAP_SPIRAM)
    #endif

#else       /*LV_MEM_CUSTOM*/
#define LV_MEM_CUSTOM_INCLUDE "esp_heap_caps.h"   /*Header for the dynamic memory function*/
#define LV_MALLOC(size)       heap_caps_malloc(size, MALLOC_CAP_SPIRAM)
#define LV_FREE(p)            heap_caps_free(p)


主程序代码流程概述

1- 初始化基础硬件信息,和基础屏幕显示

// 显示内存信息
ESP_LOGI(TAG, "Initial PSRAM free: %dKB",
heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024);
ESP_LOGI(TAG, "Initial Internal free: %dKB",
heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024);

// 1. 初始化硬件
bsp_i2c_init();
bsp_display_start();
bsp_display_backlight_on();

// 2. 获取屏幕并设置背景
bsp_display_lock(0);

lv_obj_t *scr = lv_scr_act();
lv_obj_set_style_bg_color(scr, lv_color_hex(0x202020), LV_PART_MAIN);

// 3. 先创建一个标题标签
title = lv_label_create(scr);
lv_label_set_text(title, "Digital recognization from EEPW");
lv_obj_set_style_text_color(title, lv_color_white(), 0);
lv_obj_set_style_text_font(title, &lv_font_montserrat_24, 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);

// 4. 创建信息标签
lv_obj_t *info = lv_label_create(scr);
lv_label_set_text(info, "Creating grid...");
lv_obj_set_style_text_color(info, lv_color_hex(0xAAAAAA), 0);
lv_obj_align(info, LV_ALIGN_TOP_MID, 0, 50);

bsp_display_unlock();

// 给用户一点时间看到提示
for (int i = 0; i < 10; i++)
{
lv_timer_handler();
vTaskDelay(pdMS_TO_TICKS(10));
}

// 5. 创建正方形网格(居中)
bsp_display_lock(0);

2- 绘制28 * 28 的格子

// 计算正方形区域
lv_coord_t screen_width = lv_obj_get_width(scr);
lv_coord_t screen_height = lv_obj_get_height(scr);
lv_coord_t grid_size_px = screen_width < screen_height ? screen_width : screen_height;
lv_coord_t cell_size = grid_size_px / GRID_SIZE;
lv_coord_t offset_x = (screen_width - grid_size_px) / 2;
lv_coord_t offset_y = (screen_height - grid_size_px) / 2 + 40; // 下移避开标题

// 更新信息标签
lv_label_set_text(info, "Creating grid cells...");
lv_timer_handler();

bool success = create_grid_incremental(scr, offset_x, offset_y, cell_size);

3- 创建两个按钮用于清空所有格子,和进行推理。

// 创建 Clear 按钮
lv_obj_t *btn_clear = lv_btn_create(scr);
lv_obj_set_size(btn_clear, 100, 40);
lv_obj_align(btn_clear, LV_ALIGN_BOTTOM_LEFT, 20, -20);
lv_obj_t *label_clear = lv_label_create(btn_clear);
lv_label_set_text(label_clear, "Clear");
lv_obj_center(label_clear);
lv_obj_add_event_cb(btn_clear, clear_grid_cb, LV_EVENT_CLICKED, NULL);

// 创建 Inference 按钮
lv_obj_t *btn_infer = lv_btn_create(scr);
lv_obj_set_size(btn_infer, 100, 40);
lv_obj_align(btn_infer, LV_ALIGN_BOTTOM_RIGHT, -20, -20);
lv_obj_t *label_infer = lv_label_create(btn_infer);
lv_label_set_text(label_infer, "Inference");
lv_obj_center(label_infer);
lv_obj_add_event_cb(btn_infer, infer_cb, LV_EVENT_CLICKED, NULL);

4- 主循环处理LVGL事件

// 6. 主循环
    uint32_t last_mem_check = 0;
    while (1)
    {
        uint32_t now = esp_log_timestamp();

        // 处理LVGL事件
        lv_timer_handler();

        // 定期检查内存
        if (now - last_mem_check > 10000)
        { // 每10秒
            last_mem_check = now;
            memory_monitor();
        }

        vTaskDelay(pdMS_TO_TICKS(10));
    }

    if (grid_objects)
    {
        heap_caps_free(grid_objects);
        grid_objects = NULL;
    }

5- 当按钮按下的时候收集屏幕像素区域,并且推理。在推理之后更新屏幕标签。

static void infer_cb(lv_event_t *e)
{
    // 收集屏幕网格的像素数据
    uint8_t img[28 * 28] = {0};
    for (int i = 0; i < 28 * 28; i++)
    {
        if (grid_objects && grid_objects[i])
        {
            lv_color_t color = lv_obj_get_style_bg_color(grid_objects[i], LV_PART_MAIN);
            // 黑色像素设为255,白色设为0
            img[i] = lv_color_eq(color, lv_color_black()) ? 255 : 0;
        }
    }

    // 进行推理
    char result[50];
    mnist_infer(img, result, sizeof(result));

    // 更新标题显示推理结果
    if (title)
    {
        lv_label_set_text(title, result);
    }
}

上述的代码逻辑已经是非常简化的步骤了,实际上在点亮屏幕绘制28 * 28 个格子都不是一件简单的事情(需要多次调整配置文件使其RAM能够分配到PSRAM里)。


附件代码:代码已经上传到Github


视频效果如下








关键词: M5STACKTAB5W     手写数字识别     卷积神经网    

高工
2026-02-01 13:00:33     打赏
2楼

视频快审



共2条 1/1 1 跳转至

回复

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