自动车牌识别(ANPR)练习项目学习笔记1(基于opencv)

本文用来学习的项目来自书籍《实用计算机视觉项目解析》第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用来评价机器学习算法。

自动车牌识别(ANPR)练习项目学习笔记1(基于opencv)_第1张图片

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算法,最终得出车牌号。


B.   Detect posible plate regions 检测可能的车牌区

这里定义了 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
自动车牌识别(ANPR)练习项目学习笔记1(基于opencv)_第2张图片

7. 保留下来的 rect 共有6个,分别处理每一个rect.

    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如下图:

自动车牌识别(ANPR)练习项目学习笔记1(基于opencv)_第3张图片

得到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() 

自动车牌识别(ANPR)练习项目学习笔记1(基于opencv)_第4张图片

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;  //每个字符的位置      
};

#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<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;
}


到这里,就实现了DetectRegions.cpp中的全部内容,得到了n张可能的车牌区域小图,后续进行SVM分类判别。

未完待续。



 





 
 






你可能感兴趣的:(C++,opencv,ANPR)