OpenCV C++案例实战十《车牌号识别》

Posted Zero___Chen

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OpenCV C++案例实战十《车牌号识别》相关的知识,希望对你有一定的参考价值。


前言

本文将使用OpenCV C++ 进行车牌号识别。

一、车牌检测


原图如图所示。本案例的需求是进行车牌号码识别。所以,首先我们得定位车牌所在的位置,然后将车牌切割出来。接下来我们就来看看是如何实现。

1.1.图像预处理

首先经过一些常规的图像预处理,我们可以提取出图像的大致轮廓。然后根据轮廓的特征进一步确定我们所需要查找的轮廓。在这里,不同的图像需要根据本身图像特征设定预处理算法。所以,本案例的一个缺点就是不具有鲁棒性,只针对特定需求。

	Mat gray;
	cvtColor(src, gray, COLOR_BGR2GRAY);

	Mat thresh;
	threshold(gray, thresh, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);

	//使用形态学开操作去除一些小轮廓
	Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
	Mat open;
	morphologyEx(thresh, open, MORPH_OPEN, kernel);


如图为经过二值化后的图像,接下来我们就可以使用findContours寻找我们需要的轮廓。根据图像的轮廓特征就可以定位到车牌所在位置,然后将其从原图中切割出来,以便后续的识别工作。在这里,我定义了一个License结构体,用于存储ROI图像,以及其相对于原图所在位置。这样在后续的绘制工作中,我们就可以定位到ROI所在位置。

1.2.轮廓提取

//自定义车牌结构体
struct License

	Mat mat;  //ROI图片
	Rect rect; //ROI所在矩形
;
	//使用 RETR_EXTERNAL 找到最外轮廓
	vector<vector<Point>>contours;
	findContours(open, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
	vector<vector<Point>>conPoly(contours.size());
	for (int i = 0; i < contours.size(); i++)
	
		double area = contourArea(contours[i]);
		double peri = arcLength(contours[i], true);
		//根据面积筛选出可能属于车牌区域的轮廓
		if (area > 1000)
		
			//使用多边形近似,进一步确定车牌区域轮廓
			approxPolyDP(contours[i], conPoly[i], 0.02*peri, true);

			if (conPoly[i].size() == 4)
				
				//计算矩形区域宽高比
				Rect box = boundingRect(contours[i]);
				double ratio = double(box.width) / double(box.height);
				if (ratio > 2 && ratio < 4)
				
					//截取ROI区域
					Rect rect = boundingRect(contours[i]);
					License_ROI =  src(rect),rect ;
				
			
		
	

1.3.功能效果


如图为从汽车上定位到的车牌,并将其切割出来以便下面的识别工作。

1.4.功能源码

//获取车牌所在ROI区域--车牌定位
bool Get_License_ROI(Mat src, License &License_ROI)

	Mat gray;
	cvtColor(src, gray, COLOR_BGR2GRAY);

	Mat thresh;
	threshold(gray, thresh, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);

	//使用形态学开操作去除一些小轮廓
	Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
	Mat open;
	morphologyEx(thresh, open, MORPH_OPEN, kernel);

	//使用 RETR_EXTERNAL 找到最外轮廓
	vector<vector<Point>>contours;
	findContours(open, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
	vector<vector<Point>>conPoly(contours.size());
	for (int i = 0; i < contours.size(); i++)
	
		double area = contourArea(contours[i]);
		double peri = arcLength(contours[i], true);
		//根据面积筛选出可能属于车牌区域的轮廓

		if (area > 1000)
		
			//使用多边形近似,进一步确定车牌区域轮廓
			approxPolyDP(contours[i], conPoly[i], 0.02*peri, true);

			if (conPoly[i].size() == 4)
				
				//计算矩形区域宽高比
				Rect box = boundingRect(contours[i]);
				double ratio = double(box.width) / double(box.height);
				if (ratio > 2 && ratio < 4)
				
					//截取ROI区域
					Rect rect = boundingRect(contours[i]);
					License_ROI =  src(rect),rect ;
				
			
		
	

	if (License_ROI.mat.empty())
	
		return false;
	
	return true;

二、字符切割

2.1.图像预处理

通过刚才的车牌定位,我们已经将车牌从原图中切割出来了。接下来,我们还需要将车牌上的字符一一切割出来,以便进行后续的识别工作。同理,我们也需要对车牌做同样的预处理操作。

	Mat gray;
	cvtColor(License_ROI.mat, gray, COLOR_BGR2GRAY);

	Mat thresh;
	threshold(gray, thresh, 0, 255, THRESH_BINARY | THRESH_OTSU);

	Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
	Mat close;
	morphologyEx(thresh, close, MORPH_CLOSE, kernel);

经过灰度、阈值、形态学操作后的图像如下图所示。

2.2.轮廓提取

接下来我们进行轮廓提取就可以提取出车牌上的每一个字符了。

	vector<vector<Point>>contours;
	findContours(close, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
	for (int i = 0; i < contours.size(); i++)
	
		double area = contourArea(contours[i]);
		//由于我们筛选出来的轮廓是无序的,故后续我们需要将字符重新排序
		if (area > 200)
		
			Rect rect = boundingRect(contours[i]);
			//计算外接矩形宽高比
			double ratio = double(rect.height) / double(rect.width);
			if (ratio > 1)
			
				Mat roi = License_ROI.mat(rect);
				resize(roi, roi, Size(50, 100), 1, 1, INTER_LINEAR);	 
				Character_ROI.push_back( roi ,rect );
			
		
	

如图为切割出来的字符。不过这里有一个小问题就是,我们切割出来的字符并不是按车牌号码那样顺序排列。所以,在这里我们还得对其重新进行排序,使其按车牌顺序排列。

	//冒泡排序
	for (int i = 0; i < Character_ROI.size()-1; i++)
	
		for (int j = 0; j < Character_ROI.size() - 1 - i; j++)
		
			if (Character_ROI[j].rect.x > Character_ROI[j + 1].rect.x)
			
				License temp = Character_ROI[j];
				Character_ROI[j] = Character_ROI[j + 1];
				Character_ROI[j + 1] = temp;
			
		
	

2.3.功能效果

2.4.功能源码

//获取车牌每一个字符ROI区域
bool Get_Character_ROI(License &License_ROI, vector<License>&Character_ROI)

	Mat gray;
	cvtColor(License_ROI.mat, gray, COLOR_BGR2GRAY);

	Mat thresh;
	threshold(gray, thresh, 0, 255, THRESH_BINARY | THRESH_OTSU);

	Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
	Mat close;
	morphologyEx(thresh, close, MORPH_CLOSE, kernel);

	vector<vector<Point>>contours;
	findContours(close, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
	for (int i = 0; i < contours.size(); i++)
	
		double area = contourArea(contours[i]);
		//由于我们筛选出来的轮廓是无序的,故后续我们需要将字符重新排序
		if (area > 200)
		
			Rect rect = boundingRect(contours[i]);
			//计算外接矩形宽高比
			double ratio = double(rect.height) / double(rect.width);
			if (ratio > 1)
			
				Mat roi = License_ROI.mat(rect);
				resize(roi, roi, Size(50, 100), 1, 1, INTER_LINEAR);	 
				Character_ROI.push_back( roi ,rect );
			
		
	

	//将筛选出来的字符轮廓 按照其左上角点坐标从左到右依次顺序排列
	//冒泡排序
	for (int i = 0; i < Character_ROI.size()-1; i++)
	
		for (int j = 0; j < Character_ROI.size() - 1 - i; j++)
		
			if (Character_ROI[j].rect.x > Character_ROI[j + 1].rect.x)
			
				License temp = Character_ROI[j];
				Character_ROI[j] = Character_ROI[j + 1];
				Character_ROI[j + 1] = temp;
			
		
	

	if (Character_ROI.size() != 7)
	
		return false;
	
	return true;

三、字符识别

3.1.读取文件


如图所示,为模板图像以及对应的label。我们需要读取文件,进行匹配。在这里我使用UTF8ToGB函数实现读取txt文件,目的是为了在控制台显示中文时,不会出现乱码情况。

//读取文件  图片
bool Read_Data(string filename,vector<Mat>&dataset)
		
	vector<String>imagePathList;
	glob(filename, imagePathList);
	if (imagePathList.empty())return false;

	for (int i = 0; i < imagePathList.size(); i++)
	
		Mat image = imread(imagePathList[i]);
		resize(image, image, Size(50, 100), 1, 1, INTER_LINEAR);
		dataset.push_back(image);
	

	return true;

//读取文件 标签
bool Read_Data(string filename, vector<string>&data_name)

	fstream fin;
	fin.open(filename, ios::in);
	if (!fin.is_open())
	
		cout << "can not open the file!" << endl;
		return false;
	

	string s;
	while (std::getline(fin, s))
	
		string str = UTF8ToGB(s.c_str()).c_str();
		data_name.push_back(str);
	
	fin.close();

	return true;

3.2.字符匹配

在这里,我的思路是:使用一个for循环,将我们切割出来的字符与现有的模板进行匹配。而这个匹配算法是求两张图像的像素差,以此来判断图像的相似程度。具体是使用OpenCV absdiff函数计算两张图像的像素差.。


如图为使用absdiff得到的效果图。接下来,我们只需要计算图像中灰度值为0的像素点个数就可以了。像素点个数最少的那个label即为我们的匹配结果。当然,此方法肯定是会存在误识别的情况的。进行字符匹配的方法还有:模板匹配,基于Hu矩轮廓匹配。大家可以试试。

3.3.功能源码

//识别车牌字符
bool License_Recognition(vector<License>&Character_ROI, vector<int>&result_index)

	string filename = "data/";
	vector<Mat>dataset;
	if (!Read_Data(filename, dataset)) return false;

	for (int i = 0; i < Character_ROI.size(); i++)
	
		Mat roi_gray;
		cvtColor(Character_ROI[i].mat, roi_gray, COLOR_BGR2GRAY);

		Mat roi_thresh;
		threshold(roi_gray, roi_thresh, 0, 255, THRESH_BINARY | THRESH_OTSU);

		int minCount = 1000000;
		int index = 0;
		for (int j = 0; j < dataset.size(); j++)
		
			Mat temp_gray;
			cvtColor(dataset[j], temp_gray, COLOR_BGR2GRAY);

			Mat temp_thresh;
			threshold(temp_gray, temp_thresh, 0, 255, THRESH_BINARY | THRESH_OTSU);

			//计算两张图片的像素差,以此判断两张图片是否相同
			Mat dst;
			absdiff(roi_thresh, temp_thresh, dst);
		
			int count = pixCount(dst);
			if (count < minCount)
			
				minCount = count;
				index = j;
					
		

		result_index.push_back(index);
	

	return true;

四、效果显示

//显示最终效果
bool Draw_Result(Mat src, License &License_ROI, vector<License>&Character_ROI,vector<int>&result_index)

	rectangle(src, License_ROI.rect, Scalar(0, 255, 0), 2);

	vector<string>data_name;
	if (!Read_Data("data_name.txt", data_name))return false;

	for (int i = 0; i < Character_ROI.size(); i++)
	
		cout << data_name[result_index[i]] << " ";
		//putText 中文显示会乱码,所以采用下面代码
		CvxText text("C://Windows/Fonts/方正粗黑宋简体.ttf");//字体
		string str = data_name[result_index[i]];  //string 转 char
		const char*msg = str.data();
		IplImage *temp;  //Mat 转 IplImage
		temp = &IplImage(src);
		text.putText(temp, msg, Point(License_ROI.rect.x + Character_ROI[i].rect.x, License_ROI.rect.y + Character_ROI[i].rect.y),Scalar(0,0,255));
	
	return true;

在这里,为了使用putText显示中文,我这里加了一些额外的代码。如果需要使用putText显示中文效果的朋友可以自行百度一下如何配置环境。
最终效果如图所示:


OpenCV C++案例实战三十《中文点选验证码识别》

OpenCV(项目)车牌识别4 -- 总结篇

C++中文车牌识别检测系统源码

Python opencv 简单的车牌识别 —— 简单学习

Python opencv 简单的车牌识别 —— 简单学习

在 OpenCV C++ 中为 OCR 规范化车牌