2025寒假练 - 使用“CrowPanel ESP32 Display 4.3”实现手写数字识别显示
本项目基于ESP32-S3微控制器实现嵌入式手写数字识别系统,通过触摸屏采集用户输入的手写数字图像,利用轻量化神经网络进行实时推理识别。使用LVGL编程在LCD屏幕上设定一个正方形的写字区域在写字区域里书写0-9的数字对书写的数广进行识别,
标签
嵌入式系统
故海er
更新2025-03-17
10

基于ESP32-S3的手写数字识别系统项目报告

首先是一些感想,很早就参加了本次活动,但是寒假忙小论文再加上拖延症发作,一直到最后才是整本次活动的内容。以前有ST系列单片机的经验,但接触esp32s3还是第一次。整个开发过程中遇到了许许多多古怪的bug,可能也有自己容易钻牛角尖的原因,经常一个问题卡一下午得不到进展。最让我记忆深刻的屏幕闪烁的问题,因为esp内部ram有效,就需要分配lvgl的屏幕缓冲区到外部SPIRAM,但是分配以后,不触摸屏幕则显示正常。一旦触摸屏幕,整个屏幕就会快速闪烁。我找找了方方面面各种各样的原因,尝试了各种不同的刷新和缓存方式,但是最终问题出现的原因是在SDK Config中把相关Catch缓存设置好就能解决。种种“心酸”就不一一列举了。 可惜因为拖延症,在最后一天才勉强完成任务,而且项目还有很多可以完善优化的地方。但是作为从零开始接触ESP32S3,又是在较短的时间里高强度的学习、调试,我真切感到自己有了很多收获。期待下次的训练营活动。

(以下正文开始)

然后对了在神经网络部署部分由于实在没时间整tensflow的环境了 所以采用了纯C代码,自己在ESP32s3上做了适配  通过挂载挂载SPIFFS文件系统读取网络模型参数。然后优化RAM使用空间折腾了好久好久!(需要更大的RAM)

以下是我参考的纯C神经网络代码链接:

https://gitee.com/li_ximing/CNNhandWritenRecognition 

项目介绍和硬件资源

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

 

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

主控芯片 ESP32-S3-WROOM-1-N4R2 双核240MHz4MB Flash 2MB SPIRAM(为什么不多给我点RAM?)

屏幕:TFT-LCD屏幕,分辨率:480*272,触摸类型:电阻式触摸屏,屏幕驱动:NV3047其他接口:1*TF卡槽,2*GPIO, 1*Speak, 2*UART1, 1*UARTO

 

esp32S3上完成手写数字识别后,还需要点亮LED灯矩阵。LED灯矩阵的相关信息如下。


资源:8*864颗单色LED灯,封装大小~0603

两颗串-并变换、SOIC-16封装的74HC595D20603封装的电源去耦电容

方案框图和项目设计思路

(1) 首先在ui.screen中初始画布控件,然后因为200*200像素占用的空间比较大,所以要把它初始化到外部SPIRAM上。

刚开始我想当然的想法时,可以不可以给画布绑定一个触摸事件,然后只有在触摸画布区域的时候可以开始进行手写。然后捣鼓了好久,发现只能触发“点击”事件,但是持续按压的情况下无法跳转到画布绑定的事件。默认是到在main中初始化绑定的my_touchpad_read中了。因为自己是第一次接触lvgl,折腾了半天没有成功,时间又很有限只能退而求其次在my_touchpad_read函数中通过判断屏幕区域来进行手写数字识别。

通过电阻式触摸笔在屏幕的数字手写区域输入0-9任意一个数字,屏幕对应显示手写数字,并将200*200的手写区域内图像压缩保存为28*28的二值化图像数据;

(2) 点击识别按钮首先会对屏幕区域的手写数字进行处理,最后是一个30*30的二值化图像数据按照相应格式输入已经训练好的神经网络中,经过计算后,输出识别的数字,并通过LED点阵显示;

(3) 点击清除按钮,清除屏幕上的手写数据。

关键代码和流程图

首先抱歉了,时间紧张流程图画的简陋,然后我多介绍一点重要的函数。

简单的初始化部分略过,没什么价值。

 

首先是这三个函数

 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); //历史最小剩余内存

负责来实时监控ESP32S3RAM使用情况。因为是第一次接触 我这次活动里在RAM分配上花费的时间非常多。后面就养成每分配一次较大的空间就查看一下RAM的使用情况。

然后就是load_network_params()函数,它负责加载SPIFFS文件系统,并从.txt文件中加载训练好的神经网络模型参数。

 

如图所示,为部分加载代码。代码里有几个需要尤其关注的点,比如这里

//添加内存分配校验

if (!g_network_params) {

    ESP_LOGE(TAG, "PSRAM分配失败!可用内存: %d",

            heap_caps_get_free_size(MALLOC_CAP_SPIRAM));

    // ...错误处理...

}

每次分配完内存以后,一定一定要检查下是否分配成功。我有好多次都是RAM用完了,没有分配成功足够大的空间,导致后续使用的时候会报内存越界。

另外还有即使内存分配成功了 如果不修改esp-idf的优化选项 已分配的大块内存空间后续可能会被占用,我有遇到过从在分配后检验读取的网络参数是正确的,但是在初始化后的UI回调函数中再次检查发现内存被破坏了。

后续我写了四个检查函数,来分别打印神经网络各个层的参数来确保读取成功。

 

最后就是在触摸读取函数

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;


   // static lv_point_t prev_point = {0};


    // 1. 直接读取触摸状态,不依赖时间/空间过滤
    bool hardware_touched = touch_touched();
   
    if (hardware_touched) {
        lv_point_t current_point = {(short)touch_last_x, (short)touch_last_y};


        // 2. 仅过滤微小移动,但不影响状态机
        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_PR; // 只有硬件触摸,状态保持 PRESSED
        data->state = LV_INDEV_STATE_PRESSED; // 只要硬件触摸,状态保持 PRESSED
        is_touching2 = true;
        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))
            };
            //Serial.print("运行到这里了");


            lv_draw_line_dsc_t line_dsc;
            lv_draw_line_dsc_init(&line_dsc);
            line_dsc.color = lv_color_white();
            line_dsc.width = 8;


            // 存储轨迹点
            if(point_count < sizeof(points_array)/sizeof(points_array[0])) {
                points_array[point_count++] = curr_point;
            }
        // raw_bitmap[curr_point.x][curr_point.y] = 1;
        // for (int a = 0; a< 10; a++) {
        //     raw_bitmap[curr_point.x+a][curr_point.y+a] = 1;
        // }
        //  for (int b = 0; b< 10; b++){
        //     raw_bitmap[curr_point.x-b][curr_point.y-b] = 1;
        //  }
    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; // 设置像素
            }
        }
    }
                     
            // ESP_LOGW(TAG, "ESP_LOGW test!\n");
            // lv_color_t c = lv_color_hex(0x123456);
            // // 绘制线段
            // for(int tmepi=0;tmepi<point_count;tmepi++)
            // {
            //     lv_canvas_set_px(canvas, curr_point.x, curr_point.y, c);
            // }
           // ESP_LOGW(TAG, "持续按压中\n");
               lv_canvas_draw_line(canvas, points_array, point_count, &line_dsc);
            //prev_point = curr_point;
            //prev_point = curr_point;
            //lv_obj_invalidate(canvas);      
        }
    } else {
        data->state = LV_INDEV_STATE_RELEASED;
        is_touching2 = false;



        lv_obj_invalidate(canvas);
        int temp2;
        for(temp2 = 0;temp2<point_count+2;temp2++)
        {
              points_array[temp2].x = 0;  
              points_array[temp2].y = 0;  
        }
        point_count=0;
       
        //Serial.println("hardware_touched是错误状态");
    }
   // 局部刷新优化
    // lv_area_t area;
    // lv_area_set(&area,
    //            prev_point.x - 8, prev_point.y - 8,
    //            canvas_point.x + 8, canvas_point.y + 8);
    // lv_obj_invalidate_area(canvas, &area);
    // 调试输出
    // Serial.printf("Touch: x=%d, y=%d, state=%s\n",
    //              data->point.x,
    //              data->point.y,
    //              data->state == LV_INDEV_STATE_PRESSED ? "PRESSED" : "RELEASED");


        // Serial.printf("Touch: x=%d, y=%d, state=%s\n",
        //          curr_point.x,
        //          curr_point.y,
        //          data->state == LV_INDEV_STATE_PRESSED ? "PRESSED" : "RELEASED");
}

手写数字对触摸灵敏度要求不太高,设置时间死区 有时候反而会有断触的风险。我就只做了空间上的消抖处理,当移动范围超过设定的像素点时,esp32s3才会记录触点信息。默认判断只要保持按压状态,则始终会读取数据。canvs画布上的笔画效果,用lvgl中的画笔控件和draw_line接口就可以实现。然后在触摸区域数据读取上通过“画圆” 模拟画笔的效果。


     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; // 设置像素
            }
        }
    }

总结和硬件展示

 image.png
在画布区域进行触摸手写数字,在点击begin按钮控件后,led灯屏会显示识别结果,led灯屏2s后会自动熄灭

image.png

同时通过串口也会打印识别结果和”图案“。 如图上画×的区域是有笔记的区域,.是空白区域,这一部分是我之前调试的时让它打印的,把输入给神经网络模型的数据通过串口打印出来,可以最后校验一下输入数据有没有问题。

最后是感谢部分,其实前面在代码部分阐述问题时,也断断续续讲了很多了,时间比较紧张又是初次接触,最后的效果不太好,0-9十个数字只有5个能正常识别。一部分是因为网络结构不太好,全连接层占用的参数量比较大;另一部分是为节省RAM空间做了从double到float的量化,手法也很粗暴,是直接截取的。最终导致精度不太理想。
但是总体上,我觉得收获非常大,感谢平台举行的此次活动。下次我会继续参加(下次肯定留足时间,效果肯定比这次好!)

附件下载
main.zip
附件有大小限制 所以只上传了主要文件
手写数字识别代码_百度网盘链接.txt
全部代码以网盘链接形式上传
手写数字识别代码_百度网盘链接.txt
全部代码以网盘链接形式上传
团队介绍
暂时无
评论
0 / 100
查看更多
猜你喜欢
2025寒假练 - 使用“CrowPanel ESP32 Display 4.3”实现手写数字识别显示该项目使用了CrowPanel ESP32 Display 4.3,实现了手写数字识别显示的设计,它的主要功能为:在LCD屏幕上设定一个正方形的写字区域,在写字区域里书写0-9的数字,esp32运行神经网络算法对数字进行识别。 该项目使用了74HC595驱动的led点阵模块,实现了数字显示的设计,它的主要功能为:使用esp32驱动74HC595芯片,在灯板上显示数字。。
lihuahua
36
2025寒假练-使用CrowPanel ESP32 Display 4.3英寸HMI开发板完成手写识别显示该项目使用了CrowPanel ESP32 Display 4.3英寸HMI开发板,实现了手写识别显示的设计,它的主要功能为:LVGL画板、手写数字识别、灯板显示数字。
starry-m
57
2025寒假练-基于CrowPanel ESP32 Display 4.3英寸HMI开发板实现手写数字识别该项目使用了CrowPanel ESP32 Display 4.3英寸HMI开发板,实现了手写数字识别的设计,它的主要功能为:通过触摸屏幕,在画板上书写数字,通过ESP32进行推理,然后将推理结果显示到LED点阵上。 该项目使用了74HC595,实现了点亮8X8LED点阵的设计,它的主要功能为:通过程序控制,可以使用74HC595驱动点阵显示数字。 该项目使用了CrowPanel ESP32 Display 4.3英寸HMI开发板,实现了手写计算器的设计,它的主要功能为:通过触摸屏输入数字和运算符,实现个位数的加、减、乘运算。。
Elec
62
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号