赞
踩
这篇文章的车牌识别分为以为几个步骤:
一、图像预处理
(1)转为灰度图
(2)进行高斯滤波
(3)转为二值图像
(4)边缘检测
(5)形态学处理
二、找到车牌
(1)找出预处理图像中每个部分的轮廓
(2)得出轮廓的外接矩形
(3)通过长宽条件判断为车牌的矩形
三、字符分割
(1)车牌预处理
(2)去除边框和铆钉
(3)垂直投影法分割字符
四、机器学习识别字符
本篇文章处理的示例图片如下:
下面分别详细讲述各个步骤:
预处理函数如下:
```cpp Mat preprocessing(Mat input) { //转为灰度图 Mat gray; cvtColor(input, gray, COLOR_BGR2GRAY, 1); //进行高斯滤波 Mat gauss; GaussianBlur(gray, gauss, Size(5, 5), 0, 0); //阈值化操作 Mat thres_hold; threshold(median, thres_hold, 100, 255, THRESH_BINARY); //边缘检测 Mat canny_edge; Canny(thres_hold, canny_edge, 50, 100, 5); //形态学操作 int close_size = 5; //闭操作的核的大小 int open_size = 7; //开操作的核的大小 Mat element_close = getStructuringElement(MORPH_RECT, Size(1 + 2 * close_size, 1 + 2 * close_size));//定义闭操作的核 Mat element_open = getStructuringElement(MORPH_RECT, Size(1 + 2 * open_size, 1 + 2 * open_size)); Mat morph_close, morph_open; morphologyEx(canny_edge, morph_close, MORPH_CLOSE, element_close); morphologyEx(morph_close, morph_open, MORPH_OPEN, element_open); return morph_open; }
(1)转为灰度图
```cpp
Mat gray;
cvtColor(input, gray, COLOR_BGR2GRAY, 1);
这里简单调用cvtcolor函数即可转为灰度图像
(2)进行高斯滤波
Mat gauss;
GaussianBlur(gray, gauss, Size(5, 5), 0, 0);
简单进行高斯滤波。核不用太大,防止边缘过于模糊
(3)转为二值图像
Mat thres_hold;
threshold(median, thres_hold, 100, 255, THRESH_BINARY);
这里的二值化的目的是粗略地区分前景和背景,突出车牌。 为后面的边缘检测减少干扰项。
(4)边缘检测
Mat canny_edge;
Canny(thres_hold, canny_edge, 50, 100, 5);
canny检测算子的高低阈值(第三第四个参数)比一般为2:1,或3:1.
检测出来的边缘图像如下:
(5)形态学处理
int close_size = 5; //闭操作的核的大小
int open_size = 7; //开操作的核的大小
Mat element_close = getStructuringElement(MORPH_RECT, Size(1 + 2 * close_size, 1 + 2 * close_size));//定义闭操作核
Mat element_open = getStructuringElement(MORPH_RECT, Size(1 + 2 * open_size, 1 + 2 * open_size));//定义开操作核
Mat morph_close, morph_open;
morphologyEx(canny_edge, morph_close, MORPH_CLOSE, element_close);
morphologyEx(morph_close, morph_open, MORPH_OPEN, element_open);
这里使用了形态学闭操作和形态学开操作。步骤都是定义操作核的大小—>定义核---->执行对应操作
这里闭操作的目的是:填补轮廓线中的断裂,将断开的边缘连成一个个整体。核越大,边缘连接成的整体越大。
闭操作结果:
开操作的目的是:消除一部分小块的整体和线。 核越大,效果越明显。
开操作结果:
Mat find_the_plate(Mat input) { Mat output; //提取出来的车牌 //寻找轮廓 vector<vector<Point>> contours; //定义轮廓点向量 vector<Vec4i> hierarchy; //轮廓的索引 findContours(input, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);//提取轮廓 Point2f rect_info[4]; //用于储存最小矩形的四个顶点 RotatedRect minrect; //返回的最小外接矩形 int width; //最小矩形的宽 int height; //最小矩形的高 for (int i = 0; i < contours.size(); i++) { minrect = minAreaRect(contours[i]); width = minrect.size.width; height = minrect.size.height; minrect.points(rect_info); for (int j = 0; j < 4; j++) { if ( width / height >= 2 && width / height <= 5) //利用长宽比筛选条件 { output = src(Rect(rect_info[1].x, rect_info[1].y, width, height));//根据筛选出的矩形提取车牌,相当于创建ROI line(src, rect_info[j], rect_info[(j + 1) % 4], Scalar(0, 0, 255));//在原图画出符合条件的矩形 } } } return output; }
(1)找出预处理图像中每个部分的轮廓
//寻找轮廓
vector<vector<Point>> contours; //定义轮廓点向量
vector<Vec4i> hierarchy; //轮廓的索引
findContours(input, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);//提取轮廓
(这里input即为预处理后的图像)
第二个参数contours是OutputArrayofArrays类型,储存查找出来的轮廓的。由于轮廓本来就是一系列点的集合,即有vector, 由因有许多个轮廓。故为vector<vertor>
第三个参数hierarchy是OutputArrays类型,包含图像的拓扑信息。每个轮廓contours[i]包含4个hierarchy元素,hierarchy[i][0]~
hierarchy[i][3],分别表示后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号。所以hierarchy的每个元素是Vec4i类型。
(2)得到轮廓的外接矩形,并通过长宽条件判断为车牌的矩形
Point2f rect_info[4]; //用于储存最小矩形的四个顶点 RotatedRect minrect; //返回的最小外接矩形 int width; //最小矩形的宽 int height; //最小矩形的高 for (int i = 0; i < contours.size(); i++) //contours.size()轮廓的数量 { minrect = minAreaRect(contours[i]); width = minrect.size.width; height = minrect.size.height; minrect.points(rect_info); for (int j = 0; j < 4; j++) { if ( width / height >= 2 && width / height <= 5) //利用长宽比筛选条件 { output = src(Rect(rect_info[1].x, rect_info[1].y, width, height));//根据筛选出的矩形提取车牌,相当于创建ROI line(src, rect_info[j], rect_info[(j + 1) % 4], Scalar(0, 0, 255));//在原图画出符合条件的矩形 } } } return output; }
定义RotatedRect类型的 minrect存放矩形, minAreaRect函数将轮廓的外接矩形储存到minrect中。
For循环遍访每一个外接矩形,若外接矩形的长宽比符合筛选条件(示例为2<y:x<5),将它认定为车牌,并在原图中用红框框记。
结果显示如下图:
这里要先将一下采用的垂直投影字符分割的原理:
把字符图形二值化后,图像就会变成背景为黑,字符为白的图像。这时,遍历整幅图像,统计每一列的白色像素的数量。 然后绘制出,每一列白色像素数量的直方图。
(如下图,横轴为每一列,竖轴为该列上的白色像素数量)
这时,没有白色像素的列即为字符间的分界线,只需知道是哪一列即可。
void character_division(Mat input) { Mat expansion; //车牌图片放大 resize(input, expansion, Size(input.cols * 3, input.rows * 3), 0, 0); int width = expansion.size().width; //获取图像的长和宽 int height = expansion.size().height; Mat gray; cvtColor(expansion, gray, CV_BGR2GRAY, 1); //中值滤波 medianBlur(gray, gray, 3); //核太大会使得二值化后的字变模糊 Mat thres_hold; threshold(gray, thres_hold, 0,255, THRESH_OTSU); /* int dilate_size = 2; //开操作的核的大小 Mat element_open = getStructuringElement(MORPH_RECT, Size(1 + 2 * dilate_size, 1 + 2 * dilate_size)); Mat morph_dilate; morphologyEx(thres_hold, morph_dilate, MORPH_DILATE, element_open); */ int pixelvalue; //每个像素的值 int * white_nums = new int[width](); //定义动态数组并初始化, 储存每一列的白色像素数量 //遍历每一个像素,去掉边框和铆钉,再统计剩下每一列白色像素的数量 for (int col = 0; col < width; col++) { /*去除竖直的边框和铆钉*/ int cols_convert_num = 0;//每一列黑白转变的次数 for (int i = 0; i < height - 1; i++)//遍历某一列的所有元素,计算转变次数 { if (thres_hold.at<uchar>(i, col) != thres_hold.at<uchar>(i + 1, col)) cols_convert_num++; } if (cols_convert_num < cols_thres_value) { continue; } /*去除竖直的边框和铆钉*/ for (int row = 0; row < height; row++) { /*去除水平的边框和铆钉*/ int rows_convert_num = 0;//每一行黑白转变的次数 for (int j = 0; j < width - 1; j++)//遍历某一行的所有元素,计算转变次数 { if (thres_hold.at<uchar>(row, j) != thres_hold.at<uchar>(row, j + 1)) rows_convert_num++; } if (rows_convert_num < rows_thres_value) { continue; } /*去除水平的边框和铆钉*/ pixelvalue = thres_hold.at<uchar>(row, col); if (pixelvalue == 255) white_nums[col]++; //统计白色像素的数量 } } //画出投影图 Mat verticalProjection(height,width, CV_8UC1, Scalar(0,0,0)); for (int i = 0; i < width; i++) { line(verticalProjection,Point(i, height),Point(i, height - white_nums[i]), Scalar(255,255,255)); } //根据投影图进行分割 vector<Mat> character; int character_num = 0; //字符数量 int start; //进入字符区的列数 int end; //退出字符区的列数 bool character_block = false;// 是否进入了字符区 for (int i = 0; i < width; i++) { if(white_nums[i] != 0 && character_block == false) //刚进入字符区 { start = i; character_block = true; } else if (white_nums[i] == 0 && character_block == true) //刚出来字符区 { character_block = false; end = i; if (end - start >= 6) { Mat image = expansion(Range(0, height), Range(start, end)); character.push_back(image); //push.back适用于vector类型 在数组尾部添加一个数据 } } } delete[] white_nums; //删除动态数组 imshow("pro", thres_hold); imshow("projection", verticalProjection); }
(1)车牌图像的预处理
Mat expansion; //车牌图片放大
resize(input, expansion, Size(input.cols * 3, input.rows * 3), 0, 0);
int width = expansion.size().width; //获取图像的长和宽
int height = expansion.size().height;
Mat gray;
cvtColor(expansion, gray, CV_BGR2GRAY, 1);
//中值滤波
medianBlur(gray, gray, 3); //核太大会使得二值化后的字变模糊
Mat thres_hold;
threshold(gray, thres_hold, 0,255, THRESH_OTSU);
这个过程包括:
1.使用resize函数将车牌放大,并记下放大后的width 和 height,方便后续观察和处理。
2.转成灰度图像
3.中值滤波。防止椒盐噪声的干扰。
4.二值化。这里用的是OTSU算法(最大类间差算法)。OTSU算法的好处是不用自行设定阈值,算法会自动根据图像计 算阈值,所以第三个参数是没有意义的。但是有时会将前景和背景弄反。
预处理后的图像:
可以看出这时的车牌不仅有字符,还有边框和铆钉,这会对后续的垂直投影分割字符有影响。所以接下来要把这些干扰项去掉。
(2)去除边框和铆钉
这里要先说一下,用于储存每一列的白色像素的数组。因为数组元素的值是动态的,所以要先 new 来创建数组, 后面用完了在用 delete 释放数组。
#define rows_thres_value 10 //除掉车牌框时,行跳变的阈值 #define cols_thres_value 2 //除掉车牌框时,列跳变的阈值 int pixelvalue; //每个像素的值 int * white_nums = new int[width](); //定义动态数组并初始化, 储存每一列的白色像素数量 //遍历每一个像素,去掉边框和铆钉,再统计剩下每一列白色像素的数量 for (int col = 0; col < width; col++) { /*去除水平的边框和铆钉*/ int cols_convert_num = 0;//每一列黑白转变的次数 for (int i = 0; i < height - 1; i++)//遍历某一列的所有元素,计算转变次数 { if (thres_hold.at<uchar>(i, col) != thres_hold.at<uchar>(i + 1, col)) cols_convert_num++; } if (cols_convert_num < cols_thres_value) { continue; } /*去除水平的边框和铆钉*/ for (int row = 0; row < height; row++) { /*去除竖直的边框和铆钉*/ int rows_convert_num = 0;//每一行黑白转变的次数 for (int j = 0; j < width - 1; j++)//遍历某一行的所有元素,计算转变次数 { if (thres_hold.at<uchar>(row, j) != thres_hold.at<uchar>(row, j + 1)) rows_convert_num++; } if (rows_convert_num < rows_thres_value) { continue; } /*去除竖直的边框和铆钉*/ pixelvalue = thres_hold.at<uchar>(row, col); if (pixelvalue == 255) white_nums[col]++; //统计白色像素的数量 } }
这里去除边框和铆钉的原理是: 以行为例。 统计每一行出现的白色和黑色转变的次数,因为在字符区域,跳变的次数肯定较多,而背景和边框区域跳变的次数较少。只需设置一个阈值,将跳变次数少于阈值的行,不计入计算白色像素的统计。 列的同理。
/*去除竖直的边框和铆钉*/
int rows_convert_num = 0;//每一行黑白转变的次数
for (int j = 0; j < width - 1; j++)//遍历某一行的所有元素,计算转变次数
{
if (thres_hold.at<uchar>(row, j) != thres_hold.at<uchar>(row, j + 1))//本行与下一行颜色不同
rows_convert_num++;
}
if (rows_convert_num < rows_thres_value)
{
continue;
}
/*去除竖直的边框和铆钉*/
以行为例。 从第一行j = 0,到倒数第二行j = width - 1。
本行与下一行颜色不同,则转变次数+1
if (thres_hold.at<uchar>(row, j) != thres_hold.at<uchar>(row, j + 1))//本行与下一行颜色不同
rows_convert_num++;
若跳变次数小于阈值,该列不计入统计
if (rows_convert_num < rows_thres_value)
{
continue;
}
(3)画出投影图,并进行分割。
Mat verticalProjection(height,width, CV_8UC1, Scalar(0,0,0)); for (int i = 0; i < width; i++) { line(verticalProjection,Point(i, height),Point(i, height - white_nums[i]), Scalar(255,255,255)); } //根据投影图进行分割 vector<Mat> character; int character_num = 0; //字符数量 int start; //进入字符区的列数 int end; //退出字符区的列数 bool character_block = false;// 是否进入了字符区 for (int i = 0; i < width; i++) { if(white_nums[i] != 0 && character_block == false) //刚进入字符区 { start = i; character_block = true; } else if (white_nums[i] == 0 && character_block == true) //刚出来字符区 { character_block = false; end = i; if (end - start >= 6) { Mat image = expansion(Range(0, height), Range(start, end)); character.push_back(image); //push.back适用于vector类型 在数组尾部添加一个数据 } } } delete[] white_nums; //删除动态数组
绘制出的投影图如下:
这里的算法是:用判断高度是否为0,用bool变量标记是否为字符区, 并用start和end记录下进入字符区的列数。
然后将某个字符范围的图像,储存到character数组。
if (end - start >= 6)
{
Mat image = expansion(Range(0, height), Range(start, end));
character.push_back(image); //push.back适用于vector类型 在数组尾部添加一个数据
}
学习中、后续再处理
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。