@Aurora-xi
2019-07-12T04:10:52.000000Z
字数 13183
阅读 328
图像处理 车牌识别 数字识别 KNN算法
车牌识别的过程主要包括图像数据读入、图像灰度转换、边缘检测、腐蚀处理、平滑处理、移除小对象、确定车牌位置、定位剪切、车牌图像二值化、均值滤波,膨胀或腐蚀处理、切割、分离字符几部分。
要实现车牌识别,首先要从计算机中读取含有要识别车牌的图片,我们通过imread函数将图像数据读出,赋值给变量I,从而实现图像数据的读入。
I=imread('D:\学习\数字图像处理\MATLAB代码\test1.jpg');
figure(1);
subplot(2,2,1),imshow(I);title('原图');

这里使用了rgb2gray函数进行图像的灰度转换,该函数接收一个rgb图像变量作为参数,返回该图像转换为灰度图后的图像数据。
若想观察该灰度图的灰度分布情况,可使用imhist函数画出该灰度图的灰度值分布直方图。
I1=rgb2gray(I); %转换为灰度图像
subplot(2,2,2),imshow(I1);title('灰度图');
subplot(2,2,3),imhist(I1);title('灰度图直方图');%观察灰度图的灰度分布情况

在将彩色图转换为灰度图后,便可用edge函数识别该图像的边界,edge函数通过使用一阶导数和二阶导数检测亮度的不连续来确定图像的边界,它可以使用Sobel,Prewitt,Roberts,Canny,LoG,零交叉等多种算子。
I2=edge(I1,'canny',0.08,'both');
subplot(2,2,4),imshow(I2);title('canny算子边缘检测')

由于边缘检测后的图像中无关结构太多,这里需对图像进行腐蚀处理,也就是在二值图像的基础上收缩、细化。
实现腐蚀处理的函数为imerode,它接收一个图像数据和一个结构子,图像中背景与结构子完全重合的像素点输出值为1,不完全重合的和完全不重合的像素点输出值为0,最后返回使用该结构子腐蚀过后的图像数据,以此实现削减无关结构的目的。
se=[1;1;1]; %线型结构元素
I3=imerode(I2,se); %腐蚀

腐蚀后的图像结构大多呈分散状分布,不连贯。为了方便之后确认车牌位置,这里需对该图像进行平滑处理,在此我们使用闭操作使车牌平滑,并减小噪音,闭操作可以理解为先膨胀后腐蚀,实现函数为imclose。
se=strel('rectangle',[40,40]);%生成一个矩阵 线型结构元素
I4=imclose(I3,se); %闭运算,先膨胀后腐蚀

为了使接下来车牌位置的确定更精确,这里使用了bwareaopen函数从图象中删除小面积对象。
I5=bwareaopen(I4,2000,8);%删除小面积对象
subplot(2,2,3),imshow(I5);title('从图象中移除小对象');
subplot(2,2,4),imhist(I5);title('灰度图直方图');

在经过上面的处理之后,最初要识别的彩色图像已经变成了以车牌为主要结构的二值图像,我们可以对这种主体结构清晰的二值图像进行扫描,进而确定出车牌的位置。
这里确定车牌位置的思路为:
以下为确定车牌位置的具体代码
[y,x]=size(I5); %得到该图像矩阵的行数y和列数x
myI=double(I5);
white_y=zeros(y,1);
for i=1:y %遍历每一行的像素点
for j=1:x
if(myI(i,j,1)==1)
white_y(i,1)= white_y(i,1)+1;%白色像素点统计
end
end
end
%认为白色像素最多的行为车牌中心行
[temp,MaxY]=max(white_y);%temp为向量white_y的元素中的最大值,MaxY为对应的行
PY1=MaxY;
while ((white_y(PY1,1)>=120)&&(PY1>1))
PY1=PY1-1; %找车牌最上端
end
PY2=MaxY;
while ((white_y(PY2,1)>=40)&&(PY2<y))
PY2=PY2+1; %找车牌最下端
end
%同样方式纵向扫描
white_x=zeros(1,x);%进一步确定x方向的车牌区域
for j=1:x
for i=PY1:PY2
if(myI(i,j,1)==1)
white_x(1,j)= white_x(1,j)+1;
end
end
end
PX1=1; % 找车牌X方向最小值,即车牌最左端
while ((white_x(1,PX1)<3)&&(PX1<x))
PX1=PX1+1;
end
PX2=x; % 找车牌X方向最大值,即车牌最右端
while ((white_x(1,PX2)<3)&&(PX2>PX1))
PX2=PX2-1;
end
通过以上定位,我们得到了车牌的上边界PY1,下边界PY2,左边界PX1,右边界PX2,我们可以通过这四个边界值从原彩色图像中切割出车牌。
dw=I(PY1:PY2,PX1:PX2,:); %切割出车牌
figure(3),subplot(2,2,1),imshow(dw),title('定位剪切后的彩色车牌图像')

在得到车牌的彩色图像之后,为了便于将其中的字符分离,我们将它转换为二值图像。首先将彩色图像转换为灰度图像,再确定合适的阈值(此处根据资料,最佳阈值取灰度的最大值减去最大值与最下值的1/3梯度)
b=rgb2gray(a); %彩色图像转换为灰度图像
subplot(2,2,2),imshow(b),title('车牌灰度图像')
g_max=double(max(max(b)));
g_min=double(min(min(b))); %换成双精度数值
T=round(g_max-(g_max-g_min)/3); %T为二值化的阈值
d=(double(b)>=T); %d即二值图像
subplot(2,2,3),imshow(d),title('车牌二值图像')

在车牌转换为二值图像后,为了使图像中干扰元素减少,我们对其进行均值滤波,通过fspecial函数构造均值滤波器,然后使用filter2函数进行滤波,以减小图中噪音。
h=fspecial('average',3); %均值滤波器
d=im2bw(round(filter2(h,d)));
subplot(2,2,4),imshow(d),title('均值滤波后')

由于不同原始图像的差异,处理到这里后,字符可能会不连续,也可能会连在一起,这时我们需要再次对滤波后的图像进行腐蚀或者膨胀处理,这里使用判断结构以图中白色部分的面积为判断依据决定使用腐蚀或者膨胀。
se=eye(2); %
[m,n]=size(d);
if bwarea(d)/m/n>=0.365 %计算二值图像中白的对象占总面积
c=imerode(d,se); %imerode 实现图像腐蚀 d为待处理图像,se是结构元素对象
elseif bwarea(d)/m/n<=0.235
c=imdilate(d,se); %imdilate 图像膨胀
end

在分离字符之前,我们还要切去车牌边缘的黑色部分,在这里我们定义了一个用于切割的函数qiege,思路如下:
从图像顶部向底部逐行扫描,对扫描到的行求和,若某行全部像素点求和为0(即该行所有像素点均为黑色),则切去这一行,直到扫描到某一行求和后值不为零,以这一行为上边界。
同理,再用相同的方法从其他三个方向扫描,切割出下边界,左边界和·右边界,最后得到切去边缘黑色部分的图像。
function e=qiege(d)
[m,n]=size(d);
top=1;bottom=m;left=1;right=n;
while sum(d(top,:))==0 && top<=m %切割出白色区域(横切)
top=top+1;
end
while sum(d(bottom,:))==0 && bottom>1
bottom=bottom-1;
end
while sum(d(:,left))==0 && left<n %切割出白区域(纵切)
left=left+1;
end
while sum(d(:,right))==0 && right>=1
right=right-1;
end
dd=right-left;
hh=bottom-top;
e=imcrop(d,[left top dd hh]);
end

将切割过后,就可以对图片中的字符进行分离了,这里我们定义了一个用于分离字符的函数getword,思路如下:
对图像从左到右逐列扫描,并将每列中像素点的值进行求和,若和不为零(即这一列是组成某字符的一列),则向右继续扫描下一列,直到某一列求和后值为零(即这一列是两白色字符之间的黑色间隔),图像最左侧到这一列之间即为第一个字符,将其切出,然后将该区域内所有像素点赋值为0(涂黑),重复以上操作直至分离出所有字符。
function [word,result]=getword(d)
word=[];
flag=0;
y1=8;
y2=0.5;
while flag==0
[m,n]=size(d); % 求行列
wide=0;
while sum(d(:,wide+1))~=0 && wide<=n-2 %有白色加1知道没有白色,也就是找出一个白色区域
wide=wide+1;
end
temp=qiege(imcrop(d,[1 1 wide m])); %切出第一个字符
[m1,n1]=size(temp);
if wide<y1 && n1/m1>y2
d(:,[1:wide])=0; % 第一个涂黑
if sum(sum(d))~=0
d=qiege(d); % 切割出最小范围
else word=[];flag=1;
end
else
word=qiege(imcrop(d,[1 1 wide m]));
d(:,[1:wide])=0;
if sum(sum(d))~=0;
d=qiege(d);flag=1;
else d=[];
end
end
end
result=d;
end
d=im2bw(round(filter2(h,d)));
subplot(2,2,4),imshow(d),title('均值滤波后')

在车牌识别中,我们是通过对车牌的观察来确定的特征,只适用于一小部分图片,而想要提升准确度,具体确定是哪个字符,就需要用到机器学习的相关知识。
这里的数字识别我们采用KNN算法,来进行数字的特征识别。
KNN算法又称为K近邻分类(k-nearest neighbor classification)算法.
核心思想:给定测试样本,基于某种距离度量找出训练集中与其最靠近的K个训练样本,然后基于这K个相邻点的信息进行预测。
通常,在分类任务中可使用”投票法”,即将这K个样本中出现最多的类别标记作为预测结果;在回归任务中可使用“平均法”,即将这k个样本的实际值输入标记的平均值作为预测结果;还可以基于距离远近进行加权平均或者加权投票,距离越近的样本权重越大。
一个简单的例子:
如下图,绿色圆要被决定赋予哪个类,是红色三角形还是蓝色四方形?
如果K=3,由于红色三角形所占比例为2/3,绿色圆将被赋予红色三角形类。
如果K=5,由于蓝色四方形比例为3/5,因此绿色圆被赋予蓝色四方形类。
对KNN算法的思想就是在训练集中数据和标签已知的情况下,输入测试数据,将测试数据的特征与训练集中对应的特征进行相互比较,找到训练集中与之最为相似的前K个数据,则该测试数据对应的类别就是K个数据中出现次数最多的那个分类,其算法的步骤包括:
因此,KNN算法有以下特点
输入的训练数据是一张按顺序排列好的数字矩阵(打着格子写的23333)

首先对图片进行预处理,跟车牌图像的处理大同小异,包括灰度化,平滑化,去燥二值化,腐蚀膨胀等。
具体代码如下
Mat data, labels;
Mat src = imread("train.jpg");
Mat gray_frame, thres_img, blur_img;
cvtColor(src, gray_frame, COLOR_BGR2GRAY);//灰度图
//imshow("Out1", gray_frame);//输出灰度图像
灰度图:

GaussianBlur(gray_frame, blur_img, Size(3, 3), 3, 3);//高斯平滑
//imshow("Out2", blur_img);//输出高斯平滑后图像
高斯平滑后:

adaptiveThreshold(blur_img, thres_img, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY_INV, 151, 10);//二值化为黑底白字
//imshow("Out3", thres_img);//输出二值化后图像
二值化后图像:

Mat morph_img, tmp2, tmp3;
Mat kernerl = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));//获得结构元素
morphologyEx(thres_img, morph_img, MORPH_OPEN, kernerl, Point(-1, -1));//腐蚀膨胀
//imshow("Out4", morph_img);//输出腐蚀膨胀后图像
vector<vector<Point> > contours;
vector<Vec4i> hiearachy;
int k = 0;
腐蚀膨胀后图像:

之后获取结构元素,利用findContours识别出数字的外轮廓,画出矩形标记。
findContours(morph_img, contours, hiearachy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);//找轮廓
其实思路很简单,利用KNN算法,需要训练数据,训练,测试数据三部分。
训练部分,直接使用KNN算法进行训练,KNN算法的具体实现在OpenCV的库里已经有了,所以直接调用就行。
测试数据图片导入的处理过程和训练数据处理基本相同,不再赘述。每导入一个数字,进行识别,得出结论并写在图片上,最终一起输出。
主函数代码如下
int main()
{
Mat data, labels;
Mat src = imread("train.jpg");
Mat gray_frame, thres_img, blur_img;
cvtColor(src, gray_frame, COLOR_BGR2GRAY);//灰度图
GaussianBlur(gray_frame, blur_img, Size(3, 3), 3, 3);//高斯平滑
adaptiveThreshold(blur_img, thres_img, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY_INV, 151, 10);//二值化为黑底白字
Mat morph_img, tmp2, tmp3;
Mat kernerl = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));//获得结构元素
morphologyEx(thres_img, morph_img, MORPH_OPEN, kernerl, Point(-1, -1));//腐蚀膨胀
vector<vector<Point> > contours;
vector<Vec4i> hiearachy;
int k = 0;
findContours(morph_img, contours, hiearachy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);//找轮廓
vector<vector<Point> >::iterator It;
Rect rect[150];
int i=0;
for(It = contours.begin();It < contours.end();It++)
{
Point2f vertex[4];
rect[i] = boundingRect(*It);
vertex[0] = rect[i].tl();
vertex[1].x = (float)rect[i].tl().x, vertex[1].y = (float)rect[i].br().y;
vertex[2] = rect[i].br();
vertex[3].x = (float)rect[i].br().x, vertex[3].y = (float)rect[i].tl().y;
con[i].x = (vertex[0].x+vertex[1].x+vertex[2].x+vertex[3].x) / 4.0;
con[i].y = (vertex[0].y+vertex[1].y+vertex[2].y+vertex[3].y) / 4.0;
con[i].order = i;
i++;
}
sort(con,con+i,cmp);
for (int i = 0; i < contours.size(); i++)
{
Rect minrect = boundingRect(contours[con[i].order]);
double area = contourArea(contours[con[i].order]);
double ckbi = minrect.width / minrect.height;
if( ckbi<4&& area>400 )
{
rectangle(src, minrect, Scalar(0, 255, 0), 1, 8);//标记
Rect ROI = minrect;
Mat ROI_img = morph_img(ROI);
resize(ROI_img, ROI_img, Size(20, 20));
ROI_img.copyTo(tmp2);
stringstream stream;
stream << k;
string str;
stream >> str;
putText(src, str, ROI.tl(), FONT_HERSHEY_PLAIN, 1, Scalar(255, 0, 0), 1, 8);
data.push_back(tmp2.reshape(0, 1));
labels.push_back(k / 10);
k++;
}
}
//imshow("Out", src);
data.convertTo(data, CV_32F);
int samplesNum = data.rows;
int trainNum = 100;
Mat trainData, trainLabels;
trainData = data(Range(0, trainNum), Range::all());
trainLabels = labels(Range(0, trainNum), Range::all());
int K = 7;
Ptr<TrainData> tData = TrainData::create(trainData, ROW_SAMPLE, trainLabels);
Ptr<KNearest> model = KNearest::create();
model->setDefaultK(K);
model->setIsClassifier(true);
model->train(tData);
Mat src_test = imread("test.jpg");
//imshow("Input img", src_test);
Mat gray_test, thres_test, blur_test;
cvtColor(src_test, gray_test, COLOR_BGR2GRAY);
GaussianBlur(gray_test, blur_test, Size(3, 3), 3, 3);
adaptiveThreshold(blur_test, thres_test, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY_INV, 151, 10);
Mat morph_test, predict_mat;
morphologyEx(thres_test, morph_test, MORPH_OPEN, kernerl, Point(-1, -1));
//imshow("Bin", morph_test);
vector<vector<Point> > contours_test;
vector<Vec4i> hiearachy_test;
findContours(morph_test, contours_test, hiearachy_test, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
int p=0;
for (int i = 0; i <contours_test.size();i++)
{
Rect minrect_test = boundingRect(contours_test[i]);
double area_test = contourArea(contours_test[i]);
double ckbi_test = minrect_test.width / minrect_test.height;
if (ckbi_test<4 && area_test>100)
{
rectangle(src_test, minrect_test, Scalar(0, 255, 0), 1, 8);
//imshow("1",src_test);
Rect ROI_test = minrect_test;
Mat ROI_img_test = morph_test(ROI_test);
resize(ROI_img_test, ROI_img_test, Size(20, 20));
ROI_img_test.convertTo(ROI_img_test, CV_32F);
ROI_img_test.copyTo(tmp3);
predict_mat.push_back(tmp3.reshape(0, 1));
Mat predict_simple = predict_mat.row(p);
p++;
float r = model->predict(predict_simple);
stringstream stream;
stream << r;
string str;
stream >> str;
putText(src_test, str, ROI_test.tl(), FONT_HERSHEY_PLAIN, 1, Scalar(255, 0, 0), 1, 8);
}
}
imshow("img_test", src_test);
waitKey(0);
return 0;
}
让我们输入纯数字的图片来识别一下试试吧
虽然有的地方还有一些问题,比如有的1太小没被识别出来,但整体效果看起来效果还是不错的。
为了能完整识别一个算式,我们还需要在训练集中加入运算符。在调整后的程序中,我加入了“+”、“-”、“×”、“÷”、“(”、“)”六个字符。
代码中最重要的调整是下面这段,给新加入的字符编号,从而使他们在矩阵中有自己合适的位置。
stringstream stream;
t = k / 16;
stream << t;
string str;
stream >> str;
switch (t) { //++++++
case 10: {str = "+"; break; }
case 11: {str = "-"; break; }
case 12: {str = "x"; break; }
case 13: {str = "/"; break; }
case 14: {str = "("; break; }
case 15: {str = ")"; break; }
}
putText(src, str, ROI.tl(), FONT_HERSHEY_PLAIN, 1, Scalar(255, 0, 0), 1, 8);
data.push_back(tmp2.reshape(0, 1)); //序列化后放入特征矩阵
//labels.push_back(k / 10); //对应的标注 +++ // 符号及其对应 编码 +10 -11 x12 /13 (14 )15
labels.push_back(k / 16); //对应的标注
k++;
修改后,输入的训练数据类似,只是加了六个字符。

同样的,我们再输入一张图片来测试一下

这个结果也还可以,但是运算符还是有时候会出错,包括上面图片里的“+”识别成“4”,还有“÷”识别成“+”,括号识别成1等等,在测试中都有出现。
项目文件夹下生成的分类器(KNNModel.xml)才是我们辛辛苦苦真正想要得到的东西,再次识别数字和运算符就不需要再次用这么长的代码来执行训练等操作了,我们可以直接调用分类器。下面我们就来调用一下练练手。
升级版代码主要完成的功能包括支持手动调节HSV,摄像头读取信息,调用分类器完成识别数字和运算符,组成算式并自动计算出结果等。
首先,创建两个窗口。
namedWindow("test");
namedWindow("SRC");
两个窗口中,一个用来显示摄像头的实时信息,一个用来显示处理过后的图像,并且可以手动调节HSV。
imshow("test", dst);
imshow("SRC", src);
char s = waitKey(30);
if (clc == 3000 || s == 's')
break;
clc++;
待s键按下或时间足够时捕捉摄像头信息进行识别。
之后创建控制条。控制条的目的是方便手动调节HSV,从而在不同环境下都能达到较好的识别效果。
cvCreateTrackbar("lowH", "test", &lowH, 179);
cvCreateTrackbar("highH", "test", &highH, 179);
cvCreateTrackbar("lowS", "test", &lowS, 255);
cvCreateTrackbar("highS", "test", &highS, 255);
cvCreateTrackbar("lowV", "test", &lowV, 255);
cvCreateTrackbar("highV", "test", &highV, 255);
之后就是我们刚才做过很多遍的图片预处理。虽然是摄像头读取信息,但是实际处理的仍然是通过摄像头捕捉的图片,本质是一样的。因此依然要像前文所述一样进行图片的预处理,不过有的地方有所不同,比如将RGB转化为HSV,是因为手动调节HSV相对更加方便。
cvtColor(src, img_hsv, COLOR_BGR2HSV); //RGB转换为HSV
vector<Mat> hsvSplite;
split(img_hsv, hsvSplite); //分成单通道
equalizeHist(hsvSplite[2], hsvSplite[2]); //直方图均值化只能是单通道
merge(hsvSplite, img_hsv); //合成
inRange(img_hsv, Scalar(lowH, lowS, lowV), Scalar(highH, highS, highV), dst); //color,在范围内的颜色变白色,其他变黑色
Mat element = getStructuringElement(MORPH_RECT, Size(3, 3));
morphologyEx(dst, dst, MORPH_OPEN, element); //开运算,去除一些噪点
morphologyEx(dst, dst, MORPH_CLOSE, element); //闭运算,连接一些连通域
dilate(dst, dst, element);
threshold(dst, dst, 0, 255, THRESH_BINARY);
之后就剩下识别和计算。
识别函数中加载KNN分类器(就是之前训练出来的分类器),像图片一样分别识别,并将信息按顺序保存。
计算时按顺序读出信息,分别对应成不同的数字或运算符,组成算式并计算。(组成算式之后,具体的计算就很容易了)
计算出的结果会立刻显示出来,同时也会打印在项目文件夹的一个txt文件里,里面写着“我算出来了,结果是……”。
void iJudge(Mat &src)
{
Mat_<float> nums; //需要识别的图像数据
Ptr<KNearest> knn = StatModel::load<KNearest>("KNNModel.xml"); //加载knn分类器
vector<Mat> numsImg; //存放分割下来的数字图像
cutLR(src, numsImg);
vector<Mat>::iterator it; //用迭代器遍历数组
char equation[100];
int i = 0;
cout << "Numbers: ";
for (it = numsImg.begin(); it < numsImg.end(); it++)
{
nums = (*it).reshape(0, 1); //将数据转化成一行
nums.convertTo(nums, CV_32F); //转成32位浮点型数据
Mat temp;
float result = knn->findNearest(nums, 3, temp); //进行识别
equation[i] = float2char(result);
i++;
cout << result << " ";
}
equation[i] = '\0';
cout << endl << equation << endl;
int result = calculate(equation);
cout << result << endl;
ofstream outfile("result.txt", ios::out | ios::trunc);
outfile << "我算出来了,结果是" << result;
}
为了对比显示出手动调节HSV的作用,我们进行了多次测试。
我正坐在宿舍调试,此时台灯开着,我测试的时候没有对环境做出调整,也没有调整控制条的默认参数。

这是我手拿一张写有算式的纸。从test窗口我们可以看到,默认参数的处理效果还是不错的,但是那张纸的右下角被我写了另一个算式,有一半进入了识别范围内,而我当时并没有发现这一点。
所以…由于我的疏忽而不是代码本身的原因,识别出的算式为72+9521+,计算结果为9593。
同样是这个环境,我关了台灯,重新识别了一次。

从图片中的摄像头图像可以看出,周围光明显变暗,纸上出现了一片阴影。
我没有调节默认参数,因此图像处理结果受到环境影响,也不是很好。
识别出来的算式只是7+9,结果是16。单纯看计算都没有问题,但是算式中的2和5被吞掉了。
鉴于上一次识别效果不好,我决定再识别一次,这次对HSV的参数进行调节。

从SRC窗口可以看出,光线还是和上一次一样的暗。但是这一次我们将lowS一项从默认值31调节到了86,图像处理结果比上一次好了很多。
识别也很顺利,识别出的算式是72+95,结果是167,正好是纸上的算式和我们想要的结果。
同时,计算结果也打印到了项目文件夹下的result.txt。
