本文用来学习的项目来自书籍《实用计算机视觉项目解析》第5章Number Plate Recognition 提供的源代码
CSDN上找到的源代码下载链接:http://download.csdn.net/detail/muyuxingguang/8737107
我自己的百度网盘 链接:http://pan.baidu.com/s/1jIr8w2y 密码:ndov
主要步骤包括车牌检测和车牌识别。车牌检测部分从一张含有车牌的图像中检测出可能的车牌区域,通过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
#include
#include
#include
// 标准c++头文件
#include
#include
// 自定义头文件
#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 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 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
-> 读入数据文件SVM.xml,训练,判别,最终将每一个
-> 对每一个plate,执行OCR算法,最终得出车牌号。
这里定义了 DetectRegions类,
DetectRegions.h
#ifndef DetectRegions_h
#define DetectRegions_h
#include
#include
#include "Plate.h"
#include
#include
#include
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 run(Mat input);
private:
vector segment(Mat input); //成员函数,不能被直接调用,公有函数run()里面调用了它
bool verifySizes(RotatedRect mr); //成员函数,不能被直接调用,segment()里面调用了它
Mat histeq(Mat in); //成员函数,不能被直接调用,segment()里面调用了它
};
#endif
(以上代码后注释是我初学c++做的标记,为了搞清楚哪些放在public,哪些放在private。请忽略。)
main()函数中先设置了参数,再调用detecRegions.run()就解决了问题。
DetecRegions::run()函数的实现:
vector DetectRegions::run(Mat input){
//Segment image by white
vector tmp=segment(input);
//return detected and posibles regions
return tmp;
}
于是要看下 segment() 函数实现。
vector DetectRegions::segment(Mat input) {
vector output;
......
return output; // n个Plate类型的数据 output[i].plateImg是灰度图
}
输入参数是Mat类型图像input,输出vector
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 >::iterator itc= contours.begin();
vector 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
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 pointsInterest;
Mat_::iterator itMask= mask.begin();
Mat_::iterator end= mask.end();
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
#include
#include
#include
#include
using namespace std;
using namespace cv;
//Plate 是一种数据结构,例如cout< chars; //每个字符
vector charsPos; //每个字符的位置
};
#endif
Plate.cpp
#include "Plate.h"
Plate::Plate(){
}
Plate::Plate(Mat img, Rect pos){
plateImg=img;
position=pos;
}
string Plate::str(){
string result="";
//Order numbers
vector orderIndex;
vector 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
未完待续。