基于OpenCV的车牌识别(2. 车牌字符识别)

3)车牌字符切割

a. 阈值滤波,使用CV_THRESH_BINARY参数通过把白色值变为黑色,黑色值变为白色来实现阈值输出的反转,因为需要获取字符的轮廓,而轮廓的算法寻找的是白色像素;

b. 查找轮廓;

c. 验证轮廓是否为字符,去除那些规格太小的或者宽高比不正确的区域。字符是45/77的宽高比,允许0.35的误差。如果一个区域面积高于80%(就是像素大于0的超过80%),则认为这个区域是一个黑色块,不是字符。

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

using namespace std;
using namespace cv;

//包围字符的矩形筛选
bool charDetection(Mat rect)
{
	float error = 0.35;
	const float width_height = 45.0 / 77.0;
	float char_width_height = (float)rect.cols / (float)rect.rows;

	float min_value = 0.2;
	float max_value = width_height*(1 + error);

	float min_height = 20;
	float max_height = 30;

	int pixels = countNonZero(rect);
	float area = rect.cols*rect.rows;
	float ratio = pixels / area;//获得矩形区域黑色所占的比例

	return ratio<0.8&&char_width_height>min_value&&char_width_height < max_value
		&&rect.rows >= min_height && rect.rows <= max_height;
}

void plateChar(Mat srcImg)
{
	//1. 二值化图像,像素值大于60设为0,反之设为255
	Mat plateImg;
	threshold(srcImg, plateImg, 60, 255, CV_THRESH_BINARY_INV);
	//imshow("反转图像", plateImg);

	//2. 寻找轮廓,findContours函数会改变输入图像,可对其clone
	Mat contoursImg = plateImg.clone();
	vector<vector<Point>> contours;//定义轮廓,每个轮廓是一个点集
	/*void findContours(InputOutputArray Img, OutputArrayOfArrays contours, OutputArray hierarchy,
	int mode, int method, Point offset=Point())*/
	findContours(contoursImg, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
	cout << "轮廓数量:" << contours.size() << endl;

	//3. 获得各个字符图像的外部矩形边界,在原图中找出字符
	Mat oriImg = imread("plateOri.jpg");
	for (int i = 0; i < contours.size(); i++)
	{
		drawContours(oriImg, contours, i, Scalar(0, 255, 0), 1);//绿色填充所有轮廓

		Rect rect = boundingRect(contours[i]);//获得字符轮廓点集的外部矩形边界
		rect.height += 1;//切割较大的图像
		rect.width += 1;
		/*void rectangle(Mat& img, Rect rec, const Scalar& color, int thickness=1, int lineType=8,
		int shift=0 )*/
		rectangle(oriImg, rect, Scalar(0, 0, 255), 1);//红色绘制所有轮廓的矩形

		Mat charImg(plateImg, rect);//在图像中分割rect区域
		if (charDetection(charImg))//判断rect区域是否是字符
		{
			cout << "字符的width--height: " << rect.width << "--" << rect.height << endl;
			rectangle(oriImg, rect, Scalar(255, 0, 0), 1);//蓝色绘制包含字符的矩形
			//imshow("字符", charImg);
			//imwrite(to_string(i) + ".jpg", charImg);
		}
	}
	imshow("矩形包围字符", oriImg);
}

4)车牌字符分类

a. 提取预测字符的特征,创建字符特征矩阵(水平和垂直累积直方图、低分辨率图像5*5)创建一个M列的矩阵,矩阵的每一行的每一列都是特征值水平累加直方图构造的特征,垂直方向构造的特征,低分辨图像构造的特征);

b.人工神经网络ANN分类。

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

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

const int HORIZONTAL = 1;
const int VERTICAL = 0;
const int numChar = 30;
const char strCharacters[numChar] = {
	'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
	'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M',
};

//获得水平或者垂直方向的累加直方图
Mat projectedHistogram(Mat img, int flag)
{
	int num = (flag) ? img.rows : img.cols;//获得图像的最大边长,若取行数
	Mat phist = Mat::zeros(1, num, CV_32F);//创建矩阵,存储每行的非0像素数量

	for (int i = 0; i < num; i++)
	{
		Mat data = (flag) ? img.row(i) : img.col(i);
		phist.at<float>(i) = countNonZero(data);
	}

	double min, max;
	/*void minMaxLoc(InputArray src, double* minVal, double* maxVal=0,
	Point* minLoc=0, Point* maxLoc=0, InputArray mask=noArray())*/
	minMaxLoc(phist, &min, &max);//获得各行的非0像素数量最大值

	if (max > 0)
	{
		/*src.converTo(dst, type, scale, shift)*/
		phist.convertTo(phist, -1, 1.0 / max, 0);//缩放为小数
		return phist;
	}
}

//创建特征矩阵
Mat featureMat(Mat img, int size)
{
	Mat hhist = projectedHistogram(img, HORIZONTAL);
	Mat vhist = projectedHistogram(img, VERTICAL);

	Mat lowImg;
	resize(img, lowImg, Size(size, size));//保存低分辨率图像的每个特征

	//特征矩阵=水平累加直方图+竖直累加直方图+低分辨率图像
	int numCols = hhist.cols + vhist.cols + lowImg.cols*lowImg.cols;
	Mat featMat = Mat::zeros(1, numCols, CV_32F);

	//赋值给特征矩阵 先存取水平方向累加直方图,再存取垂直方向累加直方图,最后存取低分辨率图像
	int idx = 0;
	for (int i = 0; i < vhist.cols; i++)
	{
		featMat.at<float>(idx) = vhist.at<float>(i);
		idx++;
	}
	for (int i = 0; i < hhist.cols; i++)
	{
		featMat.at<float>(idx) = hhist.at<float>(i);
		idx++;
	}
	for (int i = 0; i < lowImg.cols; i++)
	{
		for (int j = 0; j < lowImg.rows; j++)
		{
			featMat.at<float>(idx) = (float)lowImg.at<unsigned char>(i, j);
			idx++;
		}
	}
	return featMat;
}

int trainANN(Mat trainMat, Mat classesMat, int hnn, Mat featMat)
{
	//1. 生成训练数据,如果第i行的样本属于第j类,则(i,j)=1
	Mat classesData;
	classesData.create(trainMat.rows, numChar, CV_32FC1);
	for (int i = 0; i < classesData.rows; i++)
	{
		for (int j = 0; j < classesData.cols; j++)
		{
			if (j == classesMat.at<int>(i))
				classesData.at<float>(i, j) = 1;
			else
				classesData.at<float>(i, j) = 0;
		}
	}
	Ptr<TrainData> trainData = TrainData::create(trainMat, ROW_SAMPLE, classesData);

	//2.创建ANN模型
	Ptr<ANN_MLP> ann = ANN_MLP::create();

	//3.设置神经网络的层数和神经元数量
	/* setLayerSizes(InputArray _layer_sizes); */
	Mat layerSizes(1, 3, CV_32SC1);
	layerSizes.at<int>(0) = trainMat.cols;//输入层神经元数量
	layerSizes.at<int>(1) = hnn;//隐层神经元数量
	layerSizes.at<int>(2) = numChar;//输出层神经元数量=10个数字+20个字符
	ann->setLayerSizes(layerSizes);

	//4.设置激活函数
	/* setActivationFunction(int type, double param1 = 0, double param2 = 0); */
	ann->setActivationFunction(ANN_MLP::SIGMOID_SYM, 1, 1);

	//5.训练模型 
	/*samples - 训练样本; layout - 训练样本为 “行样本” ROW_SAMPLE 或 “列样本” COL_SAMPLE;
	result - 对应样本数据的分类结果*/
	/* train(InputArray samples,int layout,InputArray results)*/
	ann->train(trainData);

	//6.预测,结果为行向量
	Mat result(1, numChar, CV_32FC1);
	ann->predict(featMat, result);

	//7.获得输出矩阵的最大值
	Point maxLoc;
	double maxVal;
	minMaxLoc(result, 0, &maxVal, 0, &maxLoc);

	return maxLoc.x;
}

void annClassifier(Mat charImg)
{
	//1.仿射变换图像进行平移
	int height = charImg.rows;
	int width = charImg.cols;
	cout << "原始图像的width--height: " << width << "--" << height << endl;
	Mat warpMat = Mat::eye(2, 3, CV_32F);
	int max = (height>width) ? height : width;
	warpMat.at<float>(0, 2) = max / 2 - width / 2;//高度大于宽度,向水平两边平移
	warpMat.at<float>(1, 2) = max / 2 - height / 2;//宽度大于高度,向垂直两边平移
	Mat warpImg(max, max, charImg.type());
	warpAffine(charImg, warpImg, warpMat, warpImg.size());
	imshow("仿射图像", warpImg);
	cout << "平移后图像的width--height: " << warpImg.cols << "--" << warpImg.rows << endl;

	//3.调整大小
	Mat resizeImg;
	resize(warpImg, resizeImg, Size(20, 20));
	imshow("调整尺寸", resizeImg);
	cout << "调整图像大小width--height: " << resizeImg.cols << "--" << resizeImg.rows << endl;

	//4.读取训练数据
	FileStorage fs;
	fs.open("OCR.xml", FileStorage::READ);
	Mat trainMat, classesMat;
	fs["TrainingDataF5"] >> trainMat;
	fs["classes"] >> classesMat;
	cout << "训练数据的样本和维度: " << trainMat.rows << "--" << trainMat.cols << endl;

	//5.创建特征矩阵 20+20+25
	Mat featMat = featureMat(resizeImg, 5);
	cout << "特征矩阵的维度: " << trainMat.rows << "--" << trainMat.cols << endl;

	//6.训练预测
	int index = trainANN(trainMat, classesMat, 10, featMat);
	cout << "分类结果索引: "<< index << endl;
	cout << "分类结果: "<< strCharacters[index] << endl;
}
/* main.cpp */
#include <iostream>
#include <opencv2/opencv.hpp>

#include "plateDetection.h"

using namespace std;
using namespace cv;

int main(int argc, int ** argv)
{	
	/*1.车牌图像切割*/
	Mat carImg = imread("car.jpg");
	imgProcess(carImg);

	/*2.车牌图像识别*/
	Mat plateImg = imread("plate.jpg", 0);
	SVMClassifier(plateImg);

	/*3.车牌字符切割*/
	Mat plateImg = imread("plate.jpg", 0);
	plateChar(plateImg);

	/*4.车牌字符分类*/
	Mat charImg = imread("5.jpg", 0);
	imshow("字符", charImg);
	annClassifier(charImg);

	waitKey(0);//显示一帧图像
	return 0;
}

所有知识:

1. Ostu方法又名最大类间差方法(大津法),通过统计整个图像的直方图特性来实现全局阈值T的自动选取,其算法步骤为:

1)  先计算图像的直方图,即将图像所有的像素点按照0~255共256个bin,统计落在每个bin的像素点数量;

2)  归一化直方图,也即将每个bin中像素点数量除以总的像素点;

3)  i表示分类的阈值,也即一个灰度级,从0开始迭代;

4)  通过归一化的直方图,统计0~i 灰度级的像素(假设像素值在此范围的像素叫做前景像素) 所占整幅图像的比例w0,并统计前景像素的平均灰度u0;统计i~255灰度级的像素(假设像素值在此范围的像素叫做背景像素) 所占整幅图像的比例w1,并统计背景像素的平均灰度u1;

5) 计算前景像素和背景像素的方差 g = w0*w1*(u0-u1) (u0-u1);

6)  i++;转到4),直到i为256时结束迭代;7)将最大g相应的i值作为图像的全局阈值T;

2. OpenCV图像的深度和通道CV_<bit_depth>(S|U|F)C<number_of_channels>

S = 符号整型  U = 无符号整型  F= 浮点型

CV_8UC1 是指一个8位无符号整型单通道矩阵

CV_32FC2是指一个32位浮点型双通道矩阵

其中,通道表示每个点能存放多少个数,类似于RGB彩色图中的每个像素点有三个值,即三通道。图片中的深度表示每个值由多少位来存储,是一个精度问题,一般图片是8bit(位)的,则深度是8。


参考资料:

1. 《深入理解OpenCV实用计算机视觉项目解析》第五章 基于SVM和神经网络的车牌识别

2. 毛星云等编著《OpenCV3编程入门》

3.  基于SVM和神经网络的的车牌识别 CSDN系列博客:

https://blog.csdn.net/u010429424/article/details/75322182

https://blog.csdn.net/zhazhiqiang/article/details/21190521

4. OpenCV中的神经网络:https://www.cnblogs.com/xinxue/archive/2017/06/27/5789421.html

5. 图像处理中的仿射变换:https://blog.csdn.net/bytekiller/article/details/47803753

6. 累加直方图:https://blog.csdn.net/tkp2014/article/details/40151515

7. Ostu大津法:https://blog.csdn.net/ap1005834/article/details/51452516


猜你喜欢

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