基于OpenCV的车牌识别(1. 车牌图像识别)

本文和下文用于开源项目OpenCV的车牌识别的学习!!!

车牌识别的过程可分为四步:1)车牌图像切割;2)车牌图像分类;3)车牌字符切割;4)车牌字符分类;

1)车牌图像切割: 

a.将原图转化为灰度图,可去除多通道产生的外界噪声;

b. sobel滤波 ,车牌分割的一个重要特征是车牌中的垂直边缘比较多,为了提取垂直边缘,采用sobel一阶垂直方向导数;

c. 阈值化处理,应用一个OSTU法自动获取阈值滤波器来获得一个二值图像;

d. 闭运算形态学 ,连接含有边缘数量很多的所有区域,删除边缘之间的空白区域,将车牌区域连接起来;

e. 漫水填充, 所有的车牌都有统一的背景颜色。使用漫水填充算法来获取旋转矩阵的精确修剪。漫水填充函数用颜色把连通区域填充到掩码图像,填充从种子开始。填充的像素点都是与种子点进行比较,如果像素值为x,seed-low<=x<=seed+up,则该位置将被填充。一旦得到了用来剪切的掩码图像,进而可得到掩码图像点的最小外接矩形,再次检查矩形大小。对于每一个掩码,白色像素获得位置用minAreaRect函数重新得到最相近的修剪区域;

f. 仿射变换,用来去掉检测矩形区域的旋转;

g. 提取矩形;

h. 调整为统一大小,直方图均衡化,提取的矩形图像不能很好的在训练和分类中使用,因为他们没有相同的大小。并且,每个图像包含不同的光照条件,增加了他们之间的差别。

/* imageSlicer.cpp */
#include <iostream>
#include <opencv2/opencv.hpp>
#include <time.h>

using namespace std;
using namespace cv;

//筛选旋转矩形的面积和宽高比
bool areaDetection(RotatedRect rectArea)
{
	float error = 0.4;//允许错误率
	const float width_height = 4.7272;//获得西班牙车牌的宽高比 57200

	int min_area = 25 * 25 * width_height;//获得允许矩形的最大最小面积
	int max_area = 100 * 100 * width_height;

	float min_value = width_height*(1 - error);//获得允许矩形的最大最小宽高比 1.890-6.6180
	float max_value = width_height*(1 + error);

	int rect_area = rectArea.size.width*rectArea.size.height;//计算可旋转矩形的包围面积和宽高比
	float rect_value = rectArea.size.width / rectArea.size.height;

	rect_value = rect_value<1 ? 1 / rect_value : rect_value;

	return rect_area>min_area && rect_area<max_area && rect_value>min_value && rect_value<max_value;
}

void imgProcess(Mat carImg)
{
	//1.获得灰度图
	Mat grayImg, blurImg, sobelImg, threshImg, mopImg;
	cvtColor(carImg, grayImg, COLOR_BGR2GRAY);
	//imshow("灰度图", grayImg);

	//2.均值滤波
	/*void blur(InputArray src, OutputArray dst, Size ksize, Point anchor=Point(-1,-1),
	int borderType=BORDER_DEFAULT )*/
	blur(grayImg, blurImg, Size(5, 5));//均值滤波
	//imshow("均值滤波", blurImg);

	//3.sobel边缘检测
	/*void Sobel(InputArray src, OutputArray dst, int ddepth, int dx, int dy, int ksize=3, double scale=1,
	double delta=0, int borderType=BORDER_DEFAULT )*/
	Sobel(blurImg, sobelImg, CV_8U, 1, 0);//sobel算子边缘检测 只对x方向求一阶导
	//imshow("sobel边缘检测", sobelImg);

	//4.二值化,OTSU算法自动获取阈值获得二值化图像
	/*double threshold(InputArray src, OutputArray dst, double thresh, double maxval, int type)*/
	threshold(sobelImg, threshImg, 0, 255, CV_THRESH_OTSU+CV_THRESH_BINARY);
	//imshow("图像二值化", threshImg);

	//5.闭运算,删除边缘之间的空白区域,将车牌区域连接起来
	/*Mat getStructuringElement(int shape, Size ksize, Point anchor=Point(-1,-1))*/
	Mat element = getStructuringElement(MORPH_RECT, Size(17, 3));//定义结构元素
	/*void morphologyEx(InputArray src, OutputArray dst, int op, InputArray kernel, Point anchor=Point(-1,-1),
	int iterations=1, int borderType=BORDER_CONSTANT, const Scalar& borderValue=morphologyDefaultBorderValue() )*/
	morphologyEx(threshImg, mopImg, CV_MOP_CLOSE, element);
	//imshow("闭运算", mopImg);

	//6.寻找所有轮廓,然后用面积和宽高比筛选
	vector<vector<Point>> contours;//定义轮廓,每个轮廓是一个点集
	/*void findContours(InputOutputArray Img, OutputArrayOfArrays contours, OutputArray hierarchy,
	int mode, int method, Point offset=Point())*/
	findContours(mopImg, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);//检测最外层轮廓
	map<int, RotatedRect> mapCounter;//定义map容器,存储筛选后的矩形轮廓
	for (int i = 0; i < contours.size(); ++i)
	{
		/*void drawContours(InputOutputArray Img, InputArrayOfArrays contours, int contourIdx, const Scalar& color,
		int thickness=1, int lineType=8, InputArray hierarchy=noArray(), int maxLevel=INT_MAX, Point offset=Point() )*/
		drawContours(mopImg, contours, i, Scalar(255), 1);//绘制图像轮廓

		/*RotatedRect minAreaRect(InputArray points)*/
		RotatedRect contourRect = minAreaRect(contours[i]);//给定2D点集,寻找最小面积点集的包围矩形
		Point2f vertices[4];//定义矩形的四个顶点
		contourRect.points(vertices);
		for (int j = 0; j < 4; ++j)
		{
			line(mopImg, vertices[j], vertices[(j + 1) % 4], Scalar(255), 2);//绘制包围轮廓的矩形
		}
		if (areaDetection(contourRect))
			mapCounter[i] = contourRect;
	}

	//imshow("矩形包围所有轮廓", mopImg);
	cout << "轮廓总数: " << contours.size() << endl;
	cout << "筛选后的轮廓数量:" << mapCounter.size() << endl;

	//获得map存储筛选后矩形轮廓的起始地址用于遍历,继续筛选
	map<int, RotatedRect>::iterator it = mapCounter.begin();
	while (it != mapCounter.end())
	{
		//7.绘制通过面积和宽高比筛选的矩形
		//RotatedRect contourRect = it->second;//获得键对应的值
		//Point2f vertices[4];//定义四个顶点
		//contourRect.points(vertices);
		//for (int j = 0; j < 4; ++j)
		//{
		//	line(mopImg, vertices[j], vertices[(j + 1) % 4], Scalar(255), 5);
		//}
		//++it;

		//8.漫水填充矩形轮廓
		RotatedRect contourRect = it->second;//获得键对应的值
		/*void circle(Mat& img, Point center, int radius, const Scalar& color,
		int thickness=1, int lineType=8, int shift=0)*/
		circle(mopImg, contourRect.center, 3, Scalar(255), -1);//绘制矩形中心

		float minSize = (contourRect.size.width < contourRect.size.height) ? contourRect.size.width : contourRect.size.height;
		minSize = minSize*0.5;//设置随机种子的范围

		srand(time(NULL));
		int seedNum = 10;//设置每个掩码图像的随机种子数
		Mat mask;//绘制新的掩码图像
		mask.create(carImg.rows + 2, carImg.cols + 2, CV_8UC1);
		mask = Scalar::all(0);
		Scalar newVal = Scalar::all(255);//填充颜色
		Rect ccomp;//重绘最小矩形区域
		Scalar loDiff(30, 30, 30);//亮度或颜色负差的最大值
		Scalar upDiff(30, 30, 30);//亮度或颜色正差的最大值
		//只考虑当前像素水平和垂直方向的像素+白色填充值为255(忽略newVal)+考虑当前像素与种子像素的差+填充掩码图像
		int flags = 4 + (255 << 8) + FLOODFILL_FIXED_RANGE + FLOODFILL_MASK_ONLY;
		for (int j = 0; j < seedNum; ++j)
		{
			/*int floodFill(InputOutputArray Img, InputOutputArray mask, Point seedPoint, Scalar newVal,
			Rect* rect=0, Scalar loDiff=Scalar(), Scalar upDiff=Scalar(), int flags=4 )*/
			Point point;
			point.x = contourRect.center.x + rand() % (int)minSize - (int)(minSize / 2);//获得矩形区域中心附近的点作为种子(黄色)
			point.y = contourRect.center.y + rand() % (int)minSize - (int)(minSize / 2);
			circle(mopImg, point, 1, Scalar(255), -1);//绘制种子
			int area = floodFill(carImg, mask, point, newVal, &ccomp, loDiff, upDiff, flags);
		}
		//imshow("漫水填充图像", mopImg);
		//imshow("掩码图像", mask);

		//9.通过每个图像掩码获得最小面积的包围矩形
		vector<Point> pointInterest;
		Mat_<uchar>::iterator itMask = mask.begin<uchar>();//Mat的轻量级数据类型Mat_,需指定类型
		for (; itMask != mask.end<uchar>(); ++itMask)
		{
			if (*itMask == 255)
				pointInterest.push_back(itMask.pos());//保存白点的坐标
		}
		RotatedRect minRect = minAreaRect(pointInterest);//给定2D点集,寻找最小面积的包围矩形

		if (areaDetection(minRect))
		{
			//10.绘制通过继续筛选的矩形
			Point2f minRectPoints[4];
			minRect.points(minRectPoints);
			for (int k = 0; k < 4; k++)
				line(mopImg, minRectPoints[k], minRectPoints[(k + 1) % 4], Scalar(255));

			//11.对原图仿射变换
			float width_height = (float)minRect.size.width / (float)minRect.size.height;
			float angle = minRect.angle;
			if (width_height < 1)//处理图像中旋转角度大于90度的车牌
				angle = angle + 90;
			Mat rotMat = getRotationMatrix2D(minRect.center, angle, 1);//获得矩形的旋转矩阵
			Mat warpImg;
			/*void warpAffine(InputArray src, OutputArray dst, InputArray M, Size dsize,
			int flags=INTER_LINEAR, int borderMode=BORDER_CONSTANT, const Scalar& borderValue=Scalar())*/
			warpAffine(carImg, warpImg, rotMat, carImg.size(), INTER_CUBIC);
			//imshow("原图仿射变换", warpImg);

			//12.图像切割
			Size minRectSize = minRect.size;
			if (width_height < 1)
				swap(minRectSize.width, minRectSize.height);
			Mat plateImg;
			/*void getRectSubPix(InputArray Img, Size patchSize, Point2f center,
			OutputArray patch, int patchType=-1 )*/
			getRectSubPix(warpImg, minRectSize, minRect.center, plateImg);
			//imshow("原图车牌", plateImg);

			//13.调整车牌图像大小为标准33*144
			Mat resizeImg;
			resizeImg.create(33, 144, CV_8UC3);
			/*void resize(InputArray src, OutputArray dst, Size dsize, double fx=0,
			double fy=0, int interpolation=INTER_LINEAR )*/
			resize(plateImg, resizeImg, resizeImg.size(), 0, 0, INTER_CUBIC);
			imshow("33*144车牌", resizeImg);
			imwrite("plateOri.jpg", resizeImg);

			//14.直方图均衡化
			Mat histImg;
			cvtColor(resizeImg, histImg, COLOR_BGR2GRAY);
			blur(histImg, histImg, Size(3, 3));
			equalizeHist(histImg, histImg);
			imshow("直方图均衡化车牌", histImg);
			imwrite("plate.jpg", histImg);
		}
		++it;
	}
}

2)车牌图像分类

a. SVM

/* imageClassification.cpp */
#include <iostream>
#include <opencv2/opencv.hpp>

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

void SVMClassifier(Mat srcImg)
{
	Mat plateImg;
	srcImg.convertTo(plateImg, CV_32FC1);
	plateImg = plateImg.reshape(1, 1);//将图像转换为1行m列特征,自动计算列数

	FileStorage fs;
	fs.open("SVM.xml", FileStorage::READ);//打开xml文件

	//1.获得训练数据
	Mat trainMat, classesMat;
	fs["TrainingData"] >> trainMat;//读取训练数据集和类标签
	fs["classes"] >> classesMat;
	Ptr<TrainData> trainData = TrainData::create(trainMat,ROW_SAMPLE,classesMat);

	//2.创建分类器,设置参数
	SVM::ParamTypes param;//设置SVM参数
	SVM::KernelTypes kernel = SVM::LINEAR;
	Ptr<SVM> svm = SVM::create();
	svm->setKernel(kernel);

	//3.训练分类器
	svm->trainAuto(trainData);

	//4.预测
	int result = svm->predict(plateImg);
	cout << "预测结果: " << result << endl;
}


猜你喜欢

转载自blog.csdn.net/attitude_yu/article/details/79977113