数字识别系统经典教材

chenlian 贡献于2012-02-14

作者 赵辉  创建于2004-04-28 16:16:00   修改者MC SYSTEM  修改于2006-09-27 07:12:00字数77614

文档摘要:该系统是为了辨认识别图像中的数字而设计的,它通过对图片的一系列处理,最后识别得出图片中显示的数字。系统既可以单独使用,也可以把它作为一个识别系统的软件核心应用到车牌识别等系统中去。
关键词:

 第11章 数字识别系统 11.1 系统简介 该系统是为了辨认识别图像中的数字而设计的,它通过对图片的一系列处理,最后识别得出图片中显示的数字。系统既可以单独使用,也可以把它作为一个识别系统的软件核心应用到车牌识别等系统中去。 11.2 系统基本技术要求 下面是系统具体要达到的基本技术要求 (1)输入图片中可以含有多个数字; (2)数字的识别准确率大于90%; (3)每张图片的处理时间(识别时间)不能大于1秒; (4)对图片噪声具有较强的适应性; (5)系统要能长时间无故障运行; (6)系统的操作要求简单。 11.3 系统中用到的关键技术 在本系统中用到了好多图像处理中的相关技术:比如灰度化、二值化、图像内容自动调整、去离散点、图像的缩放、细化、曲线平滑、曲线去枝桠操作等,最后还使用了神经网络对提取到的数字信息进行分析判断。 11.4 系统软硬件平台 11.4.1 系统的硬件平台 因为系统运行的过程当中,主要进行的都是图像处理,在这个过程当中要进行大量的数据处理,所以处理器和内存要求比较高,CPU 要求主频在600hz 以上(含600hz),内存在128 兆以上(含128 兆)。 11.4.2 系统的软件平台 系统可以运行于任何一台装有Windows98 、Windows2000 或者Windows XP 的机子上。 程序调试时,需要使用Microsoft Visual C++ 6.0(SP6)。 11.4.3 本章源码使用说明 本系统的全部源码在本书配套光盘的【\Chap11\】目录下。可参看readme.txt 进行使用。 11.5 系统实现 系统在实现的过程当中,先分解成两个大块,就是图像预处理模块和数字识别模块。其中图像像预处理块在对图像进行了一系列变换后把最后提取到的数字字符提交给数字识别模块,然后进行识别并给处结果。在这里用到了很多先进的图像预处理技术及神经网络技术。 11.5.1 系统流程图 本系统总的流程结构如图11-1所示。 图11-1 系统总流程图 其中图像预处理的流程如图11-2 所示。 图11-2 图像预处理流程图 神经网络数字识别的具体流程如图11-3 所示。 图11-3 神经网络数字识别流程图 11.5.2 程序实现 整个系统的程序实现分为图像预处理和神经网络识别两大模块。在图像预处理的过程当中,我们采用了许多图像处理的技术,最后把每个数字的特征提取出来。这些技术包括图像数据读取、图像的灰度化、二值化、图像的调整、离散噪声点的去除、字符的切分、图像的缩放、字符的细化、字符的平滑、图像的求梯度等图像处理技术,最后是数字字符特征的提取。其结果再利用神经网络(这里我们选用BP网络)进行字符识别。 利用神经网络进行字符识别的过程主要包括网络的训练、数据的读取、字符的判定、结果的输出。下面按照程序执行的顺序介绍整个程序并逐一分析每一步的关键代码。最后简要讲述一下程序的使用方法、注意事项以及本章小结。 1. 图像数据的读取、保存与屏幕显示等基本函数 要进行图像分析和处理首先就要得到图像的数据,这些数据包括图像的宽、高、每个象素点的颜色值。因为每种文件都有它自己的存放格式,下面简单介绍BMP 文件的存放格式。本程序采用的输入图像即为bmp位图。 BMP(bitmap 的缩写)文件格式是Windows 本身可以直接提供读取支持的位图文件格式。一个BMP 格式的文件通常有.BMP 的扩展名,但有一些是以.rle为扩展名的,rle 的意思是行程长度编码(run length encoding)。这样的文件意味着其使用的数据压缩方法是BMP 格式文件支持的两种rle 方法中的一种。BMP 文件可用每象素1、4、8、16 或24 位来编码颜色信息,这个位数称作图像的颜色深度,它决定了图像所含的最大颜色数。一幅1-bpp(位每象素,bit per pixel)的图像只能有两种颜色。而一幅24-bpp 的图像可以有超过16 兆种不同的颜色。 一个典型BMP 文件的结构。以256 色也就是8-bpp 为例,文件被分成四个主要的部分:一个位图文件头,一个位图信息头,一个颜色表(又称为色表)和位图数据本身。 位图文件头 位图文件头包含关于这个文件的信息。如从哪里开始是位图数据的定位信息、文件大小等等。以下是位图文件头结构的定义: typedef struct tagBITMAPFILEHEADER { // bmfh WORD  bfType; DWORD  bfSize; WORD  bfReserved1; WORD  bfReserved2; DWORD  bfOffBits; } BITMAPFILEHEADER; 其中的bfType值应该是“BM”(0x4d42),标志该文件是位图文件。bfSize的值是位图文件的大小。 位图信息头 位图信息头包含了单个像素所用字节数以及描述颜色的格式,此外还包括位图的宽度、高度、目标设备的位平面数、图像的压缩格式。 以下是位图信息头结构的定义: typedef struct tagBITMAPINFOHEADER{ // bmih   DWORD biSize;   LONG  biWidth;  LONG  biHeight;     WORD  biPlanes;   WORD  biBitCount   DWORD biCompression;   DWORD biSizeImage;   LONG  biXPelsPerMeter;   LONG  biYPelsPerMeter;   DWORD biClrUsed;   DWORD biClrImportant; } BITMAPINFOHEADER; 下面是对结构体当中各个成员的说明: biSize 结构BITMAPINFOHEADER的字节数,即sizeof(BITMAPINFOHEADER)* biWidth 以像素为单位的图像宽度* biHeight 以像素为单位的图像长度* biplanes 目标设备的位平面数 biBitCount 每个像素的位数 biCompression 图像的压缩格式(这个值几乎总是为0) biSizeImage 以字节为单位的图像数据的大小(对BI_RGB压缩方式而言) biXPelsPermeter 水平方向上的每米的像素个数 biYPelsPerMeter 垂直方向上的每米的像素个数 biClrused 调色板中实际使用的颜色数 这个值通常为0,表示使用biBitCount确定的全部颜色,例外是使用的颜色的数目小于制定的颜色深度的颜色数目的最大值。 biClrImportant 现实位图时必须的颜色数 这个值通常为0,表示所有的颜色都是必需的 对于其中的biBitCount,分别有以下意义: 0,用在JPEG格式中 1,单色图,调色板中含有两种颜色,也就是我们通常说的黑白图片 4,16色图 8,256色图,通常说的灰度图 16,64K图,一般没有调色板,图像数据中每两个字节表示一个像素,5个或6个位表示一个RGB分量 24,16M真彩色图,一般没有调色板,图像数据中每3个字节表示一个像素,每个字节表示一个RGB分量 32,4G真彩色,一般没有调色板,每4个字节表示一个像素,相对24位真彩图而言,加入了一个透明度,即RGBA模式。 颜色表 颜色表(文中简称色表)一般是针对16位以下的图像而设置的,对于16位和16位以上的图像,由于其位图像素数据中直接对对应像素的RGB(A)颜色进行描述,因而省却了调色板。而对于16位以下的图像,由于其位图像素数据中记录的只是调色板索引值,因而需要根据这个索引到调色板去取得相应的RGB(A)颜色。颜色表的作用就是创建调色板。对显示卡来说,如果它不能一次显示超过256 种颜色,读取和显示BMP 文件的程序能够把这些RGB 值转换到显示卡的调色板来产生准确的颜色。 颜色表的颜色表项结构的定义如下: typedef struct tagRGBQUAD { // rgbq   BYTE  rgbBlue;   BYTE  rgbGreen;   BYTE  rgbRed;   BYTE  rgbReserved; } RGBQUAD; 其中需要注意的问题是,RGBQUAD结构中的颜色顺序是BGR,而不是平常的RGB。 位图数据 BMP 文件的位图数据格式依赖于编码每个象素颜色所用的位数。对于一个256 色的图像来说,每个象素占用文件中位图数据部分的一个字节。象素的值不是RGB 颜色值,而是文件中色表的一个索引。所以在色表中如果第一个R/G/B 值是255/0/0,那么象素值为0 表示它是鲜红色,象素值按从左到右的顺序存储,通常从最后一行开始。所以在一个 256 色的文件中,位图数据中第一个字节就是图像左下角的象素的颜色索引,第二个就是它右边的那个象素的颜色索引。如果位图数据中每行的字节数是奇数,就要在每行都加一个附加的字节来调整位图数据边界为16 位的整数倍。 并不是所有的BMP 文件结构都像表中所列的那样,例如16 和24-bpp,文件就没有色表,象素值直接表示RGB 值,另外文件私有部分的内部存储格式也是可以变化的。例如,在16和256 色BMP 文件中的位图数据采用rle 算法来压缩,这种算法用颜色加象素个数来取代一串颜色相同的序列,而且,Windows 还支持OS/2 下的BMP 文件,尽管它使用了不同的位图信息头和色表格式。为了减小运算的数据量,本程序使用256色BMP文件作为输入。 近年来,一种后缀名为jpg的位图凭借较高的压缩比和不俗的品质,并借助网络的优势而迅速成为图像/图形行业事实上的工业标准。本文没有采用jpg位图,但在这里也简要的介绍一下,以供读者参考。 jpg是24位的图像文件格式,也是一种高效率的压缩格式,文件格式是JPEG(联合图像专家组)标准的产物,该标准由ISO与CCI TT(国际电报电话咨询委员会)共同制定,是面向连续色调静止图像的一种压缩标准。其最初目的是使用64Kbps的通信线路传输720×576 分辨率压缩后的图像。通过损失极少的分辨率,可以将图像所需存储量减少至原大小的10%。由于其高效的压缩效率和标准化要求,目前已广泛用于彩色传真、静止图像、电话会议、印刷及新闻图片的传送上。但那些被删除的资料无法在解压时还原,所以* .jpg文件并不适合放大观看,输出成印刷品时品质也会受到影响。不过,普通用户不必担心,因为*.jpg的压缩算法十分先进,它对图形图像的损失影响不是很大,一幅1 6 M(24位)的*.jpg/*.jpeg图像看上去与照片没有多大差别,非专业人士甚至无法分辨。同样一幅画面,用*.jpg格式储存的文件是其他类型图形文件的1 /10~1/20。一般情况下,*.jpg文件只有几十KB,而色彩数最高可达到24位,所以它被广泛运用在Internet上,以节约宝贵的网络传输资源。同样,为了在一张光盘上储存更多的图形图像,C D出版商也乐意采用*.jpg格式。目前,网上已经有了很多jpg图像的编解码的算法,限于篇幅,本文就不再赘述。如果要使用jpg格式的图像,那么请先用画图软件如ACDSEE、PHOTOSHOP将其转化为256色bmp位图格式。另外,较常用到的图像格式还有gif、tiff、png等等。由于本文的核心主要集中在图像的预处理和BP神经网络识别部分,就不在图像格式上耗费精力了。读者需要进一步了解图像格式的可以详细查阅相关资料。 在图像预处理部分的图像数据读取部分,作者使用了微软提供的一个图像函数库dibapi.h和dibapi.cpp,里面已经含有一些基本的图像处理函数,作者在此就不再列举源代码,而紧紧将其接口加以描述,以使读者清晰每个函数的作用。同时作者自己对该库又加以扩充以满足本程序的需要。文中没有贴出而程序中又用到的代码都在本书的附带光盘中可以找到。 本小节所有的函数定义及声明位于dibapi.h、dibapi.cpp、mydiblib.h中。 首先来看7个图像数据读取/存储/创建以及图像基本信息获取函数。 图像数据读取/保存由函数ReadDIBFile、SaveDIB、FindDIBBits、DIBWidth、DIBHeight、DIBNumColors完成,其调用接口如下: (1)ReadDIBFile函数 函数原型: HDIB WINAPI ReadDIBFile(CFile& file); 参数: CFile& file - 要读取得文件文件CFile 返回值: HDIB - 成功返回DIB的句柄,否则返回NULL。 说明: 该函数将指定的文件中的DIB对象读到指定的内存区域中。除文件头之外的内容都将被读入内存。HDIB即此内存区域的指针 (2)SaveDIB函数 函数原型: BOOL WINAPI SaveDIB (HDIB hDib, CFile& file); 参数: HDIB hDib - 要保存的DIB CFile& file - 保存文件CFile 返回值: BOOL - 成功返回TRUE,否则返回FALSE或者CFileException 说明: 该函数将指定的DIB对象保存到指定的CFile中。该CFile由调用程序打开和关闭。 (3)FindDIBBits函数 函数原型: LPSTR WINAPI FindDIBBits (LPSTR lpbi); 参数: LPSTR lpbi - 指向DIB对象的指针 返回值: LPSTR - 指向DIB图像象素起始位置 说明: 该函数计算DIB中图像象素数据区的起始位置,并返回指向它的指针。 [注] LPSTR指针为指向字符串的32位的指针。在对256色图像进行象素操作时,可以用(BYTE*)或(unsigned char*)强制将其转换为8位的指针 (4)DIBWidth函数 函数原型: DWORD WINAPI DIBWidth(LPSTR lpDIB) 参数: LPSTR lpbi - 指向DIB对象的指针 返回值: DWORD - DIB中图像的宽度 说明: 该函数返回DIB中图像的宽度。对于Windows 3.0 DIB,返回BITMAPINFOHEADER 中的biWidth值;对于其他返回BITMAPCOREHEADER中的bcWidth值。 (5)DIBHeight函数 函数原型: DWORD WINAPI DIBHeight(LPSTR lpDIB) 参数: LPSTR lpDIB - 指向DIB对象的指针 返回值: DWORD - DIB中图像的高度 说明: 该函数返回DIB中图像的高度。对于Windows 3.0 DIB,返回BITMAPINFOHEADER中的biHeight值;对于其他返回BITMAPCOREHEADER中的bcHeight值。 (6)DIBNumColors函数 函数原型: WORD WINAPI DIBNumColors(LPSTR lpbi) 参数: LPSTR lpbi - 指向DIB对象的指针 返回值: WORD - 返回调色板中颜色的种数 说明: 该函数返回DIB中调色板的颜色的种数。对于单色位图,返回2,对于16色位图,返回16,对于256色位图,返回256;对于真彩色位图(24位),没有调色板,返回0。 以上6个函数是在图像处理过程中读取/保存图像以及获取图像基本信息的6个最基本的函数。还有一个NewDIB函数是作者自己编写的,用来建立一个新的DIB。此函数非常有用,可以十分便利的根据所提供的要创建的位图的基本信息(高度、宽度、颜色位数)来开辟内存,并自动完成位图信息头的填充工作。 (7)NewDIB函数 其完整程序代码及注释介绍如下: /******************************************************************* *函数名称: * NewDIB() * * 参数: * width - 将要创建DIB的宽 * height - 将要创建DIB的高 * biBitCount - 将要创建DIB的位数。比如,如果要创建256色DIB,则此值为8 * *返回值: * HDIB - 成功返回DIB的句柄,否则返回NULL。 * *说明: * 该函数指定宽、高、颜色位数来创建一个新的DIB,并返回其句柄 * ***************************************************************/ HDIB WINAPI NewDIB(long width, long height,unsigned short biBitCount) { //计算新建的DIB每行所占的字节数 long dwindth = (width*biBitCount/8+3)/4*4; //新建的DIB调色板中表项的数目 WORD color_num; //通过输入的biBitCount值来确定调色板的表项数目 switch(biBitCount) { //如果用1 bit来表示一个象素那么调色板中有两个表项 case 1: color_num=2; break; //如果用4 bit来表示一个象素那么调色板中有16个表项 case 4: color_num=16; break; //如果用8bit来表示一个象素,那么调色板中得表项有256中(本程序大多采用这种形式) case 8: color_num=256; break; //其他的情况调色扳中没有表项,即真彩位图 default: color_num=0; break; } //计算位图数据所占的空间 //dwindth *height为象素数据所占的空间 //40为位图信息头占的空间 //color_num*4为调色板的表项所占的空间(调色板每个表项占4各个字节) dwBitsSize = dwindth *height + 40 + color_num*4; //建立指向位图文件的指针 LPSTR pDIB; //申请存储空间,并建立指向位图的句柄 HDIB hDIB=(HDIB) ::GlobalAlloc(GMEM_MOVEABLE|GMEM_ZEROINIT, dwBitsSize); //如果申请空间不成功返回错误信息 if (hDIB == 0) { return NULL; } //如果申请空间成功锁定内存,并将内存的指针传给pDIB pDIB = (LPSTR) ::GlobalLock((HGLOBAL) hDIB); //建立指向位图信息头结构的指针 LPBITMAPINFO lpmf = (LPBITMAPINFO)pDIB; //给位图信息头内的各个参量赋值 //指定位图信息头结构的大小为40字节 lpmf->bmiHeader.biSize = 40; //指定新建位图的宽度 lpmf->bmiHeader.biWidth = width; //指定新建位图的高度 lpmf->bmiHeader.biHeight = height; //位平面数必须为1 lpmf->bmiHeader.biPlanes = 1; //确定新建位图表示颜色是要用到的bit数 lpmf->bmiHeader.biBitCount = biBitCount; //是否进行压缩 lpmf->bmiHeader.biCompression = 0; //新建的位图中实际的位图数据所占的字节数 lpmf->bmiHeader.biSizeImage = dwindth *height; //指定目标设备的水平分辨率 lpmf->bmiHeader.biXPelsPerMeter = 2925; //指定目标设备的垂直分辨率 lpmf->bmiHeader.biYPelsPerMeter = 2925; //新建图像实际用到的颜色数 如果为0则用到的颜色数为2的biBitCount次 lpmf->bmiHeader.biClrUsed = 0; //指定新建图像中重要的颜色数,如果为0则所有的颜色都重要 lpmf->bmiHeader.biClrImportant= 0; //如果新建的图像中含有调色板,则接下来对调色板的各种颜色分量赋初始值 if(color_num!=0) { for(int i=0;ibmiColors[i].rgbRed =(BYTE)i; lpmf->bmiColors[i].rgbGreen =(BYTE)i; lpmf->bmiColors[i].rgbBlue =(BYTE)i; } } //解除锁定 ::GlobalUnlock((HGLOBAL) hDIB); //返回新建位图的句柄 return hDIB; } 以上的7个函数是打开、保存、创建位图以及获取位图基本信息的常用函数,请读者熟练掌握运用。 下面要介绍的4个函数是显示位图、清除屏幕、以及画框的函数。其中PaintDIB函数是微软函数库提供的,由DisplayDIB函数调用,作者在此不再列举源代码,只是提供调用接口的讲解。其他的3个作者自己编写的函数将列出详细的源代码。 (8)PaintDIB函数调用接口: 函数原型 BOOL WINAPI PaintDIB (HDC, LPRECT, HDIB, LPRECT, CPalette* pPal); 参数: HDC hDC - 输出设备DC LPRECT lpDCRect - 绘制矩形区域 HDIB hDIB - 指向DIB对象的指针 LPRECT lpDIBRect - 要输出的DIB区域 CPalette* pPal - 指向DIB对象调色板的指针 返回值: BOOL - 绘制成功返回TRUE,否则返回FALSE。 说明: 该函数主要用来绘制DIB对象。其中调用了StretchDIBits()或者SetDIBitsToDevice()来绘制DIB对象。输出的设备由由参数hDC指定;绘制的矩形区域由参数lpDCRect指定;输出DIB的区域由参数* lpDIBRect指定。 (9)在屏幕上显示位图的DisplayDIB函数 其完整源代码及注释介绍如下: /*********************************** ************************************ 函数名称:DisplayDIB 参数: CDC* pDC -指向当前设备上下文(Divice Context)的指针 HDIB hDIB -要显示的位图的句柄 **********************************************************************/ void DisplayDIB(CDC* pDC,HDIB hDIB) { //锁定位图并获取指向位图的指针 BYTE* lpDIB=(BYTE*)::GlobalLock(hDIB); // 获取DIB宽度和高度 int cxDIB = ::DIBWidth((char*) lpDIB); int cyDIB = ::DIBHeight((char*)lpDIB); //设置位图的尺寸 CRect rcDIB,rcDest; rcDIB.top = rcDIB.left = 0; rcDIB.right = cxDIB; rcDIB.bottom = cyDIB; //设置目标客户区输出大小尺寸(在这里直接令其为位图的尺寸) rcDest = rcDIB; //清除屏幕 ClearAll(pDC); //在客户区显示图像 ::PaintDIB(pDC->m_hDC,rcDest,hDIB,rcDIB,NULL); //解除锁定 ::GlobalUnlock((HGLOBAL)hDIB); } (10)清除当前屏幕客户区内容的ClearAll函数 其完整源代码及注释介绍如下: void ClearAll(CDC *pDC) { //设置清除区域 CRect rect; rect.left =0;rect.top =0;rect.right =2000;rect.bottom =1000; //创建一白色画笔 CPen pen; pen.CreatePen (PS_SOLID,1,RGB(255,255,255)); pDC->SelectObject (&pen); //绘制一白色矩形以清除客户区 pDC->Rectangle (&rect); //清除画笔 ::DeleteObject (pen); } (11)画框函数DrawFrame 其完整源代码及注释介绍如下: /*********************************************************************** 函数名称:DrawFrame 参数: CDC* pDC -指向当前设备上下文的指针 HDIB hDIB -指向位图的句柄 CRectLink charRect -一个元素为Crect类对象的链表 unsigned int linewidth -指出框的宽度 COLORREF color -指出框的颜色 说明: 调用此函数之前要先完成分割操作,否则无法在分割出来的字符外画框。进行分割、标准化操作之后会自动生成一个C rect链表。关于此链表的使用涉及到STL(Standard Template Library)技术,在后面字符分割一节有个简要的介绍。 ***********************************************************************/ void DrawFrame(CDC* pDC,HDIB hDIB, CRectLink charRect,unsigned int linewidth,COLORREF color) { //创建画笔 CPen pen; pen.CreatePen (PS_SOLID,linewidth,color); pDC->SelectObject (&pen); //创建一个NULL画刷 ::SelectObject (*pDC,GetStockObject(NULL_BRUSH)); CRect rect,rect2; //锁定位图句柄并获取其指针 BYTE* lpDIB=(BYTE*)::GlobalLock ((HGLOBAL)hDIB); while(!charRect.empty()) { //从表头上得到一个矩形 rect2=rect= charRect.front(); //从链表头上面删掉一个 charRect.pop_front(); //坐标转换 //注意,这里原先的rect是相对于图像原点(左下角)的, //而在屏幕上绘图时,要转换以客户区为原点的坐标 rect.top =::DIBHeight ((char*)lpDIB)-rect2.bottom; rect.bottom =::DIBHeight ((char*)lpDIB)-rect2.top ; pDC->Rectangle (&rect); } //解除锁定 ::GlobalUnlock ((HGLOBAL)hDIB); } 至此,图像读取/保存/创建/显示/清除屏幕/画框等11个函数已介绍完毕。 再提一下本程序自动刷新的实现。所谓自动刷新,即每当屏幕内容被遮挡或者说客户区需要重画的时候,根据OnPaint消息来自动的刷新客户区。这里我们使用OnPaint和OnDraw一起来实现自动刷新。代码如下: void CChildView::OnPaint() { CPaintDC dc(this); OnDraw(&dc); } //OnDraw函数 void CChildView::OnDraw(CDC *pDC) { //如果m_hDIB不为NULL,即表示已经加载了图像文件,那么重画客户区 if(m_hDIB!=NULL) DisplayDIB(pDC,m_hDIB); } 下面来进入实质的图像预处理部分。 2. 图像的预处理 刚刚读入的图片如图11-4所示。 图 11-4 初始读入的256色位图 (1)256色位图灰度化 由于256色的位图的调色板内容比较复杂,使得图像处理的许多算法都没有办法展开,因此有必要对它进行灰度处理。所谓灰度图像就是图像的每一个象素的R、G、B分量的值是相等的。彩色图像的每个象素的R、G、B值是不相同的,所以显示出红绿蓝等各种颜色。灰度图像没有这些颜色差异,有的只是亮度上的不同。灰度值大的象素点比较亮(象素值最大为255,为白色),反之比较暗(象素值最小为0,为黑色)。图像灰度化有各种不同的算法,比较直接的一种就是给象素的rgb 值各自一个加权系数,然后求和;同时还要对调色板表项进行相应的处理。 要注意的是,最后得到的结果一定要归一到0-255 之内。因为这是每个字节表示图像数据的极限。 图像的灰度处理由Convert256toGray 函数完成。 /**************************************************************** * 函数名称: * Convert256toGray() * * 参数: * HDIB hDIB -图像的句柄 * * 返回值: * 无 * * 功能: * 将256色位图转化为灰度图 * ***************************************************************/ void Convert256toGray(HDIB hDIB) { LPSTR lpDIB; // 由DIB句柄得到DIB指针并锁定DIB lpDIB = (LPSTR) ::GlobalLock((HGLOBAL)hDIB); // 指向DIB象素数据区的指针 LPSTR lpDIBBits; // 指向DIB象素的指针 BYTE * lpSrc; // 图像宽度 LONG lWidth; // 图像高度 LONG lHeight; // 图像每行的字节数 LONG lLineBytes; // 指向BITMAPINFO结构的指针(Win3.0) LPBITMAPINFO lpbmi; // 指向BITMAPCOREINFO结构的指针 LPBITMAPCOREINFO lpbmc; // 获取指向BITMAPINFO结构的指针(Win3.0) lpbmi = (LPBITMAPINFO)lpDIB; // 获取指向BITMAPCOREINFO结构的指针 lpbmc = (LPBITMAPCOREINFO)lpDIB; // 灰度映射表 BYTE bMap[256]; // 计算灰度映射表(保存各个颜色的灰度值),并更新DIB调色板 int i,j; for (i = 0; i < 256; i ++) { // 计算该颜色对应的灰度值 bMap[i] = (BYTE)(0.299 * lpbmi->bmiColors[i].rgbRed + 0.587 * lpbmi->bmiColors[i].rgbGreen + 0.114 * lpbmi->bmiColors[i].rgbBlue + 0.5); // 更新DIB调色板红色分量 lpbmi->bmiColors[i].rgbRed = i; // 更新DIB调色板绿色分量 lpbmi->bmiColors[i].rgbGreen = i; // 更新DIB调色板蓝色分量 lpbmi->bmiColors[i].rgbBlue = i; // 更新DIB调色板保留位 lpbmi->bmiColors[i].rgbReserved = 0; } // 找到DIB图像象素起始位置 lpDIBBits = ::FindDIBBits(lpDIB); // 获取图像宽度 lWidth = ::DIBWidth(lpDIB); // 获取图像高度 lHeight = ::DIBHeight(lpDIB); // 计算图像每行的字节数 lLineBytes = WIDTHBYTES(lWidth * 8); // 更换每个象素的颜色索引(即按照灰度映射表换成灰度值) //逐行扫描 for(i = 0; i < lHeight; i++) { //逐列扫描 for(j = 0; j < lWidth; j++) { // 指向DIB第i行,第j个象素的指针 lpSrc = (unsigned char*)lpDIBBits + lLineBytes * (lHeight - 1 - i) + j; // 变换 *lpSrc = bMap[*lpSrc]; } } //解除锁定 ::GlobalUnlock ((HGLOBAL)hDIB); } 下面我们来编写由256色位图转化为灰度位图的菜单处理事件的代码: //图像预处理第1步:将256色图像转化为灰度图像 void CChildView::OnIMGPRC256ToGray() { //调用灰度转化函数 Convert256toGray(m_hDIB); //在屏幕上显示位图 CDC* pDC=GetDC(); DisplayDIB(pDC,m_hDIB); } 经过灰度处理后的256色位图如图11-5所示。 图11-5 经过灰度处理的文件 (2)灰度图像二值化 在进行了灰度化处理之后,图像中的每个象素只有一个值,那就是象素的灰度值。它的大小决定了象素的亮暗程度。为了更加便利的开展下面的图像处理操作,还需要对已经得到的灰度图像做一个二值化处理。图像的二值化就是把图像中的象素根据一定的标准分化成两种颜色。在系统中是根据象素的灰度值处理成黑白两种颜色。和灰度化相似的,图像的二值化也有很多成熟的算法。它可以采用自适应阀值法,也可以采用给定阀值法。系统中采用的是给定阀值的方法。因为考虑到所要进行处理的图像大多是从印刷出版物上扫描得来的底色大多为白色所以我们将这个阈值固定为220,读者也可以根据实际的情况来自己进行阈值的设定。 图像二值化的函数由ConvertGrayToWhiteBlack实现,源代码如下: /****************************************************************** * * 函数名称ConvertGrayToWhiteBlack () * * 参数 :HDIB hDIB -原图的句柄 * * 返回值:无 * * 功能: ConvertGrayToWhiteBlack函数采用硬阈值的方法,实现将图像二值化的功能。 * * 说明: 要求待处理的图片为256色 ************************************************************************/ void ConvertGrayToWhiteBlack(HDIB hDIB) { // 指向DIB的指针 LPSTR lpDIB; // 由DIB句柄得到DIB指针并锁定DIB lpDIB = (LPSTR) ::GlobalLock((HGLOBAL)hDIB); // 指向DIB象素数据区的指针 LPSTR lpDIBBits; // 指向DIB象素的指针 BYTE * lpSrc; // 图像宽度 LONG lWidth; // 图像高度 LONG lHeight; // 图像每行的字节数 LONG lLineBytes; // 找到DIB图像象素起始位置 lpDIBBits = ::FindDIBBits(lpDIB); // 获取图像宽度 lWidth = ::DIBWidth(lpDIB); // 获取图像高度 lHeight = ::DIBHeight(lpDIB); // 计算图像每行的字节数 lLineBytes = WIDTHBYTES(lWidth * 8); // 更换每个象素的颜色索引(即按照灰度映射表换成灰度值) int i,j; //逐行扫描 for(i = 0; i < lHeight; i++) { //逐列扫描 for(j = 0; j < lWidth; j++) { // 指向DIB第i行,第j个象素的指针 lpSrc = (unsigned char*)lpDIBBits + lLineBytes * i + j; // 二值化处理 //大于220,设置为255,即白点 if(*lpSrc>220) *lpSrc=255; //否则设置为0,即黑点 else *lpSrc=0; } } //解除锁定 ::GlobalUnlock((HGLOBAL)hDIB); } 下面我们来编写对灰度图像进行二值化处理的菜单处理时间的代码: //图像预处理第2步:将灰度图二值化 void CChildView::OnIMGPRCGrayToWhiteBlack() { //调用灰度二值化函数 ConvertGrayToWhiteBlack(m_hDIB); //在屏幕上显示位图 CDC* pDC=GetDC(); DisplayDIB(pDC,m_hDIB); } 执行结果如图11-6所示。 图11-6 二值化后的图片 (3)图像的梯度锐化 由于需要处理的图像大多有印刷出版物上扫描而来所以在很多的情况下字体模糊,对识别造成了一定的困难,所以有时我们要对图像进行锐化处理使模糊的图像变得清晰起来,同时可以对噪声起到一定的去除作用。图像锐化的方法有很多,有一种是微分法 ,有一种是高通滤波法,我们在这里所采用的梯度锐化的方法就属于微分法的一种。在这里我们采用Roberts梯度算子对图像进行锐化。 定义如下 : 设原始图像上的点为。 定义在处的梯度矢量为: 设一个判定阈值为,变化后的图像定义为: 通过公式可以看出梯度锐化可以让模糊的边缘变得清楚 同时选择合适的阈值还可以减弱和消除一些细小的噪声。 本程序中给出了梯度锐化的完整代码,读者也可以根据实际需求、读入图片的质量来选择决定是否使用梯度锐化。事实证明,梯度锐化具备一定的去噪声能力,但同时会对字符的边缘有所损伤。所以笔者建议在图片中字符较为细小的时候不要使用梯度锐化。 图像的梯度锐化可以通过函数GradientSharp来实现: /*********************************************** * * 函数名称: * radientSharp() * *参数 : * HDIB hDIB -待处理图像的句柄 * * 返回值: * 无 * *功能: * 现图像的梯度锐化 *说明: * 只能对2值图像进行处理,如果图像本身边缘较细,可能造成信息的损失 **********************************************************************/ void GradientSharp(HDIB hDIB) { // 指向DIB的指针 LPSTR lpDIB=(LPSTR) ::GlobalLock((HGLOBAL)hDIB); // 指向DIB象素指针 LPSTR lpDIBBits; // 找到DIB图像象素起始位置 lpDIBBits = ::FindDIBBits(lpDIB); //获取图像的宽度 LONG lWidth=::DIBWidth ((char*)lpDIB); //获取图像的长度 LONG lHeight=::DIBHeight ((char*)lpDIB); // 阈值 BYTE bThre = 2; // 调用GradSharp()函数进行梯度板锐化 // 指向源图像的指针 unsigned char* lpSrc; unsigned char* lpSrc1; unsigned char* lpSrc2; // 循环变量 LONG i; LONG j; // 图像每行的字节数 LONG lLineBytes; // 中间变量 BYTE bTemp; // 计算图像每行的字节数 lLineBytes = WIDTHBYTES(lWidth * 8); // 每行 for(i = 0; i < lHeight; i++) { // 每列 for(j = 0; j < lWidth; j++) { // 指向DIB第i行,第j个象素的指针 lpSrc = (unsigned char*)lpDIBBits + lLineBytes * (lHeight - 1 - i) + j; // 指向DIB第i+1行,第j个象素的指针 lpSrc1 = (unsigned char*)lpDIBBits + lLineBytes * (lHeight - 2 - i) + j; // 指向DIB第i行,第j+1个象素的指针 lpSrc2 = (unsigned char*)lpDIBBits + lLineBytes * (lHeight - 1 - i) + j + 1; //计算梯度值 bTemp = abs((*lpSrc)-(*lpSrc1)) + abs((*lpSrc)-(*lpSrc2)); // 判断是否小于阈值 if (bTemp < 255) { // 判断是否大于阈值,对于小于情况,灰度值不变。 if (bTemp >= bThre) { // 直接赋值为bTemp lpSrc = bTemp; } } else { // 直接赋值为255 *lpSrc = 255; } } } //最后还要处理一下图像中最下面那行 for(j = 0; j < lWidth; j++) { //指向最下边一行,第j个象素的指针 lpSrc = (unsigned char*)lpDIBBits + lLineBytes * 0 + j; //将此位置的象素设置为255,即白点 *lpSrc=255; } //解除锁定 ::GlobalUnlock ((HGLOBAL)hDIB); } 下面我们来编写对图像进行梯度锐化菜单处理事件的响应代码: //图像预处理第3步:梯度锐化 void CChildView::OnImgprcSharp() { //调用梯度锐化函数 GradientSharp(m_hDIB); //在屏幕上显示位图 CDC* pDC=GetDC(); DisplayDIB(pDC,m_hDIB); } 经过梯度锐化处理后的图片如图11-7所示。 图11-7 梯度锐化后的图像 从处理的结果也可以看出图像的边缘变得清晰而且少了很多细小的杂点,但是梯度锐化有其自身的缺陷。当处理的图像边缘很细的时候可能造成边缘信息的损失(如图11-8所示),所以务必提醒读者注意,要根据实际情况酌情处理。 图11-8 对边缘较细的图像进行梯队锐化的结果 (4)去除离散的杂点噪声 图像可能在扫描或者传输过程中夹带了噪声,去噪声是图像处理中常用的手法。通常去噪声用滤波的方法,比如中值滤波、均值滤波。但是那样的算法不适合用在处理字符这样目标狭长的图像中,因为在滤波的过程中很有可能会去掉字符本身的象素。系统采用的是去除杂点的方法来进行去噪声处理的。具体算法如下:扫描整个图像,当发现一个黑色点的时候,就考察和该黑色点间接或者直接相连接的黑色点的个数有多少,如果大于一定的值,那就说明该点不是离散点,否则就是离散点,把它去掉。在考察相连的黑色点的时候用的是递归的方法。 去杂点的功能由RemoveScatterNoise、DeleteScaterJudge 两个函数完成,下面分别介绍两个函数。 函数RemoveScatterNoise代码如下: /************************************************************ * * 函数名称: * RemoveScatterNoise() * * 参数: * HDIB hDIB -原图像的句柄 * * 返回值: * 无 * * 功能: * 通过对连续点长度的统计来去除离散杂点 * * 说明: * 只能对2值图像进行处理 ****************************************************************/ void RemoveScatterNoise(HDIB hDIB) { // 指向DIB的指针 LPSTR lpDIB=(LPSTR) ::GlobalLock((HGLOBAL)hDIB); // 指向DIB象素指针 LPSTR lpDIBBits; // 找到DIB图像象素数据区的起始位置 lpDIBBits = ::FindDIBBits(lpDIB); //获得图像的长度 LONG lWidth=::DIBWidth ((char*)lpDIB); //获得图像的高度 LONG lHeight=::DIBHeight ((char*)lpDIB); //设置判定噪声的长度阈值为15 //即如果与考察点相连接的黑点的数目小于15则认为考察点是噪声点 int length=15; // 循环变量 m_lianXuShu=0; LONG i; LONG j; LONG k; // 图像每行的字节数 LONG lLineBytes; // 计算图像每行的字节数 lLineBytes = WIDTHBYTES(lWidth * 8); LPSTR lpSrc; //开辟一块用来存放标志的内存数组 LPBYTE lplab = new BYTE[lHeight * lWidth]; //开辟一块用来保存离散判定结果的内存数组 bool *lpTemp = new bool[lHeight * lWidth]; //初始化标志数组 for (i=0;i=lianXuShu) return TRUE; //长度加一 m_lianXuShu++; //设定访问标志 lplab[lWidth * y +x] = true; //保存访问点坐标 lab[m_lianXuShu-1].x = x; lab[m_lianXuShu-1].y = y; //象素的灰度值 int gray; //指向象素的指针 LPSTR lpSrc; //长度判定 //如果连续长度满足要求,说明不是离散点,返回 if(m_lianXuShu>=lianXuShu) return TRUE; //下面进入递归 else { //考察上下左右以及左上、右上、左下、右下八个方向 //如果是黑色点,则调用函数自身进行递归 //考察下面点 lpSrc=(char*)lpDIBBits + lLineBytes * (y-1) + x; //传递灰度值 gray=*lpSrc; //如果点在图像内、颜色为黑色并且没有被访问过 if(y-1 >=0 && gray == 0 && lplab[(y-1)*lWidth+x] == false) //进行递归处理 DeleteScaterJudge(lpDIBBits,lLineBytes,lplab,lWidth,lHeight,x,y-1,lab,lianXuShu); //判断长度 //如果连续长度满足要求,说明不是离散点,返回 if(m_lianXuShu>=lianXuShu) return TRUE; //左下点 lpSrc=(char*)lpDIBBits + lLineBytes * (y-1) + x-1; //传递灰度值 gray=*lpSrc; //如果点在图像内、颜色为黑色并且没有被访问过 if(y-1 >=0 && x-1 >=0 && gray== 0 && lplab[(y-1)*lWidth+x-1] == false) //进行递归处理 DeleteScaterJudge(lpDIBBits,lLineBytes,lplab,lWidth,lHeight,x-1,y-1,lab,lianXuShu); //判断长度 //如果连续长度满足要求,说明不是离散点,返回 if(m_lianXuShu>=lianXuShu) return TRUE; //左边 lpSrc=(char*)lpDIBBits + lLineBytes * y + x-1; //传递灰度值 gray=*lpSrc; //如果点在图像内、颜色为黑色并且没有被访问过 if(x-1 >=0 && gray== 0 && lplab[y*lWidth+x-1] == false) //进行递归处理 DeleteScaterJudge(lpDIBBits,lLineBytes,lplab,lWidth,lHeight,x-1,y,lab,lianXuShu); //判断长度 //如果连续长度满足要求,说明不是离散点,返回 if(m_lianXuShu>=lianXuShu) return TRUE; //左上 lpSrc=(char*)lpDIBBits + lLineBytes * (y+1) + x-1; //传递灰度值 gray=*lpSrc; //如果点在图像内、颜色为黑色并且没有被访问过 if(y+1 = 0 && gray == 0 && lplab[(y+1)*lWidth+x-1] == false) //进行递归处理 DeleteScaterJudge(lpDIBBits,lLineBytes,lplab,lWidth,lHeight,x-1,y+1,lab,lianXuShu); //判断长度 //如果连续长度满足要求,说明不是离散点,返回 if(m_lianXuShu>=lianXuShu) return TRUE; //上面 lpSrc=(char*)lpDIBBits + lLineBytes * (y+1) + x; //传递灰度值 gray=*lpSrc; //如果点在图像内、颜色为黑色并且没有被访问过 if(y+1 < lHeight && gray == 0 && lplab[(y+1)*lWidth+x] == false) //进行递归处理 DeleteScaterJudge(lpDIBBits,lLineBytes,lplab,lWidth,lHeight,x,y+1,lab,lianXuShu); //判断长度 //如果连续长度满足要求,说明不是离散点,返回 if(m_lianXuShu>=lianXuShu) return TRUE; //右上 lpSrc=(char*)lpDIBBits + lLineBytes * (y+1) + x+1; //传递灰度值 gray=*lpSrc; //如果点在图像内、颜色为黑色并且没有被访问过 if(y+1 =lianXuShu) return TRUE; //右边 lpSrc=(char*)lpDIBBits + lLineBytes * y + x+1; //传递灰度值 gray=*lpSrc; //如果点在图像内、颜色为黑色并且没有被访问过 if(x+1 =lianXuShu) return TRUE; //右下 lpSrc=(char*)lpDIBBits + lLineBytes * (y-1) + x+1; //传递灰度值 gray=*lpSrc; //如果点在图像内、颜色为黑色并且没有被访问过 if(y-1 >=0 && x+1 =lianXuShu) return TRUE; } //如果递归结束,返回false,说明是离散点 return FALSE; } 下面我们来编写去除图像中离散杂点噪声的菜单处理事件的代码: //图像预处理第4步:去离散杂点噪声 void CChildView::OnImgprcRemoveNoise() { //调用去离散杂点噪声的函数 RemoveScatterNoise(m_hDIB); //在屏幕上显示位图 CDC* pDC=GetDC(); DisplayDIB(pDC,m_hDIB); } 执行后的结果如图11-9所示。 图 11-9 去除离散杂点噪声后的图像 (5)整体倾斜度调整 因为读进来的图像可能存在倾斜,所以必须对它进行调整,使得字符都处于同一水平位置,那样即便利字符的分割也可以提高字符识别的准确率。调整的算法主要是根据图像上左右两边的黑色象素的平均高度来的。一般来说,众多的字符组成的图像它的左右两边的字符象素的高度应该是处于水平位置附近的,如果两边字符象素的平均位置有比较大的起落,那就说明图像存在倾斜,需要进行调整。具体来说,首先要分别计算图像左半边和右半边的象素的平均高度,然后求的斜率,根据斜率重新组织图像,里面包含了一个从新图像到旧图像的象素的映射。如果新图像中的象素映射到旧图像中时超出了旧图像的范围,则把新图像中的该象素置白色。 图像的调整由SlopeAdjust 函数完成。 SlopeAdjust 函数代码如下: /********************************************************* * 函数名称: * SlopeAdjust() * * 参数: * HDIB hDIB -原图像的句柄 * * 返回值: * 无 * * 功能: * 通过对图像左右半边平均高度的统计来进行倾斜的调整 * * 说明: * 只能对2值图像进行处理 * ****************************************************************/ void SlopeAdjust(HDIB hDIB) { // 指向DIB的指针 LPSTR lpDIB=(LPSTR) ::GlobalLock((HGLOBAL)hDIB); // 指向DIB象素指针 LPSTR lpDIBBits; // 找到DIB图像象素起始位置 lpDIBBits = ::FindDIBBits(lpDIB); // 指向源图像的指针 unsigned char* lpSrc; // 循环变量 LONG i; LONG j; // 图像每行的字节数 LONG lLineBytes; //图像的长度 LONG lWidth; //图像的宽度 LONG lHeight; //获取图像的长度 lWidth=::DIBWidth ((char*)lpDIB); //获取图像的宽度 lHeight=::DIBHeight ((char*)lpDIB); // 计算图像每行的字节数 lLineBytes = WIDTHBYTES(lWidth * 8); //图像左半边的平均高度 double leftaver=0.0, //图像右半边的平均高度 double rightaver=0.0; //图像的倾斜度 double slope; //统计循环变量 LONG counts=0; //扫描左半边的图像,求黑色象素的平均高度 //行 for (i=0;i=lHeight ) gray = 255; else { //否则到源图像中找点,取得象素值 //指向第i_src行第j个象素的指针 lpSrc=(unsigned char *)lpDIBBits + lLineBytes * i_src + j; gray = *lpSrc; } //把新图像的点用得到的象素值填充 //指向第i行第j个象素的指针 lpDst = (char *)lpNewDIBBits + lLineBytes * i + j; *lpDst=gray; } } // 将新的图像的内容拷贝到旧的图像中 memcpy(lpDIBBits,lpNewDIBBits,lLineBytes*lHeight); // 解除锁定 ::GlobalUnlock ((HGLOBAL)hDIB); } 下面我们来编写倾斜度调整菜单处理事件的代码: //图像预处理第5步:倾斜度调整 void CChildView::OnImgprcAdjustSlope() { //调用倾斜度调整函数 SlopeAdjust(m_hDIB); //在屏幕上显示位图 CDC* pDC=GetDC(); DisplayDIB(pDC,m_hDIB); } 经过倾斜度调整后的图像如图11-10所示。 图11-10 经过倾斜度调整后的图像 (6)字符分割 系统在读进来的图像中一般会含有多个数字,识别的时候只能根据每个字符的特征来进行判断,所以还要进行字符分割的工作。这一步工作就是把图像中的字符独立的分割出来。 具体的算法如下: 第一步,先自下向上对图像进行逐行扫描直至遇到第一个黑色的象素点。记录下来。然后再由上向下对图像进行逐行扫描直至找到第一个黑色象素,这样就找到图像大致的高度范围。 第二步,在这个高度范围之内在自左向右逐列进行扫描,遇到第一个黑色象素时认为是字符分割的起始位置,然后继续扫描,直至遇到有一列中没有黑色象素,则认为这个字符分割结束,然后继续扫描,按照上述的方法一直扫描直至图像的最右端。这样就得到了每个字符的比较精确宽度范围。 第三步,在已知的每个字符比较精确的宽度范围内,按照第一步的方法,分别进行自上而下和自下而上的逐行扫描来获取每个字符精确的高度范围。 字符的分割用CharSegment函数来实现。 CharSegment 函数代码如下: /************************************************* * * 函数名称: * CharSegment() * * 参数: * HDIB hDIB -原图像的句柄 * * 返回值: * CRectLink -存放被分割的各个字符位置信息的链表 * * 功能: * 将图像中待识别的字符逐个分离出来并返回存放各个字符的位置信息的链表 * * 说明: * 此函数只能对2值化后的图像进行处理 * *********************************************************/ CRectLink CharSegment(HANDLE hDIB) { //清空用来保存每个字符区域的链表 CRectLink charRect1,charRect2; charRect1.clear(); charRect2.clear(); // 指向DIB的指针 LPSTR lpDIB=(LPSTR) ::GlobalLock((HGLOBAL)hDIB); // 指向DIB象素指针 LPSTR lpDIBBits; // 找到DIB图像象素起始位置 lpDIBBits = ::FindDIBBits(lpDIB); //指向象素的的指针 BYTE* lpSrc; //图像的长度和宽度 int height,width; //获取图像的宽度 width=(int)::DIBWidth(lpDIB); //获取图像的长度 height=(int)::DIBHeight(lpDIB); //计算图像每行的字节数 LONG lLineBytes = WIDTHBYTES(width * 8); //定义上下边界两个变量 int top,bottom; //象素的灰度值 int gray; //设置循环变量 int i,j; //用来统计图像中字符个数的计数器 digicount=0; //从上往下扫描,找到上边界 //行 for (i=0;i=0;i--) { //列 for (j=0;j=rect.top ;i--) { //列 for(j=rect.left ;j #include using namespace std; typedef deque CRectLink; typedef deque HDIBLink; 这样,以后用CRectLink就可以定义一个元素为Crect的链表了。HDIBLink也是一个链表,不过其中的元素为位图句柄HDIB。 假如用CrectLink定义了一个链表a,那么: a. push_back(rect) 可以将一个矩形区域对象插入到链表后部 a. front() 可以读取链表头部的一个矩形对象(只是读取,不删除) a. pop_front() 可以删除链表头部的一个矩形对象 a. empty() 如果链表为空,则返回1;否则,为0 至此相信读者对STL模板类中的deque已经有了一个基本的认识。 该链表的使用十分方便。 下面我们来编写字符分割菜单处理事件的代码: //图像预处理第6步:分割,并在分割出来的字符外面画框以标识 void CChildView::OnImgprcDivide() { //对位图按照字符进行分割,返回的是存有分割后的每个含字符区域的链表 m_charRect=CharSegment(m_hDIB); //在屏幕上显示位图 CDC* pDC=GetDC(); DisplayDIB(pDC,m_hDIB); //在分割好的字符周围画框标识 DrawFrame(pDC,m_hDIB,m_charRect,2,RGB(20,60,200)); } 进行字符分割后并画上边框的图像如图11-11所示。 图11-11 经过字符分割后并画上边框的图像 (7)图像的归一化处理 因为扫描进来的图像中字符大小存在较大的差异,而相对来说,统一尺寸的字符识别的标准性更强,准确率自然也更高,标准化图像就是要把原来各不相同的字符统一到同一尺寸,在系统实现中是统一到同一高度,然后根据高度来调整字符的宽度。具体算法如下:先得到原来字符的高度,跟系统要求的高度做比较,得出要变换的系数,然后根据得到的系数求得变换后应有得宽度。再得到宽度高度之后,把新图像里面得点按照插值得方法映射到原图像中。 图像的标准归一化处理由函数StdDIBbyRect来实现,代码如下: /****************************************************************** * * 函数名称: * StdDIBbyRect() * * 参数: * HDIB hDIB -图像的句柄 * int tarWidth -标准化的宽度 * int tarHeight -标准化的高度 * * 返回值: * 无 * * 功能: * 将经过分割的字符,进行缩放处理使他们的宽和高一直,以方便特征的提取 * * 说明: * 函数中用到了,每个字符的位置信息,所以必须在执行完分割操作之后才能执行标准化操作 * ******************************************************************/ void StdDIBbyRect(HDIB hDIB, int tarWidth, int tarHeight) { //指向图像的指针 BYTE* lpDIB=(BYTE*)::GlobalLock ((HGLOBAL)hDIB); //指向象素起始位置的指针 BYTE* lpDIBBits=(BYTE*)::FindDIBBits ((char*)lpDIB); //指向象素的指针 BYTE* lpSrc; //获取图像的的宽度 LONG lWidth=::DIBWidth ((char*)lpDIB); //获取图像的高度 LONG lHeight=::DIBHeight ((char*)lpDIB); // 循环变量 int i; int j; // 图像每行的字节数 LONG lLineBytes = WIDTHBYTES(lWidth * 8); //宽度、高度方向上的缩放因子 double wscale,hscale; //开辟一块临时缓存区,来存放变化后的图像信息 LPSTR lpNewDIBBits; LPSTR lpDst; //缓存区的大小和原图像的数据区大小一样 HLOCAL nNewDIBBits=LocalAlloc(LHND,lLineBytes*lHeight); //指向缓存区开始位置的指针 lpNewDIBBits=(char*)LocalLock(nNewDIBBits); //指向缓存内信息的指针 lpDst=(char*)lpNewDIBBits; //将缓存区的内容赋初始值 memset(lpDst,(BYTE)255,lLineBytes*lHeight); //进行映射操作的坐标变量 int i_src,j_src; //存放字符位置信息的Crect对象 CRect rect; CRect rectnew; //先清空一个新的矩形区域链表以便存储标准化后的矩形区域链表 m_charRectCopy.clear (); //从头到尾逐个扫描各个结点 while(!m_charRect.empty()) { //从表头上得到一个矩形 rect= m_charRect.front(); //从链表头上面删掉一个 m_charRect.pop_front(); //计算缩放因子 //横坐标方向的缩放因子 wscale=(double)tarWidth/rect.Width (); //纵坐标方向的缩放因子 hscale=(double)tarHeight/rect.Height (); //计算标准化矩形 //上边界 rectnew.top =rect.top ; //下边界 rectnew.bottom =rect.top +tarHeight; //左边界 rectnew.left =rect.left ; //右边界 rectnew.right =rectnew.left +tarWidth; //将原矩形框内的象素映射到新的矩形框内 for(i=rectnew.top ;i0) { if(!cond) continue; g[kk] = 0; shori = 1; continue; } if(n[6]==1 && n48==0 && n123>0) { if(!cond) continue; g[kk] = 0; shori = 1; continue; } if(n[8]==1 && n26==0 && n345>0) { if(!cond) continue; g[kk] = 0; shori = 1; continue; } if(n[4]==1 && n26==0 && n781>0) { if(!cond) continue; g[kk] = 0; shori = 1; continue; } if(n[5]==1 && n46==0) { if(!cond) continue; g[kk] = 0; shori = 1; continue; } if(n[7]==1 && n68==0) { if(!cond) continue; g[kk] = 0; shori = 1; continue; } if(n[1]==1 && n82==0) { if(!cond) continue; g[kk] = 0; shori = 1; continue; } if(n[3]==1 && n24==0) { if(!cond) continue; g[kk] = 0; shori = 1; continue; } cond = 1; if(!cond) continue; g[kk] = 0; shori = 1; } } for(i=0; i(w-(tem_w+1)/2) || i<((tem_h-1)/2) || i>(h-(tem_h+1)/2) ) *(newbuf+i*dw+j)=*(oldbuf+i*dw+j); //对于其他的象素进行模板操作 else { //将点(i,j)点作为模板的中心 for(m=i-((tem_h-1)/2);m<=i+((tem_h-1)/2);m++) { for(n=j-((tem_w-1)/2);n<=j+((tem_w-1)/2);n++) //将以点(i,j)为中心,与模板大小相同的范围内的象素与模板对用位置的系数 //进行相乘并线形叠加 sum+=*(oldbuf+m*dw+n)* tem[(m-i+((tem_h-1)/2))*tem_w+n-j+((tem_w-1)/2)]; } //将结果乘上总的模板系数 sum=(int)sum*xishu; //计算绝对值 sum = fabs(sum); //如果小于0,强制赋值为0 if(sum<0) sum=0; //如果大于255,强制赋值为255 if(sum>255) sum=255; //将计算的结果放到新的位图的相应位置 *(newbuf+i*dw+j)=sum; } } } //解除锁定 ::GlobalUnlock((HGLOBAL)hDIB); //返回新的位图的句柄 return(newhDIB); } 添加均值平滑的菜单相应事件: void CChildView::Onaver() { // TODO: Add your command handler code here //设定模板参数 double tem[9]={1,1,1, 1,1,1, 1,1,1}; //设定模板系数 double xishu = 0.111111; //进行模板操作 m_hDIB =Template(m_hDIB,tem ,3,3, xishu); //显示图像 CDC* pDC=GetDC(); DisplayDIB(pDC,m_hDIB); } 添加高斯平滑的菜单相响应事件: void CChildView::OnGass() { // TODO: Add your command handler code here //设定模板参数 double tem[9]={1,2,1, 2,4,2, 1,2,1}; //设定模板系数 double xishu = 0.0625; //进行模板操作 m_hDIB =Template(m_hDIB,tem ,3,3, xishu); //显示图像 CDC* pDC=GetDC(); DisplayDIB(pDC,m_hDIB); } 中值滤波技术 中值滤波也是一种典型的低通滤波器,它的目的是保护图象边缘的同时去除噪声。所谓中值滤波,就是指把以某点(x,y)为中心的小窗口内的所有像素的灰度按从大到小的顺序排列,将中间值作为(x,y)处的灰度值(若窗口中有偶数个像素,则取两个中间值的平均)。中值滤波是如何去除噪声的呢?可以通过下面的例子 原图 处理后的图 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 1 1 1 0 00 1 1 1 0 0 0 1 6 1 0 00 1 1 1 0 0 0 1 1 1 0 00 1 1 1 0 0 0 0 0 0 0 00 0 0 0 0 上图中左边是原图,数字代表该处的灰度。可以看出中间的6和周围的灰度相差很大,是一个噪声点。经过3*1窗口(即水平3个像素取中间值)的中值滤波,得到右边那幅图,可以看出,噪声点被去除了。 拿中值滤波和上面介绍的两种平滑模板做个比较,看看中值滤波有什么特点,我们以一维模板为例,只考虑水平方向,大小为3*1(宽*高)。Box模板为 ;Gauss模板为 。 先考察第一幅图: 原图 0 0 0 1 1 1 0 0 1/3 2/3 1 1 0 0 1/4 3/4 1 1 0 0 0 1 1 1 经平滑模板处理后的图 经Gauss模板处理后的图经中值滤波处理后的图 0 1/3 2/3 10 1/4 3/4 10 0 1 1 1/9 1/3 2/3 8/91/12 1/3 2/3 11/120 1/3 2/3 1 1/12 1/3 2/3 11/121/16 5/16 11/16 15/160 1/4 3/4 1 0 1/3 2/3 1 0 1/4 3/4 10 0 1 1 从原图中不难看出左边区域灰度值低,右边区域灰度值高,中间有一条明显的边界,这是一类图象,我们称之为Step(就象灰度上了个台阶)。应用平滑模板,图象平滑了,但是也使边界模糊了。应用中值滤波,就能很好地保持原来的边界。所以说,中值滤波的特点是保护图象边缘的同时去除噪声。 再看第二类图: 原图 -1 1 -1 1 -1 1/3 -1/3 1/3 -1/3 1/3 0 0 0 0 0 1 -1 1 -1 1 经平滑模板处理后的图 经Gauss模板处理后的图 经中值滤波处理后的图 -1/3 1/3 -1/30 0 0 -1 1 -1 1/9 -1/9 1/90 0 0 1/3 -1/3 1/3 0 0 00 0 00 0 0 1/3 -1/3 1/30 0 01 -1 1 从原图中不难看出,有很多噪声点(灰度为正代表灰度值高的点,为负代表灰度值低的点),而且是杂乱无章,随机分布的。这也是一类很典型的图,称之为高斯噪声。经过Box平滑,噪声的程度有所下降。Gauss模板对付高斯噪声非常有效。而中值滤波对于高斯噪声则无能为力。 再看第三类图: 原图 1 2 10 2 1 1 3 11 3 1 1 2 10 2 1 经平滑模板处理后的图 经Gauss模板处理后的图经中值滤波处理后的图 13/3 14/3 13/315/4 6 15/42 2 2 5 17/3 59/2 7 9/23 3 3 13/3 14/3 13/315/4 6 15/42 2 2 从原图中不难看出,中间的灰度要比两边高许多。这也是一类很典型的图,称之为Impulse(脉冲)。可见,中值滤波对Impulse噪声非常有效。 综合以上三类图,不难得出下面的结论:中值滤波容易去除孤立点,线的噪声同时保持图象的边缘,它能很好的去除二值噪声,但对高斯噪声无能为力。要注意的是,当窗口内噪声点的个数大于窗口宽度的一半时,中值滤波的效果不好。这是很显然的 中值滤波的源代码 /***************************************************** * * 函数名称: * Template: * * 参数: * HDIB hDIB -图像的句柄 * int tem_w -模板的宽度 * int tem_h -模板的高度 * * 功能: * 对图像进行中值 * * 说明: * 为处理方便起见,模板的宽度和高度都应为奇数 *******************************************************/ HDIB MidFilter(HDIB hDIB,int tem_w,int tem_h) { //统计中间值 double mid; BYTE *temp=(BYTE*)malloc(tem_w*tem_h*sizeof(BYTE)); //指向图像起始位置的指针 BYTE *lpDIB=(BYTE*)::GlobalLock((HGLOBAL) hDIB); //指向象素起始位置的指针 BYTE *pScrBuff =(BYTE*)::FindDIBBits((char*)lpDIB); //获取图像的颜色信息 int numColors=(int) ::DIBNumColors((char *)lpDIB); //如果图像不是256色返回 if (numColors!=256) { //解除锁定 ::GlobalUnlock((HGLOBAL) hDIB); //返回 return(hDIB); } //将指向图像象素起始位置的指针,赋值给指针变量 BYTE* oldbuf = pScrBuff; //循环变量 int i,j,m,n; int w, h, dw; //获取图像的宽度 w = (int) ::DIBWidth((char *)lpDIB); //获取图像的高度 h = (int) ::DIBHeight((char *)lpDIB); //计算图像每行的字节数 dw = (w+3)/4*4; //建立一个和原图像大小相同的25色灰度位图 HDIB newhDIB=NewDIB(w,h,8); //指向新的位图的指针 BYTE *newlpDIB=(BYTE*)::GlobalLock((HGLOBAL) newhDIB); //指向新的位图的象素起始位置的指针 BYTE *destBuf = (BYTE*)FindDIBBits((char *)newlpDIB); //将指向新图像象素起始位置的指针,赋值给指针变量 BYTE *newbuf=destBuf; //对图像进行扫描 //行 for(i=0;i(w-(tem_w+1)/2) || i<((tem_h-1)/2) || i>(h-(tem_h+1)/2) ) *(newbuf+i*dw+j)=*(oldbuf+i*dw+j); //对于其他的象素进行模板操作 else { //将点(i,j)点作为模板的中心 for(m=i-((tem_h-1)/2);m<=i+((tem_h-1)/2);m++) { for(n=j-((tem_w-1)/2);n<=j+((tem_w-1)/2);n++) //将以点(i,j)为中心,与模板大小相同的范围内的象素传递到模板矩阵中 temp[(m-i+((tem_h-1)/2))*tem_w+n-j+((tem_w-1)/2)]=*(oldbuf+m*dw+n); } //利用气泡法计算中值 for(m=0;mtemp[n+1]) mid=temp[n]; temp[n]=temp[n+1]; temp[n+1]=mid; } } //将计算的结果放到新的位图的相应位置 *(newbuf+i*dw+j)=temp[(tem_w*tem_h-1)/2]; } } } //解除锁定 ::GlobalUnlock((HGLOBAL)hDIB); //返回新的位图的句柄 return(newhDIB); } 添加中值滤波的菜单事件响应: void CChildView::OnMid() { //进行中值滤波 m_hDIB =MidFilter(m_hDIB,3,3); //显示图像 CDC* pDC=GetDC(); DisplayDIB(pDC,m_hDIB); } 直方图均衡技术 图像直方图是图像处理中一种十分重要的图像分析工具,它描述了一幅图像的灰度级内容,任何一幅图像的直方图都包含了丰富的信息,它主要用在图象分割,图像灰度变换等处理过程中。从数学上来说图像直方图是图像各灰度值统计特性与图像灰度值的函数,它统计一幅图像中各个灰度级出现的次数或概率;从图形上来说,它是一个二维图,横坐标表示图像中各个像素点的灰度级,纵坐标为各个灰度级上图像各个像素点出现的次数或概率。 直方图均衡化是灰度变换的一个重要应用,广泛应用在图像增强处理中,它是以累计分布函数变换为基础的直方图修正法,可以产生一幅灰度级分布具有均匀概率密度的图像,扩展了像素的取值动态范围。 但是直方图均衡化存在着两个缺点:   1)变换后图像的灰度级减少,某些细节消失;   2)某些图像,如直方图有高峰,经处理后对比度不自然的过分增强。 为此M.Kamel和Lian Guan等人从图像相邻像素一般高度相关这一事实出发,将灰度概率分布和空间相关性联系在一起,提出了用二维条件概率密度函数取代一维概率密度函数作为均衡化条件,很好的解决了这个问题,有兴趣的读者可以参阅一些图像处理书籍和资料。 这里笔者将直方图均衡的源代码列出如下: /************************************************************************ * * 函数名称: * Equalize() * * 参数: * HDIB hDIB * * * 说明: * 该函数用来对图像进行直方图均衡。 * ***********************************************************************/ void Equalize(HDIB hDIB) { BYTE* lpDIB=(BYTE*)::GlobalLock ((HGLOBAL)hDIB); //获得象素数据区起始指针 BYTE* lpDIBBits=(BYTE*)::FindDIBBits((char*)lpDIB); //获得图像宽度和高度 LONG lHeight=::DIBHeight ((char*)lpDIB); LONG lWidth=::DIBWidth ((char*)lpDIB); // 指向源图像的指针 unsigned char* lpSrc; // 临时变量 LONG lTemp; // 循环变量 LONG i; LONG j; // 灰度映射表 BYTE bMap[256]; // 灰度映射表 LONG lCount[256]; // 图像每行的字节数 LONG lLineBytes; // 计算图像每行的字节数 lLineBytes = WIDTHBYTES(lWidth * 8); // 重置计数为0 for (i = 0; i < 256; i ++) { // 清零 lCount[i] = 0; } // 计算各个灰度值的计数 for (i = 0; i < lHeight; i ++) { for (j = 0; j < lWidth; j ++) { lpSrc = (unsigned char *)lpDIBBits + lLineBytes * i + j; // 计数加1 lCount[*(lpSrc)]++; } } // 计算灰度映射表 for (i = 0; i < 256; i++) { // 初始为0 lTemp = 0; for (j = 0; j <= i ; j++) { lTemp += lCount[j]; } // 计算对应的新灰度值 bMap[i] = (BYTE) (lTemp * 255 / lHeight / lWidth); } // 每行 for(i = 0; i < lHeight; i++) { // 每列 for(j = 0; j < lWidth; j++) { // 指向DIB第i行,第j个象素的指针 lpSrc = (unsigned char*)lpDIBBits + lLineBytes * (lHeight - 1 - i) + j; // 计算新的灰度值 *lpSrc = bMap[*lpSrc]; } } ::GlobalUnlock ((HGLOBAL)hDIB); } 添加菜单响应代码: void CChildView::OnImgprcEqualize() { //进行直方图均衡处理 Equalize(m_hDIB); //显示处理结果 CDC* pDC=GetDC(); DisplayDIB(pDC,m_hDIB); } 下面的两副抓图是对“girl”图像做直方图均衡化前后的效果对比。 “girl”图像已经被我加入了数字,可见,在直方图均衡化处理之前,数字几乎无法辨认;处理之后,则可以从女孩的背景中分辨出数字了 原始图像: 直方图均衡之后: 可见,经过直方图均衡化,数字“0123”已经可以分辨了。 4. 用神经网络进行字符识别 (1)BP神经网络简介 下面我们首先来简要介绍一下神经网络,然后再详细介绍BP网络。 神经网络简介:人工神经网络(Artificial Neural Network,ANN),也称为神经网络(Neural Networks,NN)。即从生物学神经系统的信号传递而抽象发展而成的一门学科。在神经网络中,最基本的单元就是神经元。神经元由三部分组成:树突、细胞体和轴突。树突是树状的神经纤维接受网络,它将电信号传递给细胞体,细胞体对这些输入信号进行整合并进行阀值处理。轴突是单根长纤维,它把细胞体的输出信号导向其他的神经元。神经元的排列拓扑结构和突触的连接强度确立了神经网络的功能。形象的说,神经网络是由大量处理单元(神经元 Neurons)广泛连接而成的网络,是对人脑的抽象、简化和模拟,反映人脑的基本特性。它能够通过学习过程从外部环境中获取知识,并且它内部的很多的神经元可以用来存储这些已经学到的知识。 上图是将生物神经元模型抽象成一个信号传递的数学模型。 神经元的输入是信号P,经过一个累加器累加后的信号送入一个激活函数f,从而得到这个神经元的输出a。这个神经元的输出a同时又可以作为下一个或多个神经元的输入,从而将神经信号成网络分散状的传递出去。一个神经元可以接受多个输入,所以把神经元表示成为矢量、矩阵形式更容易去处理分析实际问题。 这里,P表示一R维的输入向量,B为偏置向量,W为网络的权值矩阵,f为激活传输函数,a为网络的输出向量。 以上是对一个单层神经网络的描述。一般就实际情况而言,多层网络用处要广的多。后面用到的BP网络也是多层的。 在多层网络中,一般至少有3个层:一个输入层、一个输出层、一个或多个隐层。多层网络可以解决很多单层网络无法解决的问题,比如多层网络可以用来进行非线性分类,可以用来做精度极高的函数逼近,只要有足够多的层和足够多的神经元,这些都可以办到。 一个多层网络的输入和输出层的神经元个数是由外部描述定义的。例如如果有4个外部变量作为输入,那网络就要有4个输入。关于隐层神经元的确定,将在BP网络的设计中详细讨论。 文中曾多次提到过,感知器是神经网络的基础,也是BP网络的基础。所谓感知器(perceptron),也即给定一个或多个已知类别的输入,通过对网络的训练以实现所有输入数据的正确分类的数学模型。注意,感知器是单层网络。这里,训练的意思就是通过感知器的输出来反复调整网络的权值,以使其满足所有分类都正确的要求。不过一般来说,感知器的分类能力较差。 神经网络有好多种,比如径向基网络、BP网络,Hopfield 网络等。 本神经网络识别系统采用的是使用最为广泛的BP网络。 下面介绍BP学习算法,BP 学习过程可以描述如下: 作信号正向传播:输入信号从输入层经过隐含层,传向输出层,在输出端生输出信 这是工作信号的正向传播。在信号传递的过程中网络的权值是固定不变的,每一层神经元的状态只影响下一层神经元的状态。如果在输出层得到的输出和期望输出的偏差比较大,则转入误差信号的反向传播。 误差信号反向传播:网络的实际输出和期望输出的差值就是误差信号。误差信号的反向传播就是误差信号从输出端传向输入端。在这个过程当中,网络的权值由误差反馈进行调节。通过不断的修改网络权值从而使得网络的输出不断的逼近期望值。 图11-17 是神经网络的示意图。图11-18是多层BP网络结构示意图。 图11-17 神经网络示意图 图11-18 多层BP网络示意图 神经网络的激励函数。激励函数将输出信号压缩在一个允许的范围内,使其成为有限值,通常神经元输出的范围在【0,1】或者【-1,1】的闭区间上。常用的基本激励函数有阈值函数、分段线性函数、Sigmoid 函数。其中Sigmoid 函数也称为s型函数,它是人工神经网络中用的最多的激励函数。S型函数的定义如下: 公式(1) 神经网络的反馈调节。在误差信号的反向传播中,网络不断的修正各个节点的权值。 设有含n个节点的BP网络,采用s型激励函数。为简单起见,可以假设网络只有一个输出y,任一节点的输出为,并设有N个样本k = 1,2,…,N),对某一输入,网络的输出为,节点i的输出为,节点j的输入为: 公式(2) 使用平方型误差函数: 公式(3) 其中为网络之实际输出,定义如下: 公式(4) 公式(5) 公式(6) 其中: 公式(7) 于是有: 公式(8) 当j为输出节点时, 公式(9) 若j不是输出节点时,有: 公式(10) 公式(11) 因此可得: BP网络的学习过程。BP学习算法可以归纳如下: 第一步 设置变量和参数,其中包括训练样本,权值矩阵,学习速率。 第二步 初始化,给各个权值矩阵一个较小的随机非零向量。 第三步 输入随机样本。 第四步 对输入样本,前向计算BP网络每层神经元的输入信号和输出信号。 第五步 由实际输出和期望输出求得误差。判断是否满足要求,若满足转第八步;步满足转第六步。 第六步 判断是否已经到了最大迭代次数,若到,转第八步,否则反向计算每层神经元的局部梯度。 第七步 根据局部梯度修正各个矩阵的权值。 第八步 判断是否学习完所有的样本,是则结束,否则转第三步。 据笔者经验,BP学习中需要注意的几点: A. 权值的初始化。权值的初始值应该选择均匀分布的小数经验值。初始值过大或者过小都会影响学习速度。为了避免权值的调整是同向的,应该将初始值设为随机数。 B. 初始权值不要太大。否则可能会处于误差平面较平坦的区域,从而导致算法无法收敛,训练失败。 C. 神经元的激励函数是s型函数。所以如果函数的渐近值是0,1的话,期望输出只能是小于1大于0的数,而不能是1或者0,否则可能会导致算法不收敛。在程序中建议读者用0.1来代表0,0.9代表1。 (2)本程序中BP网络的设计及其编程实现 下面就开始用BP网络的思想来设计实现一个真正的实际的神经网络。 BP网络的一个重要的用途就是用于模式识别。我们的任务是要设计并训练出一个可行、高效的BP网络,以实现对0到9共10个数字和识别。 经图像预处理过程之后,可以将最终提取到的字符的特征送入BP网络进行训练及识别了。这里,假设我们设定的字符标准归一化的宽度为8,高度为16,那么对于每个字符就有128维的特征。 设计BP网络的关键之处在于高效的特征提取方法、大量有代表性的训练样本、高效稳定速收敛的学习方法。 BP网络应用过程如图11-19所示。 图11-19 BP网络应用流程 下面来讲讲BP网络三个层的神经元数目的确定。这也是BP网络设计的关键。 BP网络应用的第一步就是要用已知训练样本来训练BP网络。这里,BP网络的输入层的结点个数,为图像预处理后所输出的特征的维数。例如,我们采用了8×16归一化,特征提取采用的是逐个象素特征提取方法,也即直接利用每个点的象素值来作为特征,这样,对于每一个输入样本,就有8×16=128个特征。所以就可以确定,输入层的结点数为128。 对于隐层的结点数,没有什么硬性规定,一般来说,隐层神经元的数目越多,那么BP网络也就越精确,训练时间也越长。但要注意,隐层神经元不易选取太多,否则会造成识别率的急剧下降,也会降低网络的抗噪声能力。在本程序中,笔者推荐使用10个隐层神经元。读者可以自行测试一下,当将隐层神经元个数改为30个时,训练时间和识别率的变化。看来神经元的个数也不是越多越好,过犹不及。 对于输入层的结点数的确定,这决定于我们如何设定标准输出。也就是说如何对目标期望输出进行编码。在本程序中,笔者采用了8421码来对0、1、2、3、4、5、6、7、8、9来进行编码。对于输出“0”,我们采用(0,0,0,0)这样的目标输出向量来表示,对于输出“1”,我们采用(0,0,0,1)这样的输出向量来表示,同理,对于输出“9”,我们采用(1,0,0,1)这样的输出向量来表示。这样一来,我们就可以确定输出层的神经元数目为:4。也即为输出向量的维数。 其实通过后续编程实践我们可以发现,当采用(0,0,0,0)这样的目标输出向量的时候,BP网络无法收敛。那是因为我们采用的激活函数(传输函数)的输出永远不可能达到0或1,而只能是接近。所以,在这里我们要纠正一下目标输出向量。对其重新编码后,最终确定编码方案如下: 0的编码: 0.1,0.1,0.1,0.1 1的编码: 0.1,0.1,0.1,0.9 2的编码: 0.1,0.1,0.9,0.1 3的编码: 0.1,0.1,0.9,0.9 4的编码: 0.1,0.9,0.1,0.1 5的编码: 0.1,0.9,0.1,0.9 6的编码: 0.1,0.9,0.9,0.1 7的编码: 0.1,0.9,0.9,0.9 8的编码: 0.9,0.1,0.1,0.1 9的编码: 0.9,0.1,0.1,0.9 使用BP网络来进行数字识别的流程如下: 首先,利用大量的训练样本来训练网络,以得到以文件形式保存的权值。训练样本为精心选择的可以很好的反应样本可分性特性的已知数据。在程序中采用训练样本图片的形式。将训练样本图片进行特征提取后,就可以送入BP网络进行训练。在这里,作者采用了含有40个字符数据的图像作为训练样本。这副图像包含了Arial字体书写的普通的0到9的10个数字、斜体的10个数字、粗体的10个数字和10个倾斜的数字,总共40个,如图11-20所示。 图11-20 40个训练样本 实验证明,这40个训练样本训练出来的BP网络对于Arial字体的数字可以达到90%以上的识别率,而且网络训练时间也是可以接受的(大约1~5秒钟)。当然如果采用400个训练样本甚至更多的话,那无疑可以进一步提高识别率,但训练时间往往会达到分钟甚至更高到小时的级别,实际意义不大了。 其次,训练完BP网络后,就可以用它对为止数据进行识别了。识别首先要经过图像预处理、特征提取,最后送入BP网络识别,直接得到结果。 在训练之前,程序要求输入训练参数,如训练误差、步长等,如图11-21所示。 图11-21 BP训练参数输入框 下面作者列出bp网络训练及识别的完整源代码bp.h。 #include #include #include #include #define BIGRND 32767 /************************函数声明部分*****************************/ //随机数产生函数 double drnd(); double dpn1(); //S函数 double squash(double x); //分配1维double型的内存 double *alloc_1d_dbl(int n); //分配2维double型的内存 double **alloc_2d_dbl(int m, int n); //初始化BP网络 void bpnn_initialize(int seed); //随机初始化权值 void bpnn_randomize_weights(double **w, int m, int n); //零初始化权值 void bpnn_zero_weights(double **w, int m, int n); //信息的前向传输 void bpnn_layerforward(double *l1, double *l2, double **conn, int n1, int n2); //误差输出 void bpnn_output_error(double *delta, double *target, double *output, int nj); //隐层误差 void bpnn_hidden_error(double* delta_h, int nh, double *delta_o, int no, double **who, double *hidden); //根据误差调整权值 void bpnn_adjust_weights(double *delta, int ndelta, double *ly, int nly, double** w, double **oldw, double eta, double momentum); //保存权值 void w_weight(double **w,int n1,int n2,char*name); //读取权值 bool r_weight(double **w,int n1,int n2,char *name); //保存Bp网络各层结点数目 void w_num(int n1,int n2,int n3,char*name); //读取Bp网络各层结点数目 bool r_num(int *n,char *name); //特征提取 void code(BYTE*image ,int *p,int w,int h,int dw); //BP网络训练 void BpTrain(HDIB hDIB,int n_hidden,double min_ex,double momentum,double eta ,int width,int height); //利用BP网络进行识别 void CodeRecognize(HDIB hDIB,int width ,int height ,int n_in ,int n_hidden,int n_out); /**********************************************************************/ /****************** 以下是函数的实现部分***********************************/ /*** 返回0-1的双精度随机数 ***/ double drnd() { //BIGRND定义为随机数的最大范围 return ((double) rand() / (double) BIGRND); } /*** 返回-1.0到1.0之间的双精度随机数 ***/ double dpn1() { return ((drnd() * 2.0) - 1.0); } double squash(double x) { //返回S激活函数 return (1.0 / (1.0 + exp(-x))); } /*** 申请1维双精度实数数组 ***/ double *alloc_1d_dbl(int n) { double *new1; //申请内存 new1 = (double *) malloc ((unsigned) (n * sizeof (double))); //申请内存失败处理 if (new1 == NULL) { printf("ALLOC_1D_DBL: Couldn't allocate array of doubles\n"); return (NULL); } //返回申请到的内存的指针 return (new1); } /*** 申请2维双精度实数数组 ***/ double **alloc_2d_dbl(int m, int n) { int i; //定义一二维指针 double **new1; //先申请一维内存 new1 = (double **) malloc ((unsigned) (m * sizeof (double *))); //申请失败处理 if (new1 == NULL) { // printf("ALLOC_2D_DBL: Couldn't allocate array of dbl ptrs\n"); return (NULL); } //再申请二维内存。一维内存中存放的是指针 for (i = 0; i < m; i++) { new1[i] = alloc_1d_dbl(n); } //返回申请到的二维内存(可以看作矩阵) return (new1); } /*** 设置随机数种子 ***/ void bpnn_initialize(int seed) { //printf("Random number generator seed: %d\n", seed); srand(seed); } /*** 随机初始化权值 ***/ void bpnn_randomize_weights(double **w, int m, int n) { int i, j; //调用dpn1随机初始化权值 for (i = 0; i <= m; i++) { for (j = 0; j <= n; j++) { w[i][j] = dpn1(); } } } /******* 零初始化权值 *******/ void bpnn_zero_weights(double **w, int m, int n) { int i, j; //将权值逐个赋0 for (i = 0; i <= m; i++) { for (j = 0; j <= n; j++) { w[i][j] = 0.0; } } } /*********前向传输*********/ void bpnn_layerforward(double *l1, double *l2, double **conn, int n1, int n2) { double sum; int j, k; /*** 设置阈值 ***/ l1[0] = 1.0; /*** 对于第二层的每个神经元 ***/ for (j = 1; j <= n2; j++) { /*** 计算输入的加权总和 ***/ sum = 0.0; for (k = 0; k <= n1; k++) { sum += conn[k][j] * l1[k]; } l2[j] = squash(sum); } } /* 输出误差 */ void bpnn_output_error(double *delta, double *target, double *output, int nj) { int j; double o, t, errsum; //先将误差归零 errsum = 0.0; //循环计算delta for (j = 1; j <= nj; j++) { o = output[j]; t = target[j]; //计算delta值 delta[j] = o * (1.0 - o) * (t - o); } } /* 隐含层误差 */ void bpnn_hidden_error(double* delta_h, int nh, double *delta_o, int no, double **who, double *hidden) { int j, k; double h, sum, errsum; //误差归零 errsum = 0.0; //计算新delta for (j = 1; j <= nh; j++) { h = hidden[j]; sum = 0.0; for (k = 1; k <= no; k++) { sum += delta_o[k] * who[j][k]; } delta_h[j] = h * (1.0 - h) * sum; } } /* 调整权值 */ void bpnn_adjust_weights(double *delta, int ndelta, double *ly, int nly, double** w, double **oldw, double eta, double momentum) { double new_dw; int k, j; ly[0] = 1.0; //请参考文章中BP网络权值调整的计算公式 for (j = 1; j <= ndelta; j++) { for (k = 0; k <= nly; k++) { new_dw = ((eta * delta[j] * ly[k]) + (momentum * oldw[k][j])); w[k][j] += new_dw; oldw[k][j] = new_dw; } } } /*******保存权值**********/ void w_weight(double **w,int n1,int n2,char*name) { int i,j; double *buffer; //创建文件指针 FILE *fp; //打开文件 fp=fopen(name,"wb+"); //分配缓冲区 buffer=(double*)malloc((n1+1)*(n2+1)*sizeof(double)); //填写缓冲区内容 for(i=0;i<=n1;i++) { for(j=0;j<=n2;j++) buffer[i*(n2+1)+j]=w[i][j]; } //将缓冲区内容写入文件 fwrite((char*)buffer,sizeof(double),(n1+1)*(n2+1),fp); //关闭文件 fclose(fp); //清空缓冲区 free(buffer); } /************读取权值*************/ bool r_weight(double **w,int n1,int n2,char *name) { int i,j; //临时缓冲区指针 double *buffer; //文件指针 FILE *fp; //判断是否可以正确打开文件。若失败则返回false if((fp=fopen(name,"rb"))==NULL) { ::MessageBox(NULL,"无法读取权值信息",NULL,MB_ICONSTOP); return (false); } //将临时缓冲区指针执行新申请的内存 buffer=(double*)malloc((n1+1)*(n2+1)*sizeof(double)); //读取文件内容到缓冲区 fread((char*)buffer,sizeof(double),(n1+1)*(n2+1),fp); //由缓冲区内容填写权值 for(i=0;i<=n1;i++) { for(j=0;j<=n2;j++) w[i][j]=buffer[i*(n2+1)+j]; } //关闭文件 fclose(fp); //释放临时缓冲区 free(buffer); //返回true表示已经正确读取 return(true); } /*****保存Bp网络各层结点的数目******/ void w_num(int n1,int n2,int n3,char*name) { //文件指针 FILE *fp; //打开文件 fp=fopen(name,"wb+"); //缓冲区指针 int *buffer; //分配内存并指向缓冲区指针 buffer=(int*)malloc(3*sizeof(int)); //将网络各层参数信息保存在临时缓冲区 buffer[0]=n1; buffer[1]=n2; buffer[2]=n3; //将临时缓冲区内容写入文件 fwrite((char*)buffer,sizeof(int),3,fp); //关闭文件 fclose(fp); //清空缓冲区 free(buffer); } /********读取Bp网络各层结点数目*********/ bool r_num(int *n,char *name) { //临时缓冲区指针 int *buffer; //文件指针 FILE *fp; //分配内存空间 buffer=(int *)malloc(3*sizeof(int)); if((fp=fopen(name,"rb"))==NULL) { ::MessageBox(NULL,"结点参数",NULL,MB_ICONSTOP); return (false); } //读取文件到缓冲区 fread((char*)buffer,sizeof(int),3,fp); //从缓冲区中取出数据,存入n[0]~n[2] n[0]=buffer[0]; n[1]=buffer[1]; n[2]=buffer[2]; //关闭文件 fclose(fp); //清空缓冲区 free(buffer); //正确读取,返回true return(true); } /********************************** * 函数名称 code() * * 参量: * BYTE* lpDIBBits -指向输入图像的象素其实位置的指针 * int num -图片中样本的个数 * LONG lLineByte -输入图片每行的字节数 * LONG lSwidth -预处理时归一化的宽度 * LONG lSheight -预处理时归一化的长度 * * 函数功能 : * 对于输入样本提取特征向量,在这里把归一化样本的 * 每一个象素都作为特征提取出来 **************************************/ double** code (BYTE* lpDIBBits,int num, LONG lLineByte,LONG lSwidth,LONG lSheight) { //循环变量 int i,j,k; BYTE* lpSrc; // 建立保存特征向量的二维数组 double **data; // 为这个数组申请二维存储空间 data = alloc_2d_dbl(num,lSwidth*lSheight); // 将归一化的样本的每个象素作为一个特征点提取出来 //逐个数据扫描 for(k=0;kmin_ex) { str.Format("迭代%d次,平均误差%.4f\n我已经尽了最大努力了还是达不到您的要求\n请调整参数重新训练吧!",l,ex); ::MessageBox(NULL,str,"训练结果",NULL); } //释放内存空间 free(input_unites); free(hidden_unites); free(output_unites); free(hidden_deltas); free(output_deltas); free(target); free(input_weights); free(hidden_weights); free(input_prev_weights); free(hidden_prev_weights); } /******************************************* * 函数名称 * CodeRecognize() * 参量 * double **data_in -指向待识别样本特征向量的指针 * int num -待识别的样本的个数 * int n_in -Bp网络输入层结点的个数 * int n_hidden -Bp网络隐层结点的个数 * int n_out -Bp网络输出层结点的个数 * 函数功能: * 读入输入样本的特征相量并根据训练所得的权值 * 进行识别,将识别的结果写入result.txt ****************************************/ void CodeRecognize(double **data_in, int num ,int n_in,int n_hidden,int n_out) { //循环变量 int i,k; // 指向识别结果的指针 int *recognize; //为存放识别的结果申请存储空间 recognize=(int*)malloc(num*sizeof(int)); //指向输入层数据的指针 double* input_unites; //指向隐层数据的指针 double* hidden_unites; //指向输出层数据的指针 double* output_unites; //指向输入层于隐层之间权值的指针 double** input_weights; //指向隐层与输出层之间的权值的指针 double** hidden_weights; //为各个数据结构申请内存空间 input_unites= alloc_1d_dbl(n_in + 1); hidden_unites=alloc_1d_dbl(n_hidden + 1); output_unites=alloc_1d_dbl(n_out + 1); input_weights=alloc_2d_dbl(n_in + 1, n_hidden + 1); hidden_weights = alloc_2d_dbl(n_hidden + 1, n_out + 1); //读取权值 if( r_weight(input_weights,n_in,n_hidden,"win.dat")==false) return; if(r_weight(hidden_weights,n_hidden,n_out,"whi.dat")==false) return; //逐个样本扫描 for(k=0;k0.5) result+=(int)pow(2,double(4-i)); } //如果判定的结果小于等于9,认为合理 if(result<=9) recognize[k]=result; //如果判定的结果大于9,认为不合理将结果定位为一个特殊值20 if(result>9) recognize[k]=20; } //将识别结果写到文本中 FILE *fp; fp=fopen("result.txt","w+"); for(i=0;i typedef tmat CMatrix; typedef tvec CVector; //此时就可以用CMatrix和CVector来定义矩阵和向量对象了 //矩阵对象可以是权值,偏置,而输入和标准输出则可以用向量来表示 CMatrix W,A,B,N; N=W*A+B; //就这么简单!当然,还有很多前叙工作这里没做。这里只是简单说明一下 矩阵类库的完整代码我在这里就不列出了,仅仅举一个用类来实现重载“+”运算符的简单的例子: template tmat operator + (tmat a, tmat b) { //判断两个矩阵的行、列是否相等,否则不予完成相加操作 if (a.nrows() != b.nrows() || a.ncols() != b.ncols()) { a.cerror("matrices do not conform in addition\n"); return a; } //新建立一个临时矩阵,存放相加的结果 tmat c(a.nrows(), a.ncols()); //执行矩阵元素逐个相加的运算 for (long i = 1; i <= c.nrows(); i++) for (long j = 1; j <= c.ncols(); j++) c(i, j) = a(i, j) + b(i, j); //返回相加后的矩阵 return c; } 可见该矩阵类库采用的是值传递。采用值传递,函数调用比较简单,也不必考虑内存释放的问题,但却带来了效率的降低。 下面给出调用该矩阵类库的方法和几个简单实例,以供读者参考。 源代码位于test.cpp中;矩阵和向量库为tmat.h和tvec.h。 //test.cpp 矩阵的测试 #include "tvec.h" #include "tmat.h" #include typedef tmat CMatrix; typedef tvec CVector; /*********************************************************************** 一个简单易用的矩阵类库 使用简介: 初始化矩阵: tmaxA(3,2); -生成一个3行2列的零阵 初始化矩阵: tmaxB(2,2,a); -生成一个2行2列的矩阵并利用数组a的前2×2个元素为其赋值。这里要求a至少要包含2×2个元素 给矩阵元素赋值: A(1,2)=1.0; -将1.0赋给矩阵A的第1行、第2列 取矩阵元素的值: f=A(2,2); -取出矩阵A第2行、第2列的值赋给f 矩阵的加、减、乘: A=B+C;C=B*A;等等,可以像普通数据类型那样直接进行运算符运算 矩阵转置: transpose(A); 求矩阵行列式: det(A); 注意:必须是方阵 矩阵求逆: inv(A); 注意:必须是方阵 得到矩阵的行数和列数: A.nrows(); A.ncols(); resize矩阵: A.resize(2,2); -取原来A矩阵的前2×2个元素来构造新尺寸的A +-+-+-+-+-+-+-+-+-++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- 另外,tvec.h则包含了一个矢量(向量)类库,这里也简要介绍一下 初始化向量: tvecA; -生成一个一维的向量。默认初始值为0 初始化向量: tvecB(4); -生成一个4维的向量 初始化向量: tvecC(4,0.1); -生成一个4维的向量,并将每一维元素初始化为0.1 初始化向量: tvecD(5,a); -生成一个5维的向量,并用数组a的前5个元素为其赋值 初始化向量: tvecE(B); -由向量B来初始化向量E。相当于复制向量 给向量的元素赋值: B(1)=2.0; -给B向量的第1维元素赋值为2.0 提取向量元素的值: f=B(2); -将B向量的第2维元素提取出来,赋给变量f 得到向量的维数: B.nsize(); ************************************************************************* 【注意】: 1.本类库的矩阵、向量运算采用值传递 2.本类库的矩阵和向量的第一个元素的起始下标为1,不是0。这点尤其要注意。这和C语言中的数组不一样 ************************************************************************/ CVector V[3]; bool test() { V[0].resize (4); V[0](1)=1; double k=V[0](1); cout< 二值化-> 锐化->去离散噪声-> 整体倾斜调整-> 字符分割-> 尺寸标准归一化-> 紧缩重排。至此,原先散落在图像中的亮度不一、大小不一、斜度不一、粗细不一、同时还含有噪声的数字已经被提取出来,亮度一致,尺寸标准化,去掉了倾斜,并且在很大程度上也去除了噪声。 (4)下面就进入BP网络训练部分。训练之前首先要输入BP训练的参数,这里主要是训练步长和允许误差。BP训练的时候,首先要对步骤(3)的结果来进行特征提取,然后将提取的特征送入BP网络进行训练。如果网络设计得当,参数选择也正确的话,那么BP网络将会在有限步内收敛。训练完毕的BP网络将网络的权值保存到文件中,以便下一步识别的时候可以直接调用。 (5)现在可以真正进入识别部分了。打开一副含有数字的图像,然后是预处理,然后是特征提取,最后将提取后的特征送入BP网络,通过网络的输出可以判定输入的字符,以实现数字字符的识别。识别的结果显示在屏幕上,同时也存储在文件中以保存。 11.5.4 程序使用说明、测试及注意事项 下面笔者来简要的讲述一下该程序的运行,并对该BP网络设计的效果作以评测。 程序主界面如图11-26所示。 图11-26 程序主界面 程序主要功能都通过这几个菜单来实现。常用的菜单也已经集成到了工具栏上,像打开、保存、重新加载、训练网络、识别功能都已经做成了工具栏上的按钮。 “文件”菜单主要负责文件的打开、保存以及重新加载(取消一切更改,重新打开)。“图像预处理”菜单主要来对图像进行预处理,含归一化信息的输入。“神经网络识别”菜单主要实现对网络的训练以及识别。这3个菜单的执行效果如下: “文件”菜单如图11-27所示。 图11-27 “文件”菜单 “图像预处理”菜单如图11-28所示。 图11-28 “图像预处理”菜单 “神经网络识别”菜单如图11-29所示。 图11-29 “神经网络识别”菜单 下面我们来打开一副训练样本进行训练。效果如图11-30。 图11-30 训练结果 然后,我们打开一副测试图片来进行识别,如图11-31所示。 图11-31 测试图片(待识别图片) 进行图像预处理的结果(归一化后图片过小,可能无法清晰的显示了),如图11-32。 图11-32 预处理后的测试图片 下面就可以点击工具栏中的“R”按钮来进行识别了。识别结果如11-33所示。 图11-33 对测试图片的识别结果 保存到文件中的结果如图11-34所示。 图11-34 保存在文本文件中的识别结果 至此,程序已经完整的运行了一次由训练到识别的过程。再下一次进行识别的时候,由于本次训练好的网络已经保存了下来,那么就可以直接识别了,无需再训练。当然如果识别的对象发生了较大变化而难以识别的时候,就需要重新制作训练样本并进行识别了。 这是对另一副图片的识别,如图11-35所示: 图11-35 另一副待识别图片 识别结果如图11-36所示。 图11-36 识别结果 最后来说一下使用该程序的注意事项。 (1)该程序设计时考虑了数字字符的很多变化情况,具备良好的适应性,但识别率对于倾斜字符或者不同字体的字符来说就不是很高了,甚至出现无法识别的情况。如下图所示,字符“3”倾斜后就无法识别出来了。 图11-37 另一副待识别图片 识别结果如图11-38所示。 图11-38 相应的识别结果 此时读者可以通过加大训练样本的数目来解决。将训练样本数目增大到150个,并考虑进来各种倾斜角度的以及其他几种常用字体,那么该BP网络将会有更加普遍的适应性 和更高的识别率。 (2)考虑程序设计的方便,该程序中设计BP网络的目标输出的时候,假定了目标输出为0,1,2,3,4,5,6,7,8,9的循环。所以读者在自行设计训练样本的时候一定要注意,不要使用“0,2,1,3”这样的训练样本,这会导致无法训练成功。应该使用“0,1,2,3”这样的训练样本。一定要遵循由0到9的顺序来设计训练样本。对于待识别图片当然没有这种要求。 (3)在执行图像预处理时,尽量使用“一次性图像预处理”。若要单独执行每一步,请务必记住:每一步要按顺序执行,且只能执行一次。否则将会有意想不到的后果。比如说,就像梯度锐化,连续梯度锐化3次,那图片上的数字基本上就没了。这也就无法完成下一步的分割等操作了。 (4)识别时不要再改动归一化信息。否则无法识别。归一化信息应该在训练之前设定,而且仅仅设定一次。识别时归一化信息与训练时候的一致,特征提取才能进行,识别才能进行。 (5)待识别的图片要与训练好的权值(win.dat和whi.dat)位于同一目录中。 11.6 本章小结 本章详细的分析了一个数字识别系统,介绍了许多图像图形处理的方法,以及在处理字符识别中有针对性的算法。并介绍了利用神经网络的相关知识以及用神经网络进行数字识别的整个过程。 在该BP网络的设计及使用过程中主要存在下面几个难题:无法从理论上严格的确定最佳的层数和隐层的神经元数目,这只能通过不断的测试、对比来求最佳;存在局部极小值问题,以导致网络没有收敛到最小误差便停止训练了,这里解决的核心办法就是多次选择不同的初始权值并在平坦区域加快学习速度,还有动量因子方法,以使其尽量可以跳出这个局部极小值;训练时间过长的问题;误差曲线振荡的问题,可以采用低通滤波器来改善;对于加噪过多的数据识别率不高的问题,可以通过优化网络结构来改进;等等。这些问题都在本文中有或多或少的描述和解决。借此,笔者提出这些问题,希望可以引起读者的注意。有兴趣的读者可以在本程序的基础上自行开发更为优化的BP网络。 另外关于本章所用到的图像处理和神经网络知识,读者可以参考下面几本书: 1. 《Neural Network Design》 Martin T.Hagan 2. 《数字图像处理》 冈萨雷斯

下载文档到电脑,查找使用更方便

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享文档获得金币 ] 1 人已下载

下载文档