一、 项目介绍
本项目通过CrowPanel ESP32 Display 4.3英寸HMI开发板实现了手写识别显示。该项目在vscode的platformio平台使用C++进行开发,通过LVGL编程实现ui界面,在LCD屏幕上设定了一个正方形的写字区域,可以在写字区域内书写0-9的数字,并对书写的数字进行识别,将结果显示在4.3寸的屏幕和一个外接的LED点阵屏上。手写数字识别模型使用torch进行训练,数据集采用MNIST手写数字数据集,该模型在仿真的过程中识别准确率可以到达97%左右。esp32上总的资源占用情况为RAM使用36.2%,Flash资源使用21.5%。
二、 硬件介绍
本项目的硬件分为两部分,首先是CrowPanel ESP32 Display 4.3英寸HMI开发板,Elecrow ESP32 4.3英寸LCD显示屏是一款功能强大的HMI触摸屏,具有480*272分辨率。它使用ESP32-S3-WROOM-1-N4R2模组作为主控处理器,适用于物联网应用设备等场景。
开发板的实物图和原理图如下
然后是用于数字显示的led点阵屏,此点阵屏8*8共64颗单色LED灯,同时搭配了两颗串并变换、SOCIC-16封装的74HC595D芯片,并且引出了需要输入信号的引脚。
灯板的实物图和原理图如下
对于灯板和开发板的连接,选择开发板的IO17、18、37分别和灯板的->d、SRCLK、RCLK引脚进行连接即可。
三、 方案框图和设计思路
上图为方案的框图,从图中可以看出,我们先使用torch训练出手写数字识别的模型,然后将其转换成相同结构的keras模型,并将用torch训练好的权重赋值给keras模型。完成模型转化后,使用Tinymlgen库把keras模型转换成C++能调用的头文件。对于开发板的UI设计使用LVGL库来完成,使用LVGL库的画布控件配合触摸屏函数完成手写数字的输入,并使用按钮控件完成清空和识别功能。识别完成后将结果显示在显示屏上,并用相应的io控制将数字显示在点阵屏上。
四、 软件流程图
上图为软件流程图,从图中可以看出,程序开始运行先进行初始化,包括画布控件的位置、大小、不透明度、画笔的粗细、按钮的大小、功能等。然后程序就进入loop,开始循环处理一些操作。首先是判断用于输入手写字体的画布区域是否被按下,如果被按下了,就要将每个触摸点之间用线段连起来,绘制出需要的线条,否则直接判断清除按钮是否被按下,如果被按下了就清除画布上已经画的内容,同时将点阵屏用于显示的数组清零;否则直接进入下一步,判断预测按钮有没有被按下,如果被按下了,则调用模型进行预测,将预测结果显示在屏幕上,同时更新用于点阵屏显示的数组;否则直接完成一次点阵屏显示。
五、关键代码介绍
首先是python的训练模型
class MnistModel(nn.Module):
def __init__(self):
super(MnistModel, self).__init__()
self.conv1 = Conv2d(in_channels=1, out_channels=5, kernel_size=5, stride=1, padding=0)
self.maxpool1 = MaxPool2d(2)
self.conv2 = Conv2d(in_channels=5, out_channels=5, kernel_size=5, stride=1, padding=0)
self.maxpool2 = MaxPool2d(4)
self.linear1 = Linear(20, 10)
self.relu = ReLU()
def forward(self, x):
x = self.relu(self.maxpool1(self.conv1(x)))
# conv1_output = x
x = self.relu(self.maxpool2(self.conv2(x)))
# conv2_output = x
x = x.view(x.size(0), -1)
x = self.linear1(x)
return x
这是用torch定义的模型,模型的结构第一层为输入通道为1,输出通道为5,卷积核为5,步长为1,不填充的卷积层,然后经过池化和激活函数,第二层为输入通道为5,输出通道为5,卷积核为5*5,步长为1不填充的卷积层,然后经过池化和激活函数;将得到的数据进行铺平,最后经过一个全连接得到每个分类的预测值,最后再通过排序得到最有可能的预测结果。
然后是esp32的核心代码框图如下:
从图中可以看出,在main文件中定义的setup函数复制完成各部分的初始化,包括串口、引脚、lvgl、lcd显示等功能的初始化,在loop函数中处理lvgl的定时器处理器,手写数字数据处理与预测,led点阵屏显示的功能。在ui_screen1文件中完成ui界面的控件配置,包括按钮、画布控件的位置大小等配置;在ui文件中完成按钮事件的定义,这部分主要用于将外部变量进行修改,然后交给loop函数进行相关处理。
void WriteByte(unsigned char Byte_arr[])
{
unsigned char col_num = 0x80;
for(int j=0;j<8;j++)
{
unsigned char Byte = Byte_arr[j];
for(int i=0;i<8;i++)
{
if((col_num&(0x80>>i))!= 0x00 )
{
digitalWrite(17,HIGH);
}else{
digitalWrite(17,LOW);
}//向右移位 8个数据排好队依次移动位置
digitalWrite(18,HIGH);
digitalWrite(18,LOW);
}
col_num = col_num>>1;
for(int i=0;i<8;i++)
{
if((Byte&(0x80>>i))!= 0x00 )
{
digitalWrite(17,HIGH);
}else{
digitalWrite(17,LOW);
}//向右移位 8个数据排好队依次移动位置
digitalWrite(18,HIGH);
digitalWrite(18,LOW);
}
digitalWrite(37,HIGH);
digitalWrite(37,LOW);
delay(1);
}
}
上述代码为led点阵屏的显示驱动代码,主要通过两个串并转换移位寄存器和人眼的视觉暂留效应实现led点阵屏的图像显示
void loop()
{
lv_timer_handler();
// if(led == 1)
// {
// //printf("LED_ON\r\n");
// }
if(led == 0)
{
predict_led_num = led_Byte[10];
}
if(led == 1 && process_flag == 0)
{
lv_img_dsc_t * img_dsc = lv_canvas_get_img(canvas);
float pic_data[784];
int pic_pos = 0;
for (int y = 0; y < 224; y=y+8) {
// printf("[");
for (int x = 0; x < 224; x=x+8) {
int index = y * 224 + x;
uint8_t byte = img_dsc->data[index / 4];
uint8_t alpha = (3-(byte >> ((index % 4) * 2)) & 0x03);
// printf("%d",alpha);
pic_data[pic_pos] = (alpha/3.0);
pic_pos++;
// if(x!=216)
// {
// printf(",");
// }
}
// printf("],");
// printf("\r\n");
}
float predicted[10];
float mnlss=ml.predict(pic_data,predicted);
// for(int t=0;t<10;t++)
// {
// printf("%f\r\n",predicted[t]);
// }
// printf("%d\r\n",predicted);
int max_num = 0;
float max_value = predicted[0];
for(int t=1;t<10;t++)
{
if(max_value < predicted[t])
{
max_num = t;
max_value = predicted[t];
}
}
lv_label_set_text_fmt(num_label,"num: %d",max_num);
predict_led_num = led_Byte[max_num];
process_flag = 1;
//printf("LED_OFF\r\n");
}
WriteByte(predict_led_num);
}
上述代码为loop中一直循环的操作,通过按键事件触发修改led和process_flag这个两个外部变量,然后在循环中判断变量的值,完成预测和清除的操作。同时在预测操作被按下时,会对手写图片进行下采样,变成28*28,此处的处理操作比较简单,后续可以继续改进。
六、功能展示及说明
功能展示如下图所示,在中间黑色的区域内写下数字,按下预测按钮后显示预测的结果,并在led点阵屏上显示
七、遇到的困难和解决方法
在本项目中,遇到的困难主要有两个,首先,如何选择合适的模型结构应用在esp32上,因为esp32的资源有限,不可能去运行网上准确率非常高,参数非常多的模型,因此需要对别人的网络结构进行删减训练,到达合适的程度;第二个困难是如何把python训练好的模型移植到esp32上,这里主要通过python的tinymlgen库进行转化成可以调用的头文件。
八、心得体会
经过这次寒假在家一起练,让我了解到了一些深度学习的知识、esp32的lvgl开发,收获很大!