基于SVM与人工神经网络的车牌识别系统

最近研究了支持向量机(Support Vector Machine,SVM)和人工神经网络(Artifical Neural Network,ANN)等模式识别理论,结合OpenCV的书:《Mastering OpenCV with Practical Computer Vision Projects》,将两种思想运用到车辆的车牌识别算法中。车辆识别结合了多种图像处理技术,如视频监控、图像检测、图像分割和光学字符识别(OCR)等,在道路交通监控中有着重要的作用。以下内容主要包含几个方面:

车牌检测

? 图像预处理(图像分割)
? SVM分类器(对分割图像的分类)

车牌识别

? OCR分割
? 特征提取
? OCR分类(使用多层感知器Multi-Layer Perceptron,MLP)

一、实验准备

由于图像素材有限,且对于不同国家,车牌的规格与尺寸不尽相同,因此只能选择资料中已有的西班牙车牌进行研究。这里的素材来源于最常见的西班牙车牌(在西班牙,也有多种形状的车牌)。如下图所示,车牌的大小为520mm*110mm,其中左右两组字符由41mm的空间分离,左边包含四个数字,右边包含三个字母,每个字符之间的距离为14mm。所有字符的大小均为45mm*77mm。

参考书籍中给出了一个已经定义好的车牌类Plate,后续的图像处理需要用到,直接使用即可,毕竟研究的重点是后续的处理和模式分类算法:

#ifndef Plate_h
#define Plate_h

#include <string.h>
#include <vector>

#include <opencv/cv.h>
#include <opencv/highgui.h>
#include <opencv/cvaux.h>

using namespace std;
using namespace cv;

class Plate{
public:
    Plate();
    Plate(Mat img, Rect pos);
    string str();
    Rect position;
    Mat plateImg;
    vector<char> chars;
    vector<Rect> charsPos;
};

#endif
#include "Plate.h"

Plate::Plate(){
}

Plate::Plate(Mat img, Rect pos){
    plateImg = img;
    position = pos;
}

string Plate::str(){
    string result = "";
    vector<int> orderIndex;
    vector<int> xpositions;
    for (int i = 0; i< charsPos.size(); i++){
        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;
            }
        }
        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]];
    }
    return result;
}

二、算法流程

正如上面叙述的,车牌识别有两个主要步骤,即检测与识别。其中车牌检测的目标是在图像或视频帧中检测到车牌的位置。在完成这一步后,进行识别部分,这里使用OCR算法来识别车牌上的字符,其中有数字,也包含字母。

三、车牌检测

  • 图像分割

车牌识别的第一步自然是检测图像或视频帧中的车牌,并去除其他多余的信息,这一部分主要依靠图像分割来完成。而对于图像分割工作主要包含以下步骤:

1.Sobel滤波器;

2.阈值算子;

3.闭形态学算子;

4.一个填充区域掩码;

5.用颜色标记图像中可能检测到的车辆;

6.执行SVM分类器后检测出车牌。

使用边缘检测的原因是一般情况下拍摄到的车牌有大量竖直的边缘,且车牌没有旋转和透视扭曲,通过检测竖直边可以删除图像中多余的区域。在使用Sobel滤波器之前,需要确保图像为灰度图像,否则需要转化;另一个预处理操作是进行适当的高斯滤波,从而消除可能由摄像机或其他环境产生的噪声,这里使用5*5的高斯滤波去噪。

以下是图像分割的源代码,ImageEecognition.h和ImageEecognition.cpp,其中一些主要的函数调用方法已给出,如Sobel函数:

#ifndef ImageEecognition_h
#define ImageEecognition_h

#include <string.h>
#include <vector>

#include "Plate.h"

#include <opencv/cv.h>
#include <opencv/highgui.h>
#include <opencv/cvaux.h>

using namespace std;
using namespace cv;

class ImageRecognition{
public:
    ImageRecognition();
    string filename;
    void setFilename(string f);
    bool saveRecognition;
    bool showSteps;
    vector<Plate> run(Mat input);

    vector<Plate> segment(Mat input);
    bool verifySizes(RotatedRect mr);
    Mat histeq(Mat in);
};

#endif
#include "ImageRecognition.h"

void ImageRecognition::setFilename(string name) {
    filename = name;
}

ImageRecognition::ImageRecognition(){
    showSteps = false;
    saveRecognition = false;
}

bool ImageRecognition::verifySizes(RotatedRect ROI){
    // 以下设置车牌默认参数,用于识别矩形区域内是否为目标车牌
    float error = 0.4;
    // 西班牙车牌宽高比: 520 / 110 = 4.7272
    float aspect = 4.7272;
    // 设定区域面积的最小/最大尺寸,不在此范围内的不被视为车牌
    int min = 15 * aspect * 15;    // 15个像素
    int max = 125 * aspect * 125;  // 125个像素
    float rmin = aspect - aspect*error;
    float rmax = aspect + aspect*error;

    int area = ROI.size.height * ROI.size.width;
    float r = (float)ROI.size.width / (float)ROI.size.height;
    if (r<1)
        r = (float)ROI.size.height / (float)ROI.size.width;

    // 判断是否符合以上参数
    if ((area < min || area > max) || (r < rmin || r > rmax))
        return false;
    else
        return true;
}

// 对图像进行直方图均衡处理,调整亮度
Mat ImageRecognition::histeq(Mat ima)
{
    Mat imt(ima.size(), ima.type());
    // 若输入图像为彩色,需要在HSV空间中做直方图均衡处理
    // 再转换回RGB格式
    if (ima.channels() == 3)
    {
        Mat hsv;
        vector<Mat> hsvSplit;
        cvtColor(ima, hsv, CV_BGR2HSV);
        split(hsv, hsvSplit);
        equalizeHist(hsvSplit[2], hsvSplit[2]);
        merge(hsvSplit, hsv);
        cvtColor(hsv, imt, CV_HSV2BGR);
    }
    // 若输入图像为灰度图,直接做直方图均衡处理
    else if (ima.channels() == 1){
        equalizeHist(ima, imt);
    }
    return imt;
}

// 图像分割函数
vector<Plate> ImageRecognition::segment(Mat input)
{
    vector<Plate> output;

    //n图像转换为灰度图
    Mat grayImage;
    cvtColor(input, grayImage, CV_BGR2GRAY);
    blur(grayImage, grayImage, Size(5, 5));  // 对图像进行滤波,去除噪声

    // 通常车牌拥有显著的边缘特征,这里使用sobel算子检测边缘
    Mat sobelImage;
    Sobel(grayImage,       // 输入图像
          sobelImage,      // 输出图像
          CV_8U,           //输出图像的深度
          1,               // x方向上的差分阶数
          0,               // y方向上的差分阶数
          3,               // 扩展Sobel核的大小,必须是1,3,5或7
          1,               // 计算导数值时可选的缩放因子,默认值是1
          0,               // 表示在结果存入目标图之前可选的delta值,默认值为0
          BORDER_DEFAULT); // 边界模式,默认值为BORDER_DEFAULT
    if (showSteps)
        imshow("Sobel", sobelImage);

    // 阈值分割得到二值图像,所采用的阈值由Otsu算法得到
    Mat thresholdImage;
    // 输入一幅8位图像,自动得到优化的阈值
    threshold(sobelImage, thresholdImage, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY);
    if (showSteps)
        imshow("Threshold Image", thresholdImage);

    // 形态学之闭运算
    // 定义一个结构元素structuringElement,维度为17*3
    Mat structuringElement = getStructuringElement(MORPH_RECT, Size(17, 3));
    // 使用morphologyEx函数得到包含车牌的区域(但不包含车牌号)
    morphologyEx(thresholdImage, thresholdImage, CV_MOP_CLOSE, structuringElement);
    if (showSteps)
        imshow("Close", thresholdImage);

    // 找到可能的车牌的轮廓
    vector< vector< Point> > contours;
    findContours(thresholdImage,
                 contours, // 检测的轮廓数组,每一个轮廓用一个point类型的vector表示
                 CV_RETR_EXTERNAL, // 表示只检测外轮廓
                 CV_CHAIN_APPROX_NONE); // 轮廓的近似办法,这里存储所有的轮廓点

    // 对每个轮廓检测和提取最小区域的有界矩形区域
    vector<vector<Point> >::iterator itc = contours.begin();
    vector<RotatedRect> rects;
    // 若没有达到设定的宽高比要求,移去该区域
    while (itc != contours.end())
    {
        RotatedRect ROI = minAreaRect(Mat(*itc));
        if (!verifySizes(ROI)){
            itc = contours.erase(itc);
        }
        else{
            ++itc;
            rects.push_back(ROI);
        }
    }

    // 在白色的图上画出蓝色的轮廓
    cv::Mat result;
    input.copyTo(result);
    cv::drawContours(result,
                     contours,
                     -1,                    // 所有的轮廓都画出
                     cv::Scalar(255, 0, 0), // 颜色
                     1);                    // 线粗

    // 使用漫水填充算法裁剪车牌获取更清晰的轮廓
    for (int i = 0; i< rects.size(); i++){

        circle(result, rects[i].center, 3, Scalar(0, 255, 0), -1);
        // 得到宽度和高度中较小的值,得到车牌的最小尺寸
        float minSize = (rects[i].size.width < rects[i].size.height) ? rects[i].size.width : rects[i].size.height;
        minSize = minSize - minSize * 0.5;
        // 在块中心附近产生若干个随机种子
        srand(time(NULL));
        // 初始化漫水填充算法的参数
        Mat mask;
        mask.create(input.rows + 2, input.cols + 2, CV_8UC1);
        mask = Scalar::all(0);
        // loDiff表示当前观察像素值与其部件邻域像素值或者待加入
        // 该部件的种子像素之间的亮度或颜色之负差的最大值
        int loDiff = 30;
        // upDiff表示当前观察像素值与其部件邻域像素值或者待加入
        // 该部件的种子像素之间的亮度或颜色之正差的最大值
        int upDiff = 30;
        int connectivity = 4; // 用于控制算法的连通性,可取4或者8
        int newMaskVal = 255;
        int NumSeeds = 10;
        Rect ccomp;
        // 操作标志符分为几个部分
        int flags = connectivity + // 用于控制算法的连通性,可取4或者8
                    (newMaskVal << 8) +
                    CV_FLOODFILL_FIXED_RANGE + // 设置该标识符,会考虑当前像素与种子像素之间的差
                    CV_FLOODFILL_MASK_ONLY; // 函数不会去填充改变原始图像, 而是去填充掩模图像
        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);

        // 得到裁剪掩码后,检查其有效尺寸
        // 对于每个掩码的白色像素,先得到其位置
        // 再使用minAreaRect函数获取最接近的裁剪区域
        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());

        RotatedRect minRect = minAreaRect(pointsInterest);

        if (verifySizes(minRect)){
            // 旋转矩形图
            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);

            // 得到旋转图像区域的矩阵
            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);

            // 通过仿射变换旋转输入的图像
            Mat img_rotated;
            warpAffine(input, img_rotated, rotmat, input.size(), CV_INTER_CUBIC);

            // 最后裁剪图像
            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);

            Mat resultResized;
            resultResized.create(33, 144, CV_8UC3);
            resize(img_crop, resultResized, resultResized.size(), 0, 0, INTER_CUBIC);
            // 为了消除光照影响,对裁剪图像使用直方图均衡化处理
            Mat grayResult;
            cvtColor(resultResized, grayResult, CV_BGR2GRAY);
            blur(grayResult, grayResult, Size(3, 3));
            grayResult = histeq(grayResult);
            if (saveRecognition){
                stringstream ss(stringstream::in | stringstream::out);
                ss << "tmp/" << filename << "_" << i << ".jpg";
                imwrite(ss.str(), grayResult);
            }
            output.push_back(Plate(grayResult, minRect.boundingRect()));
        }
    }
    if (showSteps)
        imshow("Contours", result);

    return output;
}

vector<Plate> ImageRecognition::run(Mat input)
{
    vector<Plate> tmp = segment(input);
    // 返回检测结果
    return tmp;
}
  • 图像分类

对分割完的图像使用SVM分类,并由代码自动创建正负样本。这里需要了解一下SVM(Support Vector Machine),即支持向量机算法,这是一种有监督学习方法。OpenCV开发SVM算法是基于LibSVM软件包开发的,LibSVM软件包是台湾大学林智仁开发设计的一个简单、易于使用和快速有效的SVM模式识别与回归的软件包。用OpenCV使用SVM算法的大概流程如下:

1)设置训练样本集,一般需要两组数据,一组是数据的类别,一组是数据的向量信息。

2)设置SVM参数。利用CvSVMParams类实现类内的成员变量svm_type表示SVM类型:

CvSVM::C_SVC     // C-SVC
CvSVM::NU_SVC    // v-SVC
CvSVM::ONE_CLASS // 一类SVM
CvSVM::EPS_SVR   // e-SVR
CvSVM::NU_SVR    // v-SVR

成员变量kernel_type表示核函数的类型:

CvSVM::LINEAR 线性 u‘v
CvSVM::POLY 多项式:(r*u‘v + coef0)^degree
CvSVM::RBF RBF函数:exp(-r|u-v|^2)
CvSVM::SIGMOID sigmoid函数:tanh(r*u‘v + coef0)

成员变量degree针对多项式核函数degree的设置,gamma针对多项式/rbf/sigmoid核函数的设置,coef0针对多项式/sigmoid核函数的设置,Cvalue为损失函数,在C-SVC、e-SVR、v-SVR中有效,nu设置v-SVC、一类SVM和v-SVR参数,p为设置e-SVR中损失函数的值,class_weightsC_SVC的权重,term_crit为SVM训练过程的终止条件。其中默认值degree = 0,gamma = 1,coef0 = 0,Cvalue = 1,nu = 0,p = 0,class_weights = 0

3)在分类之前,需要训练分类器。

在这里,使用75张包含车牌的图像(正样本)和35张不包含车牌的大小为144*33像素的图像(对应负样本)。(若要使车牌识别系统具有普适性,需要更多的训练数据,在本实验中这些数据已经够用)。

在得到分割后的车牌和非车牌图像后,我们把二者都执行reshaple(1,1),再存放到trainImage的矩阵中,并修改对应trainLables矩阵的0-1值,然后把trainData改为32为浮点数系,再把trainData和trainLabel直接写进xml文件。

训练SVM的代码如下:

// Main entry code OpenCV

#include <cv.h>
#include <highgui.h>
#include <cvaux.h>

#include <iostream>
#include <vector>

using namespace std;
using namespace cv;

int main ( int argc, char** argv )
{
    cout << "OpenCV Training SVM Automatic Number Plate Recognition\n";
    cout << "\n";

    char* path_Plates;
    char* path_NoPlates;
    int numPlates;
    int numNoPlates;
    int imageWidth=144;
    int imageHeight=33;

    //Check if user specify image to process
    if(argc >= 5 )
    {
        numPlates= atoi(argv[1]);
        numNoPlates= atoi(argv[2]);
        path_Plates= argv[3];
        path_NoPlates= argv[4];

    }else{
        cout << "Usage:\n" << argv[0] << " <num Plate Files> <num Non Plate Files> <path to plate folder files> <path to non plate files> \n";
        return 0;
    }        

    Mat classes;//(numPlates+numNoPlates, 1, CV_32FC1);
    Mat trainingData;//(numPlates+numNoPlates, imageWidth*imageHeight, CV_32FC1 );

    Mat trainingImages;
    vector<int> trainingLabels;

    for(int i=0; i< numPlates; i++)
    {

        stringstream ss(stringstream::in | stringstream::out);
        ss << path_Plates << i << ".jpg";
        Mat img=imread(ss.str(), 0);
        img= img.reshape(1, 1);
        trainingImages.push_back(img);
        trainingLabels.push_back(1);
    }

    for(int i=0; i< numNoPlates; i++)
    {
        stringstream ss(stringstream::in | stringstream::out);
        ss << path_NoPlates << i << ".jpg";
        Mat img=imread(ss.str(), 0);
        img= img.reshape(1, 1);
        trainingImages.push_back(img);
        trainingLabels.push_back(0);

    }

    Mat(trainingImages).copyTo(trainingData);
    //trainingData = trainingData.reshape(1,trainingData.rows);
    trainingData.convertTo(trainingData, CV_32FC1);
    Mat(trainingLabels).copyTo(classes);

    FileStorage fs("SVM.xml", FileStorage::WRITE);
    fs << "TrainingData" << trainingData;
    fs << "classes" << classes;
    fs.release();

    return 0;
}

在代码中,调用CvSVM::train函数建立SVM模型,第一个参数为训练数据,第二个参数为分类结果,最后一个参数即CvSVMParams。

4)用这个SVM进行分类。调用函数CvSVM::predict实现分类。

5)获得支持向量

除了分类,也可以得到SVM的支持向量,调用函数CvSVM::get_support_vector_count获得支持向量的个数,CvSVM::get_support_vector获得对应的索引编号的支持向量。

在OpenCV中,SVM函数的调用方法如下:

    // 训练SVM,用于训练和测试的图像数据保存在SVM.xml文件中
    FileStorage fs;
    fs.open("SVM.xml", FileStorage::READ);
    Mat SVM_TrainingData;
    Mat SVM_Classes;
    fs["TrainingData"] >> SVM_TrainingData;
    fs["classes"] >> SVM_Classes;
    // 设置SVM的基本参数
    CvSVMParams SVM_params;  // CvSVMParams结构用于定义基本参数
    SVM_params.svm_type = CvSVM::C_SVC;     // SVM类型
    SVM_params.kernel_type = 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);

    // 创建并训练分类器
    CvSVM svmClassifier(SVM_TrainingData, SVM_Classes, Mat(), Mat(), SVM_params);

更多关于SVM算法的介绍可参照:http://zh.wikipedia.org/wiki/SVM

取各个图像预处理步骤的结果,如下图所示:

其中图1为原彩色图像;

图2将彩色图像转化为灰度图,并采用5*5模版对图像进行高斯滤波,去除环境噪声;

图3是使用Sobel滤波器求一阶水平方向导数,输出垂直边缘的结果;

图4使用Otsu自适应阈值算法获得图像二值化的阈值,从而得到二值图像;

图5采用闭操作,去除每个垂直边缘线之间的空白空格,并连接所有包含 大量边缘的区域(这步过后,我们将有许多包含车牌的候选区域);

图6显示了执行SVM分类器后得到的正负样本,此时需要对这些正负样本(其实是分割后的图像块)进行分类。

四、车牌识别

车牌识别算法的第二步是使用光学字符识别获取车牌上的字符,而对于车牌号识别工作主要包含以下步骤:

1.ROC分割:对于每个车牌,可以将每个字符分割出来

2.训练人工神经网络(Artificial Neural Network, ANN)

3.使用人工神经网络以识别字符

  • ROC分割

分割步骤如下图所示:

首先,图1为输入分割后的车牌,之后,对图像进行二值化,如图2所示;之后求每个字符的轮廓、最小外接矩形进而求矩形的纵横比及面积。最后统一矩形大小,并将每个字符的图片保存下来。

  • 训练人工神经网络

人工神经网络实际是一个多层感知器,是一种前馈人工神经网络模型,其将输入的多个数据集映射到单一的输出的数据集上。对比单层感知器,多层感知器的一大优点是可以轻松实现非线性分类。一个简单的例子是:单个感知器无法解决异或问题,但将多个感知器进行组合可以实现这种较为复杂的空间分割,如下图所示。

实际上,上述模型就是多层感知器神经网络(Multi-layer perceptron neural networks,MLP neural netwoks)的基础模型。神经网络中每个节点为一个感知器,模型生物神经网络中神经元的基础功能:来自外界(环境或其他细胞)的电信号通过突触传递给神经元,当细胞收到的信号总和超过一定阈值后,细胞被激活,通过轴突向下一个细胞发送电信号,完成对外界信息的加工。

更多关于多层感知器的资料可参考:http://blog.csdn.net/xiaowei_cqu/article/details/9004331

  • 使用人工神经网络以识别字符

主要包含以下步骤:

1.读取一张车牌图像

2.设置人工神经网络参数,并使用xml文件训练神经网络

3.提取车牌图像的累计直方图和低分辨率图像特征矩阵

4.将该特征矩阵作为神经网络输入,经过神经网络计算从而得到预测结果

5.按照每个字符图像的相对位置,进行字符重新排序

6.得到最终的字符串并打印出来

车牌识别部分的完整代码如下:

#ifndef OCR_h
#define OCR_h

#include <string.h>
#include <vector>

#include "Plate.h"

#include <opencv/cv.h>
#include <opencv/highgui.h>
#include <opencv/cvaux.h>
#include <opencv/ml.h>

using namespace std;
using namespace cv;

#define HORIZONTAL    1
#define VERTICAL    0

class CharSegment{
public:
    CharSegment();
    CharSegment(Mat i, Rect p);
    Mat img;
    Rect pos;
};

class OCR{
public:
    bool DEBUG;
    bool saveSegments;
    string filename;
    static const int numCharacters;
    static const char strCharacters[];
    OCR(string trainFile);
    OCR();
    string run(Plate *input);
    int charSize;
    Mat preprocessChar(Mat in);
    int classify(Mat f);
    void train(Mat trainData, Mat trainClasses, int nlayers);
    int classifyKnn(Mat f);
    void trainKnn(Mat trainSamples, Mat trainClasses, int k);
    Mat features(Mat input, int size);

private:
    bool trained;
    vector<CharSegment> segment(Plate input);
    Mat Preprocess(Mat in, int newSize);
    Mat getVisualHistogram(Mat *hist, int type);
    void drawVisualFeatures(Mat character, Mat hhist, Mat vhist, Mat lowData);
    Mat ProjectedHistogram(Mat img, int t);
    bool verifySizes(Mat r);
    CvANN_MLP  ann;
    CvKNearest knnClassifier;
    int K;
};

#endif
#include "OCR.h"

const char OCR::strCharacters[] = { ‘0‘, ‘1‘, ‘2‘, ‘3‘, ‘4‘, ‘5‘, ‘6‘, ‘7‘, ‘8‘, ‘9‘, ‘B‘, ‘C‘, ‘D‘, ‘F‘, ‘G‘, ‘H‘, ‘J‘, ‘K‘, ‘L‘, ‘M‘, ‘N‘, ‘P‘, ‘R‘, ‘S‘, ‘T‘, ‘V‘, ‘W‘, ‘X‘, ‘Y‘, ‘Z‘ };
const int OCR::numCharacters = 30;

CharSegment::CharSegment(){}
CharSegment::CharSegment(Mat i, Rect p)
{
    img = i;
    pos = p;
}

OCR::OCR()
{
    DEBUG = false;
    trained = false;
    saveSegments = false;
    charSize = 20;
}
OCR::OCR(string trainFile)
{
    DEBUG = false;
    trained = false;
    saveSegments = false;
    charSize = 20;

    // 读取OCR.xml文件
    FileStorage fs;
    fs.open("OCR.xml", FileStorage::READ);
    Mat TrainingData;
    Mat Classes;
    fs["TrainingDataF15"] >> TrainingData;
    fs["classes"] >> Classes;

    train(TrainingData, Classes, 10);

}

Mat OCR::preprocessChar(Mat in){
    //Remap image
    int h = in.rows;
    int w = in.cols;
    Mat transformMat = Mat::eye(2, 3, CV_32F);
    int m = max(w, h);
    transformMat.at<float>(0, 2) = m / 2 - w / 2;
    transformMat.at<float>(1, 2) = m / 2 - h / 2;

    Mat warpImage(m, m, in.type());
    warpAffine(in, warpImage, transformMat, warpImage.size(), INTER_LINEAR, BORDER_CONSTANT, Scalar(0));

    Mat out;
    resize(warpImage, out, Size(charSize, charSize));

    return out;
}

bool OCR::verifySizes(Mat r){
    // 正确的车牌字符宽高比为45/77
    float aspect = 45.0f / 77.0f;
    float charAspect = (float)r.cols / (float)r.rows;
    float error = 0.35;  // 允许误差达到35%
    float minHeight = 15;
    float maxHeight = 28;
    // 最小比例
    float minAspect = 0.2;
    float maxAspect = aspect + aspect*error;
    // 区域像素
    float area = countNonZero(r);
    // bb区域
    float bbArea = r.cols*r.rows;
    // 像素占区域的百分比
    float percPixels = area / bbArea;

    // 若一块区域的比率超过标准比率的80%,则认为该区域为黑色快,而不是一个字符
    if (DEBUG)
        cout << "Aspect: " << aspect << " [" << minAspect << "," << maxAspect << "] " << "Area " << percPixels << " Char aspect " << charAspect << " Height char " << r.rows << "\n";
    if (percPixels < 0.8 && charAspect > minAspect && charAspect < maxAspect && r.rows >= minHeight && r.rows < maxHeight)
        return true;
    else
        return false;

}

// 阈值分割
vector<CharSegment> OCR::segment(Plate plate){
    Mat input = plate.plateImg;
    vector<CharSegment> output;
    Mat thresholdImage;
    threshold(input, thresholdImage, 60, 255, CV_THRESH_BINARY_INV);
    if (DEBUG)
        imshow("Threshold plate", thresholdImage);
    Mat img_contours;
    thresholdImage.copyTo(img_contours);
    // 找到可能的车牌的轮廓
    vector< vector< Point> > contours;
    findContours(thresholdImage,
        contours, // 检测的轮廓数组,每一个轮廓用一个point类型的vector表示
        CV_RETR_EXTERNAL, // 表示只检测外轮廓
        CV_CHAIN_APPROX_NONE); // 轮廓的近似办法,这里存储所有的轮廓点

    // 在白色的图上画出蓝色的轮廓
    cv::Mat result;

    thresholdImage.copyTo(result);
    cvtColor(result, result, CV_GRAY2RGB);
    cv::drawContours(result, contours,
        -1,  // 所有的轮廓都画出
        cv::Scalar(255, 0, 0), // 颜色
        1); // 线粗

    // 对每个轮廓检测和提取最小区域的有界矩形区域
    vector<vector<Point> >::iterator itc = contours.begin();

    char res[20];
    int i = 0;
    // 若没有达到设定的宽高比要求,移去该区域
    while (itc != contours.end())
    {
        Rect mr = boundingRect(Mat(*itc));
        rectangle(result, mr, Scalar(0, 255, 0));
        // 裁剪图像
        Mat auxRoi(thresholdImage, mr);
        if (verifySizes(auxRoi)){
            auxRoi = preprocessChar(auxRoi);
            output.push_back(CharSegment(auxRoi, mr));
            //保存每个字符图片
            sprintf(res, "PlateNumber%d.jpg", i);
            i++;
            imwrite(res, auxRoi);
            rectangle(result, mr, Scalar(0, 125, 255));
        }
        ++itc;
    }
    if (DEBUG)
        cout << "Num chars: " << output.size() << "\n";

    if (DEBUG)
        imshow("SEgmented Chars", result);
    return output;
}

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<float>(j) = countNonZero(data);
    }

    // 归一化直方图
    double min, max;
    minMaxLoc(mhist, &min, &max);

    if (max>0)
        mhist.convertTo(mhist, -1, 1.0f / max, 0);

    return mhist;
}

Mat OCR::getVisualHistogram(Mat *hist, int type)
{

    int size = 100;
    Mat imHist;

    if (type == HORIZONTAL){
        imHist.create(Size(size, hist->cols), CV_8UC3);
    }
    else{
        imHist.create(Size(hist->cols, size), CV_8UC3);
    }

    imHist = Scalar(55, 55, 55);

    for (int i = 0; i<hist->cols; i++){
        float value = hist->at<float>(i);
        int maxval = (int)(value*size);

        Point pt1;
        Point pt2, pt3, pt4;

        if (type == HORIZONTAL)
        {
            pt1.x = pt3.x = 0;
            pt2.x = pt4.x = maxval;
            pt1.y = pt2.y = i;
            pt3.y = pt4.y = i + 1;

            line(imHist, pt1, pt2, CV_RGB(220, 220, 220), 1, 8, 0);
            line(imHist, pt3, pt4, CV_RGB(34, 34, 34), 1, 8, 0);

            pt3.y = pt4.y = i + 2;
            line(imHist, pt3, pt4, CV_RGB(44, 44, 44), 1, 8, 0);
            pt3.y = pt4.y = i + 3;
            line(imHist, pt3, pt4, CV_RGB(50, 50, 50), 1, 8, 0);
        }
        else
        {
            pt1.x = pt2.x = i;
            pt3.x = pt4.x = i + 1;
            pt1.y = pt3.y = 100;
            pt2.y = pt4.y = 100 - maxval;

            line(imHist, pt1, pt2, CV_RGB(220, 220, 220), 1, 8, 0);
            line(imHist, pt3, pt4, CV_RGB(34, 34, 34), 1, 8, 0);

            pt3.x = pt4.x = i + 2;
            line(imHist, pt3, pt4, CV_RGB(44, 44, 44), 1, 8, 0);
            pt3.x = pt4.x = i + 3;
            line(imHist, pt3, pt4, CV_RGB(50, 50, 50), 1, 8, 0);
        }
    }
    return imHist;
}

void OCR::drawVisualFeatures(Mat character, Mat hhist, Mat vhist, Mat lowData){
    Mat img(121, 121, CV_8UC3, Scalar(0, 0, 0));
    Mat ch;
    Mat ld;

    cvtColor(character, ch, CV_GRAY2RGB);

    resize(lowData, ld, Size(100, 100), 0, 0, INTER_NEAREST);
    cvtColor(ld, ld, CV_GRAY2RGB);

    Mat hh = getVisualHistogram(&hhist, HORIZONTAL);
    Mat hv = getVisualHistogram(&vhist, VERTICAL);

    Mat subImg = img(Rect(0, 101, 20, 20));
    ch.copyTo(subImg);

    subImg = img(Rect(21, 101, 100, 20));
    hh.copyTo(subImg);

    subImg = img(Rect(0, 0, 20, 100));
    hv.copyTo(subImg);

    subImg = img(Rect(21, 0, 100, 100));
    ld.copyTo(subImg);

    line(img, Point(0, 100), Point(121, 100), Scalar(0, 0, 255));
    line(img, Point(20, 0), Point(20, 121), Scalar(0, 0, 255));

    imshow("Visual Features", img);

    cvWaitKey(0);
}

// 特征提取
Mat OCR::features(Mat in, int sizeData){
    //Histogram features
    Mat vhist = ProjectedHistogram(in, VERTICAL);
    Mat hhist = ProjectedHistogram(in, HORIZONTAL);

    Mat lowData;
    resize(in, lowData, Size(sizeData, sizeData));

    if (DEBUG)
        drawVisualFeatures(in, hhist, vhist, lowData);

    int numCols = vhist.cols + hhist.cols + lowData.cols*lowData.cols;

    Mat out = Mat::zeros(1, numCols, CV_32F);

    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++;
        }
    }
    if (DEBUG)
        cout << out << "\n===========================================\n";
    return out;
}

// 用于创建所有需要的矩阵并用训练数据、类标签矩
// 阵、在隐藏层的神经元数量来训练一个识别系统
void OCR::train(Mat TrainData, Mat classes, int nlayers){
    Mat layers(1, 3, CV_32SC1);
    layers.at<int>(0) = TrainData.cols;
    layers.at<int>(1) = nlayers;
    layers.at<int>(2) = numCharacters;
    ann.create(layers, CvANN_MLP::SIGMOID_SYM, 1, 1);

    // 创建一个矩阵,其中存放n个训练数据,并将其分为m类
    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 (k == classes.at<int>(i))
                trainClasses.at<float>(i, k) = 1;
            else
                trainClasses.at<float>(i, k) = 0;
        }
    }
    Mat weights(1, TrainData.rows, CV_32FC1, Scalar::all(1));

    // 分类器学习
    ann.train(TrainData, trainClasses, weights);
    trained = true;
}

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);
    // 我们需要知道在输出的最大值,x代表类别

    return maxLoc.x;
}

int OCR::classifyKnn(Mat f){
    int response = (int)knnClassifier.find_nearest(f, K);
    return response;
}
void OCR::trainKnn(Mat trainSamples, Mat trainClasses, int k){
    K = k;
    // 分类器学习
    knnClassifier.train(trainSamples, trainClasses, Mat(), false, K);
}

string OCR::run(Plate *input){

    // 字符分割
    vector<CharSegment> segments = segment(*input);

    for (int i = 0; i<segments.size(); i++){
        // 对每个字符进行预处理,使得对所有图像均有相同的大小
        Mat ch = preprocessChar(segments[i].img);
        if (saveSegments){
            stringstream ss(stringstream::in | stringstream::out);
            ss << "tmpChars/" << filename << "_" << i << ".jpg";
            imwrite(ss.str(), ch);
        }
        // 为每个分段提取特征
        Mat f = features(ch, 15);
        // 对于每个部分进行分类
        int character = classify(f);
        input->chars.push_back(strCharacters[character]);
        input->charsPos.push_back(segments[i].pos);
    }
    return "-";//input->str();
}

五、效果测试

算法的识别效果如下图所示,其中控制台打印出检测信息和识别结果,若检测到合格车牌,Num plates detected置1,并打印出车牌号码:

六、注意事项

在运行代码时,可能会出现以下错误:

原因是Visual C++使用了更加安全的run-time library routines。

新的Security CRT函数:

一个解决方法是将原来的旧函数替换成新的Security CRT 函数。

这里使用了另外一个解决办法,就是在解决方案资源管理器中右键点击项目,进入属性->配置属性->C/C++/预处理器,修改预处理器定义。

在其中添加:_CRT_SECURE_NO_WARNINGS,重新运行程序时错误消失。

六、完整代码

http://download.csdn.net/detail/liyuefeilong/8722405

七、参考资料

《Mastering OpenCV with Practical Computer Vision Projects》

http://blog.csdn.net/viewcode/article/details/12840405

http://baike.baidu.com/link?url=M4hOG3-PiW97tJsw-b2N-SAACYwQucz-Xak2r44ep_7yPBsyJbbQ2M2S6gSsoBcMPOqfoVDZAb9gh14tejjKDK3oi5kUc–KVHDF8PmMKHi

http://blog.csdn.net/jinshengtao/article/details/17954427

http://jingyan.baidu.com/article/ce436649fd61543773afd32e.html

时间: 2024-10-11 05:29:06

基于SVM与人工神经网络的车牌识别系统的相关文章

手机端车牌识别系统(支持安卓、ios系统)

私家车的数量的急剧增长,既带来了喜也同样带来悲.一方面给我们的生活带来的了很多便利,另一方面也同样带来了交通拥堵.交通事故等问题.因为对于车辆的管理尤为重要! 比如说违章停车登记,传统方法是:执法人员通过携带的数码相机对违法停车的车辆拍照取证,然后手动输入车牌号码进行登记. 针对于这种违章停车的管理问题,北京易泊时代研发了一款基于安卓/ios的手机端车牌识别系统, 执法人员通过持有集成了安卓ios移动端车牌识别系统的手机,通过摄像头对违法停车车辆进行拍照取证的同时,可以自动获取车牌号码.违法停车

基于opencv的车牌识别系统

前言 学习了很长一段时间了,需要沉淀下,而最好的办法就是做一个东西来应用学习的东西,同时也是一个学习的过程. 概述     OpenCV的全称是:Open Source Computer Vision Library.OpenCV是一个基于(开源)发行的跨平台计算机视觉库,可以运行在Linux.Windows和Mac OS操作系统上.它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python.Ruby.MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算

车牌识别系统,助力智慧城市

城市在发展进程中总会遇到各种各样的问题,如今看来,例如停车,停车难已经成为一线二线城市,城市化过程中一个畅聊的话题,停车位少.停车位不好找.停车收费慢等等,所以今天我们要讨论的话题就是车牌识别一体机对提高城市停车管理的重要性,车牌识别一体机不仅可以减轻停车场负担,收费难管理难的压力,还能为安防等行业,提供便利. 城市在设计的过程中,优化了停车场的布局,在建设停车场的时候更加的人性化.方便化,比如立体车库的不断涌现.当然,我们如何知道停车场内是否有空余车位.哪个位置有停车位.这就离不开车牌识别的作

EasyPR是一个中文的开源车牌识别系统

EasyPR EasyPR是一个中文的开源车牌识别系统,其目标是成为一个简单.高效.准确的车牌识别引擎. 相比于其他的车牌识别系统,EasyPR有如下特点: 它基于openCV这个开源库.这意味着你可以获取全部源代码,并且移植到opencv支持的所有平台. 它能够识别中文.例如车牌为苏EUK722的图片,它可以准确地输出std:string类型的"苏EUK722"的结果. 它的识别率较高.图片清晰情况下,车牌检测与字符识别可以达到80%以上的精度. 跨平台 目前除了windows平台以

【基于WPF+OneNote+Oracle的中文图片识别系统阶段总结】之篇四:关于OneNote入库处理以及审核

篇一:WPF常用知识以及本项目设计总结:http://www.cnblogs.com/baiboy/p/wpf.html 篇二:基于OneNote难点突破和批量识别:http://www.cnblogs.com/baiboy/p/wpf1.html 篇三:批量处理后的txt文件入库处理:http://www.cnblogs.com/baiboy/p/wpf2.html 篇四:关于OneNote入库处理以及审核:http://www.cnblogs.com/baiboy/p/wpf3.html [

车牌识别系统应用之“无感支付”

好消息来了! 本月底前山东高速管辖范围内的所有收费站出口都将实现"无感支付"!又是一项车牌识别系统的应用. 未来将实现不停车不掏手机 刷一下车牌, 自动抬杆就走人~ 超方便! 什么是"无感支付"? "无感支付"就是采用"车牌识别+手机APP"的模式,实现车主自助绑定车牌和支付方式,出口车道无干预地完成通行费快速支付,车辆不停车即可成功付费的互联网支付产品"ITC",是传统ETC业务的升级产品. 由于目前仅有

【基于WPF+OneNote+Oracle的中文图片识别系统阶段总结】之篇三:批量处理后的txt文件入库处理

篇一:WPF常用知识以及本项目设计总结:http://www.cnblogs.com/baiboy/p/wpf.html 篇二:基于OneNote难点突破和批量识别:http://www.cnblogs.com/baiboy/p/wpf1.html 篇三:批量处理后的txt文件入库处理:http://www.cnblogs.com/baiboy/p/wpf2.html 篇四:关于OneNote入库处理以及审核:http://www.cnblogs.com/baiboy/p/wpf3.html [

车牌识别系统如何实现缩短停车时间?

当车牌识别系统被作为进程列入事件检测系统时,车辆探测器或交通执法系统一旦检测到事件发生,系统的相机就会启动并获得的图像数据,然后把图像数据发送到车牌识别系统,把车牌区域从图像中提取出来.如果无法找到车牌区域,则等待另一输入图像.然而,有时检测到多个车牌区域,它们在车牌识别阶段被仔细检查,对所有的候选车牌执行蒙板判别程序,确定最终的车牌区域,在确定车牌区域之后,还需要进行两个步骤--字符分割和字符识别. 这两项任务需要交替引用才能获得分割与识别最佳的效果.在这阶段,从候选车牌中恢复的字符将在确认核

使用opencv的SVM和神经网络实现车牌识别

一.前言 本文参考自<深入理解Opencv 实用计算机视觉项目解析>中的自动车牌识别项目,并对其中的方法理解后,再进行实践.深刻认识到实际上要完成车牌区域准确定位.车牌区域中字符的准确分割,字符准确识别这一系列步骤的困难.所以最后的识别效果也是有待进一步提高. 二.程序流程 程序流程如下所示: 相应的main函数如下 #include "carID_Detection.h" int main() { Mat img_input = imread("testCarI