Opencv简易车牌识别

Opencv车牌识别

概述

这篇文章的车牌识别分为以为几个步骤:
一、图像预处理
(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类型 在数组尾部添加一个数据
		   }

四、机器学习识别字符

学习中、后续再处理

猜你喜欢

转载自blog.csdn.net/weixin_43960499/article/details/102773588
今日推荐