当前位置:   article > 正文

《熟练掌握OpenCV----实用计算机视觉工程案例》第5章 车牌号码识别_mastering opencv with practical computer vision pr

mastering opencv with practical computer vision projects

人工智能系列经典图书翻译
原   名:《Mastering OpenCV with Practical Computer Vision Projects》
作   者:Shervin Emami 等
出 版 社:Packt Publishing
出 版 时 间:2012-12-03
译   名:《熟练掌握OpenCV----实用计算机视觉工程案例》
译   者:Jiazhen

本翻译行为仅作为自学参考,欢迎讨论留言


第5章 车牌号码识别
============================================================

本章介绍了创建自动车牌识别(Automatic Number Plate Recognition 缩写为ANPR)应用程序的过程。在不同的使用条件下比如红外摄像机,固定的汽车位置,不同光照环境等等,采用不同的方法和技术。构建了一个自动车牌识别系统应用程序来检测2~3米处所拍的汽车车牌照片,其中还特别考虑了模糊的光照条件,斜对着车牌拍照出现图像扭曲等。

本章主要介绍了图像分割,特征提取,模式识别基本原理,以及支持向量机和人工神经网络两种重要的模式识别算法。本章将包括:

  • 自动识别车牌算法
  • 车牌检测
  • 车牌识别

5.1 自动车牌识别简介

自动车牌识别 (Automatic Number Plate Recognition缩写为ANPR),又叫自动牌照识别(Automatic License-Plate Recognition缩写为ALPR),自动车辆识别 (Automatic Vehicle Identification缩写为AVI),车牌识别 (Car Plate Recognition缩写为CPR),它是一种监控方法,应用了光学字符识别(Optical Character Recognition缩写为OCR) 以及其它方法比如图像分割和检测读取车辆牌照。

自动车牌识别系统的最佳效果是要能够通过红外摄像机获取到车牌。因为这样在做检测和光学字符识别时候字符分割步骤更简单干净并且错误最少。这样做的依据源自最基本的光学定律,即入射角等于反射角。当我们看到一个光滑的表面,比如平面镜的时候,我们可以看到这个基本的反射过程。粗糙的表面比如纸的反射,通常称为漫反射或散射。大多数的车牌都有一个特征被称之为逆向反射,它的表面覆盖着一种材料,上面是成千上万的小半球,它们能让光线反射回到光源,我们可以从下图中看到:
img

如果我们用一个带有滤光片的摄像机和一个特殊结构的红外发射器,就可以获取到红外光并生成一个高质量的图像,然后进行分割、检测和识别,得到一个与周围环境无关的车牌号码,就像下图这样:
这里写图片描述

这里我们不使用红外摄像机拍的照片,而是使用常规照片。这样我们得到的不是期望的最佳结果,检测出错会更多,识别错误率会更高。不管怎样,对这两种照片的处理过程都是一样的。

每个国家都有不同的车牌尺寸和规格,了解这些规范有助于获得最好的结果并减少错误。在本章所涉及的一些算法目的是为了解释自动车牌识别的基本原理,虽然用到的车牌特征取自西班牙,但是我们可以将之扩展到任何其它国家或其它特征的车牌。

在这一章,我们以西班牙车牌为例。在西班牙有三种不同尺寸和形状的车牌。我们采用其中最通用的一种(大号),它的大小为520x110毫米,两组字符之间被41毫米的空格分开,字符之间间距为14毫米,第一组字符是4个数字,第二组字符是3个字母(不包括元音字符A,E,I,O,U,也不包括字母N和Q),所有字符尺寸都是45x77毫米。

这些数据对于字符分割来说非常的重要,因为我们要通过检查字符和空格来验证得到的是字符而不是其它图像。下面就是一副这样的车牌图片:
这里写图片描述

5.2 自动车牌识别算法

在解释自动车牌识别代码之前,我们需要说明自动车牌识别算法的几个主要步骤。车牌自动识别主要分为两步:车牌检测和车牌识别。车牌检测的目的是检测车牌在整幅图像中所处的位置。当检测到图像中有一个车牌,就接着进行下一步------车牌识别,通过光学字符检测算法来确定车牌中的字母和数字。

下图包括了两个主要的算法步骤:车牌检测和车牌识别。经过这些步骤后程序在图片上画出了车牌字符。这些算法可能会返回错误的结果甚至得不到任何结果。
这里写图片描述

上图中每一步都定义了三种额外的步骤,他们常用于模式识别算法:

  • 图像分割:检测和移除图像中的感兴趣区域/块。
  • 特征提取:从每个图像块中提取一系列特征。
  • 分类:从车牌识别阶段提取字符特征或在车牌检测阶段将每个图像块分类为“有车牌”或 “无车牌”。

下面图片描述了整个算法应用中的模式识别步骤:
这里写图片描述

除了主要应用程序,其目的是检测和识别汽车牌照号码,我们将简要解释两个通常不被提及的问题:

  • 如何训练模式识别系统
  • 如何评价这样一个系统

然而,这两个问题可能比主应用程序本身更重要,因为如果不正确地训练模式识别系统,这个系统可能会失败而不能正常工作。不同的模式需要不同类型的训练和评估。需要在不同的环境、条件下评估这个系统里的不同功能,以获得最佳结果。这两个有时一起使用,因为不同的功能可以产生不同的结果,我们可以在评估部分看到。

5.3 车牌检测

第一步要检测图像中所有的车牌,要实现这一点,又分为两小步:分割和分类。这里不解释特征步因为使用图像块作为矢量特征。

第一步(分割),采用不同的滤波器,形态学操作,轮廓提取算法,验证算法,来检索图像图像中存在车牌的部分。

第二步(分类),采用支持向量机(Support Vector Machine缩写为SVM)对每个图像块(所说的特征)进行分类。在构建主应用程序之前进行两种不同的分类:有车牌和无车牌。采用样本都是正面平行对着汽车车牌从2~4米远处拍的宽度在800像素左右的照片。也可以创建多种尺寸的图像算法来进行检测。

下面的图片演示了车牌检测的所有处理过程:

  • Sobel 滤波器
  • 阈值操作
  • 形态学闭操作
  • 掩码填充
  • 可能检测到的红色车牌(特征图像)

这里写图片描述

5.3.1 图像分割

分割是一个将图像切分成多个片段的过程,目的是为了简化要分析的图像,使特征提取更容易。

车牌分割的一个重要依据特征是在没有旋转角度和透视畸变的情况下,位于车辆正前方拍得的车牌图像会有大量的垂直边缘。这一特性可以在图像分割阶段加以利用,排除掉那些没有垂直边缘的区域。

在找到垂直边缘之前,需要将彩色图像转换为灰度图像(因为在这个过程中中颜色没有任何用处),这样可以消除由摄像机或其他环境因素产生的图像噪声。应用高斯模糊5×5消除图像噪声。如果不应用噪声去除方法,会得到很多的垂直边缘,这样就检测结果失败。

//convert image to gray
Mat img_gray;
cvtColor(input, img_gray, CV_BGR2GRAY);
blur(img_gray, img_gray, Size(5,5));
  • 1
  • 2
  • 3
  • 4

要找到垂直边沿,可以采用Sobel滤波器取水方向一阶导数。倒数是一种数学方法,它可以用在图像上查找垂直边沿。在OpenCV中Sobel函数的定义如下:

void Sobel(InputArray src, OutputArray dst, int ddepth, int xorder, int yorder, int ksize=3, double scale=1, double delta=0, int borderType=BORDER_DEFAULT )
  • 1

这里,ddepth是目标图像深度,xorder是x轴上的倒数阶数,yorder是y轴上的倒数阶数,ksize是内核的尺寸大小,可选尺寸大小有1,3,5或7,scale是缩放倍数,delta是偏移量,borderType是像素插值方法类型。

本章案例中用到的参数是 xorder=1,yorder=0, ksize=3:

//Find vertical lines. Car plates have high density of vertical lines Mat img_sobel;
Sobel(img_gray, img_sobel, CV_8U, 1, 0, 3, 1, 0);
  • 1
  • 2

在Sobel滤波之后采用阈值滤波器可以得到一个二值化的图像。通过大津法(Otsu’s method)来计算阈值大小。大津算法只需要输入一个8位的图像就可以自动得到最佳的阈值大小:

//threshold image
Mat img_threshold;
threshold(img_sobel, img_threshold, 0, 255, CV_THRESH_OTSU+CV_THRESH_ BINARY);
  • 1
  • 2
  • 3

在threshold函数中如果使用了CV_THRESH_OTSU作为参数,那么就会采用大津法来自动计算阈值,再指定阈值大小就会无效。

[外链图片转存失败(img-s08n4OGd-1562636682222)(第5章%20车牌识别.files/image013.jpg)] [外链图片转存失败(img-lOGtEAh1-1562636682226)(第5章%20车牌识别.files/image015.jpg)]

一旦 CV_THRESH_OTSU 被定义,threshold函数就会返回大津法所获取的最佳阈值。

[外链图片转存失败(img-I8C4GsQr-1562636682228)(第5章%20车牌识别.files/image017.jpg)]
使用闭操作可以去除垂直边沿线中的空白处,并将数量众多的边沿区域连在一起,就有可能找到包含车牌的区域。

首先定义用于形态学运算的结构元素。这里使用getStructuringElement函数定义一个17×3二维大小的矩形结构元素,其他图像中用到的大小尺寸可能会不一样:

Mat element = getStructuringElement(MORPH_RECT, Size(17, 3));
  • 1

morphologyEx函数实现形态学闭操作的时候可以使用这个结构元素作为参数:

morphologyEx(img_threshold, img_threshold, CV_MOP_CLOSE, element);
  • 1

应用这些函数可以得到图像中包含车牌的区域,但是大多数区域中可能没有车牌。可以采用连通分量分析或findContours函数来划分这些区域。findContours函数用不同的方法和结果来检索二值图像的轮廓。我们只需要得到任意层次关系和任意近似多边形的外部轮廓:

//Find contours of possibles plates
vector< vector< Point> > contours;
findContours(img_threshold,
contours,  // a vector of contours
CV_RETR_EXTERNAL, // retrieve the external contours
CV_CHAIN_APPROX_NONE); // all pixels of each contour
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

对检测到的每个轮廓提取它的最小外接矩形。OpenCV里面的minAreaRect函数可以实现这个功能。这个函数返回一个称为RotatedRect的旋转矩形类。采用向量迭代器遍历所有轮廓可以得到对应的旋转矩形,并在进行分类之前可以做初步验证:

//Start to iterate to each contour found vector >::iterator itc= contours.begin(); vector rects;
//Remove patch that has no inside limits of aspect ratio and area.
while (itc!=contours.end()) {
	//Create bounding rect of object
	RotatedRect mr= minAreaRect(Mat(*itc));
	if( !verifySizes(mr)){
		itc= contours.erase(itc);
	}else{
		++itc;
	    rects.push_back(mr);
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

根据外接矩形的面积和长宽比来做基本的验证。矩形的长宽比接近520/110 = 4.727272(车牌的长度/宽度)并且允许40%的误差,车牌高度像素在15~125之间,满足这些条件才被考虑为有车牌候选区域。这些值得计算取决于图像得尺寸大小以及摄像机得拍摄角度。

bool DetectRegions::verifySizes(RotatedRect candidate ){
	float error=0.4;
	//Spain car plate size: 52x11 aspect 4,7272
	const float aspect=4.7272;
	//Set a min and max area. All other patches are discarded int min= 15*aspect*15; // minimum area
	int max= 125*aspect*125; // maximum area
	//Get only patches that match to a respect ratio.
	float rmin= aspect-aspect*error;
	float rmax= aspect+aspect*error;
	int area= candidate.size.height * candidate.size.width;
	float r= (float)candidate.size.width / (float)candidate.size.height; if(r<1)
	r= 1/r;
	if(( area < min || area > max ) || ( r < rmin || r > rmax )){ 
		return false;
	}else{
		return true;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

根据车牌背景为白色这一特征还可以做更多优化。所有的车牌背景颜色是一样的,为了更精确截取车牌图像可以采用漫水填充算法来检索旋转矩形。

要截取车牌首先需要获取旋转矩形中心附近的一些种子点,得到车牌长度和宽度中较小的那个值,用它生成接近矩形区域中心的随机种子点。

几个随机种子点中最少要有一个像素点是白色的,才能被认为那是白色区域。对每个种子点,采用floodFill函数绘出新的掩码图像来保存最接近的截取区域。

for(int i=0; i< rects.size(); i++){
	//For better rect cropping for each possible box
	//Make floodfill algorithm because the plate has white background //And then we can retrieve more clearly the contour box circle(result, rects[i].center, 3, Scalar(0,255,0), -1); //get the min size between width and height
	float minSize=(rects[i].size.width < rects[i].size.height)?rects[i].
	size.width:rects[i].size.height;
	minSize=minSize-minSize*0.5;
	//initialize rand and get 5 points around center for floodfill algorithm
	srand ( time(NULL) );
	//Initialize floodfill parameters and variables Mat mask;
	mask.create(input.rows + 2, input.cols + 2, CV_8UC1);
	mask= Scalar::all(0);
	int loDiff = 30;
	int upDiff = 30;
	int connectivity = 4;
	int newMaskVal = 255;
	int NumSeeds = 10;
	Rect ccomp;
	int flags = connectivity + (newMaskVal << 8 ) + CV_FLOODFILL_FIXED_
	RANGE + CV_FLOODFILL_MASK_ONLY;
	for(int j=0; j pointsInterest;
	Mat_::iterator itMask= mask.begin();
	Mat_::iterator end= mask.end();
	for( ; itMask!=end; ++itMask)
		if(*itMask==255)
			pointsInterest.push_back(itMask.pos());
	RotatedRect minRect = minAreaRect(pointsInterest);
	if(verifySizes(minRect)){
	…
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

到此图像分割过程已经完成并得到了有效的图像区域,截取检测区域,去掉可能存在的图像旋转,截取对应的图像区域进行缩放,并对裁剪图像区域的亮度进行均衡处理。

首先通过getRotationMatrix2D函数生成变换矩阵,去除检测区域可能存在的角度旋转。要注意图像的高度因为返回RotatedRect类可能会被旋转90度,还要检查矩形宽高比,如果这个比值小于1那么就要将图像旋转90度:

//Get rotation matrix
float r= (float)minRect.size.width / (float)minRect.size.height;
float angle=minRect.angle;
if(r<1)
	angle=90+angle;
Mat rotmat= getRotationMatrix2D(minRect.center, angle,1);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

有了变换矩阵,再通过仿射变换(几何中的仿射变化是一种平行线到平行线之间的变换)可以将输入图像进行旋转。warpAffine函数可以设置输入图像、输出图像、变换矩阵、输出尺寸大小(范例中此处和输入尺寸大小是一致的)以及所用的插值方法。如果需要的话还可以指定border方法和forder值。

//Create and rotate image
Mat img_rotated;
warpAffine(input, img_rotated, rotmat, input.size(), CV_INTER_CUBIC);
  • 1
  • 2
  • 3

完成图像旋转之后,用getRectSubPix函数截取图像,它会围绕中心点按照给定的宽度和高度来截取并拷贝图像。如果图像被旋转了,还要通过C++ 里的swap函数来对调宽度和高度。

//Crop image
Size rect_size=minRect.size;
if(r < 1)
	swap(rect_size.width, rect_size.height);
Mat img_crop;
getRectSubPix(img_rotated, rect_size, minRect.center, img_crop);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

被截取的图像不适合用来做训练和分类,因为他们的尺寸大小不一,而且每个图像包含了不同的光线条件,这就会增加它们的相对差异大小。要解决这个问题,要将所有图像都缩放到同样的宽度和高度,并对亮度进行均衡处理。

Mat resultResized;
resultResized.create(33,144, CV_8UC3);
resize(img_crop, resultResized, resultResized.size(), 0, 0, INTER_ CUBIC);
//Equalize cropped image
Mat grayResult;
cvtColor(resultResized, grayResult, CV_BGR2GRAY); blur(grayResult, grayResult, Size(3,3));
equalizeHist(grayResult, grayResult);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

对于每一个检测区域,要保存好截取的图像及对应的坐标。

output.push_back(Plate(grayResult,minRect.boundingRect()));
  • 1
5.3.2 图像分类

在进行预处理和分割可能的图像区域之后,接下来就要决定是不是每个区域都有(或没有)一个车牌。要实现这一点,就要用到支持向量机(Support Vector Machine缩写为SVM)算法。

支持向量机是一种模式识别算法,它属于一种有监督学习算法最开始源于二元分类法。有监督学习是一种机器学习算法,它采用带标签的数据进行学习。要训练这种算法就需要大量的带标签的数据。每个数据集都有一个分类。

SVM创建了一个或多个超平面,用来识别每一种数据。

一个典型的例子是将二位平面的点集分为两类,然后用SVM找到一条最佳的线来区分它们。
这里写图片描述
在分类之前首先要做的是训练分类器,这个工作要排在主应用程序开始之前,被称之为离线训练。这个工作不太容易是因为它要求足够数量的数据来训练这个系统,但是更大的数据集并不意味着更好的结果。范例中并没有足够的数据因为事实上并没有公开的车牌数据库。所以这就需要先拍几百张汽车照片再预处理和分割这些照片。

这里采用75张有车牌的图片和35张无车牌的像素图片来进行训练,图片大小是144x33。接下来的图片里我们会用其中的一些来举例。这并不是一个大的数据集,但也足够使用。在实际应用中,需要更多的数据用来训练。
这里写图片描述

为了便于理解机器学习是如何工作的,将会采用图像像素特征的分类器算法(记住,有一些更好的方法和特征用于训练SVM,比如主成分分析,傅里叶变换,纹理分析等等)。

用DetectRegions类创建一些图像来训练这个系统,设定savingRegions变量为真可以保存这些图像。在一个目录下调用bash脚本segmentAllFiles.sh来重复处理所有的的图片文件。从本书所附的源码里可以找到对应的代码。

要简化操作,可以保存所有处理过和准备好的图片训练数据到XML文件,然后直接调用SVM函数。trainSVM.cpp代码里用目录和图片创建了这样一个XML文件。
[外链图片转存失败(img-cdFAp6w6-1562636682231)(第5章%20车牌识别.files/image023.jpg)]

OpenCV机器学习算法的训练数据存储在N×M矩阵中,代表n种样本和m种特征。每个数据集被保存为训练矩阵中的一行。
这些分类被存储在另一个N×1大小的矩阵中,每个分类用一个浮点数来标识。

[外链图片转存失败(img-RUdAOhMx-1562636682233)(第5章%20车牌识别.files/image025.jpg)] [外链图片转存失败(img-KkVSd9FG-1562636682234)(第5章%20车牌识别.files/image027.jpg)]

OpenCV中的FileStorage类使得对XML或JSON格式的文件的处理变得非常简单,这个类可以存储和读取OpenCV的变量和结构以及一些自定义的变量。使用这个功能可以读取训练数据矩阵和训练分类并保存到svm_trainingdata和svm_classes:

FileStorage fs;
fs.open("SVM.xml", FileStorage::READ);
Mat SVM_TrainingData;
Mat SVM_Classes;
fs["TrainingData"] >> SVM_TrainingData;
fs["classes"] >> SVM_Classes;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

要使用SVM算法,需要设定一些基本的参数,可以使用CvSVMParams结构体来定义这些参数。这是对训练数据的映射,可以提高线性可分数据集的相似性。这种映射包括增加数据的维度,采用内核函数有效的实现这一点,这里采用CvSVM::LINEAR类型就意味着不做任何映射。

//Set SVM params
CvSVMParams SVM_params;
SVM_params.kernel_type = CvSVM::LINEAR;
  • 1
  • 2
  • 3

创建和训练分类器,OpenCV为支持向量机(SVM)算法定义了CvSVM类,可以用训练数据、分类和参数来初始化分类器。

CvSVM svmClassifier(SVM_TrainingData, SVM_Classes, Mat(), Mat(), SVM_ params);
  • 1

这个分类器用SVM类中的predict函数来预测一个可能的截取图像,这个函数返回分类器i,在范例中有车牌类被标记为1,无车牌类被标记为0。对每个可能含有车牌的检测区域,采用SVM来进行有车牌或无车牌的分类,保存正确的响应。下面的代码是主程序的一部分,被称为在线处理:

vector plates;

for(int i=0; i< possible_regions.size(); i++)

{
	Mat img=possible_regions[i].plateImg;
	Mat p= img.reshape(1, 1);//convert img to 1 row m features p.convertTo(p, CV_32FC1);
	int response = (int)svmClassifier.predict( p );
	if(response==1)
	plates.push_back(possible_regions[i]);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

5.4 车牌识别

第二步中车牌识别的目标是采用光学字符识别方法检索到车牌中的字符。对与每个检测到的车牌,分割其中的字符,运用人工神经网络(Artificial Neural Network 缩写为ANN)机器学习算法来识别字符。本节还会学习如何评估一个分类算法。

5.4.1 字符分割

首先,将获取到的车牌图像块作为OCR图像分割方法的输入,并对其进行直方图均衡处理,再用阈值滤波器得到阈值图像,将之作为轮廓查找算法的输入,就会看到下图所示处理过程:
这里写图片描述
图像分割过程的代码如下:

Mat img_threshold;
threshold(input, img_threshold, 60, 255, CV_THRESH_BINARY_INV); if(DEBUG)
imshow("Threshold plate", img_threshold);
Mat img_contours;
img_threshold.copyTo(img_contours);
//Find contours of possibles characters
vector< vector< Point> > contours;
findContours(img_contours,
contours,  // a vector of contours
CV_RETR_EXTERNAL,  // retrieve the external contours
CV_CHAIN_APPROX_NONE); // all pixels of each contour
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

用CV_THRESH_BINARY_INV参数反转输出,将白色部分变黑,黑色部分变白。这对获取字符轮廓非常重要,因为轮廓提取算法只会查找白色像素点。

对每个检测到的轮廓,进行尺寸大小验证排除掉其中尺寸过小的或者比例不正确的。在范例中,字符比例是45/77,考虑到字符的旋转或畸变,可以允许35%的误差。如果一个区域大小超出了80%,将被视作空白块区域而不是一个字符。用countNonZero函数来计算像素值大于0的像素个数。

bool OCR::verifySizes(Mat r)
{
	//Char sizes 45x77
	float aspect=45.0f/77.0f;
	float charAspect= (float)r.cols/(float)r.rows;
	float error=0.35;
	float minHeight=15;
	float maxHeight=28;
	//We have a different aspect ratio for number 1, and it can be //~0.2
	float minAspect=0.2;
	float maxAspect=aspect+aspect*error;
	//area of pixels
	float area=countNonZero(r);
	//bb area
	float bbArea=r.cols*r.rows;
	//% of pixel in area
	float percPixels=area/bbArea;
	if(percPixels < 0.8 && charAspect > minAspect && charAspect <
	maxAspect && r.rows >= minHeight && r.rows < maxHeight)
	return true;
	else
	return false;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

如果一个分割的字符被验证是有效的,那就可以对所有字符的尺寸大小和位置做同样的设定,并保存到一个辅助的CharSegment类向量中。这个类保存分割的字符图像及用来排序的对应位置,因为轮廓查找算法按照要求的顺序返回查找结果。

5.4.2 特征提取

下一步要对每个分割的字符进行特征提取用来进行训练和分类人工神经网络算法。

不像车牌检测中SVM用到的特征提取步骤那样,这里不需要用到所有的图像像素,在光学字符识别中只需要更多普通特征包括水平和垂直累积直方图和低分辨率图像样本。在下面图像中可以直观地看到这些特征,每个图像只有5x5的低分辨率和直方图累积。
这里写图片描述
对每个字符用函数计算每行每列值不为0的像素个数,并将之保存到一个新的称为mhist的数据矩阵中。对mhist作归一化处理,用minMaxLoc函数查找mhist中的最大值,用convertTo函数将mhist中所有的元素都除以这个最大值。并用创建ProjectedHistogram函数来构建累积直方图,将输入的二值化图像和需要直方图类型(水平或垂直)作为输入:
···
Mat OCR::ProjectedHistogram(Mat img, int t)
{
int sz=(t)?img.rows:img.cols;
Mat mhist=Mat::zeros(1,sz,CV_32F);
for(int j=0; j<sz; j++){
Mat data=(t)?img.row(j):img.col(j);
mhist.at(j)=countNonZero(data);
}

//Normalize histogram
double min, max;
minMaxLoc(mhist, &min, &max);

if(max>0)
	mhist.convertTo(mhist,-1 , 1.0f/max, 0);
return mhist;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

}
···
其它特征只需要用一个低分辨率采样图像而不是整个字符图像。创建一个低分辨率的字符例如5x5,训练这个系统可以用5x5,10x10,15x15,20x20的字符,然后评估哪一个能够返回最佳结果就在系统中选择用哪个。一旦得到所有这些特征,就创建一个1行M列的矩阵,所有的特征都在列数据中:

Mat OCR::features(Mat in, int sizeData)
{
	//Histogram features
	Mat vhist=ProjectedHistogram(in,VERTICAL);
	Mat hhist=ProjectedHistogram(in,HORIZONTAL);
	
	//Low data feature
	Mat lowData;
	resize(in, lowData, Size(sizeData, sizeData) );
	int numCols=vhist.cols + hhist.cols + lowData.cols * lowData.cols;
	Mat out=Mat::zeros(1,numCols,CV_32F);
	
	//Assign values to feature
	int j=0;
	for(int i=0; i<vhist.cols; i++)
	{
		out.at<float>(j)=vhist.at<float>(i);
		j++;
	}
	for(int i=0; i<hhist.cols; i++)
	{
		out.at<float>(j)=hhist.at<float>(i);
		j++;
	}
	for(int x=0; x<lowData.cols; x++)
	{
		for(int y=0; y<lowData.rows; y++)
		{
			out.at<float>(j)=(float)lowData.at<unsigned char>(x,y); j++;
		}
	}
	return out;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
5.4.3 字符分类

在分类步骤会用到人工神经网络机器学习算法。确切地说,多层感知器(Multi-Layer Perceptron缩写为 MLP)是最常用的人工神经网络算法。

多层感知器由一个具有输入层、输出层和一个或多个隐藏层的神经元网络组成。每层都有一个或多个与前一层和下一层相连的神经元。

下面的例子是一个三层感知器(它是一个二元分类映射的一个实值向量输入到一个单一的二进制值输出),有三个输入,两个输出和一个具有五个神经元的隐藏层:
这里写图片描述
多层感知器中的所有神经元都是相似的,每个神经元都有几个输入(上一个连接的神经元)和几个具有相同值的输出链接(下一个相连的神经元)。每个神经元计算加权输入之和再加一个偏移量作为总的输出值,并由选定的激活函数进行变换:
这里写图片描述
有三种广泛应用的激活函数:Identity,Sigmoid 和Gaussian。常见默认的激活函数是Sigmoid函数。它的alpha和beta值设置为1:
这里写图片描述
人工神经网络训练具有特征向量输入,它将值传递到隐藏层,并用权重和激活函数计算结果,它将输出进一步传递到下游,直到得到具有一定数量神经元类的输出层。

通过训练神经网络算法计算和学习每个层、突触和神经元的权重。为了训练所使用的分类器,创建了两个数据矩阵,这和SVM训练中所做的类似,只是训练标签有点不一样。N x 1矩阵中n是训练数据行数1是列数,但这里要用标签数字来标识。创建一个n×m矩阵,其中n是训练/样本数据,m是类别(范例中是10个数字+ 20个字母)。在位置(i,j)中设置值为1,这个位置就是第j类中的第i行。
这里写图片描述
创建一个OCR::train函数来构建所有需要的矩阵并训练神经网络系统,其中包括训练数据矩阵、类矩阵和隐藏层中隐藏神经元的个数。训练数据是从XML文件中加载的,就像我们训练SVM一样。

要定义每个层中的神经元个数来初始化人工神经网络类。范例中只使用一个隐层,然后定义一个1行3列的矩阵。第一列中的是特征的数量,第二列中的是隐藏层中隐藏的神经元的数目,第三列中的是类的数量。

OpenCV为人工神经网络定义了CvANN_MLP类。在定义层数、神经元的数量、激活函数alpha和beta参数后,用create函数初始化类:

void OCR::train(Mat TrainData, Mat classes, int nlayers)
{
	Mat layerSizes(1,3,CV_32SC1);
	layerSizes.at(0)= TrainData.cols;
	layerSizes.at(1)= nlayers;
	layerSizes.at(2)= numCharacters;
	ann.create(layerSizes, CvANN_MLP::SIGMOID_SYM, 1, 1); //ann is
	global class variable
	
	//Prepare trainClasses
	//Create a mat with n trained data by m classes Mat trainClasses;
	trainClasses.create( TrainData.rows, numCharacters, CV_32FC1 );
	for( int i = 0; i < trainClasses.rows; i++ ) {
		for( int k = 0; k < trainClasses.cols; k++ )
		{
			//If class of data i is same than a k class
			if( k == classes.at(i) )
			trainClasses.at(i,k) = 1;
			else
			trainClasses.at(i,k) = 0;
		}
	}
	Mat weights( 1, TrainData.rows, CV_32FC1, Scalar::all(1) );
	
	//Learn classifier
	ann.train( TrainData, trainClasses, weights ); trained=true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

训练完之后,用OCR::classify函数可以分类任意被分割过的车牌:

int OCR::classify(Mat f)
{
	int result=-1;
	Mat output(1, numCharacters, CV_32FC1);
	ann.predict(f, output);
	Point maxLoc;
	double maxVal;
	minMaxLoc(output, 0, &maxVal, 0, &maxLoc);
	//We need to know where in output is the max val, the x (cols) is //the class.
	return maxLoc.x;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

CvANN_MLP类采用predict函数来对特征向量进行分类。与SVM classify函数不同,人工神经网络中的predict函数返回与类数相等的行数,其概率取决于每个类的输入特征。

为了得到最好的结果可以用minMaxLoc函数得到矩阵中最大值和最小值。我们的字符的类是由高值的x位置指定的:
这里写图片描述
为了完成车牌检测,可以用Plate类中str()函数返回排序后的字符串,还可以将结果在原始图像上显示出来:

string licensePlate=plate.str();

rectangle(input_image, plate.position, Scalar(0,0,200)); putText(input_image, licensePlate, Point(plate.position.x, plate. position.y), CV_FONT_HERSHEY_SIMPLEX, 1, Scalar(0,0,200),2);

5.4.4 综合评估

到本节项目已经完成,当我们训练像OCR这样的机器学习算法时,需要知道最佳功能和参数,以及如何纠正系统中的分类、识别和检测错误。

这里也需要评估系统在不同的使用环境和参数时产生的错误,并找到最佳的参数尽量减少这些错误。

在本章中,我们用以下变量评估了OCR工作情况:低分辨率图像的特征和隐藏层中神经元的数量。

trainOCR.cpp程序可以生成XML格式的训练数据文件,evalOCR.cpp程序可以使用这些文件。通过下采样图像特征得到的5×5, 10×10,15×15,20×20训练数据矩阵被保存在OCR.xml文件中。

Mat classes;
Mat trainingData;
//Read file storage.
FileStorage fs;
fs.open("OCR.xml", FileStorage::READ);
fs[data] >> trainingData;
fs["classes"] >> classes;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

评估程序取得每个下采样特征矩阵,随机选取100行进行训练,以及更多行用于测试的人工神经网络算法和检查错误的。

在训练系统之前,先测试每一个随机样本并检查响应是否正确。如果响应不正确,就递增错误计数器变量,然后除以样本数来进行评估。这意味随机数据训练的错误率在0和1之间:

float test(Mat samples, Mat classes)
{
	float errors=0;
	for(int i=0; i(i))
		errors++;
	}
	return errors/samples.rows;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

采用不同规格样本输出的错误率都会返回到应用程序命令行里。对于一个好的评估方式,需要用不同的随机行数来训练应用程序,这就产生了不同的测试误差值,然后可以把所有的错误加起来取平均值。要完成这个任务,可以创建以下bash UNIX脚本实现自动化处理:

#!/bin/bash
echo "#ITS \t 5 \t 10 \t 15 \t 20" > data.txt folder=$(pwd)
for numNeurons in 10 20 30 40 50 60 70 80 90 100 120 150 200 500 do
s5=0;
s10=0;
s15=0;
s20=0;
for j  in {1..100}
do
	echo $numNeurons $j
	a=$($folder/build/evalOCR $numNeurons TrainingDataF5)
	s5=$(echo "scale=4; $s5+$a" | bc -q 2>/dev/null)
	a=$($folder/build/evalOCR $numNeurons TrainingDataF10)
	s10=$(echo "scale=4; $s10+$a" | bc -q 2>/dev/null)
	a=$($folder/build/evalOCR $numNeurons TrainingDataF15)
	s15=$(echo "scale=4; $s15+$a" | bc -q 2>/dev/null)
	a=$($folder/build/evalOCR $numNeurons TrainingDataF20)
	s20=$(echo "scale=4; $s20+$a" | bc -q 2>/dev/null)
done

echo "$i \t $s5 \t $s10 \t $s15 \t $s20"
echo "$i \t $s5 \t $s10 \t $s15 \t $s20" >> data.txt done
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

这个脚本保存的data.txt文件里包含了所有的结果,其中有每个和神经元隐藏层数量及大小。此文件可用gnuplot绘制出来,如下图:
这里写图片描述

我们可以看到最低的错误率在8%,它用到了1个包括20个神经元的隐藏层,采用下采样10×10图像块提取字符特征。

5.5 小结

这一章,我们学习了自动拍照识别程序是如何工作的,它包括两个重要步骤:车牌定位和车牌识别。

在第一步中我们学习了如何分割图像来查找匹配得到有车牌的图像位置,以及如何使用简单的启发式算法和支持向量机算法来生成有车牌和无车牌的二元分类。

在第二步中我们学习了如何用查找轮廓算法进行字符分割,从每个字符中提取特征向量,并使用人工神经网络对字符类中的每个特征进行分类。

我们还学习了如何使用随机抽样对机器算法进行评估,并使用不同的参数和特性对其进行评估。


声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/327124
推荐阅读
相关标签
  

闽ICP备14008679号