基于OpenCV+HOG特征提取+KNN分类算法的简易车牌识别程序
项目介绍
本项目是基于OpenCV+HOG特征提取+KNN分类算法的车牌识别项目,暂时只能识别蓝牌,其实也能够识别绿牌、黄牌,留给大家发挥~ 本程序的识别速度、准确率不像gitHub中的EasyPR等开源的车牌识别项目那么高,但是也是能识别一些清晰、背景不是特别复杂的车牌的。最重要的是代码相对比较简单~可以作为入门
背景介绍
本项目仅是个人一点经验的分享,希望可以帮助苦苦寻找课设资料的同学,并不是什么很高大上的项目,技术浅薄,希望大家可以多提点意见,一起学习共同进步。
学校的综合课程设计中选了基于OpenCV、机器学习的OpenCV项目,虽然以前模式识别也做过车牌识别课程作业,但是那时基本就是在网上抄一抄改一点代码完事,也基本没有鲁棒性可言只能识别那一张车牌的图片。内心想着,自己也拿到大offer了,是时候认真做一下这个烂大街却没有认真做过的项目了。
车牌识别
想识别车牌基本需要分成以下几个步骤:
- 寻找车牌位置
- 分割车牌的文字
- 提取文字特征
- 通过机器学习的方法进行文字识别
项目环境
OpenCV4.1.0 + Qt5.14.2 + msvc2015
车牌位置的识别
想要识别车牌的位置有不少方法,最简单的就是通过基础图像处理的手段(边缘检测、腐蚀膨胀等操作)去识别车牌位置,效果好一点的就可以通过HSV颜色空间来进行车牌的判断,我个人的方法是结合上述两者来寻找车牌位置。大概思路就是降维→去噪→边缘检测→腐蚀膨胀→findContours(寻找连通区域)→转换为HSV颜色空间进一步检测车牌位置
Mat srcImg, grayImg, bulrImg, binaryImg, cannyImg;
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);
cvtColor(srcImg, grayImg, COLOR_BGR2GRAY);
if(grayImg.empty())
{
cout << "convert gray failed" << endl;
return -1;
}
imshow("gray",grayImg);
GaussianBlur(grayImg, bulrImg, Size(3,3), 0.7);
if(bulrImg.empty())
{
cout << "blur failed" << endl;
return -1;
}
imshow("blur",bulrImg);
Canny(bulrImg, cannyImg, 500, 200, 3);
if(cannyImg.empty())
{
cout << "canny failed" << endl;
return -1;
}
imshow("canny",cannyImg);
Mat dilateImg, erodeImg;
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);
erode(dilateImg, erodeImg, elementY, point, 1);
dilate(erodeImg, dilateImg, elementY, point, 2);
if(dilateImg.empty())
{
cout << "dilate failed" << endl;
return -1;
}
imshow("dilate",dilateImg);
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;
}
Mat tmp = srcImg.clone();
rectangle(tmp,targetRect,Scalar(255,255,0));
if(tmp.empty())
{
cout << "contour failed" << endl;
return -1;
}
imshow("contour",tmp);
Mat targetImg = srcImg(targetRect);
if(targetImg.empty())
{
cout << "targetImg failed" << endl;
waitKey();
return -1;
}
imshow("targetImg",targetImg);
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);
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、原图
2、灰度图 3、去噪 4、基于canny算子的边缘检测 5、通过腐蚀、膨胀处理获取大概的矩形区域 6、通过findContours游程法获取连通区域,并根据长宽比例识别车牌区域 7、截获出大概的车牌区域 8、通过转换为HSV色彩空间+inRange获取符合颜色的区域 看到这里可能有小伙伴已经发现了,本程序暂时只能识别汽车蓝牌,当然其实绿牌、黄牌也能识别,只要在inRange处加一点if-else即可,留给大家自己发挥~
分割车牌的文字
分割车牌中的文字其实跟识别车牌位置大同小异,甚至更加简单 大概思路: 首先将截获的车牌进行灰度化处理,然后进行二值化处理,二值化后理论上大概的字符轮廓就已经呈现出来了,先进行findContours将height最大的连通区域找出来,其中要排除太贴近左边缘和右边缘并且width太窄的连通区域,然后再找出连通区域中比height最大的连通区域*0.8要大的连通区域,并且用boundingRect将每个字符都圈出来。大部分情况下这样只能找到6个非中文字符,然后我们再找出最左侧的三个联通区域并且根据左二、左三区域的间隔与左一区域的坐标找出中文字符的区域。最后对7个字符的x坐标排序,就可以截取出7个字符了。
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);
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)
{
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特征。
HOG特征提取
来到提取特征的步骤了,其实进行文字识别也有很多方法,我在网上看了几篇文章,有不少都是不提取特征,直接将文字像素值reshape成一维数据用KNN或者SVM进行分类训练与识别。但是这样无论是识别率还是鲁棒性都非常差,经过我的实践可以说这种方法完全不能用于识别车牌,甚至不如模板匹配!因此必须进行特征提取,而特征提取又分为很多种,我选择HOG是因为HOG的原理简单、计算不复杂、效果也很不错。 简单来说,HOG就是将图片分为一个个小的cells并且计算统计出其梯度(纹理方向)来作为一个物体的特征。 具体HOG的原理希望大家能认真研究一下,不要只会调用api,可以参考这篇文章Histogram of Oriented Gridients(HOG) 方向梯度直方图
HOGDescriptor *hog = new HOGDescriptor(Size(40, 32),Size(16,16),Size(8,8),Size(8,8),9);
vector<float> tmp;
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,这就是我们的训练集 2、然后通过C++文件遍历的方法(具体可参考这篇文章C++ 中利用 _findfirst遍历所有文件夹及文件,以及findnext win10报错解决办法)结合HOG特征提取,将刚才我们准备好的训练集的特征都提取出来,其中labelMap是根据文件夹名称生成的一个标签对应的哈希表,trainDataMat就是最终提取出来的所有特征——训练集,labelsMat就是对应的标签集
#include <iostream>
#include <string>
#include <vector>
#include <io.h>
#include <ml.hpp>
#include <highgui.hpp>
#include <opencv.hpp>
#include <core.hpp>
#include <imgproc.hpp>
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
{
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->compute(src,tmp,Size(1,1),Size(0,0));
Mat trainDataTmpMat(1,tmp.size(),CV_32FC1,tmp.data());
trainDataMat.push_back(trainDataTmpMat);
labelsMat.push_back(classNum);
cout << trainNum << endl;
++trainNum;
}
} while(_findnext(handle,&file) == 0);
_findclose(handle);
cout << classNum << endl;
++classNum;
return ;
}
int main()
{
HOGDescriptor *hog = new HOGDescriptor(Size(40, 32),Size(16,16),Size(8,8),Size(8,8),9);
int classNum = 33;
vector<string> labelMap;
int totalNum = 330;
int imgCols = 40;
int imgRows = 32;
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;
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 <iostream>
#include <core.hpp>
#include <highgui.hpp>
#include <imgproc.hpp>
#include <opencv.hpp>
#include <ml.hpp>
#include <vector>
using namespace std;
using namespace cv;
using namespace ml;
class MyKNN
{
public:
MyKNN()
{
hog = new HOGDescriptor(Size(40, 32),Size(16,16),Size(8,8),Size(8,8),9);
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"};
};
然后我们就可以在主识别程序最后加入识别七个字符的代码啦~
vector<string> ans;
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;
车牌识别测试
总结
第一次写博客,有写得不好的地方希望大家多多提意见,多多包涵。 这个项目的从0到1确实不容易,通了几晚宵,主要还是因为自己知识浅薄,当然这个过程中真的学到了很多,对HOG、KNN、SVM的理解更加深入了,对各种图像处理的操作理解更加具体了。也希望写这篇博客帮助完成课设的同学、帮助想入门OpenCV项目的同学,希望大家多多留言积极点赞,共同学习,一起进步~ 祝大家圣诞节快乐,新年快乐~
参考
《OpenCV3编程入门》 ——毛星云
Histogram of Oriented Gridients(HOG) 方向梯度直方图
opencv学习笔记(七)SVM+HOG
教你用OpenCV实现机器学习最简单的k-NN算法
opencv——基于KNN的数字识别
使用opencv进行车牌提取及识别
|