首页 > 编程语言 > 详细

C++-车牌识别

时间:2021-04-28 16:47:36      阅读:21      评论:0      收藏:0      [点我收藏+]

车牌识别整体流程示意图如下所示:

技术分享图片
\(\quad\) 在车牌识别之前先利用HOG特征提取和支持向量机SVM线性分类器对汉字模型和数字字母进行训练,将训练完成后的xml文件导入到车牌识别程序中,对车牌进行识别。核心思想:获取一张图片后会将图片特征写入到容器中,紧接着会将标签写入另一个容器中,这样就保证了特征和标签是一一对应的关系。先从二值车牌的文件夹中读取图片训练数据集,依次循环遍历每个文件夹中的图片,对图像进行灰度变换并二值化,并将每个图片统一尺寸为16×32的大小,便于训练。循环读取每张图片,依次放在input_images内,进行HOG特征提取,并将提取结果序列化向量依次存入。
\(\quad\) 创建SVM模型:创建分类器并设置参数,C_SVC用于分类,C_SVR用于回归,LINEAR线性核函数。SIGMOID为高斯核函数,核函数中的参数degree,针对多项式核函数,核函数中的参数gamma,针对多项式/RBF/SIGMOID核函数,核函数中的coef0参数,针对多项式/SIGMOID核函数,SVM最优问题参数,设置C-SVC,EPS_SVR、NU_SVR、NU_SVC, ONE_CLASS 、NU_SVR的参数,并且设置EPS_SVR 中损失函数p的值,结束条件为训练1000次或者误差小于0.01结束,训练数据和标签的结合进行训练分类器,将训练好的模型进行保存,最后用二值车牌的具体图片进行测试验证,汉字模型和数字字母的训练算法都是遵循上述思路。
\(\quad\) 训练好之后将xml文件导入到车牌识别的程序中进行车牌识别,车牌识别的具体算法步骤为:先进行车牌图片的读取,本实验为了方便图片的读取,设置了一个滑动组件,用户可以拖动滑动组件进行车牌图像的读取。读取完图像之后,重新调整图像大小为900×900,将RGB图像转为HSV图像,进行二值化处理,并自定义矩形元素进行开、闭运算。因为大部分车牌都是蓝色车牌,因此我在实验中设置了h_lower = 100;s_lower = 52;v_lower = 150;h_upper = 120;s_upper = 255;v_upper = 255; 之后设置亮度阈值为160,并循环进行减10衰减,直到找到车牌或者亮度v_lower过低,对查找到的区域进行轮廓检测,利用minAreaRect找出最小外界矩形,即为符合车牌形状的矩形,标记下来,再根据车牌长宽特征进行筛选。
\(\quad\) 由于图像的拍摄角度等因素的干扰,可能使得图片不是标准正方向而出现倾斜,变形,因此我对图像进行透视变换矫正变形图像,调整顺序:左上-右上-右下-左下,最终获得ROI图像,利用边缘检测和模型匹配筛出,最有可能是车牌的图,先将ROI图像灰度化,进行Canny边缘检测,再进行阈值分割,自定义矩形元素对齐进行闭运算,利用findcontours寻找轮廓,找外接矩形,对车牌ROI上下分割,利用投影法进行x方向的投影切除字母上下多余的部分,通过车牌在y方向的映射分离分割每个字母。
\(\quad\) 之后进行七个字符的分割,首先用findcontours进行轮廓检测获取轮廓在图像中的矩形坐标,根据轮廓坐标使用方框标记出来,将存放7个字符的矩形图片,依次利用xml模型文件排列进行汉字和数字字母的识别,并在源图中用putText显示车牌号码,并在终端返回识别结果及所用时间。下图分别为二值车牌的汉字及数字字母的模板图.
技术分享图片

二值车牌模板图

技术分享图片

二值车牌数字0模板图

技术分享图片

二值车牌数字6模板图

技术分享图片

二值车牌字母F模板图

技术分享图片

二值车牌字母H模板图

技术分享图片

二值车牌文字模板图

技术分享图片

二值车牌文字“苏”模板图

技术分享图片

二值车牌文字“青”模板图

程序结构

技术分享图片

HOG+SVM汉字模型训练:

#include <stdio.h>  
#include <time.h>  
#include <opencv2/opencv.hpp>  
#include <iostream> 
#include <opencv2/core/core.hpp>  
#include <opencv2/highgui/highgui.hpp>  
#include <opencv2/ml/ml.hpp>  
#include <io.h> //查找文件相关函数
#include<opencv2/imgproc/types_c.h>

/*选择是否重新训练*/
#define TRAIN true

using namespace std;
using namespace cv;
using namespace ml;

/*全局变量*/
void GetAllFiles(string path, vector<string>& files);

ostringstream oss;
int num = -1;
Mat src;
Mat yangben_gray;
Mat yangben_thresh;

int main()
{
	//核心思路://获取一张图片后会将图片特征写入到容器中,
				//紧接着会将标签写入另一个容器中,这样就保证了特征
				//  和标签是一一对应的关系。
	////===============================读取训练数据===============================////
#if TRAIN
	//训练数据,每一行一个训练图片
	Mat trainingData;
	//训练样本标签
	Mat labels;
	//最终的训练样本标签
	Mat clas;
	//最终的训练数据
	Mat traindata;

	/*文件夹下的文件夹名*/
	vector<string>files_name;
	string files_path = "../../二值车牌/汉字/";
	GetAllFiles(files_path, files_name);
	/********************获得汉字文件夹下的所有文件夹的名称***********************/
	for (int num = 0; num < files_name.size(); num++)//依次提取0到9文件夹中的图片 减去汉字这个文件夹 加上中间少的两个字母
	{
		oss << "../../二值车牌/汉字/";
		int label = num;  //省份标签0~28
		oss << files_name[num] << "/*.png";//图片名字后缀,oss可以结合数字与字符串

		string pattern = oss.str();//oss.str()输出oss字符串,并且赋给pattern
		oss.str("");//每次循环后把oss字符串清空
		vector<String> input_images_name;
		glob(pattern, input_images_name, false);
		//为false时,仅仅遍历指定文件夹内符合模式的文件,当为true时,会同时遍历指定文件夹的子文件夹
		//此时input_images_name存放符合条件的图片地址
		//文件夹下总共有几个图片
		int all_num = input_images_name.size();

		for (int i = 0; i < all_num; i++)//依次循环遍历每个文件夹中的图片
		{
			/********HOG特征提取*****/
			/*读取图像:
			大小:size(32,64)
			*/
			Mat src = imread(input_images_name[i]);
			Mat gray;
			cvtColor(src, gray,CV_BGR2GRAY);
			HOGDescriptor detector(Size(16,32),Size(16,16),Size(8,8),Size(8,8),9); //向量大小:((36-8)/4+1)*((64-8)/4+1)*2*2*9=4320;
			/*临时存放每张图临时生成的一维向量*/
			vector<float>descriptors;
			detector.compute(gray,descriptors,Size(0,0),Size(0,0));
			trainingData.push_back(static_cast<Mat>(descriptors).reshape(1,1));//序列化后的图片依次存入  放在下一个row
			labels.push_back(label);//把每个图片对应的标签依次存入 放在下一个row
		}
	}
	//图片数据和标签转变下
	trainingData.copyTo(traindata);//复制
	traindata.convertTo(traindata, CV_32FC1);//更改图片数据的类型,必要,不然会出错
	labels.copyTo(clas);//复制


	////===============================创建SVM模型===============================////
	// 创建分类器并设置参数
	Ptr<SVM> SVM_params = SVM::create();
	SVM_params->setType(SVM::C_SVC);//C_SVC用于分类,C_SVR用于回归
	SVM_params->setKernel(SVM::LINEAR);  //LINEAR线性核函数。SIGMOID为高斯核函数

	SVM_params->setDegree(0);//核函数中的参数degree,针对多项式核函数;
	SVM_params->setGamma(1);//核函数中的参数gamma,针对多项式/RBF/SIGMOID核函数; 
	SVM_params->setCoef0(0);//核函数中的参数,针对多项式/SIGMOID核函数;
	SVM_params->setC(1);//SVM最优问题参数,设置C-SVC,EPS_SVR和NU_SVR的参数;
	SVM_params->setNu(0);//SVM最优问题参数,设置NU_SVC, ONE_CLASS 和NU_SVR的参数; 
	SVM_params->setP(0);//SVM最优问题参数,设置EPS_SVR 中损失函数p的值. 
	//结束条件,即训练1000次或者误差小于0.01结束
	SVM_params->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER + TermCriteria::EPS, 1000, 0.01));

	//训练数据和标签的结合
	Ptr<TrainData> tData = TrainData::create(traindata, ROW_SAMPLE, clas);

	// 训练分类器
	SVM_params->train(tData);//训练

	//保存模型
	SVM_params->save("HOG字svm.xml");
	cout << "训练好了!!!" << endl;
#else
	Ptr<SVM> SVM_params = SVM::load("HOG字svm.xml");

#endif

	////===============================预测部分===============================////
	Mat src = imread("../../二值车牌/汉字/guizhou/21.png");
	Mat Gray;
	cvtColor(src, Gray, CV_BGR2GRAY);
	imshow("原图像", src);
	Mat input;
	HOGDescriptor testDetector(Size(16, 32), Size(16, 16), Size(8, 8), Size(8, 8), 9);
	vector<float>testDescriptor;
	testDetector.compute(Gray,testDescriptor,Size(0,0),Size(0,0));
	input.push_back(static_cast<Mat>(testDescriptor).reshape(1,1));

	float r = SVM_params->predict(input);   //对所有行进行预测

	cout<<r<<endl;
	waitKey(0);
	return 0;
}

/*获得文件夹下的文件夹名称*/
void GetAllFiles(string path, vector<string>& files)
{

	long long   hFile = 0;
	//文件信息  
	struct _finddata_t fileinfo;//用来存储文件信息的结构体  
	string p;
	if ((hFile = _findfirst(p.assign(path).append("/*").c_str(), &fileinfo)) != -1)  //第一次查找
	{
		do
		{
			if ((fileinfo.attrib & _A_SUBDIR))  //如果查找到的是文件夹
			{
				if (strcmp(fileinfo.name, ".") != 0 && strcmp(fileinfo.name, "..") != 0)  //进入文件夹查找
				{
					files.push_back(p.assign(fileinfo.name));

				}
			}
			else //如果查找到的不是是文件夹 
			{
				//files.push_back(p.assign(fileinfo.name));  //将文件路径保存,也可以只保存文件名:    p.assign(path).append("\\").append(fileinfo.name)
			}

		} while (_findnext(hFile, &fileinfo) == 0);

		_findclose(hFile); //结束查找
	}

}

训练结果:

技术分享图片

此时生成一个xml的模型

技术分享图片

HOG+SVM数字字母训练

#include <stdio.h>  
#include <time.h>  
#include <opencv2/opencv.hpp>  
#include <iostream> 
#include <opencv2/core/core.hpp>  
#include <opencv2/highgui/highgui.hpp>  
#include <opencv2/ml/ml.hpp>  
#include <io.h> //查找文件相关函数
#include<opencv2/imgproc/types_c.h>


using namespace std;
using namespace cv;
using namespace ml;

#define PATH  "../../二值车牌/"

#define  TRIAN 1  //0不用训练
/*全局变量*/
void GetAllFiles(string path, vector<string>& files);

ostringstream oss;
int num = -1;
Mat dealimage;
Mat src;
Mat yangben_gray;
Mat yangben_thresh;

int main()
{
	//核心思路://获取一张图片后会将图片特征写入到容器中,
				//紧接着会将标签写入另一个容器中,这样就保证了特征
				//  和标签是一一对应的关系。
	////===============================读取训练数据===============================////

	//训练数据,每一行一个训练图片
	Mat trainingData;
	//训练样本标签
	Mat labels;
	//最终的训练样本标签
	Mat clas;
	//最终的训练数据
	Mat traindata;
	
	/*文件夹下的文件夹名*/
	vector<string>files_name;
	string files_path = PATH;
	GetAllFiles(files_path, files_name);
	//////////////////////从指定文件夹下提取图片//////////////////
#if TRIAN
	for (int num = 0; num < (files_name.size()-1+2); num++)//依次提取0到9文件夹中的图片 减去汉字这个文件夹 加上中间少的两个字母
	{
		oss << files_path;
		int label = num;
		if (num >= 10) {
			label = num + 65 - 10; //以字母对应的十进制数字作为标签
			char Num = num + 65 - 10;
			if (Num == ‘I‘ || Num == ‘O‘) { //车牌没有这两个字母
				oss.str("");
				continue;
			}
			oss <<  Num << "/*.png";//图片名字后缀,oss可以结合数字与字符串
		}
		else {
			oss << num << "/*.png";//图片名字后缀,oss可以结合数字与字符串

		}
		string pattern = oss.str();//oss.str()输出oss字符串,并且赋给pattern
		oss.str("");//每次循环后把oss字符串清空
		vector<String> input_images_name;
		glob(pattern, input_images_name, false);
		//为false时,仅仅遍历指定文件夹内符合模式的文件,当为true时,会同时遍历指定文件夹的子文件夹
		//此时input_images_name存放符合条件的图片地址
		//文件下总共有几个图片
		int all_num = input_images_name.size();

		for (int i = 0; i < all_num; i++)//依次循环遍历每个文件夹中的图片
		{
			cvtColor(imread(input_images_name[i]), yangben_gray, COLOR_BGR2GRAY);//灰度变换
			threshold(yangben_gray, yangben_thresh, 0, 255, THRESH_OTSU);//二值化
			resize(yangben_thresh, yangben_thresh, Size(16, 32), 0, 0);

			//循环读取每张图片并且依次放在vector<Mat> input_images内
			dealimage = yangben_thresh;


			//特征提取的方式有很多,比如LBP,HOG等等
			//我们利用reshape()函数完成特征提取,
			//eshape(1, 1)的结果就是原图像对应的矩阵将被拉伸成一个一行的向量,作为特征向量。 

			/*HOG特征提取*/
			HOGDescriptor detector(Size(16, 32), Size(16, 16), Size(8, 8), Size(8, 8), 9); //向量大小:((16-16)/8+1)*((32-16)/8+1)*2*2*9=108;
			/*临时存放每张图临时生成的一维向量*/
			vector<float>descriptors;
			detector.compute(dealimage, descriptors, Size(0, 0), Size(0, 0));
			trainingData.push_back(static_cast<Mat>(descriptors).reshape(1, 1));//序列化后的图片依次存入  放在下一个row
			labels.push_back(label);

		}
	}
	//图片数据和标签转变下
	trainingData.copyTo(traindata);//复制
	//traindata.convertTo(traindata, CV_32FC1);//更改图片数据的类型,必要,不然会出错
	labels.copyTo(clas);//复制


	////===============================创建SVM模型===============================////
	// 创建分类器并设置参数
	Ptr<SVM> SVM_params = SVM::create();
	SVM_params->setType(SVM::C_SVC);//C_SVC用于分类,C_SVR用于回归
	SVM_params->setKernel(SVM::LINEAR);  //LINEAR线性核函数。SIGMOID为高斯核函数

	SVM_params->setDegree(0);//核函数中的参数degree,针对多项式核函数;
	SVM_params->setGamma(1);//核函数中的参数gamma,针对多项式/RBF/SIGMOID核函数; 
	SVM_params->setCoef0(0);//核函数中的参数,针对多项式/SIGMOID核函数;
	SVM_params->setC(1);//SVM最优问题参数,设置C-SVC,EPS_SVR和NU_SVR的参数;
	SVM_params->setNu(0);//SVM最优问题参数,设置NU_SVC, ONE_CLASS 和NU_SVR的参数; 
	SVM_params->setP(0);//SVM最优问题参数,设置EPS_SVR 中损失函数p的值. 
	//结束条件,即训练1000次或者误差小于0.01结束
	SVM_params->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER + TermCriteria::EPS, 10000, 0.001));

	//训练数据和标签的结合
	Ptr<TrainData> tData = TrainData::create(traindata, ROW_SAMPLE, clas);

	// 训练分类器
	SVM_params->train(tData);//训练

	//保存模型
	SVM_params->save("HOG数字字母svm.xml");
	cout << "训练好了!!!" << endl;

#else
	////===============================预测部分===============================////
	Ptr<SVM> SVM_params = SVM::load("HOG数字字母svm.xml");
#endif

	Mat src = imread("../../二值车牌/7/50.png");
	Mat gray;
	cvtColor(src, gray, COLOR_BGR2GRAY);
	threshold(gray, gray, 0, 255, CV_THRESH_OTSU);
	resize(gray, gray, Size(16, 32), 0, 0);
	imshow("原图像", gray);
	Mat input;
	HOGDescriptor Detector(Size(16, 32), Size(16, 16), Size(8, 8), Size(8, 8), 9); //向量大小:((16-16)/8+1)*((32-16)/8+1)*2*2*9=108;
/*临时存放每张图临时生成的一维向量*/
	vector<float>Descriptors;
	Detector.compute(gray, Descriptors, Size(0, 0), Size(0, 0));
	input.push_back(static_cast<Mat>(Descriptors).reshape(1, 1));//序列化后的图片依次存入  放在下一个row
	//input.convertTo(input, CV_32FC1);//更改图片数据的类型,必要,不然会出错

	float r = SVM_params->predict(input);   //对所有行进行预测

	if (r>9) {
		cout << (char)r << endl;
	}
	else {
		cout << r << endl;

	}
	waitKey(0);
	return 0;
}

/*获得文件夹下的文件夹名称*/
void GetAllFiles(string path, vector<string>& files)
{

	long long   hFile = 0;
	//文件信息  
	struct _finddata_t fileinfo;//用来存储文件信息的结构体  
	string p;
	if ((hFile = _findfirst(p.assign(path).append("/*").c_str(), &fileinfo)) != -1)  //第一次查找
	{
		do
		{
			if ((fileinfo.attrib & _A_SUBDIR))  //如果查找到的是文件夹
			{
				if (strcmp(fileinfo.name, ".") != 0 && strcmp(fileinfo.name, "..") != 0)  //进入文件夹查找
				{
					files.push_back(p.assign(fileinfo.name));

				}
			}
			else //如果查找到的不是是文件夹 
			{
				//files.push_back(p.assign(fileinfo.name));  //将文件路径保存,也可以只保存文件名:    p.assign(path).append("\\").append(fileinfo.name)
			}

		} while (_findnext(hFile, &fileinfo) == 0);

		_findclose(hFile); //结束查找
	}

}

训练结果:

技术分享图片

此时生成一个xml的模型

技术分享图片

训练完成后的两个xml文件放在程序文件夹中

程序总体框架

技术分享图片

技术分享图片

putText.h

#pragma once
#ifndef PUTTEXT_H_
#define PUTTEXT_H_

#include <windows.h>
#include <string>
#include <opencv2/opencv.hpp>

using namespace cv;

void GetStringSize(HDC hDC, const char* str, int* w, int* h);
void putTextZH(Mat& dst, const char* str, Point org, Scalar color, int fontSize,
	const char* fn = "Arial", bool italic = false, bool underline = false);

#endif // PUTTEXT_H_


putText.cpp

#include "putText.h"
#pragma comment(lib,"ws2_32.lib")

void GetStringSize(HDC hDC, const char* str, int* w, int* h)
{
	SIZE size;
	GetTextExtentPoint32A(hDC, str, strlen(str), &size);
	if (w != 0) *w = size.cx;
	if (h != 0) *h = size.cy;
}

void putTextZH(Mat& dst, const char* str, Point org, Scalar color, int fontSize, const char* fn, bool italic, bool underline)
{
	CV_Assert(dst.data != 0 && (dst.channels() == 1 || dst.channels() == 3));

	int x, y, r, b;
	if (org.x > dst.cols || org.y > dst.rows) return;
	x = org.x < 0 ? -org.x : 0;
	y = org.y < 0 ? -org.y : 0;

	LOGFONTA lf;
	lf.lfHeight = -fontSize;
	lf.lfWidth = 0;
	lf.lfEscapement = 0;
	lf.lfOrientation = 0;
	lf.lfWeight = 5;
	lf.lfItalic = italic;   //斜体
	lf.lfUnderline = underline; //下划线
	lf.lfStrikeOut = 0;
	lf.lfCharSet = DEFAULT_CHARSET;
	lf.lfOutPrecision = 0;
	lf.lfClipPrecision = 0;
	lf.lfQuality = PROOF_QUALITY;
	lf.lfPitchAndFamily = 0;
	strcpy_s(lf.lfFaceName, fn);

	HFONT hf = CreateFontIndirectA(&lf);
	HDC hDC = CreateCompatibleDC(0);
	HFONT hOldFont = (HFONT)SelectObject(hDC, hf);

	int strBaseW = 0, strBaseH = 0;
	int singleRow = 0;
	char buf[1 << 12];
	strcpy_s(buf, str);
	char* bufT[1 << 12];  // 这个用于分隔字符串后剩余的字符,可能会超出。
	//处理多行
	{
		int nnh = 0;
		int cw, ch;

		const char* ln = strtok_s(buf, "\n", bufT);
		while (ln != 0)
		{
			GetStringSize(hDC, ln, &cw, &ch);
			strBaseW = max(strBaseW, cw);
			strBaseH = max(strBaseH, ch);

			ln = strtok_s(0, "\n", bufT);
			nnh++;
		}
		singleRow = strBaseH;
		strBaseH *= nnh;
	}

	if (org.x + strBaseW < 0 || org.y + strBaseH < 0)
	{
		SelectObject(hDC, hOldFont);
		DeleteObject(hf);
		DeleteObject(hDC);
		return;
	}

	r = org.x + strBaseW > dst.cols ? dst.cols - org.x - 1 : strBaseW - 1;
	b = org.y + strBaseH > dst.rows ? dst.rows - org.y - 1 : strBaseH - 1;
	org.x = org.x < 0 ? 0 : org.x;
	org.y = org.y < 0 ? 0 : org.y;

	BITMAPINFO bmp = { 0 };
	BITMAPINFOHEADER& bih = bmp.bmiHeader;
	int strDrawLineStep = strBaseW * 3 % 4 == 0 ? strBaseW * 3 : (strBaseW * 3 + 4 - ((strBaseW * 3) % 4));

	bih.biSize = sizeof(BITMAPINFOHEADER);
	bih.biWidth = strBaseW;
	bih.biHeight = strBaseH;
	bih.biPlanes = 1;
	bih.biBitCount = 24;
	bih.biCompression = BI_RGB;
	bih.biSizeImage = strBaseH * strDrawLineStep;
	bih.biClrUsed = 0;
	bih.biClrImportant = 0;

	void* pDibData = 0;
	HBITMAP hBmp = CreateDIBSection(hDC, &bmp, DIB_RGB_COLORS, &pDibData, 0, 0);

	CV_Assert(pDibData != 0);
	HBITMAP hOldBmp = (HBITMAP)SelectObject(hDC, hBmp);

	//color.val[2], color.val[1], color.val[0]
	SetTextColor(hDC, RGB(255, 255, 255));
	SetBkColor(hDC, 0);
	//SetStretchBltMode(hDC, COLORONCOLOR);

	strcpy_s(buf, str);
	const char* ln = strtok_s(buf, "\n", bufT);
	int outTextY = 0;
	while (ln != 0)
	{
		TextOutA(hDC, 0, outTextY, ln, strlen(ln));
		outTextY += singleRow;
		ln = strtok_s(0, "\n", bufT);
	}
	uchar* dstData = (uchar*)dst.data;
	int dstStep = dst.step / sizeof(dstData[0]);
	unsigned char* pImg = (unsigned char*)dst.data + org.x * dst.channels() + org.y * dstStep;
	unsigned char* pStr = (unsigned char*)pDibData + x * 3;
	for (int tty = y; tty <= b; ++tty)
	{
		unsigned char* subImg = pImg + (tty - y) * dstStep;
		unsigned char* subStr = pStr + (strBaseH - tty - 1) * strDrawLineStep;
		for (int ttx = x; ttx <= r; ++ttx)
		{
			for (int n = 0; n < dst.channels(); ++n) {
				double vtxt = subStr[n] / 255.0;
				int cvv = vtxt * color.val[n] + (1 - vtxt) * subImg[n];
				subImg[n] = cvv > 255 ? 255 : (cvv < 0 ? 0 : cvv);
			}

			subStr += 3;
			subImg += dst.channels();
		}
	}

	SelectObject(hDC, hOldBmp);
	SelectObject(hDC, hOldFont);
	DeleteObject(hf);
	DeleteObject(hBmp);
	DeleteDC(hDC);
}

car recognition.cpp

/*
	程序思路:
		1.颜色定位:通过改变亮度,从而得到不同亮度阈值下的颜色区域矩形框;
		2.矩形框处理:先进行初步的筛选,再通过透视透视变换得到颜色矩形的ROI图像
		3.对ROI图像逐一处理:通过白色像素在y方向的投影,由于字母都在同一水平高度,字母所投影到y方向的值就比较大
			再根据阈值进行上下切割;
		4.再对处理后的ROI对7个字符的提取:进行边缘检测,再外接矩形,如果外接矩形的数目少于7个,则一定不是车牌的ROI,
			舍弃处理下一个ROI。大于则按照7字符的特点进行提取
		5.对7个字符进行匹配:HOG+SVM,先用HOG进行特征提取,再加载训练好的模型进行匹配。
	注:文件putText.h和putText.cpp仅仅是为了能在图片中能够显示汉字而加上去的
*/

#include<opencv2/opencv.hpp>
#include<sstream>  //文件操作
#include<iostream>
#include<fstream>
#include<cmath>
#include "putText.h"
#include <time.h>
#include<opencv2/imgproc/types_c.h>

using namespace std;
using namespace cv;
using namespace ml;

/**************全局变量声明****************/

void on_bgrThresh(int, void*);//回调函数
int index = 0;  //将在第几张图片
int indexMax;  //catImage文件夹下的jpg文件张数
vector<Mat>src;  //加载进来的图像集

/*蓝色颜色值变量*/
int h_lower = 100;
int s_lower = 52;
int v_lower = 150;
int h_upper = 120;
int s_upper = 255;
int v_upper = 255;

/*源图*/
Mat srcImg;   //源图

/*读取图片*/
void ReadImage(vector<Mat>& im);
/*将七组字母返回到前七个*/
void selectRect(vector<Rect>rt, vector<Rect>& numRect);
/*输入标签,返回省*/
string Province_Show(int label);

int main() 
{
	ReadImage(src);
	indexMax = src.size() - 1;
	namedWindow("图片序号");
	resizeWindow("图片序号", Size(600, 100));
	createTrackbar("index", "图片序号", &index, indexMax, on_bgrThresh);
	on_bgrThresh(0, 0);

	waitKey(0);
	return 0;
}

/*读取图像*/
void ReadImage(vector<Mat>& im) {
	vector<String>path_name;
	string path = "catImage/*";
	glob(path, path_name, false);
	for (int i = 0; i < path_name.size(); i++) {
		im.push_back(imread(path_name[i]));
	}
}

void on_bgrThresh(int, void*) {

	/*计算识别一张图片的时间*/
	clock_t start, finish;
	double totaltime;
	start = clock();

	int catch_success = 0;//判别车牌识别是否成功 1成功 0失败
	srcImg = src[index].clone(); //得到要处理的源图
	if (srcImg.empty()) {
		cout << "srcImg is empty...." << endl;
	}
	if (srcImg.rows > 900 || srcImg.cols > 900) { //重新调整图像的大小
		int wide = max(srcImg.rows, srcImg.cols);
		resize(srcImg, srcImg, Size(), (double)900 / wide, (double)900 / wide);
	}

	v_lower = 160; //设置亮度阈值,并循环衰减,直到找到车牌或者亮度v_lower过低
	while (v_lower >= 30 && catch_success == 0) {
		v_lower -= 10;

		/*转灰度值*/
		/***************1.hsv颜色定位***************/

		Mat hsv_srcImg;
		Mat bin_HsvImg;
		/*转HSV图像*/
		cvtColor(srcImg, hsv_srcImg, CV_BGR2HSV);
		/*将符合hsv颜色区域的边为白色,其他边黑色*/
		inRange(hsv_srcImg, Scalar(h_lower, s_lower, v_lower), Scalar(h_upper, s_upper, v_upper), bin_HsvImg);
		//inRange可实现二值化功能,可以同时针对多通道进行操作
		/*开运算-闭运算*/
		Mat kernel1 = getStructuringElement(MORPH_RECT, Size(5, 5));//自定义矩形元素
		Mat kernel2 = getStructuringElement(MORPH_RECT, Size(5, 5));
		morphologyEx(bin_HsvImg, bin_HsvImg, MORPH_CLOSE, kernel1);
		morphologyEx(bin_HsvImg, bin_HsvImg, MORPH_OPEN, kernel2);

		/*轮廓检测*/
		vector<vector<Point>>ColorContours;
		findContours(bin_HsvImg, ColorContours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
		/*最小外接矩形--符合车牌形状的矩形*/
		vector<RotatedRect>ColorRect;
		vector<int>carLabel;	/*用来标签车牌矩形*/
		for (int i = 0; i < ColorContours.size(); i++) {
			RotatedRect rt;
			rt = minAreaRect(ColorContours[i]);
			double wide, hiegh;
			wide = max(rt.size.width, rt.size.height);
			hiegh = min(rt.size.width, rt.size.height);
			/*符合车牌特征的筛选*/
			if (wide * hiegh > 300 && wide * hiegh < srcImg.rows * srcImg.cols / 4 && wide / hiegh > 2 && wide / hiegh < 6) {
				ColorRect.push_back(rt);
			}
		}

#ifdef SHOW
		/*画矩形*/
			/*寻找最外轮廓*/
		Mat showImg;
		showImg = srcImg.clone();
		for (int i = 0; i < ColorRect.size(); i++) {
			Point2f rect_point[4];
			ColorRect[i].points(rect_point);
			for (int j = 0; j < 4; j++) {
				line(showImg, rect_point[j], rect_point[(j + 1) % 4], Scalar(0, 255, 0), 2);
			}
		}


		imshow("颜色轮廓", bin_HsvImg);
		imshow("源图", showImg);
		waitKey(0);

#endif // SHOW

		/*透视变换*/
		vector<Mat>ColorROI(ColorRect.size());//通过矩形大小初步筛选的图

		for (int i = 0; i < ColorRect.size(); i++) {
			Point2f src_vertices[4]; //存放4个有顺序的点
			Point2f midpts;  //调整点的中间变量
			ColorRect[i].points(src_vertices);
			/*调整顺序*/
			/*顺序---左上-右上-右下-左下*/
			for (int k = 0; k < 4; k++) {  //先按y值从上到下排列
				for (int l = k; l < 4; l++) {
					if (src_vertices[k].y >= src_vertices[l].y) {
						midpts = src_vertices[k];
						src_vertices[k] = src_vertices[l];
						src_vertices[l] = midpts;
					}
				}
			}
			/*判断最上面那两个点是不是最长边*/
			if (pow(abs(src_vertices[0].x - src_vertices[1].x), 2) < pow(abs(src_vertices[0].x - src_vertices[2].x), 2)) {
				midpts = src_vertices[1];
				src_vertices[1] = src_vertices[2];
				src_vertices[2] = midpts;
			}
			if (src_vertices[0].x > src_vertices[1].x) {
				midpts = src_vertices[1];
				src_vertices[1] = src_vertices[0];
				src_vertices[0] = midpts;
			}
			if (src_vertices[2].x < src_vertices[3].x) {
				midpts = src_vertices[2];
				src_vertices[2] = src_vertices[3];
				src_vertices[3] = midpts;
			}

			Point2f dst_vertices[4];
			double carWidth = max(ColorRect[i].size.width, ColorRect[i].size.height);
			double carHeigh = min(ColorRect[i].size.width, ColorRect[i].size.height);

			dst_vertices[0] = Point(0, 0);
			dst_vertices[1] = Point(carWidth, 0);
			dst_vertices[2] = Point(carWidth, carHeigh);
			dst_vertices[3] = Point(0, carHeigh);
			Mat H = getPerspectiveTransform(src_vertices, dst_vertices);
			ColorROI[i] = Mat(Size(carWidth, carHeigh), CV_8UC3);
			warpPerspective(srcImg, ColorROI[i], H, ColorROI[i].size(), 1);  //是直接在源图上得到ROI
		}


		vector<Mat>new_colorROI; //通过边缘检测和模型匹配筛出,最有可能是车牌的图
		/*寻找颜色矩形里面最有可能的车牌*/
		for (int i = 0; i < ColorROI.size(); i++) {
			Mat gray;
			cvtColor(ColorROI[i], gray, CV_BGR2GRAY);

			/****************基于边缘匹配************************/
			Mat edegImg;
			Canny(gray, edegImg, 100, 150, 3);
			threshold(edegImg, edegImg, 0, 255, THRESH_BINARY | THRESH_OTSU);
			/*闭操作*/
			Mat element = getStructuringElement(MORPH_RECT, Size(ColorROI[i].cols / 6, 1));
			morphologyEx(edegImg, edegImg, MORPH_CLOSE, element, Point(-1, -1));
			/*寻找轮廓,找外接矩形*/
			vector<vector<Point>>edgeContours;
			findContours(edegImg, edgeContours, RETR_EXTERNAL, CHAIN_APPROX_NONE);
			for (int j = 0; j < edgeContours.size(); j++) {
				Rect rt;
				rt = boundingRect(edgeContours[j]);
				if (rt.width > ColorROI[i].cols * 0.8 && rt.height > ColorROI[i].rows * 0.8) {
					new_colorROI.push_back(ColorROI[i]);
					carLabel.push_back(i);
					break;
				}
			}
		}
		if (new_colorROI.size() == 0) {
			continue;
		}
		/***********************对车牌ROI上下切割****************/
		for (int num = 0; num < new_colorROI.size(); num++) {
			/*统一大小*/
			int ratio = 6000 * new_colorROI[num].cols / 126;  //用来筛选字母上下限的值  //4000
			Mat gray_carImg, bin_carImg;  //Size(126,40)
			cvtColor(new_colorROI[num], gray_carImg, CV_BGR2GRAY);
			threshold(gray_carImg, bin_carImg, 0, 255, THRESH_OTSU);
			/********************进行x方向的投影切除字母上下多余的部分*****************************/
			Mat RowSum;
			reduce(bin_carImg, RowSum, 1, CV_REDUCE_SUM, CV_32SC1);
			Point  cutNumY; //Y方向的切割点
			int sign = 0;
			int Lenth = 0;
			for (int j = 0; j < RowSum.rows; j++) {
				if (RowSum.ptr<int>(0)[0] > ratio && sign == 0) {
					while (RowSum.ptr<int>(Lenth)[0] >= ratio) {
						Lenth++;
						if (Lenth == RowSum.rows - 1) {
							break;
						}
					}
					if (Lenth > RowSum.rows * 0.6) { //以字母开始
						cutNumY = Point(0, Lenth);
						break;
					}
					else {
						sign = 1;//开始分割字
						j = Lenth;
						Lenth = 0;
					}
				}
				else {
					sign = 1;
				}
				if (RowSum.ptr<int>(j)[0] > ratio && sign == 1) {
					Lenth++;
					if (j == RowSum.rows - 1) {
						if (Lenth > RowSum.rows * 0.6) {
							cutNumY = Point(j - Lenth, j);
						}
						else {
							cutNumY = Point(RowSum.rows * 0.15, RowSum.rows * 0.85);
						}
					}
				}
				else if (RowSum.ptr<int>(j)[0] <= ratio && sign == 1) {
					if (Lenth > RowSum.rows * 0.6) {
						cutNumY = Point(j - Lenth, j);
						break;
					}
					else if (j == RowSum.rows - 1) {
						cutNumY = Point(RowSum.rows * 0.15, RowSum.rows * 0.85);
					}
					else {
						Lenth = 0;
					}
				}
			}
			/*通过车牌在y方向的映射分离分割每个字母*/

			Mat Newbin_carImg; //字母上下被切割后的图像
			Newbin_carImg = bin_carImg(Rect(Point(0, cutNumY.x), Point(bin_carImg.cols, cutNumY.y)));
#ifdef SHOW
			imshow("上下切割后", Newbin_carImg);
			waitKey(0);
#endif // SHOW

			/**********************七个字符分割*******************/
			/*轮廓检测进行分割*/
			Mat midbin_carImg = Mat::zeros(Size(Newbin_carImg.cols + 4, Newbin_carImg.rows + 4), CV_8UC1); //宽增加4个像素点
			for (int row = 2; row < midbin_carImg.rows - 2; row++) {
				for (int col = 2; col < midbin_carImg.cols - 2; col++) {
					midbin_carImg.ptr<uchar>(row)[col] = Newbin_carImg.ptr<uchar>(row - 2)[col - 2];
				}
			}
			vector<vector<Point>>Num_contours;
			findContours(midbin_carImg, Num_contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
			/*最小外接矩形*/
			vector<Rect>num_rect;
			for (int k = 0; k < Num_contours.size(); k++) {
				num_rect.push_back(boundingRect(Num_contours[k]));
			}
#ifdef SHOW
			Mat img;
			img = midbin_carImg.clone();
			for (int i = 0; i < num_rect.size(); i++) {
				rectangle(img, num_rect[i], Scalar(255), -1);
			}
			imshow("字母矩形", img);
			waitKey(0);
#endif // SHOW

			/*矩形框的处理*/
			vector<Rect>real_numRect;//存放七个字母的矩形
			if (num_rect.size() < 7) {
				continue;
			}
			selectRect(num_rect, real_numRect);
			if (real_numRect.size() >= 7) {
				cout << "7个字母提取成功..." << endl;
			}
			else {
				continue;
			}

			/*防止第七个越界*/
			if (real_numRect[6].x + real_numRect[6].width > midbin_carImg.cols) {
				real_numRect[6].width = real_numRect[6].x + real_numRect[6].width - midbin_carImg.cols;
			}
			/*车牌从左到右字符图像*/
			Mat NumImg[7];
			for (int i = 0; i < 7; i++) {
				NumImg[i] = midbin_carImg(Rect(real_numRect[i].x, real_numRect[i].y, real_numRect[i].width, real_numRect[i].height));
			}
#ifdef SHOW
			for (int i = 0; i < 7; i++) {
				imshow("字母", NumImg[i]);
				waitKey(0);

			}
#endif // SHOW


			ostringstream carnumber;
			string charater;


			/*汉字*/
			Ptr<SVM> SVM_paramsH = SVM::load("HOG字svm.xml");

			Mat Input;
			Mat bin_charater;
			resize(NumImg[0], bin_charater, Size(16, 32), 0, 0);
			HOGDescriptor testDetector(Size(16, 32), Size(16, 16), Size(8, 8), Size(8, 8), 9);
			vector<float>testDescriptor;
			testDetector.compute(bin_charater, testDescriptor, Size(0, 0), Size(0, 0));
			Input.push_back(static_cast<Mat>(testDescriptor).reshape(1, 1));

			int r = SVM_paramsH->predict(Input);   //对所有行进行预测
			charater = Province_Show(r);
			cout << "识别结果:" << Province_Show(r) << endl;


			/*数字字母识别*/
			Ptr<SVM> SVM_paramsZ = SVM::load("HOG数字字母svm.xml");
			for (int i = 1; i < 7; i++) {
				Mat bin_num = Mat::zeros(Size(16, 32), CV_8UC1);
				Mat midBin_num;
				resize(NumImg[i], bin_num, Size(16, 32), 0, 0);
				Mat input;
				HOGDescriptor Detector(Size(16, 32), Size(16, 16), Size(8, 8), Size(8, 8), 9);
				vector<float>Descriptors;
				Detector.compute(bin_num, Descriptors, Size(0, 0), Size(0, 0));
				input.push_back(static_cast<Mat>(Descriptors).reshape(1, 1));//序列化后的图片依次存入  放在下一个row
				float r = SVM_paramsZ->predict(input);   //对所有行进行预测
				/*对0和D再进行区分*/
				if (r == 0 || r == ‘D‘) {
					if (bin_num.ptr<uchar>(0)[0] == 255 || bin_num.ptr<uchar>(31)[0] == 255)
						r = ‘D‘;
					else
						r = 0;
				}
				if (r > 9) {
					cout << "识别结果" << (char)r << endl;
					carnumber << (char)r;

				}
				else {
					cout << "识别结果" << r << endl;
					carnumber << r;
				}

			}
			/*在源图中显示车牌号码*/
			charater = charater + carnumber.str();
			putTextZH(srcImg,
				&charater[0],
				Point(ColorRect[carLabel[num]].boundingRect().x, abs(ColorRect[carLabel[num]].boundingRect().y - 30)),
				Scalar(0, 0, 255), 30, "宋体");
			Point2f pts[4];
			ColorRect[carLabel[num]].points(pts);
			for (int j = 0; j < 4; j++) {
				line(srcImg, pts[j], pts[(j + 1) % 4], Scalar(0, 255, 0), 2);
			}
			catch_success = 1; //成功提取
		}
	}
	finish = clock();
	totaltime = (double)(finish - start) / CLOCKS_PER_SEC;
	cout << "\n此程序的运行时间为" << totaltime << "秒!" << endl;
	namedWindow("result", WINDOW_NORMAL);
	imshow("result", srcImg);

}

/*输入标签,返回省*/
string Province_Show(int label) {
	string pv = "";
	switch (label)
	{
	case 0:pv = "皖";
		break;
	case 1:pv = "京";
		break;
	case 2:pv = "渝";
		break;
	case 3:pv = "闽";
		break;
	case 4:pv = "甘";
		break;
	case 5:pv = "粤";
		break;
	case 6:pv = "桂";
		break;
	case 7:pv = "贵";
		break;
	case 8:pv = "琼";
		break;
	case 9:pv = "翼";
		break;
	case 10:pv = "黑";
		break;
	case 11:pv = "豫";
		break;
	case 12:pv = "鄂";
		break;
	case 13:pv = "湘";
		break;
	case 14:pv = "苏";
		break;
	case 15:pv = "赣";
		break;
	case 16:pv = "吉";
		break;
	case 17:pv = "辽";
		break;
	case 18:pv = "蒙";
		break;
	case 19:pv = "宁";
		break;
	case 20:pv = "青";
		break;
	case 21:pv = "鲁";
		break;
	case 22:pv = "晋";
		break;
	case 23:pv = "陕";
		break;
	case 24:pv = "川";
		break;
	case 25:pv = "津";
		break;
	case 26:pv = "新";
		break;
	case 27:pv = "云";
		break;
	case 28:pv = "浙";
		break;
	default:
		break;
	}
	return pv;
}

/*将七组字母返回到前七个*/
void selectRect(vector<Rect>rt, vector<Rect>& numRect) {
	numRect.resize(2);
	Rect mid;
	/*按面积排顺序*/
	int averArea;
	int averWidth;//平均宽度
	int averHieght;//平均高度
	for (int i = 0; i < rt.size(); i++) {
		for (int j = i; j < rt.size(); j++) {
			if (rt[i].area() <= rt[j].area()) {
				mid = rt[i];
				rt[i] = rt[j];
				rt[j] = mid;
			}
		}
	}
	averHieght = (rt[0].height + rt[1].height) / 2;
	averWidth = (rt[0].width + rt[1].width) / 2;
	averArea = (rt[0].area() + rt[1].area()) / 2;
	/*计算更准确的平均值*/
	int new_averHieght = 0;
	int new_averWidth = 0;
	int new_averArea = 0;
	int add = 0; //个数
	for (int q = 0; q < rt.size(); q++) {
		if (rt[q].area() > 0.4 * averArea && rt[q].height > averHieght * 0.7) {
			new_averHieght += rt[q].height;
			new_averWidth += rt[q].width;
			new_averArea += rt[q].area();
			add++;
		}
	}//add不可能是0
	new_averHieght /= add;
	new_averWidth /= add;
	new_averArea /= add;

	/*按x坐标排顺序*/
	for (int i = 0; i < rt.size(); i++) {
		for (int j = i; j < rt.size(); j++) {
			if (rt[i].x >= rt[j].x) {
				mid = rt[i];
				rt[i] = rt[j];
				rt[j] = mid;
			}
		}
	}

	/*判断矩形的位置*/
	int sum_one = 0; //用来计算一的个数
	Rect ForSrect; //第一或第二个字母  如果第二个字母是1就很有可能出错
	int taltol = 0; //ForSrect前面的面积总和
	int minX = rt[0].x; //左边不为0的最小值
	for (int h = 0; h < rt.size() - 4; h++) {
		if (rt[h].x <= 2 + new_averWidth / 20) { //将矩形框边缘的白色像素去掉
			minX = rt[h + 1].x;
			continue;
		}
		taltol += rt[h].area();
		/*判断是否是川*/
		if (rt[h].area() < new_averArea * 0.5 && rt[h].height > new_averHieght * 0.60) {
			sum_one++;
		}
		if (rt[h].area() > new_averArea * 0.8) {  //找到大的字母停止for循环
			ForSrect = rt[h];
			taltol -= ForSrect.area();
			if (taltol > new_averArea * 0.4 || sum_one >= 3) {
				/*第二个*/
				numRect[1] = ForSrect;
				/*求第一个*/
				/*水平方向*/
				int maxX = rt[h - 1].x + rt[h - 1].width;
				/*竖直方向*/
				int minY = 1000;
				int maxY = 0;
				for (int w = 0; w < h; w++) {
					if (minY >= rt[w].y)
						minY = rt[w].y;
					if (maxY <= rt[w].y + rt[w].height)
						maxY = rt[w].y + rt[w].height;
				}

				numRect[0] = Rect(Point(minX, minY), Point(maxX, maxY));
			}
			else {
				/*第一个*/
				numRect[0] = ForSrect;
				/*求第二个*/
				h++;
				while (rt[h].area() < new_averArea * 0.7 && h < (rt.size() - 2)) { //若是以h!= rt.size()-2条件退出循环,分出的字符定时少于7个
					h++;
				}
				numRect[1] = rt[h];
			}
			h++;
			for (int d = h; d < rt.size(); d++) {
				if (rt[d].height > new_averHieght * 0.6) { //有一定的高度
					if (rt[d].width < new_averWidth * 0.5) {
						rt[d].x = rt[d].x - (new_averWidth - rt[d].width) / 2;
						rt[d].width = new_averWidth;
					}
					numRect.push_back(rt[d]);
				}
			}
			break;
		}
	}

}

结果测试

选取25张车牌图像进行测试
技术分享图片

车牌识别测试图像集

技术分享图片

车牌识别(测试图像1)

技术分享图片

车牌识别(测试图像2)

技术分享图片

车牌识别(测试图像3)

技术分享图片

车牌识别(测试图像4)

技术分享图片

车牌识别(测试图像5)

技术分享图片

车牌识别(测试图像6)

技术分享图片

车牌识别(测试图像7)

技术分享图片

车牌识别(测试图像8)

技术分享图片

车牌识别(测试图像9)

技术分享图片

车牌识别(测试图像10)

技术分享图片

车牌识别(测试图像11)

技术分享图片

车牌识别(测试图像12)

技术分享图片

车牌识别(测试图像13)

技术分享图片

车牌识别(测试图像14)

技术分享图片

车牌识别(测试图像15)

技术分享图片

车牌识别(测试图像16)

技术分享图片

车牌识别(测试图像17)

技术分享图片

车牌识别(测试图像18)

技术分享图片

车牌识别(测试图像19)

技术分享图片

车牌识别(测试图像20)

技术分享图片

车牌识别(测试图像21)

技术分享图片

车牌识别(测试图像22)

技术分享图片

车牌识别(测试图像23)

技术分享图片

车牌识别(测试图像24)

技术分享图片

车牌识别(测试图像25)

C++-车牌识别

原文:https://www.cnblogs.com/jgg54335/p/14711349.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!