摘要
手写识别,是指对在手写设备上书写时产生的有序轨迹信息进行识别的过程,是人机交互最自然、最方便的手段之一。随着智能手机和平板电脑移动设备的普及,手写识别的应用也被越来越多的设备采用。手写识别能够使用户按照最自然、最方便的输入方式进行文字输入,易学易用,可取代键盘或者鼠标。用于手写输入的设备有许多种,比如电磁感应手写板、压感式手写板、触摸屏、触控板、超声波笔等。
本文介绍基于STM32的触控屏手写数字识别。主控制器采用STM32F767ZGT6,通过触摸屏获取用户在输入区上的有序轨迹信息作为输入,再通过keras深度学习框架训练好相应的神经网络模型 ,利用STM32CUBE.AI工具箱将模型转换为C代码,移植到工程中,输入经过神经网络前向传播后,输出0~9每个数字的预测概率,预测概率最大的数字则认为是用户书写的数字,以此达到手写数字识别目的。
关键字:STM32、神经网络、手写识别
一、 主程序及硬件设计
1.1触摸屏简介
目前最常用的触摸屏有两种:电阻式触摸屏与电容式触摸屏。
(1)电阻式触摸屏
电阻触摸屏的主要部分是一块与显示器表面非常配合的电阻薄膜屏,这是一种多层的复合薄膜,它以一层玻璃或硬塑料平板作为基层,表面涂有一层透明氧化金属(透明的导电电阻)导电层,上面再盖有一层外表面硬化处理、光滑防擦的塑料层、它的内表面也涂有一层涂层、在他们之间有许多细小的(小于 1/1000 英寸)的透明隔离点把两层导电层隔开绝缘。当手指触摸屏幕时,两层导电层在触摸点位置就有了接触,电阻发生变化,在 X 和 Y 两个方向上产生信号,然后送触摸屏控制器。控制器侦测到这一接触并计算出(X, Y)的位置,再根据获得的位置模拟鼠标的方式运作。这就是电阻技术触摸屏的最基本的原理。
电阻触摸屏的优点:精度高、价格便宜、抗干扰能力强、稳定性好。
电阻触摸屏的缺点:容易被划伤、透光性不太好、不支持多点触摸。
(2)电容式触摸屏
电容屏是利用人体感应进行触点检测控制,不需要直接接触或只需要轻微接触,通过检测感应电流来定位触摸坐标。
本设计中的电容式触摸屏属于投射式电容触摸屏,该触摸屏采用纵横两列电极组成感应矩阵来感应触摸。以两个交叉的电极矩阵,即X轴电极和Y轴电极,来检测每一格感应单元的电容变化。

图1-1 投射式电容屏电极矩阵示意图
X、Y轴的电极电容屏的精度、分辨率与X、Y轴的通道数有关,通道数越多,精度越高。
电容触摸屏的优点: 手感好、无需校准、支持多点触摸、透光性好。
电容触摸屏的缺点:成本高、精度不高、抗干扰能力差。
1.2主程序设计
电容触摸屏一般需要一个驱动IC来检测电容触摸,本设计中的电容触摸屏采用OTT2001A作为驱动IC(10个感应通道,17个驱动通道)。通过此IC,MCU可知道当前触摸屏上是否有触摸及触摸位置的具体坐标。
主程序中,首先将用到的外设初始化,再初始化神经网络,初始化全部完成后,进入while(1)中不断获取输入并计算出结果。通过OTT2001A驱动IC不断获取当前触摸坐标,以此形成用户的输入轨迹,但由于手写区域大小为420*420个像素点,如果将这个420*420的数组直接作为输入进行运算,显然对于STM32,计算量和内存都难以接受,所以在获取轨迹输入时就将手写区域进行缩放,即将420*420的数组缩放到28*28,这样数据量和计算量就大大减少了。
在实际操作中,发现单就以触摸屏捕获的轨迹作为输入来说,轨迹太细长了特征不够明显,也和实际中的字体不相符,所以对轨迹进行了膨胀操作,即检测到一个像素点为实际轨迹,则将周围的八个像素点也标记为轨迹。

图1-2 单个像素点膨胀

图1-3 运动轨迹膨胀
在获取用户输入的过程中,同时进行着缩放和膨胀操作,当检测到用户手抬起(离开触摸屏表面)0.4S后即认为输入完成,将当前得到的轨迹传给神经网络的输入进行前向传播,最终获得神经网络的预测值。当再次检测到触摸屏有按压,即开始下一次识别过程。

图1-4 实际运行效果
二、 神经网络在stm32上的部署

图2-1 STM32CUBE.AI文档首页
2.1 数据准备
准备数据时,有以下两种思路:直接采用网络上现有开源数据集,或自己收集并标注数据。
2.1.1 Mnist数据集
Mnist数据集是一个手写体数据集,来自美国国家标准与技术研究所,由250个不同人手写的数字构成,其中共包含60000个训练样本。由于其样本充足,方便易得,所以本次设计第一次尝试用此数据集训练神经网络来预测。

图2-2 Mnist示例
在后续预测中发现准确率并不高,对比分析发现,Mnist数据集为真是手写数字样本与在屏幕上通过触摸产生的轨迹有一定差别,除此之外,Mnist数据集每个样本为单精度浮点型28*28二维数组,而实际输入只有0和1,这在一定程度上也影响了识别准确率。在尝试使用现有开源数据集失败后,只能自己制作数据集。
2.1.2 数据集制作
单片机每捕获一次用户输入,便通过串口将输入上传至电脑上位机,上位机接受单片机上传的28*28二维数组,并转换为图片格式保存在本地。

图2-3 数据集示例
但收集数据集确实是一个重复、繁重的工作,在0~9每个数字都收集了60个左右的样本后,决定直接采用数据增强赖扩充数据集。
数据增强是指,为了避免出现过拟合,通过图像的几何变换,使用一种或多种组合数据增强变换来增加输入数据的量,其中包括旋转|反射变换、翻转变换、缩放变换、平移变换、尺度变换、对比度变换等。

图2-4 数据增强示例
通过数据增强最终将总样本数增加到1200个。
2.2 神经网络训练
对于此类较简单的输入,首先可考虑全连接网络,首次采用的网络为3层全连接网络,其中两层隐藏层的神经元个数分别为256、128,网络结构如下:

图2-5 全连接网络结构图
40个epoch后:

图2-6 训练准确率曲线
通过全连接网络,已经取得了较好的效果在训练集上准确率到达99%,但是在验证集上的准确率仅90%,存在过拟合现象,在图2-5中可以发现,仅3层全连接网络参数就达到了惊人的235164个。接下来,尝试卷积神经网络观察这一状况是否能够得到改善。
卷积神经网络结构如下:

图2-7 卷积网络结构图
40个epoch后:

图2-7 训练准确率曲线
改为卷积网络后,过拟合现象并没有得到明显的改善,这可能是由训练样本不足以及在验证集中大多为通过数据增强产生的数据造成的。但本身训练集上99%,验证集上93%的准确率也足够用了,接下来就是在实际中检验。
最终应用的网络选择卷积神经网络,尽管在此类场景下无论是准确率还是过拟合现象的抑制,卷积神经网络较全连接网络的改善都不明显,但是对比两个网络的参数发现,全连接网络总参数为235146个,而卷积神经网络仅为37674个,从程序运行的实时性与资源的消耗上考虑,采用卷积神经网络是更好的选择。
2.3 代码转换及应用部署
STM32CUBE.AI工具箱是于2019年1月4日意法半导体公司借助STM32系列微控制器的市场领导地位,扩展了STM32微控制器开发生态环境STM32CUBE,增加的先进的人工智能(AI)功能,开发人员可以用STM32CUBE.AI将预先训练的神经网络转成可在STM32微控制器上运行的C代码,调用经过优化的函数库。需要注意的是,要运行此类代码,需要DSP库的支持,所以硬件平台仅支持如STM32F4、STM32F7等包含DSP内核系列的微控制器。

图2-8 模型消耗的资源
通过工具箱不但可以将神经网络转换为C代码,而且还能验证其能否在微控制器上正常工作,并计算出每层网络花费的时间。

图2-9 神经网络(CNN)每层时间消耗
由图2-9可以看到神经网络预测一次输入仅需要42ms左右,对于手写识别场景实时性完全足够,作为对比,我也验证了上文中三层全连接网络前向传播需要的时间。

图2-10 神经网络(ANN)每层时间消耗
达到同等效果的全连接网络消耗的时间仅6.5ms,但在生成network_data.c文件中可以看到,储存网络参数的数组长度达到了940584,而在卷积神经网络下仅150696。可见一般情况下轻量与速度不可兼得,神经网络模型的结构需结合实际应用场景选择,此处的应用场景对实时性的要求没那么高,所以选择消耗资源较少的卷积神经网络模型。
最后,将由AI工具箱转化的代码添加进先前创建好的工程,在程序中调用相关函数完成神经网络初始化后,即可通过其来预测用户输入。
三、总结
通过本次设计,实践了神经网络在STM32微控制器上的部署与应用。传统的手写识别与其他识别系统在训练学习的过程中,需要耗费大量的时间去进行特征提取、数据降维,最后再通过分类器得出最后的结果,同时还需要大量的知识储备,有着较高的技术门槛。而AI技术使用经过训练的人工神经网络对运动和振动传感器、环境传感器、麦克风和图像传感器的数据信号进行分类,比传统的手工信号处理方法更加快速、高效。我认为,不管是机器学习还是深度学习,它们的舞台绝不仅仅局限于在大型服务器亦或是高速计算机上运行,在嵌入式设备上有着更大的天地,如物联网、智能楼宇、工业和医疗等应用中的深度嵌入式设备。得益于近年来,神经网络领域和微控制器领域的飞速发展,使在微控制器上运行大规模神经网络成为可能,而ST的新型神经网络开发工具箱正在将AI引入基于微控制器的智能边缘和节点设备中。
附录一:部分程序
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* <h2><center>© Copyright (c) 2019 STMicroelectronics.
* All rights reserved.</center></h2>
*
* This software component is licensed by ST under Ultimate Liberty license
* SLA0044, the "License"; You may not use this file except in compliance with
* the License. You may obtain a copy of the License at:
* www.st.com/SLA0044
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "BSP_Init.h"
#include "APP_LCD.h"
#include "app_x-cube-ai.h"
#include "string.h"
/***************函数声明*****************/
uint8_t argmax(float my_array[]);
ai_float input_data[28][28]={{0}}; //神经网络输入
ai_float out_data[10]; //神经网络输出
int main(void)
{
uint8_t tcnt=100;
uint8_t Finish_Flag = 0; //用户输出完成标志位
uint8_t led_i=0; //循环变量
uint16_t lastpos[2]; //记录上一次触点位置
uint8_t posi_x,posi_y; //触点映射到神经网络输入的位置
BSP_Init(); //硬件外设初始化
MX_X_CUBE_AI_Init(); //神经网络初始化
printf("CUBE_AI_Init Ok!\r\n"); //打印信息,提示初始化完成
Interface_Init(); //LCD界面初始化
/* * * * 清空输入 * * * */
memset(input_data, 0, sizeof(input_data[0][0]) * 28 * 28);
while(1)
{
tp_dev.scan(0);
if(tp_dev.sta & TP_PRES_DOWN)
{
delay_ms(1); //必要的延时,否则程序会移植判断触摸屏按下
if(Finish_Flag == 0)
LCD_Fill(32,352,448,768,WHITE),LCD_Fill(423,271,449,297,WHITE);
tcnt=0;
//如果为有效输入(在手写区域内)
if( tp_dev.x[0]<450 && tp_dev.x[0]>30 &&
tp_dev.y[0]<770 && tp_dev.y[0]>350)
{
/****************坐标变换,将在手写区域内的轨迹缩放到28*28的大小******************/
posi_x =
(uint8_t)((tp_dev.y[0] - 350) * ((float)28 / 420.0) - 1);
posi_y =
(uint8_t)((tp_dev.x[0] - 30)* ((float)28 / 420.0) - 1);
if(posi_x>0 && posi_x<27 && posi_y>0 && posi_y<27)
{
/************轨迹膨胀**************/
input_data[posi_x-1][posi_y+1] = 1;
input_data[posi_x][posi_y+1] = 1;
input_data[posi_x+1][posi_y+1] = 1;
input_data[posi_x-1][posi_y] = 1;
input_data[posi_x+1][posi_y] = 1;
input_data[posi_x-1][posi_y+1] = 1;
input_data[posi_x][posi_y+1] = 1;
input_data[posi_x+1][posi_y+1] = 1;
}
input_data[posi_x][posi_y] = 1;
/* 在手写区域上方的28*28区域内绘制当前神经网络真实的输入 */
Draw_TrueImg(posi_x,posi_y);
if(lastpos[0]==0XFFFF)
{
lastpos[0] = tp_dev.x[0];
lastpos[1] = tp_dev.y[0];
}
lcd_draw_bline(lastpos[0],lastpos[1],tp_dev.x[0],tp_dev.y[0],2,BLUE);//画线
lastpos[0]=tp_dev.x[0];
lastpos[1]=tp_dev.y[0]; //记录此次触点,以描点绘线
Finish_Flag = 1;
}
}
else if(Finish_Flag == 1)
{
lastpos[0]=0XFFFF;
tcnt++;
led_i++;
delay_ms(10);
if(tcnt == 40) //400ms内无触摸即认为输入完成,开始预测
{
LCD_Fill(0,0,450,210,WHITE);
/* 将输入喂给神经网络,并将输出返回给out_data */
MX_X_CUBE_AI_Process(input_data,out_data);
/* 显示最终预测值 */
LCD_ShowNum(245,270,argmax(out_data),2,32);
/* 画出概率分布直方图 */
Draw_BarChart(out_data);
/* * * * 清空输入 * * * */
memset(input_data, 0, sizeof(input_data[0][0]) * 28 * 28);
// LCD_Fill(32,352,448,768,WHITE);
Finish_Flag = 0;
}
}
if(KEY_Scan(0)) //如果有按键按下则重新绘制LCD界面
{
LCD_Clear(WHITE);
Interface_Init();
}
delay_ms(5);led_i++;
if(led_i%40 == 0)LED0_Toggle; //LED闪烁,提示程序正常运行
}
}
/****************************************
* * *函数说明:
* 返回输入数组中最大值的下标
* **************************************/
uint8_t argmax(float my_array[])
{
uint8_t argmax_i,argmax_temp;
float max = -10.0;
for(argmax_i = 0;argmax_i < 10;++argmax_i)
{
if(my_array[argmax_i] > max)
{
max = my_array[argmax_i];
argmax_temp = argmax_i;
}
}
return argmax_temp;
}