本文用来学习的项目来自书籍《实用计算机视觉项目解析》第5章Number Plate Recognition 提供的源代码
源代码下载链接:http://download.csdn.net/detail/muyuxingguang/8737107
主要步骤包括车牌检测和车牌识别。车牌检测部分从一张含有车牌的图像中检测出可能的车牌区域,通过SVM分类器识别出车牌区域和非车牌区域;对车牌区域进行分割,识别出每一个字符,这里用到了神经网络分类器;最终给出车牌号。
代码中的一些数据与当地车牌尺寸和规格有关,本例使用西班牙车牌,大小为520mm x 110mm, 两组字符由41mm的空间分离,每个字符间距为14mm。第一组字符为四个数字,第二组字符有三个字母,但不包括元音字母A E I O U也不包括字母N或Q,所有字符的大小为45mm x 77mm.
解决方案主要包括6个工程,ANPR实现车牌识别;trainSVM用来生成SVM.xml文件,为SVM分类器的原始数据;trainOCR用来生成OCR.xml文件,为OCR算法的原始数据;evalOCR用来评价机器学习算法。
A. main()函数,先看下代码的主要处理步骤。
/***************************************************************************** * Number Plate Recognition using SVM and Neural Networks ****************************************************************************** * by David Mill醤 Escriv? 5th Dec 2012 * http://blog.damiles.com ****************************************************************************** * Ch5 of the book "Mastering OpenCV with Practical Computer Vision Projects" * Copyright Packt Publishing 2012. * http://www.packtpub.com/cool-projects-with-opencv/book *****************************************************************************/ // Main entry code OpenCV // opencv头文件 #include <cv.h> #include <highgui.h> #include <cvaux.h> #include <ml.h> // 标准c++头文件 #include <iostream> #include <vector> // 自定义头文件 #include "DetectRegions.h" #include "OCR.h" using namespace std; using namespace cv; int main ( int argc, char** argv ) { cout << "OpenCV Automatic Number Plate Recognition\n"; //下面是自己改的部分,不从DOS命令行输入,直接给出 Mat input_image = imread("9588DWV.jpg", 1); imshow("input_image", input_image ); string filename_whithoutExt="9588DWV"; cout << "working with file: "<< filename_whithoutExt << "\n"; //Detect posibles plate regions DetectRegions detectRegions; detectRegions.setFilename(filename_whithoutExt); detectRegions.saveRegions=true; detectRegions.showSteps=true; vector<Plate> posible_regions= detectRegions.run( input_image ); //2个 //SVM for each plate region to get valid car plates //Read file storage. FileStorage fs; fs.open("SVM.xml", FileStorage::READ); Mat SVM_TrainingData; Mat SVM_Classes; fs["TrainingData"] >> SVM_TrainingData; fs["classes"] >> SVM_Classes; //Set SVM params CvSVMParams SVM_params; SVM_params.svm_type = CvSVM::C_SVC; SVM_params.kernel_type = CvSVM::LINEAR; //CvSVM::LINEAR; SVM_params.degree = 0; SVM_params.gamma = 1; SVM_params.coef0 = 0; SVM_params.C = 1; SVM_params.nu = 0; SVM_params.p = 0; SVM_params.term_crit = cvTermCriteria(CV_TERMCRIT_ITER, 1000, 0.01); //Train SVM CvSVM svmClassifier(SVM_TrainingData, SVM_Classes, Mat(), Mat(), SVM_params); //For each possible plate, classify with svm if it's a plate or no vector<Plate> plates; for(int i=0; i< posible_regions.size(); i++) { Mat img=posible_regions[i].plateImg; //这是一张灰度图 Mat p= img.reshape(1, 1); //Mat::reshape() 重新给出通道数(cn)和行数(rows),自动计算列数,把Mat重新排列 p.convertTo(p, CV_32FC1); int response = (int)svmClassifier.predict( p ); if(response==1) plates.push_back(posible_regions[i]); } cout << "Num plates detected: " << plates.size() << "\n"; //For each plate detected, recognize it with OCR OCR ocr("OCR.xml"); ocr.saveSegments=true; ocr.DEBUG=false; ocr.filename=filename_whithoutExt; for(int i=0; i< plates.size(); i++){ Plate plate=plates[i]; string plateNumber=ocr.run(&plate); string licensePlate=plate.str(); cout << "================================================\n"; cout << "License plate number: "<< licensePlate << "\n"; cout << "================================================\n"; 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); if(false){ imshow("Plate Detected seg", plate.plateImg); cvWaitKey(0); } } imshow("Plate Detected", input_image); for(;;) { int c; c = cvWaitKey(10); if( (char) c == 27) break; } return 0; }
读入待检测图片 -> 检测出可能的车牌区域,保存到vector<Plate> 类型的posible_regions
-> 读入数据文件SVM.xml,训练,判别,最终将每一个<Plate>分成有效车牌区域及无效区域,输出vector<Plate> 类型的plates
-> 对每一个plate,执行OCR算法,最终得出车牌号。
这里定义了 DetectRegions类,
DetectRegions.h
#ifndef DetectRegions_h #define DetectRegions_h #include <string.h> #include <vector> #include "Plate.h" #include <cv.h> #include <highgui.h> #include <cvaux.h> using namespace std; using namespace cv; class DetectRegions{ public: DetectRegions(); //默认构造函数 string filename; //数据成员,segment()函数中调用了它;OCR:run()里面直接调用了它;main()函数里面的filename变量跟这里的有没有关系? void setFilename(string f); bool saveRegions; bool showSteps; vector<Plate> run(Mat input); private: vector<Plate> segment(Mat input); //成员函数,不能被直接调用,公有函数run()里面调用了它 bool verifySizes(RotatedRect mr); //成员函数,不能被直接调用,segment()里面调用了它 Mat histeq(Mat in); //成员函数,不能被直接调用,segment()里面调用了它 }; #endif
(以上代码后注释是我初学c++做的标记,为了搞清楚哪些放在public,哪些放在private。请忽略。)
main()函数中先设置了参数,再调用detecRegions.run()就解决了问题。
DetecRegions::run()函数的实现:
vector<Plate> DetectRegions::run(Mat input){ //Segment image by white vector<Plate> tmp=segment(input); //return detected and posibles regions return tmp; }于是要看下 segment() 函数实现。
vector<Plate> DetectRegions::segment(Mat input) { vector<Plate> output; ...... return output; // n个Plate类型的数据 output[i].plateImg是灰度图 }
输入参数是Mat类型图像input,输出vector<Plate>类型的output,里面存储了所有可能牌照区域。每一个plate是一个长方条的小图片,可能包含拍照信息,可能不包含,要等后面的SVM算法来分类判别。
省略号部分的代码分段来看。1.首先将输入的彩色图像转换为灰度图cvtColor(),再进行均值滤波blur()
//convert image to gray Mat img_gray; cvtColor(input, img_gray, CV_BGR2GRAY); blur(img_gray, img_gray, Size(5,5)); //均值滤波
2.灰度图转梯度图,求出水平方向的sobel梯度图。这样车牌区域就能显示出许多垂直线来,例如车牌左右两边和每个数字的竖直线。
//Finde 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, BORDER_DEFAULT); //求x方向梯度 if(showSteps) imshow("Sobel", img_sobel);
3. 梯度图转二值图threshold(),采用OTSU法自动二值化,
//threshold image Mat img_threshold; threshold(img_sobel, img_threshold, 0, 255, CV_THRESH_OTSU+CV_THRESH_BINARY); if(showSteps) imshow("Threshold", img_threshold);
4.形态学闭操作morphologyEx() 结构因子是一个长方条,可以将二值化后的车牌区域白色区域连接起来,如图
//Morphplogic operation close Mat element = getStructuringElement(MORPH_RECT, Size(17, 3) ); //用一个水平方向的长方条(长17,宽3?)闭操作 morphologyEx(img_threshold, img_threshold, CV_MOP_CLOSE, element); if(showSteps) imshow("Close", img_threshold);
5. 在二值图中找出所有轮廓findContours() 轮廓迭代, 将不符合要求的轮廓消除 contours.erase(),将符合要求的轮廓求其最小面积包围矩形并保存到rect中。
//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 contours //Start to iterate to each contour founded vector<vector<Point> >::iterator itc= contours.begin(); vector<RotatedRect> rects; //Remove patch that are 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); //把最小包围矩形保存下来 } }怎样算符合要求呢?通过verifySizes()来判断,转到该函数定义,发现该函数实现如下:
bool DetectRegions::verifySizes(RotatedRect mr){ float error=0.4; //Spain car plate size: 52x11 aspect 4.7272(=52/11) float aspect=4.7272; //Set a min and max area. All other patchs are discarded int min= 15*aspect*15; // minimum area int max= 125*aspect*125; // maximum area //Get only patchs that match to a respect ratio. float rmin= aspect-aspect*error; float rmax= aspect+aspect*error; int area= mr.size.height * mr.size.width; float r= (float)mr.size.width / (float)mr.size.height; if(r<1) r= (float)mr.size.height / (float)mr.size.width; if(( area < min || area > max ) || ( r < rmin || r > rmax )){ return false; }else{ return true; } }输入的是RotatedRect mr, 一个旋转矩形。通过判断该矩形与车牌的相似程度。其中aspect=4.7272是由520mm/110mm计算来的,就是车牌的长宽比,在此基础上算上一点误差error,得出rmin, rmax, 要求输入矩形的长宽比介于两者之间。另外一个判断条件是输入矩形的面积,算出最小最大面积min,max (这个面积不知道为什么要这么算?),要求输入矩形的面积介于两者之间。若两个条件均符合则是符合条件的矩形。
6. 在输入图像上画出所有保留下来的轮廓,用蓝色表示
// Draw blue contours on a white image cv::Mat result; input.copyTo(result); cv::drawContours(result,contours, -1, // draw all contours cv::Scalar(255,0,0), // in blue 1); // with a thickness of 1
for(int i=0; i< rects.size(); i++){ }为了进一步判断轮廓圈出来的区域是否是车牌区域,采用“漫水填充”算法提取连通域。因为车牌区域是白色背景连城一片,大体上类似长方条,这是一个特征,非车牌区域通过漫水填充算法提取出来的区域可能是别的形状,就不是长方条的车牌了,因此可以初步鉴别。
floodFill() 函数定义:
int floodFill(InputOutputArray image, Point seedPoint, Scalar newVal, Rect* rect=0, Scalar loDiff= Scalar(), Scalar upDiff=Scalar(), int flags=4 )image:输入图像。 seedPoint:种子点。 newVal:填充连通域的新设定的值。 rect:可选参数,设定修改区域的边界矩形。 loDiff 和 upDiff 设定当前观察点与参考点灰度值是上下范围。 flags:共24位,分成3个8位,后面再注释。
漫水填充算法部分:
//For better rect cropping for each posible 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; //填充值为255,即白色填充 int NumSeeds = 10; Rect ccomp; // 4连通,白色填充到mask上,比较像素点与种子点的颜色差 int flags = connectivity + (newMaskVal << 8 ) + CV_FLOODFILL_FIXED_RANGE + CV_FLOODFILL_MASK_ONLY; // 为什么要循环10次呢?在中心点周围随机生成10个种子点,用黄色圈画出来。每一个种子点做一次floodfill,用白色画在mask上。 // Scalar(255,0,0)会被忽略,它是用来填充原图的,这里flag选择了CV_FLOODFILL_MASK_ONLY,所以不填充原图,只填充mask // 10次循环以后mask上画的是10次的累积还是最后一次? for(int j=0; j<NumSeeds; j++){ Point seed; seed.x=rects[i].center.x+rand()%(int)minSize-(minSize/2); seed.y=rects[i].center.y+rand()%(int)minSize-(minSize/2); circle(result, seed, 1, Scalar(0,255,255), -1); int area = floodFill(input, mask, seed, Scalar(255,0,0), &ccomp, Scalar(loDiff, loDiff, loDiff), Scalar(upDiff, upDiff, upDiff), flags); } if(showSteps) imshow("MASK", mask); //最终imshow的mask是最后一个rect填充后的mask,因为后一个会覆盖前一个 //cvWaitKey(0);circle() 以rect 的中心点为圆心画圆,半径3,用绿色填充。见上图中绿色部分。
minSize 为rect 长/宽中较小的那一条边的1/2.
mask 必须比原图大出一个像素的边框;
flags 低8位设定为4,即4连通;中间8位为255,即用白色值填充;高8位CV_FLOODFILL_FIXED_RANGE表示比较当前像素点与种子点而不是与邻域点;CV_FLOODFILL_MASK_ONLY表示只填充mask,不修改原图,则函数中的 Scalar newVal就被忽略了,因为它是设置填充原图颜色的。
for() 循环中随机生成10个种子点,这些种子点是围绕rect.center周围1/2minSize范围内,并用黄色点画出来了,参见上图。10个种子点做10次漫水填充,提取出的连通域累积叠加到mask上。比如车标附近的一个rect 处理后的mask如下图:
得到mask以后,重新获取最小面积包围矩形。
//Check new floodfill mask match for a correct patch. //Get all points detected for get Minimal rotated Rect vector<Point> pointsInterest; Mat_<uchar>::iterator itMask= mask.begin<uchar>(); Mat_<uchar>::iterator end= mask.end<uchar>(); for( ; itMask!=end; ++itMask) if(*itMask==255) pointsInterest.push_back(itMask.pos()); // mask中的白点的坐标被保存下来 RotatedRect minRect = minAreaRect(pointsInterest); //这些感兴趣点的最小包围矩形,为什么不直接用mask求最小包围矩形?因为mask的尺寸不符合吗?首先将mask中白色的点作为感兴趣点保存到pointInterest。然后求出这些点的最小包围矩形,也是个旋转矩形 minRect . 求最小包围矩形需要给出点坐标。上面的mr也是根据轮廓点坐标求出的。
接下来判断minRect是否是符合要求的矩形,跟mr的判断方法一样。如果符合,执行以下代码,以rect 为模板,抠出原图中的长方条小图。
if(verifySizes(minRect)){ // rotated rectangle drawing Point2f rect_points[4]; minRect.points( rect_points ); for( int j = 0; j < 4; j++ ) line( result, rect_points[j], rect_points[(j+1)%4], Scalar(0,0,255), 1, 8 ); //这种写法保证了3到0的线也画出来 //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); //获得仿射变换矩阵,scale=1,和原矩阵有什么不同?为什么要仿射? //Create and rotate image 牌照区域是个歪斜的长方条,现在把整个输入图像都根据这种歪斜方式变换,后面才可以通过minRect(mask)获取原图像的子区域 Mat img_rotated; warpAffine(input, img_rotated, rotmat, input.size(), CV_INTER_CUBIC); // 仿射变换,img_rotated也是斜的了,目的是使得牌照区域是正的 imshow("img_rotated", img_rotated); //Crop image Size rect_size=minRect.size; if(r < 1) swap(rect_size.width, rect_size.height); Mat img_crop; //输出的目标区域 旋转的时候是按照minRect.center这个中心点来旋转的,所以该中心点一直未变,下面获取的就是正的(非斜的)牌照区域 getRectSubPix(img_rotated, rect_size, minRect.center, img_crop);//img_rotated中分割出一个矩形到img_crop(牌照大小的长条图) imshow("img_crop", img_crop); Mat resultResized; resultResized.create(33,144, CV_8UC3); resize(img_crop, resultResized, resultResized.size(), 0, 0, INTER_CUBIC); //resize成33*144的矩阵。minRect是斜的, //Equalize croped image Mat grayResult; cvtColor(resultResized, grayResult, CV_BGR2GRAY); blur(grayResult, grayResult, Size(3,3)); grayResult=histeq(grayResult); //明度直方图均衡化 if(saveRegions){ stringstream ss(stringstream::in | stringstream::out); ss << "tmp/" << filename << "_" << i << ".jpg"; imwrite(ss.str(), grayResult); //imwrite()中用string保存文件名的方法 } output.push_back(Plate(grayResult,minRect.boundingRect())); }1.首先将 minRect.points 四个顶点赋值给rect_points[4] . minRect.points( rect_points ); 这种用法不理解,先放在这。
连接4个顶点,画出旋转矩形。
2. 获得仿射变换矩阵 getRotationMatrix2D() 牌照区域minRect 是个歪斜的长方条,求出它歪斜的角度,以它的中心minRect.center为中心,将整个图像旋转,这样原本歪斜的长方条就变正了。
3. 进行仿射变换warpAffine()
4. 获得以minRect为模板的原图像的小图。getRectSubPix()类似于抠图, 从img_rotated中抠出一个矩形到 img_crop 牌照大小的长条图
5. 将 img_crop resize成 33x144的Mat resultResized,采用的是CUBIC差值方法。
6. 将resultResized 从彩图转换成灰度图,再进行直方图均衡化histeq()
7. 保存该长方条到某个文件夹下。
8. 以Plate的格式保存到 output中。
注意,到这里为止,i=0,还要继续循环,直到 i< rects.size()
其中的 Plate 类是自定义类,资料中给出了 Plate.h 和 Plate.cpp 但是Plate.cpp 没有把函数实现写完整,所有我不理解。这里把代码贴出来
Plate.h
#ifndef Plate_h #define Plate_h #include <string.h> #include <vector> #include <cv.h> #include <highgui.h> #include <cvaux.h> using namespace std; using namespace cv; //Plate 是一种数据结构,例如cout<<Plate.str 可是Plate.chars又不对,是因为Plate.cpp里面没定义吗? class Plate{ public: Plate(); //默认构造函数 Plate(Mat img, Rect pos); //返回什么值? string str(); //车牌以string形式表示 Rect position; //车牌在图中的位置,用来在最后显示的时候框出来 Mat plateImg; vector<char> chars; //每个字符 vector<Rect> charsPos; //每个字符的位置 }; #endifPlate.cpp
#include "Plate.h" Plate::Plate(){ } Plate::Plate(Mat img, Rect pos){ plateImg=img; position=pos; } string Plate::str(){ string result=""; //Order numbers vector<int> orderIndex; vector<int> xpositions; for(int i=0; i< charsPos.size(); i++){ //charsPos.size()=? orderIndex.push_back(i); xpositions.push_back(charsPos[i].x); } float min=xpositions[0]; int minIdx=0; for(int i=0; i< xpositions.size(); i++){ min=xpositions[i]; minIdx=i; for(int j=i; j<xpositions.size(); j++){ if(xpositions[j]<min){ min=xpositions[j]; minIdx=j; //minIdx=x坐标最小的那个索引号 } } int aux_i=orderIndex[i]; int aux_min=orderIndex[minIdx]; orderIndex[i]=aux_min; orderIndex[minIdx]=aux_i; float aux_xi=xpositions[i]; float aux_xmin=xpositions[minIdx]; xpositions[i]=aux_xmin; xpositions[minIdx]=aux_xi; } for(int i=0; i<orderIndex.size(); i++){ result=result+chars[orderIndex[i]]; //chars[]从哪里来的?这个定义不完整吧? } return result; }
未完待续。