第五十三章 手写识别实验
[mw_shl_code=c,true]1.硬件平台:正点原子探索者STM32F407开发板
2.软件平台:MDK5.1
3.固件库版本:V1.4.0[/mw_shl_code]
现在几乎所有带触摸屏的手机都能实现手写识别。本章,我们将利用ALIENTEK提供的手写识别库,在ALIENTEK探索者STM32F4开发板上实现一个简单的数字字母手写识别。本章分为如下几个部:
53.1 手写识别简介
53.2 硬件设计
53.3 软件设计
53.4 下载验证
53.1 手写识别简介
手写识别,是指对在手写设备上书写时产生的有序轨迹信息进行识别的过程,是人际交互最自然、最方便的手段之一。随着智能手机和平板电脑等移动设备的普及,手写识别的应用也被越来越多的设备采用。
手写识别能够使用户按照最自然、最方便的输入方式进行文字输入,易学易用,可取代键盘或者鼠标。用于手写输入的设备有许多种,比如电磁感应手写板、压感式手写板、触摸屏、触控板、超声波笔等。ALIENTEK探索者STM32F4开发板自带的TFTLCD触摸屏(2.8/3.5/4.3寸),可以用来作为手写识别的输入设备。接下来,我们将给大家简单介绍下手写识别的实现过程。
手写识别与其他识别系统如语音识别图像识别一样分为两个过程:训练学习过程;识别过程。如图53.1.1所示:
图53.1.1 字母数字识别系统示意图。
|
上图中虚线部分为训练学习过程,该过程首先需要使用设备采集大量数据样本,样本类别数目为0~9,a~z,A~Z总共62类,每个类别5~10个样本不等(样本越多识别率就越高)。对这些样本进行传统的把方向特征提取,提取后特征维数为512维,这对STM32来讲,计算量和模板库的存储量来说都难以接受,所以需要运行一些方法进行降维,这里采用LDA线性判决分析的方法进行降维,所谓线性判决分析,即是假设所有样本服从高斯分布(正态分布)对样本进行低维投影,以达到各个样本间的距离最大化。关于LDA的更多知识可以阅读(http://wenku.baidu.com/view/f05c731452d380eb62946d39.html)等参考文档。这里将维度降到64维,然后针对各个样本类别进行平均计算得到该类别的样本模板。
而对于识别过程,首先得到触屏输入的有序轨迹,然后进行一些预处理,预处理主要包括重采样,归一化处理。重采样主要是因为不同的输入设备不同的输入处理方式产生的有序轨迹序列有所不同,为了达到更好的识别结果我们需要对训练样本和识别输入的样本进行重采样处理,这里主要应用隔点重采样的方法对输入的序列进行重采样;而归一化就是因为不同的书写风格采样分辨率的差异会导致字体太小不同,因此需要对输入轨迹进行归一化。这里把样本进行线性缩放的方法归一化为64*64像素。
接下来进行同样的八方向特征提取操作。所谓八方向特征就是首相将经过预处理后的64*64输入进行切分成8*8的小方格,每个方格8*8个像素;然后对每个8*8个小格进行各个方向的点数统计。如某个方格内一共有10个点,其中八个方向的点分别为:1、3、5、2、3、4、3、2那么这个格子得到的八个特征向量为[0.1, 0.3, 0.5,0.2, 0.3, 0.4, 0.3, 0.2]。总共有64个格子于是一个样本最终能得到64*8=512维特征,更多八方向特征提取可以参考一下两个文档:
1,http://wenku.baidu.com/view/d37e5a49e518964bcf847ca5.html;
2,http://wenku.baidu.com/view/3e7506254b35eefdc8d333a1.html;
由于训练过程进行了LDA降维计算,所以识别过程同样需要对应的LDA降维过程得到最终的64维特征。这个计算过程就是在训练模板的过程中可以运算得到一个512*64维的矩阵,那么我们通过矩阵乘运算可以得到64维的最终特征值。
关于手写识别原理,我们就介绍到这里。如果想自己实现手写识别,那得花很多时间学习和研究,但是如果只是应用的话,那么就只需要知道怎么用就OK了,相对来说,简单的多。
ALIENTEK提供了一个数字字母识别库,这样我们不需要关心手写识别是如何实现的,只需要知道这个库怎么用,就能实现手写识别。ALIENTEK提供的手写识别库由4个文件组成:
ATKNCR_M_V2.0.lib、ATKNCR_N_V2.0.lib、atk_ncr.c和atk_ncr.h。
ATKNCR_M_V2.0.lib和ATKNCR_N_V2.0.lib是两个识别用的库文件(两个版本),使用的时候,选择其中之一即可。ATKNCR_M_V2.0.lib用于使用内存管理的情况,用户必须自己实现alientek_ncr_malloc和alientek_ncr_free两个函数。而ATKNCR_N_V2.0.lib用于不使用内存管理的情况,通过全局变量来定义缓存区,缓存区需要提供至少3K左右的RAM。大家根据自己的需要,选择不同的版本即可。ALIENTEK手写识别库资源需求:FLASH:52K左右,RAM:6K左右。
atk_ncr.c代码如下:
#include "atk_ncr.h"
#include "malloc.h"
//内存设置函数
void alientek_ncr_memset(char *p,char c,unsigned
long len)
{
mymemset((u8*)p,(u8)c,(u32)len);
}
//内存申请函数
void *alientek_ncr_malloc(unsigned int size)
{
return
mymalloc(SRAMIN,size);
}
//内存清空函数
void alientek_ncr_free(void *ptr)
{
myfree(SRAMIN,ptr);
}
这里,主要实现了alientek_ncr_malloc、alientek_ncr_free和alientek_ncr_memset等三个函数。
atk_ncr.h则是识别库文件同外部函数的接口函数声明
#ifndef __ATK_NCR_H
#define __ATK_NCR_H
//当使用ATKNCR_M_Vx.x.lib的时候,不需要理会ATK_NCR_TRACEBUF1_SIZE和
//ATK_NCR_TRACEBUF2_SIZE
//当使用ATKNCR_N_Vx.x.lib的时候,如果出现识别死机,请适当增加
//ATK_NCR_TRACEBUF1_SIZE和ATK_NCR_TRACEBUF2_SIZE的值
#define ATK_NCR_TRACEBUF1_SIZE 500*4
//定义第一个tracebuf大小(单位为字节),如果出现死机,请把该数组适当改大
#define ATK_NCR_TRACEBUF2_SIZE 250*4
//定义第二个tracebuf大小(单位为字节),如果出现死机,请把该数组适当改大
//输入轨迹坐标类型
__packed typedef struct _atk_ncr_point
{
short
x; //x轴坐标
short
y; //y轴坐标
}atk_ncr_point;
//外部调用函数
//初始化识别器
//返回值:0,初始化成功 1,初始化失败
unsigned char alientek_ncr_init(void);
void alientek_ncr_stop(void); //停止识别器
//识别器识别
//track:输入点阵集合 potnum:输入点阵的点数,就是track的大小
//charnum:期望输出的结果数,就是你希望输出多少个匹配结果
//mode:识别模式
//1,仅识别数字 2,进识别大写字母
//3,仅识别小写字母 4,混合识别(全部识别)
//result:结果缓存区(至少为:charnum+1个字节)
void alientek_ncr(atk_ncr_point * track,int potnum,int
charnum,unsigned char mode,char*result);
void alientek_ncr_memset(char *p,char c,unsigned
long len); //内存设置函数
//动态申请内存,当使用ATKNCR_M_Vx.x.lib时,必须实现.
void *alientek_ncr_malloc(unsigned int size);
//动态释放内存,当使用ATKNCR_M_Vx.x.lib时,必须实现.
void alientek_ncr_free(void *ptr);
#endif
此段代码中,我们定义了一些外部接口函数以及一个轨迹结构体等。
alientek_ncr_init,该函数用与初始化识别器,该函数在.lib文件实现,在识别开始之前,我们应该调用该函数。
alientek_ncr_stop,该函数用于停止识别器,在识别完成之后(不需要再识别),我们调用该函数,如果一直处于识别状态,则没必要调用。该函数也是在.lib文件实现。
alientek_ncr,该函数就是识别函数了。它有5个参数,第一个参数track,为输入轨迹点的坐标集(最好200以内);第二个参数potnum,为坐标集点坐标的个数;第三个参数charnum,为期望输出的结果数,即希望输出多少个匹配结果,识别器按匹配程度排序输出(最佳匹配排第一);第四个参数mode,该函数用于设置模式,识别器总共支持4中模式:
1,仅识别数字
2,进识别大写字母
3,仅识别小写字母
4,混合识别(全部识别)
最后一个参数是result,用来输出结果,注意这个结果是ASCII码格式的。
alientek_ncr_memset、alientek_ncr_free和alientek_ncr_free这3个函数在atk_ncr.c里面实现,这里就不多说了。
最后,我们看看通过ALIENTEK提供的手写数字字母识别库实现数字字母识别的步骤:
1) 调用alientek_ncr_init函数,初始化识别程序
该函数用来初始化识别器,在手写识别进行之前,必须调用该函数。
2) 获取输入的点阵数据
此步,我们通过触摸屏获取输入轨迹点阵坐标,然后存放到一个缓存区里面,注意至少要输入2个不同坐标的点阵数据,才能正常识别。注意输入点数不要太多,太多的话,需要更多的内存,我们推荐的输入点数范围:100~200点。
3) 调用alientek_ncr函数,得到识别结果.
通过调用alientek_ncr函数,我们可以得到输入点阵的识别结果,结果将保存在result参数里面,采用ASCII码格式存储
4) 调用alientek_ncr_stop函数,终止识别.
如果不需要继续识别,则调用alientek_ncr_stop函数,终止识别器。如果还需要继续识别,重复步骤2和步骤3即可。
以上4个步骤,就是使用ALIENTEK手写识别库的方法,十分简单。
53.2 硬件设计
本章实验功能简介:开机的时候先初始化手写识别器,然后检测字库,之后进入等待输入状态。此时,我们在手写区写数字/字符,在每次写入结束后,自动进入识别状态,进行识别,然后将识别结果输出在LCD模块上面(同时打印到串口)。通过按KEY0可以进行模式切换(4种模式都可以测试),通过按KEY2,可以进入触摸屏校准(如果发现触摸屏不准,请执行此操作)。DS0用于指示程序运行状态。
本实验用到的资源如下:
1) 指示灯DS0
2) KEY0和KEY2两个按键
3) 串口
4) TFTLCD模块(含触摸屏)
5) SPI FLASH
这些用到的硬件,我们在之前都已经介绍过,这里就不再介绍了。
53.3 软件设计
打开本章实验工程目录可以看到,我们在工程根目录文件夹下新建一个ATKNCR的文件夹。将ALIETENK提供的手写识别库文件(ATKNCR_M_V2.0.lib、ATKNCR_N_V2.0.lib、atk_ncr.c和atk_ncr.h这四个个文件,在光盘à 4,程序源码à5,ATKNCR(数字字母手写识别库) 文件夹里面)拷贝到该文件夹下,然后在工程里面新建一个ATKNCR的组,将atk_ncr.c和ATKNCR_M_V2.0.lib加入到该组下面(这里我们使用内存管理版本的识别库)。最后,将ATKNCR文件夹加入头文件包含路径。
关于ATKNCR_M_V2.0.lib和atk_ncr.c前面已有介绍,我们这里就不再多说,我们在main.c里面修改代码如下:
//最大记录的轨迹点数
atk_ncr_point
READ_BUF[200];
//画水平线
//x0,y0:坐标
len:线长度 color:颜色
void
gui_draw_hline(u16 x0,u16 y0,u16 len,u16 color)
{
if(len==0)return;
LCD_Fill(x0,y0,x0+len-1,y0,color);
}
//画实心圆
//x0,y0:坐标
r:半径 color:颜色
void
gui_fill_circle(u16 x0,u16 y0,u16 r,u16 color)
{
u32 i;
u32 imax = ((u32)r*707)/1000+1;
u32 sqmax = (u32)r*(u32)r+(u32)r/2;
u32 x=r;
gui_draw_hline(x0-r,y0,2*r,color);
for (i=1;i<=imax;i++)
{
if ((i*i+x*x)>sqmax)// draw
lines from outside
{
if
(x>imax)
{
gui_draw_hline
(x0-i+1,y0+x,2*(i-1),color);
gui_draw_hline
(x0-i+1,y0-x,2*(i-1),color);
}
x--;
}
// draw lines from inside
(center)
gui_draw_hline(x0-x,y0+i,2*x,color);
gui_draw_hline(x0-x,y0-i,2*x,color);
}
}
//两个数之差的绝对值
//x1,x2:需取差值的两个数
//返回值:|x1-x2|
u16 my_abs(u16
x1,u16 x2)
{
if(x1>x2)return x1-x2;
else return x2-x1;
}
//画一条粗线
//(x1,y1),(x2,y2):线条的起始坐标
//size:线条的粗细程度
//color:线条的颜色
void
lcd_draw_bline(u16 x1, u16 y1, u16 x2, u16 y2,u8 size,u16 color)
{
u16 t;
int xerr=0,yerr=0,delta_x,delta_y,distance;
int incx,incy,uRow,uCol;
if(x1<size|| x2<size||y1<size||
y2<size)return;
delta_x=x2-x1; //计算坐标增量
delta_y=y2-y1;
uRow=x1; uCol=y1;
if(delta_x>0)incx=1; //设置单步方向
else if(delta_x==0)incx=0;//垂直线
else {incx=-1;delta_x=-delta_x;}
if(delta_y>0)incy=1;
else if(delta_y==0)incy=0;//水平线
else{incy=-1;delta_y=-delta_y;}
if( delta_x>delta_y)distance=delta_x;
//选取基本增量坐标轴
else distance=delta_y;
for(t=0;t<=distance+1;t++ )//画线输出
{
gui_fill_circle(uRow,uCol,size,color);//画点
xerr+=delta_x ; yerr+=delta_y ;
if(xerr>distance){
xerr-=distance; uRow+=incx;}
if(yerr>distance){
yerr-=distance;uCol+=incy;}
}
}
int main(void)
{
u8
i=0; u8 tcnt=0; u8 key;u8 res[10];
u16 pcnt=0; u8 mode=4; //默认是混合模式
u16
lastpos[2]; //最后一次的数据
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2
delay_init(168); //初始化延时函数
uart_init(115200); //初始化串口波特率为115200
LED_Init(); //初始化LED
LCD_Init(); //LCD初始化
KEY_Init(); //按键初始化
W25QXX_Init(); //初始化W25Q128
tp_dev.init(); //初始化触摸屏
my_mem_init(SRAMIN); //初始化内部内存池
my_mem_init(SRAMCCM); //初始化CCM内存池
alientek_ncr_init(); //初始化手写识别
POINT_COLOR=RED;
while(font_init())
//检查字库
{
LCD_ShowString(60,50,200,16,16,"Font
Error!"); delay_ms(200);
LCD_Fill(60,50,240,66,WHITE);//清除显示
}
RESTART:
POINT_COLOR=RED;
Show_Str(60,10,200,16,"探索者STM32F407开发板",16,0);
Show_Str(60,30,200,16,"手写识别实验",16,0);
Show_Str(60,50,200,16,"正点原子@ALIENTEK",16,0);
Show_Str(60,70,200,16,"KEY0:MODE
KEY2:Adjust",16,0);
Show_Str(60,90,200,16,"识别结果:",16,0);
LCD_DrawRectangle(19,114,lcddev.width-20,lcddev.height-5);
POINT_COLOR=BLUE;
Show_Str(96,207,200,16,"手写区",16,0);
tcnt=100; tcnt=100;
while(1)
{
key=KEY_Scan(0);
if(key==KEY2_PRES&&(tp_dev.touchtype&0X80)==0)
{
TP_Adjust(); //屏幕校准
LCD_Clear(WHITE);
goto RESTART; //重新加载界面
}
if(key==KEY0_PRES)
{
LCD_Fill(20,115,219,314,WHITE);//清除当前显示
mode++;
if(mode>4)mode=1;
switch(mode)
{
case 1:Show_Str(80,207,200,16,"仅识别数字",16,0);break;
case 2:Show_Str(64,207,200,16,"仅识别大写字母",16,0);break;
case 3:Show_Str(64,207,200,16,"仅识别小写字母",16,0);break;
case 4:Show_Str(88,207,200,16,"全部识别",16,0);break;
}
tcnt=100;
}
tp_dev.scan(0);//扫描
if(tp_dev.sta&TP_PRES_DOWN)//有按键被按下
{
delay_ms(1);//必要的延时,否则老认为有按键按下.
tcnt=0;//松开时的计数器清空
if((tp_dev.x[0]<(lcddev.width-20-2)&&tp_dev.x[0]>=(20+2))&&(tp_dev.y[0]
<(lcddev.height-5-2)&&tp_dev.y[0]>=(115+2)))
{
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];
if(pcnt<200)//总点数少于200
{
if(pcnt)
{
if((READ_BUF[pcnt-1].y!=tp_dev.y[0])&&(READ_BUF[pcnt-1]
.x!=tp_dev.x[0]))//x,y不相等
{
READ_BUF[pcnt].x=tp_dev.x[0];
READ_BUF[pcnt].y=tp_dev.y[0];
pcnt++;
}
}else
{
READ_BUF[pcnt].x=tp_dev.x[0];
READ_BUF[pcnt].y=tp_dev.y[0];
pcnt++;
}
}
}
}else //按键松开了
{
lastpos[0]=0XFFFF;
tcnt++;delay_ms(10);
i++;
if(tcnt==40)
{
if(pcnt)//有有效的输入
{
printf("总点数:%d\r\n",pcnt);
alientek_ncr(READ_BUF,pcnt,6,mode,(char*)res);
printf("识别结果:%s\r\n",res);
pcnt=0;
POINT_COLOR=BLUE;//设置画笔蓝色
LCD_ShowString(60+72,90,200,16,16,res);
}
LCD_Fill(20,115,lcddev.width-20-1,lcddev.height-5-1,WHITE);
}
}
if(i==30) {i=0; LED0=!LED0;}
}
}
这里代码看上去比较多,其实很多都是为lcd_draw_bline函数服务的,lcd_draw_bline函数用于实现画指定粗细的直线,以得到较好的画线效果。而main函数,则实现53.1.2节提到的功能。其中,READ_BUF用来存储输入轨迹点阵,大小为200,即最大输入不能超过200点,注意:这里我们采集的都是不重复的点阵(即相邻的坐标不相等)。这样可以避免重复数据,而重复的点阵数据对识别是没有帮助的。
至此,本实验的软件设计部分结束。
53.4 下载验证
在代码编译成功之后,我们下载代码到ALIENTEK探索者STM32F4开发板上,得到,如图53.4.1所示:
图53.4.1 手写识别界面
此时,我们在手写区写数字/字母,即可得到识别结果,如图53.4.2所示:
图53.4.2 手写识别结果
按下KEY0可以切换识别模式,同时在识别区提示当前模式。按下KEY2可以进行屏幕校准。每次识别结束,会在串口打印本次识别的输入点数和识别结果,大家可以通过串口助手查看。
正点原子探索者STM32F407开发板购买地址:http://item.taobao.com/item.htm?id=41855882779
|