赞
踩
最近入门Opencv,分配到的任务是识别车牌。作为刚入门的小白,我查阅了大量相关代码,但在学习过程中发现了一些不足(emmm自己垃圾还嫌别人的不好…),例如我搜到的大多是直接用边缘检测加形态学处理来定位车牌,然而在过程中会发现这准确率实在不高,还有一些问题没有考虑到比如说如果图片中的车是斜的,如果定位后不处理一下,切割的时候回把字符切掉。虽然我是小白,但是经过大佬的指点以及自己的摸索,写了一套方法低级但识别率相对高的代码,在此分享一些处理方法,供大家参考。(作为小白我采用的方法真的比较低级,没有用SVM训练或者ANN系统什么的,但是经过我多达30多张图片的测试,个人感觉车牌定位和切割那部分很准,最后的字符识别除去汉字识别率也挺好的。)
网上用的大多数是RGB来识别蓝色,但hsv更符合人眼的颜色识别。
hsv中显示出来大多是比较鲜艳的绿色或者荧光黄色,如下图(下文分享的图片基本以左图作为原图)
**将hsv进行通道分离,提取其中绿色通道,再将较为鲜艳的颜色阈值化,所得到的,是一张黑色为背景,有几处为白色的图片,而车牌就在这几处白色区域当中,所以在将这种图作为模板盖到原图当中,就可显示出车牌。**通过该步可有效的减少干扰,代码如下
split(srcHsv, channels);
threshold(channels.at(1), channels.at(1), 150, 255, THRESH_BINARY);`
srcRoi.copyTo(dust, channels.at(1));
运行情况如下
可以看到基本上显示的就是蓝色车牌部分,但是 为了在复杂情况下更加准确的识别,我还加了一步,代码见下
Mat srcRange;
inRange(dust, Scalar(80, 0, 0), Scalar(255, 200, 120), srcRange);//提取原图中蓝色部分
这一步其实很简单,就是提取图片中的蓝色部分(inRange函数真的很有用!!!)。对于上面那张车牌,这一步似乎没什么用,但是如果环境复杂或者干扰比较多的时候这一步可大大提高精准率。如下图,
此外,为了识别蓝色的车,我还结合了边缘检测。
代码如下
//滤波
Mat srcGau;
GaussianBlur(src, srcGau, Size(3, 3), 0, 0);
cvtColor(srcGau, src_grey, CV_BGR2GRAY);
//Sobel水平边缘检测
Mat src_sobel;
Sobel(src_grey, src_sobel, CV_8UC1, 1, 0, 3);
convertScaleAbs(src_sobel, src_sobel);
//闭运算
Mat srcClose;
Mat elementRect = getStructuringElement(MORPH_RECT, Size(25, 25));
morphologyEx(src_sobel, srcClose, MORPH_CLOSE, elementRect);
imshow("srcClose", srcClose);
所得到的图片如下
可以看到白色车牌部分是白(灰)色了,但是还有很多其他的白(灰)色区域,所以我选择将Sobel边缘检测的与hsv颜色识别结合起来,首先分享一下结合部分相关代码,基本上是前面的处理“加”起来,然后对“加”起来的图片进行形态学处理。(rangeClose是对前面srcRange进行了一些形态学处理后的图片)
//图像相与
Mat src_add;
bitwise_and(rangeClose, srcClose, src_add);
//相与预想==图像二值化
Mat add_thre;
threshold(src_add, add_thre, 200, 250, THRESH_BINARY);
namedWindow("add_thre",WINDOW_NORMAL);
imshow("add_thre", add_thre);
运行如下
这里分享一下蓝色车的运行情况
左图是原图;中间那张是不加上边缘检测,仅用hsv识别的效果;右图是hsv结合边缘检测的效果,虽然还有一些干扰,但还是好了很多。(鼓掌)接下来还要对右图进行形态学运算,虽然还会存在干扰可在通过接下来的寻找轮廓环节通过设置轮廓的相关参数来去除这些干扰。
找轮廓直接调用opencv里的findContours函数,记得当时被这个函数折磨的很惨,总是异常中断,后来才想起看书的时候说这个函数要求输入的图像为8位单通道,而有时候图片仅有黑白并不意味着是单通道。
在寻找轮廓的时候,考虑到车牌可能是倾斜,后期可能还要旋转车牌(仅仅是车牌!!!而不是整张图片),我用了RotatedRect,以及最小旋转矩形。详细见下
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
Point2f rectRotate[4];//存储倾斜矩形顶点
float min_x, min_y, max_x, max_y;
findContours(close_image, contours, RETR_TREE, CHAIN_APPROX_SIMPLE);
再查找每一个轮廓的最小外接矩形
RotatedRect r = minAreaRect(Mat(contours[i]));
这里要注意的是RotatedRect的长宽并不是我们所想的,所以要对长宽进行一些处理以便筛选,具体代码就不展示了。
车牌轮廓框确定以后,要在原图中将其框出来。因为前面讲倾斜矩形的坐标储存了下来而且是一个矩形,所以我用的是line函数将四个点首尾相连,代码见下
for (int j = 0; j < 4; j++)
{
line(src_copy, rectRotate[j], rectRotate[(j + 1) % 4], Scalar(0, 0, 255), 2, 4); //绘制最小外接矩形每条边
}
运行结果见下图
因为保存了相应坐标,所以可以将车牌区域裁剪出来,见下图
对于上面原本车牌就很正的图我们只需将框出的区域就单独弄出来进行切割就可以了。但是如果是下面这种图,我们就得将倾斜的矩形车牌的外接矩形框出来,对这个外接矩形进行处理以便后期的切割字符。
查找外接矩形的方法很简单,无非是让四个顶点中x,y最小的值和最大的值,然后框出矩形,这一步很简单,弄出来的效果见下
因为前面将轮廓的最小外接矩形存储在rectRotate中所以这里用rectRotate的angle参数就可以知道矩形的旋转角度,我们通过if判断将旋转角度太大的图片(设置if 是为了提高代码的效率)进行旋转(这里旋转的外接矩形二值化图)。
if (fabs(angle) > 45) angle += 90;
Point center(src_rect.cols / 2, src_rect.rows / 2);
Mat src_rot = getRotationMatrix2D(center, angle, 0.8);
Mat warp;
warpAffine(src_rect, warp, src_rot, src_rect.size());
效果见下
因为旋转是绕着图片中心旋转的,旋转前后图片的中心没变,且前面有将最小外接矩形的长宽(即车牌的长宽)数据存储下来,所以可以进行相关裁剪提取感兴趣区域即除去图中非车牌区域,处理后图片见下
接下来再用一次前面的hsv通道分离和inRange查找蓝色,得到的图如下
可以看到两边还有两条“黑杠”,甚至在有的图中上下也有黑杠,见下图
所以我们要把这些东西裁剪掉!
这里有多种方法,我用的是根据每行每列像素突变个数来判断这行(列)是不是我所感兴趣的区域(即车牌字符所占据的长方形),可以看到未包括字符在的行或者列那一行(列)要么是白色要么黑色,而字符所在的行(列)存在着黑色变白色或者白色变黑色的像素突变,所以我们可以根据以每行(列)像素突变的个数
是否大于某个值作为条件,找出上下左右边界。代码见下:
for (int j = 0; j < picRange.rows; j++) { num = 0; count = 0; uchar* date = picRange.ptr<uchar>(j);//获取第i行首地址 for (int i = 0; i < picRange.cols - 1; i++) { if (date[i] != date[i + 1]) count++;//跃变 if (date[i] == 255) num++;//白色个数 } if (count > 8 && (num > int(picRange.cols*0.1)) && (num<int(picRange.cols*0.8))) { row[k] = j;//记录有效行的行数 k++; } }
这一部分是是把行突变像素个数大于某个值的的行数存在数组中下来作为有效行
bool in = false;
for (int i = 0; i <= k - 2; i++)
{
if ((row[i] == row[i + 1] - 1) && (row[i + 1] == row[i + 2] - 1))
{
in = true;
row_start = row[i] - 2;//有效行
if (row_start < 0) row_start = 0;
break;
}
}
从最上边开始若有限行连续,则认为是上边界
同理也可获得下边界左边界右边界的位置,然后再次获得感兴趣区域见下图
接下来就正式进入切割啦~
上图的二值化图如下
网上切割字符的方法有很多种,最简单的就是自己算比例每隔一段距离裁剪一次,当然这种方法极其不准,还有的是做水平投影,我用的是查找最外层轮廓法
findContours(pic, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);
drawContours(picCopy, contours, -1, Scalar(0, 0, 255), 1);
查找的轮廓见下图
有时前期处理的不好还会有别的干扰,例如
可以看到出除了字符以外,还有一些别的白色的区域,所以我们要对轮廓进行筛选,筛选方法与前面筛选车牌差不多,只不过这次是直接用最小矩形将轮廓框起来,然后筛选矩形。这里筛选完矩形以后要把矩形的坐标信息(包括左右)存储下来以便切割
if ((rate > 0.5) && (rectBounding.height > (int(picCopy.rows*0.6)) &&
(int(rectBounding.width > 2)) && (rectBounding.x > 0) && (rectBounding.x < picCopy.cols - 7)))
{
int i = 0;
Mat roi(picCopy, rectBounding);
carOutorder.push_back(roi);
xLeft.push_back(rectBounding.x);
xRight.push_back(rectBounding.x + rectBounding.width);
yUp.push_back(rectBounding.y);
yDown.push_back(rectBounding.y + rectBounding.height);
rectangle(picCopy, rectBounding, Scalar(0, 255, 0));
}
这是 我筛选部分的代码,第六行的代码是直接把单个字符存储在vector中。如果恰好有七个矩形符合条件,则筛选切割字符成功。但是,这时候存储的字符图片并不是按照它在车牌中的顺序存储的,所以要对其进行排序,这时我们存储的矩形坐标信息就派上用场了。
(也不知道为啥这里代码没法用那个代码快,如果有朋友知道就告诉我一下,万分感激!!!!)
有时候一些汉字的轮廓是分开的,比如“沪”等,对于这种情况则补充预语句,如果符合条件的矩形个数为6,则将picCut最左边至第一个切割的字符的左边界划为一个区域。再进行上述切割操作。
最后判断一下,是否有7个矩形块。
因为模板是20*40的,所以 先用rezise把切割的每一个矩形调整一下大小,但是resize后的图是灰度图,所以resize后要再次二值化。
七个字符,第一个一定是汉字,第二个是字母,剩下五个是汉字与字母的组合。
而我的模板文件中是按照数字、字母、汉字排列的,所以为了提高识别率我让切割的第一个字符只与汉字的模板做减法,第二个字符只与字母的模板做减法,剩下的模板与汉字和字符的模板做减法。每个字符对应与它匹配率最高的模板所对应的字符。
字符减法就是讲两张图对应的像素做减法,为了提高速度,推荐用指针访问。
(这一块代码比较简单就不展示了。)
PS:第一篇博客终于写完了!!!(开心!!!)这学期因为种种原因要学习视觉,学长给的任务是先识别车牌。最开始的时候真的是零基础,连C++也不会只学过C,后来买了一本书当时还打算老老实实把那本书看完再写,后来发现根本来不及。期间也遇到过很多大坑小坑,但是代码弄出来真的很有成就感!!因为本人水平有限,可能有些东西说的不对,希望大家能给予指正。
补一个字符识别的模板:
链接:https://pan.baidu.com/s/1lpKlhhBkKZPaiC_6gIgx8g
提取码:87nv
复制这段内容后打开百度网盘手机App,操作更方便哦
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。