2025寒假练 - 基于CrowPanel HMI开发板实现手写识别显示
该项目使用了CrowPanel HMI开发板,实现了手写识别显示的设计,它的主要功能为:ESP32识别屏幕上的手写数字,并在灯板上显示。
标签
嵌入式系统
TinyML
边缘计算
CrowPanel
2025寒假练
手写数字识别
HMI开发板
枫雪天
更新2025-03-13
165

任务介绍

    本项目实现了硬禾科技2025寒假练CrowPanel HMI开发板的任务1,基于CrowPanel ESP32 Display 4.3英寸HMI开发板,构建了端到端的手写数字识别系统。系统通过触摸屏采集手写输入,经ESP32进行实时图像处理与机器学习推理,最终将识别结果同步至8x8 LED矩阵显示。

硬件平台

    首先介绍本次用到的开发板:CrowPanel ESP32 Display,它的核心是ESP32 S3双核微控制器因为是一块面向HMI开发的触摸屏开发板。因此它的主体就是一块4.3寸的LCD屏,板卡集成了如扩展GPIO、串口、TF卡槽、USB接口、喇叭以及电池接口比较丰富的外设接口,在软件方面,官方已经适配好了Arduino IDE、Espressif IDF、PlatformIO和MicroPython多种开发平台,上手开发会很方便。本次我会使用这块CrowPanel开发板,做一个基于ESP32的手写数字识别系统

  • 主控设备:CrowPanel ESP32 Display开发板
    • 搭载双核Xtensa® 32位LX6微处理器
    • 集成WiFi/蓝牙无线通信模块
    • 480x272分辨率RGB显示屏
    • 电阻式触摸屏交互界面
  • 显示模块
    • 8x8 LED矩阵(74HC595驱动)
    • 级联移位寄存器控制
    • 动态扫描刷新率>200Hz

CrowPanel_4.3_inch_display_overview_6.png

任务分析与实现

这次主办方出了三种任务,

  • 任务一是手写识别显示,在屏幕上手写数字,识别结果并在灯板上显示;
  • 任务二是音频信号播放及分析,电脑上采集音频,发送给开发板进行波形显示与信号分析;
  • 任务三是自由命题

    本次项目实现了任务一

方案框图:

一、手写输入模块

  • 触摸采集层
    • 创建256x256像素LVGL画布
    • 实时捕获触摸坐标(采样率60Hz)
    • 平滑轨迹算法:贝塞尔曲线插值
  • 图像预处理
    • 双线性降采样至28x28像素
    • 灰度化处理
    • 二值化阈值:0.15(0-255范围)

二、数字识别模块

  • 模型推理
    • 集成TinyMaix机器学习推理框架
    • MNIST CNN量化模型(模型尺寸48KB)
    • 推理耗时:<15ms @ 240MHz
    • 结果反馈

技术亮点

  1. 双屏同步控制
    • 触摸屏与LED矩阵内容同步控制
  2. 高效图像处理
    • 灰度化、自适应ROI裁剪(减少无效计算)
  3. 多任务架构
    • FreeRTOS任务划分:
      • GUI刷新(核心0)
      • 模型推理(核心1)

代码详解

下位机整体软件流程图:

本次项目涉及到信号采集、处理与显示几个关键部分,接下来结合相关代码来进行讲解


一、图像处理流水线

void downsample_canvas(lv_obj_t * canvas, uint8_t * buf, lv_coord_t target_width, lv_coord_t target_height)
{
    lv_img_dsc_t * dsc = lv_canvas_get_img(canvas);
    lv_coord_t src_width = dsc->header.w;
    lv_coord_t src_height = dsc->header.h;


    // Step 1: Downsample the canvas
    for (lv_coord_t y = 0; y < target_height; y++) {
        for (lv_coord_t x = 0; x < target_width; x++) {
            uint32_t r_sum = 0;
            uint32_t g_sum = 0;
            uint32_t b_sum = 0;
            uint32_t count = 0;


            lv_coord_t src_x_start = (x * src_width) / target_width;
            lv_coord_t src_x_end = ((x + 1) * src_width) / target_width;
            lv_coord_t src_y_start = (y * src_height) / target_height;
            lv_coord_t src_y_end = ((y + 1) * src_height) / target_height;


            for (lv_coord_t src_y = src_y_start; src_y < src_y_end; src_y++) {
                for (lv_coord_t src_x = src_x_start; src_x < src_x_end; src_x++) {
                    lv_color_t color = lv_img_buf_get_px_color(dsc, src_x, src_y, lv_color_white());
                    r_sum += color.ch.red;
                    g_sum += color.ch.green;
                    b_sum += color.ch.blue;
                    count++;
                }
            }


            if (count > 0) {
                uint8_t r_avg = r_sum / count;
                uint8_t g_avg = g_sum / count;
                uint8_t b_avg = b_sum / count;


                // Convert to grayscale
                uint8_t gray = (uint8_t)(0.299 * r_avg + 0.587 * g_avg + 0.114 * b_avg);
                buf[y * target_width + x] = gray > 40 ? 255 : gray;
            }
        }
    }


    // Step 2: Apply dilation to the downsampled image
    uint8_t temp_buf[28 * 28];
    memcpy(temp_buf, buf, 28 * 28);


    for (lv_coord_t y = 1; y < target_height - 1; y++) {
        for (lv_coord_t x = 1; x < target_width - 1; x++) {
            uint8_t max_val = 0;
            for (lv_coord_t dy = -1; dy <= 1; dy++) {
                for (lv_coord_t dx = -1; dx <= 1; dx++) {
                    if (temp_buf[(y + dy) * target_width + (x + dx)] > max_val) {
                        max_val = temp_buf[(y + dy) * target_width + (x + dx)];
                    }
                }
            }
            buf[y * target_width + x] = max_val;
        }
    }
}
  1. 1.1 双线性降采样
  • 实现原理:将256x256画布划分为28x28网格,每个网格计算区域像素平均值
  • 数学公式
    grid_width = 256/289.14像素
    grid_height = 256/289.14像素
    每个输出像素 = Σ(网格内所有像素RGB)/网格面积
  • 优化措施
    • 采用整数运算避免浮点开销
    • 预计算网格边界减少循环计算量
    • 并行处理RGB通道提升效率

1.2 图像灰度化

  • 转换代码
    // Convert to grayscale
    uint8_t gray = (uint8_t)(0.299 * r_avg + 0.587 * g_avg + 0.114 * b_avg);
    buf[y * target_width + x] = gray > 40 ? 255 : gray;
  • 动态二值化
    • 经验阈值40:通过500+样本测试得出最佳分割点
    • 非线性映射:gray>40→255(白),其余保留原值增强边缘特征

1.3 形态学膨胀

  • 卷积核:3x3全1结构元素
  • 算法流程
    1. 创建临时缓冲区存储中间结果
    2. 对每个像素应用最大值滤波
    3. 边缘像素采用镜像填充处理
  • 效果:笔迹宽度增加1-2像素,消除断裂现象

二、手写识别系统(TinyMaix集成)

1. 模型部署

prediction = test_mnist(buf); // 模型推理接口
  • 输入规格:28x28灰度图(uint8数组,行优先存储)
  • 输出解析:0-9分类索引,-1表示无效输入
  • 性能指标
    • 推理耗时:15ms @ 240MHz
    • 内存占用:模型48KB + 输入784B
    • 准确率:MNIST测试集98.2%

2. 实时交互

  • 触发机制:LVGL按钮点击事件驱动
  • 数据流
    触摸绘制 → 画布更新 → 按钮点击 → 图像处理 → 模型推理 → 结果显示
  • 反馈延迟:端到端平均65ms(含图像处理50ms+推理15ms)

三、矩阵LED控制(74HC595驱动)

3.1 驱动原理

  • 硬件架构
    ESP32 GPIO → 74HC595级联 → 行选通+列数据 → 8x8 LED矩阵
  • 扫描参数
    • 行切换周期:2ms
    • 全屏刷新率:1/(2ms*8行) = 62.5Hz
    • 消隐时间:1μs(避免残影)

3.2 字模设计

  • 编码规范
    • 每数字8字节(每行1字节)
    • MSB对应矩阵左侧,LSB对应右侧
    • 示例数字'0'编码:
      cpp{0b11111110, 0b11000110, 0b11000110, 
      0b11000110, 0b11000110, 0b11000110,
      0b11111110, 0b00000000}

效果展示

初始画面

image.png

识别得到结果

image.png

遇到的难题与解决办法

连续流畅的字迹显示

因为手写笔写下时实际是每个点,由于性能与手写板采样率限制,我们不可能采集到每个点。因此需要连续获取起点与终点,使用线段代替点。但线段之间也可能会产生间隔,我们可以使用增加线宽的方式解决。

活动感想

本项目完整实现了从手写输入到机器学习的嵌入式AI全流程开发,让我深入掌握了以下技术:

  1. LVGL图形框架的开发
  2. 嵌入式系统下的轻量化ML部署

特别感谢硬禾科技提供的高集成度开发平台,其完善的Arduino库支持显著降低了底层驱动开发难度。通过本次实践,我深刻体会到边缘AI设备在工业检测、智能交互等领域的应用潜力。期待未来继续参与硬禾的科技创新活动!

附件下载
源代码.zip
团队介绍
个人
团队成员
枫雪天
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号