基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序

基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序

  • 项目介绍
  • 背景介绍
  • 车牌识别
    • 项目环境
    • 车牌位置的识别
    • 分割车牌的文字
    • HOG特征提取
    • KNN训练
    • KNN识别
  • 车牌识别测试
  • 总结
  • 参考

项目介绍

本项目是基于OpenCV+HOG特征提取+KNN分类算法的车牌识别项目,暂时只能识别蓝牌,其实也能够识别绿牌、黄牌,留给大家发挥~
本程序的识别速度、准确率不像gitHub中的EasyPR等开源的车牌识别项目那么高,但是也是能识别一些清晰、背景不是特别复杂的车牌的。最重要的是代码相对比较简单~可以作为入门

背景介绍

本项目仅是个人一点经验的分享,希望可以帮助苦苦寻找课设资料的同学,并不是什么很高大上的项目,技术浅薄,希望大家可以多提点意见,一起学习共同进步。

学校的综合课程设计中选了基于OpenCV、机器学习的OpenCV项目,虽然以前模式识别也做过车牌识别课程作业,但是那时基本就是在网上抄一抄改一点代码完事,也基本没有鲁棒性可言只能识别那一张车牌的图片。内心想着,自己也拿到大offer了,是时候认真做一下这个烂大街却没有认真做过的项目了。

车牌识别

想识别车牌基本需要分成以下几个步骤:

  1. 寻找车牌位置
  2. 分割车牌的文字
  3. 提取文字特征
  4. 通过机器学习的方法进行文字识别

项目环境

OpenCV4.1.0 + Qt5.14.2 + msvc2015

车牌位置的识别

想要识别车牌的位置有不少方法,最简单的就是通过基础图像处理的手段(边缘检测、腐蚀膨胀等操作)去识别车牌位置,效果好一点的就可以通过HSV颜色空间来进行车牌的判断,我个人的方法是结合上述两者来寻找车牌位置。大概思路就是降维→去噪→边缘检测→腐蚀膨胀→findContours(寻找连通区域)→转换为HSV颜色空间进一步检测车牌位置

    Mat srcImg, grayImg, bulrImg, binaryImg, cannyImg;
    //1、读入并显示原图
    string car = "car26";
    srcImg = imread("C:\\Users\\RR\\Desktop\\car\\" + car + ".jpg");
    if(srcImg.empty())
    {
        cout << "open srcimg failed" << endl;
        return -1;
    }
    imshow("src",srcImg);

    //2、转化为灰度图
    cvtColor(srcImg, grayImg, COLOR_BGR2GRAY);
    if(grayImg.empty())
    {
        cout << "convert gray failed" << endl;
        return -1;
    }
    imshow("gray",grayImg);

    //3、高斯滤波去噪
    GaussianBlur(grayImg, bulrImg, Size(3,3), 0.7);
    if(bulrImg.empty())
    {
        cout << "blur failed" << endl;
        return -1;
    }
    imshow("blur",bulrImg);

    //4、canny算子提取边界
    Canny(bulrImg, cannyImg, 500, 200, 3);
    if(cannyImg.empty())
    {
        cout << "canny failed" << endl;
        return -1;
    }
    imshow("canny",cannyImg);

    //5、利用膨胀、腐蚀获取车牌大概的位置的mask
    //图片膨胀处理
    Mat dilateImg, erodeImg;
    //自定义 核进行 x 方向的膨胀腐蚀
    Mat elementX = getStructuringElement(MORPH_RECT, Size(25, 1));
    Mat elementY = getStructuringElement(MORPH_RECT, Size(1, 19));
    Point point(-1, -1);
    dilate(cannyImg, dilateImg, elementX, point, 2);
    erode(dilateImg, erodeImg, elementX, point, 3);
    dilate(erodeImg, dilateImg, elementX, point, 2);
    //自定义 核进行 Y 方向的膨胀腐蚀
    erode(dilateImg, erodeImg, elementY, point, 1);
    dilate(erodeImg, dilateImg, elementY, point, 2);
    if(dilateImg.empty())
    {
        cout << "dilate failed" << endl;
        return -1;
    }
    imshow("dilate",dilateImg);

    //6、通过findContours找出最大的mask
    Mat contourImg;
    contourImg = dilateImg.clone();
    vector<vector<Point>> contours;
    findContours(contourImg,contours,RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
    vector<Point> rectPoint;
    Rect targetRect;
    for(int i=0;i<(int)contours.size();++i)
    {
        Rect r = boundingRect(contours[i]);
        if(r.width>r.height*2 && r.width>targetRect.width)
            targetRect = r;
    }
    //此处必须深拷贝一个用于显示rect区域,在原图上修改会影响后面的分割
    Mat tmp = srcImg.clone();
    rectangle(tmp,targetRect,Scalar(255,255,0));
    if(tmp.empty())
    {
        cout << "contour failed" << endl;
        return -1;
    }
    imshow("contour",tmp);

    //7、提取出大概的车牌位置
    Mat targetImg = srcImg(targetRect);
    if(targetImg.empty())
    {
        cout << "targetImg failed" << endl;
        waitKey();
        return -1;
    }
    imshow("targetImg",targetImg);

    //8、将大概位置转化为HSV,通过inrange、膨胀操作来提取出蓝色车牌的位置的mask
    Mat hsv,target;
    cvtColor(targetImg,hsv,COLOR_BGR2HSV);
    //此处阈值可以再调整一下
    inRange(hsv,Scalar(100,100,100),Scalar(124,255,255),target);
    Mat element = getStructuringElement(MORPH_RECT,Size(3,3));
    dilate(target,target,element);


    //9、通过findContours找出最大的mask,提取出车牌的位置
    vector<vector<Point>> targetContours;
    findContours(target,targetContours,RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
    Rect maxRect;
    for(int i=0;i<(int)targetContours.size();++i)
    {
        Rect r = boundingRect(targetContours[i]);
        if(r.width > maxRect.width)
            maxRect = r;
    }
    Mat carLicense = targetImg(maxRect);  //彩色
    if(carLicense.empty())
    {
        cout << "carLicense failed" << endl;
        waitKey();
        return -1;
    }
    imshow("carLicense",carLicense);

1、原图基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第1张图片

基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第2张图片
2、灰度图
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第3张图片
3、去噪
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第4张图片
4、基于canny算子的边缘检测
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第5张图片
5、通过腐蚀、膨胀处理获取大概的矩形区域
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第6张图片
6、通过findContours游程法获取连通区域,并根据长宽比例识别车牌区域
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第7张图片
7、截获出大概的车牌区域
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第8张图片
8、通过转换为HSV色彩空间+inRange获取符合颜色的区域
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第9张图片
看到这里可能有小伙伴已经发现了,本程序暂时只能识别汽车蓝牌,当然其实绿牌、黄牌也能识别,只要在inRange处加一点if-else即可,留给大家自己发挥~

分割车牌的文字

分割车牌中的文字其实跟识别车牌位置大同小异,甚至更加简单
大概思路:
首先将截获的车牌进行灰度化处理,然后进行二值化处理,二值化后理论上大概的字符轮廓就已经呈现出来了,先进行findContours将height最大的连通区域找出来,其中要排除太贴近左边缘和右边缘并且width太窄的连通区域,然后再找出连通区域中比height最大的连通区域*0.8要大的连通区域,并且用boundingRect将每个字符都圈出来。大部分情况下这样只能找到6个非中文字符,然后我们再找出最左侧的三个联通区域并且根据左二、左三区域的间隔与左一区域的坐标找出中文字符的区域。最后对7个字符的x坐标排序,就可以截取出7个字符了。

    //10、分隔字符
    Mat grayCarLincese;
    //二值化
    cvtColor(carLicense,grayCarLincese,COLOR_BGR2GRAY); //灰度图
    threshold(grayCarLincese,binaryImg,0,255,THRESH_OTSU);
    if(binaryImg.empty())
    {
        cout << "threshold failed" << endl;
        return -1;
    }
    imshow("threshold",binaryImg);

    //提取连通区域
    vector<vector<Point>> splitContours;
    findContours(binaryImg,splitContours,RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
    //找最大 && 结合一点字符区域的判断
    Rect maxSplitRect;
    Mat showAllCarLicense = carLicense.clone();
    for(int i=0;i<(int)splitContours.size();++i)
    {
        Rect r = boundingRect(splitContours[i]);
        rectangle(showAllCarLicense,r,Scalar(0,0,255));
        if(!(r.y <= 3 && r.y+r.height >= carLicense.rows-4) && r.x > 5 && !(r.x+r.width >= carLicense.cols - 4 && r.x >= carLicense.cols - 3) && (double)r.width < (double)carLicense.cols/7.0 && (double)r.height > (double)r.width*1.5 && r.height > maxSplitRect.height)
            maxSplitRect = r;
    }
    imshow("showAllCarLicense",showAllCarLicense);

    //如果连通区域小于6即识别失败,6个非中文字符
    if(splitContours.size() < 6)
    {
        cout << "check lisence position failed" << endl;
        waitKey();
        return -1;
    }

    //字符方块
    vector<Rect> wordsRects;
    //找最左侧三个
    vector<Rect> leftSplitRects(3);
    leftSplitRects[0] = maxSplitRect;
    leftSplitRects[0].x = carLicense.cols - leftSplitRects[0].width;
    leftSplitRects[1] = leftSplitRects[0];
    leftSplitRects[2] = leftSplitRects[0];
    for(int i=0;i<(int)splitContours.size();++i)
    {
        Rect r = boundingRect(splitContours[i]);
        if(!(r.y <= 3 && r.y+r.height >= carLicense.rows-4) && r.x > 5 && !(r.x+r.width >= carLicense.cols - 3 && r.x >= carLicense.cols - 4) && (double)r.width < (double)carLicense.cols/7.0 && (double)r.height > (double)r.width*1.5 && (double)r.height>=(double)maxSplitRect.height*0.8) //&& (double)r.width>=(double)maxSplitRect.width*0.8
        {
            wordsRects.push_back(r);
            rectangle(carLicense,r,Scalar(255,255));
            if(r.x < leftSplitRects[0].x)
            {
                leftSplitRects[2] = leftSplitRects[1];
                leftSplitRects[1] = leftSplitRects[0];
                leftSplitRects[0] = r;
            }
            else if(r.x < leftSplitRects[1].x)
            {
                leftSplitRects[2] = leftSplitRects[1];
                leftSplitRects[1] = r;
            }
            else if(r.x < leftSplitRects[2].x)
                leftSplitRects[2] = r;
        }
    }


    if(wordsRects.size() < 7)
    {
        //找汉字
        int deltaX = leftSplitRects[2].x - leftSplitRects[1].x;
        deltaX += 2;

        Rect leftRect = leftSplitRects[0];
        if(leftRect.x >= deltaX+1)
            leftRect.x -= (deltaX+1);
        else
            leftRect.x = 0;

        if(leftRect.y >= 1)
            leftRect.y -= 1;
        else
            leftRect.y = 0;

        if(leftRect.y + leftRect.height + 2 < carLicense.rows)
            leftRect.height += 2;

        if(leftRect.width+5 < leftSplitRects[0].x)
            leftRect.width += 5;
        else
            leftRect.width += leftSplitRects[0].x - leftRect.width;

        wordsRects.push_back(leftRect);
        rectangle(carLicense,leftRect,Scalar(255,255));
    }

    imshow("carLicense",carLicense);

    if(wordsRects.size() != 7)
    {
        cout << "check lisence position failed" << endl;
        waitKey();
        return -1;
    }

    //一个个分割出来
    sort(wordsRects.begin(),wordsRects.end(),[](Rect &r1,Rect &r2){ return r1.x < r2.x; });
    vector<Mat> licenses((int)wordsRects.size());
    for(int i=0;i<(int)wordsRects.size();++i)
    {
        licenses[i] = binaryImg(wordsRects[i]);
        Mat tmp;
        resize(licenses[i],tmp,Size(40,85));
        copyMakeBorder(tmp,tmp,5,5,5,5,BORDER_CONSTANT,Scalar(0,0,0));
        resize(tmp,tmp,Size(40,32));
        licenses[i] = tmp;
        imshow("license"+to_string(i+1),tmp);
    }

PS:最后要注意一下,在截取出字符的时候我进行了resize跟makeBorder的处理,一方面因为之后的HOG特征提取需要特定的尺寸,另一方面如果不makeBorder会丢失一些HOG特征。
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第10张图片
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第11张图片
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第12张图片
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第13张图片
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第14张图片
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第15张图片
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第16张图片
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第17张图片
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第18张图片

HOG特征提取

来到提取特征的步骤了,其实进行文字识别也有很多方法,我在网上看了几篇文章,有不少都是不提取特征,直接将文字像素值reshape成一维数据用KNN或者SVM进行分类训练与识别。但是这样无论是识别率还是鲁棒性都非常差,经过我的实践可以说这种方法完全不能用于识别车牌,甚至不如模板匹配!因此必须进行特征提取,而特征提取又分为很多种,我选择HOG是因为HOG的原理简单、计算不复杂、效果也很不错。
简单来说,HOG就是将图片分为一个个小的cells并且计算统计出其梯度(纹理方向)来作为一个物体的特征。
具体HOG的原理希望大家能认真研究一下,不要只会调用api,可以参考这篇文章Histogram of Oriented Gridients(HOG) 方向梯度直方图

    //Hog特征提取类
    HOGDescriptor *hog = new HOGDescriptor(Size(40, 32),Size(16,16),Size(8,8),Size(8,8),9);
    vector<float> tmp;
    //提取HOG!!!
    hog->compute(src,tmp,Size(1,1),Size(0,0));

这里的tmp就是提取出来的特征,转换为Mat后即可作为特征输入到KNN中进行训练。具体OpenCV的HOG的api的各个参数就不多解释了,可以自行百度谷歌。不理解的同学就按我的Size设置即可。

KNN训练

首先说一下KNN,可以说是机器学习中最简单的算法了,当初数据挖掘的期末考可是要手撕的。可以看一下这篇文章教你用OpenCV实现机器学习最简单的k-NN算法,只能说有手就行~
简单来说就是把数据映射到一个空间中并且按照一定的规则(如欧氏距离)以及K值(搜索多少个邻近数据),然后统计出K个邻近数据中最多的标签就是预测结果。

1、首先准备好足够的图片,然后我们需要用之前的分割字符的方法分割出足够的字符并且手动分类好,最好每类的是同样的个数,我这里是33类,没类10个数据,多多益善,记得resize与makeBorder,这就是我们的训练集
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第19张图片
2、然后通过C++文件遍历的方法(具体可参考这篇文章C++ 中利用 _findfirst遍历所有文件夹及文件,以及findnext win10报错解决办法)结合HOG特征提取,将刚才我们准备好的训练集的特征都提取出来,其中labelMap是根据文件夹名称生成的一个标签对应的哈希表,trainDataMat就是最终提取出来的所有特征——训练集,labelsMat就是对应的标签集

#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 
#include 

using namespace std;
using namespace cv;
using namespace ml;

void findFile(string path,string mode,vector<string> &labelMap,Mat &trainDataMat,Mat &labelsMat,HOGDescriptor *hog)
{
    static int trainNum = 0;
    static int classNum = 0;
    _finddata_t file;
    intptr_t handle;
    string onePath = path + mode;
    handle = _findfirst(onePath.c_str(),&file);

    if(handle == -1LL)
    {
        cout << "no path" << endl;
        return ;
    }

    do
    {
        if(file.attrib & _A_SUBDIR)
        {

            if ((strcmp(file.name, ".") != 0) && (strcmp(file.name, "..") != 0))
            {
                string newPath = path +"\\" + file.name;
                labelMap.push_back(file.name);
                findFile(newPath,mode,labelMap,trainDataMat,labelsMat,hog);
            }
        }
        else
        {
//            cout << file.name << endl;
            string imgName = path + "//" + file.name;
            Mat src = imread(imgName, 0);
            if(src.empty())
            {
                cout << "img error!!!" << endl;
                exit(-1);
            }
            threshold(src,src,0,255,THRESH_OTSU);

            vector<float> tmp;
            //提取HOG!!!
            hog->compute(src,tmp,Size(1,1),Size(0,0));
            Mat trainDataTmpMat(1,tmp.size(),CV_32FC1,tmp.data());
            trainDataMat.push_back(trainDataTmpMat);
//            cout << "dims:" << trainData[trainNum].size() << endl;


            labelsMat.push_back(classNum);
            cout << trainNum << endl;
            ++trainNum;
        }
    } while(_findnext(handle,&file) == 0);
    _findclose(handle);

    cout << classNum << endl;
    ++classNum;
    return ;
}

int main()
{
    //Hog
    HOGDescriptor *hog = new HOGDescriptor(Size(40, 32),Size(16,16),Size(8,8),Size(8,8),9);

    //样本类型总数
    int classNum = 33;
    //样本标签
    vector<string> labelMap;//{"0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","gui","J","K","L","lu","min","N","P","Q","R","S","su","V","X","Y","yue"};
    //样本总数
    int totalNum = 330;
    //样本尺寸
    int imgCols = 40;   //WIDTH
    int imgRows = 32;   //HEIGHT
    int imgSize = imgRows * imgCols;

    //训练集
    Mat trainDataMat(0,0,CV_32FC1);
    //标签集
    Mat labelsMat(0,1,CV_32FC1);
    //读取训练集标签集数据
    string path = "C:\\Users\\RR\\Desktop\\car_chars";
    string mode = "\\*";
    findFile(path,mode,labelMap,trainDataMat,labelsMat,hog);

    cout << trainDataMat.rows << " " << trainDataMat.cols << endl;
    cout << labelsMat.rows << " " << labelsMat.cols << endl;

    //knn
    Ptr<KNearest> model = KNearest::create();
    model->setDefaultK(10);
    model->setIsClassifier(true);
    Ptr<TrainData> pTrainData = TrainData::create(trainDataMat, ROW_SAMPLE, labelsMat);
    model->train(pTrainData);

    cout << "trian finish" << endl;

    model->save("C:\\Users\\RR\\Desktop\\car_train_result\\car_train_result.xml");
    return 0;
}

最后这里我们可以调用save函数把训练结果保存下来,方便主识别程序使用。
KNN调用OpenCV的api很容易可以实现,但是希望大家还是可以好好理解一下原理,毕竟真的很简单。然后需要注意的是setDefalutK,K值我们说了就是找K个邻近的数据,这个K推荐是每类数据的个数。

KNN识别

终于到最后一步了,非常开心哈哈哈,万事俱备只欠东风,首先我们需要在主测试程序中写一个用于读取训练结果、识别文字的MyKNN类。

#include 

#include 
#include 
#include 
#include 
#include 

#include 

using namespace std;
using namespace cv;
using namespace ml;

class MyKNN
{
public:
    MyKNN()
    {
        //HOG
        hog = new HOGDescriptor(Size(40, 32),Size(16,16),Size(8,8),Size(8,8),9);
        //读取knn
        string path = "C:\\Users\\RR\\Desktop\\car_train_result\\car_train_result.xml";
        model = StatModel::load<KNearest>(path);
        model->setDefaultK(10);
    }

    ~MyKNN()
    {
		delete hog;
    }

    string predict(Mat predictChar)
    {
        threshold(predictChar,predictChar,0,255,THRESH_OTSU);
        vector<float> predictFeature;
        hog->compute(predictChar,predictFeature,Size(1,1),Size(0,0));
        Mat sampleMat(1,predictFeature.size(),CV_32FC1,predictFeature.data());
        int res = model->predict(sampleMat);
        return labelMap[res];
    }

private:
    HOGDescriptor *hog;
    Ptr<KNearest> model;
    vector<string> labelMap{"0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","gui","J","K","L","lu","min","N","P","Q","R","S","su","V","X","Y","yue"};
};

然后我们就可以在主识别程序最后加入识别七个字符的代码啦~

    //kNN机器学习
    vector<string> ans;
    //创建myKNN对象
    MyKNN *myKNN = new MyKNN;
    for(int i=0;i<(int)licenses.size();++i)
        ans.push_back(myKNN->predict(licenses[i]));

    //输出结果
    for(int i=0;i<(int)ans.size();++i)
        cout << ans[i] << " ";
    cout << endl;

    waitKey(0);
    return 0;

基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第20张图片

车牌识别测试

基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第21张图片
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第22张图片
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第23张图片
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第24张图片
基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序_第25张图片

总结

第一次写博客,有写得不好的地方希望大家多多提意见,多多包涵。
这个项目的从0到1确实不容易,通了几晚宵,主要还是因为自己知识浅薄,当然这个过程中真的学到了很多,对HOG、KNN、SVM的理解更加深入了,对各种图像处理的操作理解更加具体了。也希望写这篇博客帮助完成课设的同学、帮助想入门OpenCV项目的同学,希望大家多多留言积极点赞,共同学习,一起进步~
祝大家圣诞节快乐,新年快乐~

参考

《OpenCV3编程入门》 ——毛星云

Histogram of Oriented Gridients(HOG) 方向梯度直方图

opencv学习笔记(七)SVM+HOG

教你用OpenCV实现机器学习最简单的k-NN算法

opencv——基于KNN的数字识别

使用opencv进行车牌提取及识别

你可能感兴趣的:(OpenCV,C++,个人项目,opencv,计算机视觉,c++,机器学习,人工智能)