找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

532

积分

0

好友

72

主题
发表于 4 天前 | 查看: 20| 回复: 0

本项目基于ESP32-S3微控制器实现嵌入式手写数字识别系统,通过触摸屏采集用户输入的手写数字图像,利用轻量化神经网络进行实时推理识别。项目完整集成了LVGL图形库构建触摸界面,并驱动LED点阵显示结果。在开发过程中,深入解决了S3的内存管理、SPIRAM使用、SPIFFS文件系统读取、LVGL布局与刷新以及神经网络模型移植等一系列典型问题。

项目介绍和硬件资源

本项目基于ESP32-S3微控制器实现嵌入式手写数字识别系统,通过触摸屏采集用户输入的手写数字图像,利用轻量化神经网络进行实时推理识别。使用LVGL编程在LCD屏幕上设定一个正方形的写字区域,在区域内书写0-9的数字,对书写的数字进行识别,并将识别的结果传递给LED灯板进行显示。

硬件模块型号参数和功能说明:

主控芯片:

  • ESP32-S3-WROOM-1-N4R2双核240MHz;
  • 4MB Flash;
  • 2MB SPIRAM。

屏幕:

  • TFT-LCD屏幕;
  • 分辨率:480*272;
  • 触摸类型:电阻式触摸屏;
  • 屏幕驱动:NV3047。

其他接口:

  • 1*TF卡槽;
  • 2*GPIO;
  • 1*Speaker;
  • 2*UART1;
  • 1*UART0。

图片

在ESP32-S3上完成手写数字识别后,还需点亮一个8x8的LED灯矩阵来显示识别结果。LED灯阵由两颗74HC595D芯片驱动。

图片

方案框图和项目设计思路

图片

(1) UI与触摸输入处理 首先在UI界面中初始化一个画布(Canvas)控件,由于其需要200*200像素的缓冲区,占用空间较大,故将其初始化到外部SPIRAM上。

原计划是为画布绑定触摸事件,仅在触摸画布区域时开启手写功能。但在实际调试中发现,LVGL的控件事件在持续按压时难以完美捕获连续坐标,事件默认会传递到底层的触摸屏读取函数中。因此,最终方案是在全局的触摸屏读取函数my_touchpad_read中,通过判断触点坐标是否位于画布区域内来触发手写逻辑。

通过电阻式触摸笔在屏幕的数字手写区域输入0-9任意一个数字,屏幕实时显示笔迹,并将200200区域内的图像数据压缩并二值化为2828的矩阵,供神经网络使用。

(2) 识别与显示 点击识别按钮后,系统对采集到的手写数字图像进行处理,得到一个28*28的二值化矩阵。该矩阵被输入到预先训练并部署好的轻量化神经网络中进行计算,输出识别出的数字(0-9)。识别结果一方面在串口打印,另一方面通过GPIO控制外部的8x8 LED点阵进行可视化显示。

(3) 清除功能 点击清除按钮,清除画布上的笔迹和缓存数据,重置识别状态。

关键代码和流程图

图片

以下是项目中的几个核心函数与关键点:

1. 内存监控函数 由于是从零开始接触ESP32-S3,在内存分配上花费了大量时间。因此养成了分配大内存后立即检查的习惯。以下函数用于实时监控内部RAM和PSRAM的使用情况:

free_8bit = esp_get_free_internal_heap_size();        //内部RAM
free_spiram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);   // PSRAM
min_free = heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT);//历史最小剩余内存

2. 神经网络参数加载 load_network_params()函数负责挂载SPIFFS文件系统,并从.txt文件中加载训练好的神经网络模型参数到PSRAM中。

图片

代码中的内存分配校验至关重要:

//添加内存分配校验
if (!g_network_params) {
    ESP_LOGE(TAG, “PSRAM分配失败!可用内存: %d”,
            heap_caps_get_free_size(MALLOC_CAP_SPIRAM));
    // ...错误处理...
}

每次分配完内存后,必须检查是否成功。曾有多次因RAM耗尽导致分配失败,进而引发后续的内存越界错误。此外,即使分配成功,若不谨慎配置ESP-IDF的编译优化选项,已分配的大块内存也可能在后续操作中被异常占用。为此,专门编写了四个检查函数,分别打印神经网络各层的参数以确保数据加载完整无误。

3. 触摸读取与绘图函数 触摸读取函数my_touchpad_read是项目的核心之一,负责采集笔迹坐标并实时绘制到画布上。 图片

static lv_point_t points_array[3000]; // 存储触摸轨迹
static uint16_t point_count = 0;
#define BRUSH_RADIUS 8

void my_touchpad_read(lv_indev_drv_t *indev_driver, lv_indev_data_t *data) {
    static lv_point_t last_point = {0, 0};
    static bool is_touching2 = false;
    const uint16_t dead_zone = 1;

    bool hardware_touched = touch_touched();
    if (hardware_touched) {
        lv_point_t current_point = {(short)touch_last_x, (short)touch_last_y};
        // 空间消抖:仅当移动超过死区阈值时更新点
        if (abs(current_point.x - last_point.x) > dead_zone ||
            abs(current_point.y - last_point.y) > dead_zone) {
            last_point = current_point;
        }
        data->point = last_point;
        data->state = LV_INDEV_STATE_PRESSED;
        is_touching2 = true;

        // 判断触点是否在画布区域内 (150,50) 到 (350,250)
        if(((150<data->point.x)&&(data->point.x<350))&&((50<data->point.y)&&(data->point.y<250))) {
             lv_point_t curr_point = {
                .x =(short int) (data->point.x - lv_obj_get_x(canvas)),
                .y = (short int)(data->point.y - lv_obj_get_y(canvas))
             };
             // 存储轨迹点,用于连线绘制
             if(point_count < sizeof(points_array)/sizeof(points_array[0])) {
                 points_array[point_count++] = curr_point;
             }
             // 核心:以当前触点为中心“画圆”,模拟笔刷,填充二值化矩阵
             for (int dx = -BRUSH_RADIUS; dx <= BRUSH_RADIUS; dx++) {
                 for (int dy = -BRUSH_RADIUS; dy <= BRUSH_RADIUS; dy++) {
                     int x = curr_point.x + dx;
                     int y = curr_point.y + dy;
                     if (x < 0 || x >= CANVAS_WIDTH || y < 0 || y >= CANVAS_HEIGHT) continue;
                     // 圆形判断(仅填充圆形区域内点)
                     if (dx*dx + dy*dy <= BRUSH_RADIUS * BRUSH_RADIUS) {
                         raw_bitmap[x][y] = 1; // 设置像素
                     }
                 }
             }
             // 使用LVGL的线段绘制功能连接轨迹点,实现平滑笔迹
             lv_draw_line_dsc_t line_dsc;
             lv_draw_line_dsc_init(&line_dsc);
             line_dsc.color = lv_color_white();
             line_dsc.width = 8;
             lv_canvas_draw_line(canvas, points_array, point_count, &line_dsc);
        }
    } else {
        data->state = LV_INDEV_STATE_RELEASED;
        is_touching2 = false;
        lv_obj_invalidate(canvas); // 释放时刷新画布
        // 清除轨迹点缓存
        for(int temp2 = 0; temp2 < point_count+2; temp2++) {
              points_array[temp2].x = 0;
              points_array[temp2].y = 0;
        }
        point_count=0;
    }
}

手写数字识别对触摸灵敏度要求适中,因此仅做了空间消抖(当移动距离超过1个像素才记录新点),而未加入时间消抖,以避免断触。画布上的笔迹效果通过LVGL的lv_canvas_draw_line接口实现。同时,在读取坐标时,通过一个“画圆”算法来模拟笔刷的粗细,并同步更新底层用于神经网络模型移植的二值化矩阵raw_bitmap

硬件展示

image.png 在画布区域手写数字,点击“Begin”按钮后,LED灯屏会显示识别结果,并在2秒后自动熄灭。

image.png 系统同时通过串口打印识别结果和输入数据的二维图案(“x”表示笔迹点,“.”表示空白)。这部分调试信息用于最终校验输入神经网络的数据是否正确。

项目总结

本次项目在有限时间内,于ESP32-S3上成功搭建了一个集图形界面、触摸交互与轻量AI推理于一体的手写数字识别系统。最终识别率(0-9中能正确识别5个)尚有提升空间,主要原因在于:一是采用的神经网络全连接层参数量较大,在资源受限的MCU上并非最优选择;二是为内存分配与优化空间,对模型参数进行了从double到float的粗暴量化,损失了部分精度。

尽管结果未尽完美,但整个开发过程极具价值。从LVGL的集成、SPIFFS文件系统的使用、PSRAM的有效管理,到纯C语言神经网络的移植与调试,每一步都充满了挑战,也积累了宝贵的嵌入式AI实战经验,清晰地展现了“MCU+轻量AI”可能实现的技术边界。




上一篇:React2Shell漏洞深度解析:React Server Components远程代码执行与防御
下一篇:基于C++26反射自动生成多语言绑定:mirror_bridge实战解析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-10 02:21 , Processed in 0.143481 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表