3)车牌字符切割
a. 阈值滤波,使用CV_THRESH_BINARY参数通过把白色值变为黑色,黑色值变为白色来实现阈值输出的反转,因为需要获取字符的轮廓,而轮廓的算法寻找的是白色像素;
b. 查找轮廓;
c. 验证轮廓是否为字符,去除那些规格太小的或者宽高比不正确的区域。字符是45/77的宽高比,允许0.35的误差。如果一个区域面积高于80%(就是像素大于0的超过80%),则认为这个区域是一个黑色块,不是字符。
/* charSlicer.cpp */
#include
#include
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> 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
#include
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(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(idx) = vhist.at(i);
idx++;
}
for (int i = 0; i < hhist.cols; i++)
{
featMat.at(idx) = hhist.at(i);
idx++;
}
for (int i = 0; i < lowImg.cols; i++)
{
for (int j = 0; j < lowImg.rows; j++)
{
featMat.at(idx) = (float)lowImg.at(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(i))
classesData.at(i, j) = 1;
else
classesData.at(i, j) = 0;
}
}
Ptr trainData = TrainData::create(trainMat, ROW_SAMPLE, classesData);
//2.创建ANN模型
Ptr ann = ANN_MLP::create();
//3.设置神经网络的层数和神经元数量
/* setLayerSizes(InputArray _layer_sizes); */
Mat layerSizes(1, 3, CV_32SC1);
layerSizes.at(0) = trainMat.cols;//输入层神经元数量
layerSizes.at(1) = hnn;//隐层神经元数量
layerSizes.at(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(0, 2) = max / 2 - width / 2;//高度大于宽度,向水平两边平移
warpMat.at(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
#include
#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_
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