opencv 视觉项目学习笔记(二): 基于 svm 和 knn 车牌识别

时间:2021-11-16 22:10:38

车牌识别的属于常见的 模式识别 ,其基本流程为下面三个步骤:

1) 分割: 检测并检测图像中感兴趣区域;

2)特征提取: 对字符图像集中的每个部分进行提取;

3)分类: 判断图像快是不是车牌或者 每个车牌字符的分类。

车牌识别分为两个步骤, 车牌检测, 车牌识别, 都属于模式识别

基本结构如下:

一、车牌检测

  1、车牌局部化(分割车牌区域),根据尺寸等基本信息去除非车牌图像;

  2、判断车牌是否存在 (训练支持向量机 -svm, 判断车牌是否存在)。

二、车牌识别

  1、字符局部化(分割字符),根据尺寸等信息剔除不合格图像

  2、字符识别 ( knn  分类)

1.1 车牌局部化、并剔除不合格区域  

vector<Plate> DetectRegions::segment(Mat input) {
vector<Plate> output; //转为灰度图,并去噪
Mat img_gray;
cvtColor(input, img_gray, CV_BGR2GRAY);
blur(img_gray, img_gray, Size(, )); //找垂直边
Mat img_sobel;
Sobel(img_gray, img_sobel, CV_8U, , , , , , BORDER_DEFAULT); // 阈值化过滤像素
Mat img_threshold;
threshold(img_sobel, img_threshold, , , CV_THRESH_OTSU + CV_THRESH_BINARY); // 开运算
Mat element = getStructuringElement(MORPH_RECT, Size(, ));
morphologyEx(img_threshold, img_threshold, CV_MOP_CLOSE, element); //查找轮廓
vector<vector<Point>> contours;
findContours(img_threshold, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); vector<vector<Point>>::iterator itc = contours.begin();
vector<RotatedRect> rects; // 去除面积以及宽高比不合适区域
while (itc != contours.end())
{
// create bounding rect of object
RotatedRect mr = minAreaRect(Mat(*itc));
if (!verifySizes(mr))
{
itc = contours.erase(itc);
}
else
{
++itc;
rects.push_back(mr);
}
} // 绘出获取区域
cv::Mat result;
input.copyTo(result);
cv::drawContours(result, contours, -, cv::Scalar(, , ), ); for (int i = ; i < rects.size(); i++) { //For better rect cropping for each posible box
//Make floodfill algorithm because the plate has white background
//And then we can retrieve more clearly the contour box
circle(result, rects[i].center, , Scalar(, , ), -);
//get the min size between width and height
float minSize = (rects[i].size.width < rects[i].size.height) ? rects[i].size.width : rects[i].size.height;
minSize = minSize - minSize * 0.5;
//initialize rand and get 5 points around center for floodfill algorithm
srand(time(NULL));
//Initialize floodfill parameters and variables
Mat mask;
mask.create(input.rows + , input.cols + , CV_8UC1);
mask = Scalar::all();
int loDiff = ;
int upDiff = ;
int connectivity = ;
int newMaskVal = ;
int NumSeeds = ;
Rect ccomp;
int flags = connectivity + (newMaskVal << ) + CV_FLOODFILL_FIXED_RANGE + CV_FLOODFILL_MASK_ONLY;
for (int j = ; j < NumSeeds; j++) {
Point seed;
seed.x = rects[i].center.x + rand() % (int)minSize - (minSize / );
seed.y = rects[i].center.y + rand() % (int)minSize - (minSize / );
circle(result, seed, , Scalar(, , ), -);
int area = floodFill(input, mask, seed, Scalar(, , ), &ccomp, Scalar(loDiff, loDiff, loDiff), Scalar(upDiff, upDiff, upDiff), flags);
}
if (showSteps)
imshow("MASK", mask);
//cvWaitKey(0); //Check new floodfill mask match for a correct patch.
//Get all points detected for get Minimal rotated Rect
vector<Point> pointsInterest;
Mat_<uchar>::iterator itMask = mask.begin<uchar>();
Mat_<uchar>::iterator end = mask.end<uchar>();
for (; itMask != end; ++itMask)
if (*itMask == )
pointsInterest.push_back(itMask.pos()); RotatedRect minRect = minAreaRect(pointsInterest); if (verifySizes(minRect)) {
// rotated rectangle drawing
Point2f rect_points[];
minRect.points(rect_points);
for (int j = ; j < ; j++)
line(result, rect_points[j], rect_points[(j + ) % ], Scalar(, , ), , ); // 获取旋转矩阵
float r = (float)minRect.size.width / (float)minRect.size.height;
float angle = minRect.angle;
if (r < )
angle = + angle;
Mat rotmat = getRotationMatrix2D(minRect.center, angle, ); // 获取映射图像
Mat img_rotated;
warpAffine(input, img_rotated, rotmat, input.size(), CV_INTER_CUBIC); // Crop image
Size rect_size = minRect.size;
if (r < )
swap(rect_size.width, rect_size.height);
Mat img_crop;
getRectSubPix(img_rotated, rect_size, minRect.center, img_crop); Mat resultResized;
resultResized.create(, , CV_8UC3);
resize(img_crop, resultResized, resultResized.size(), , , INTER_CUBIC);
// 直方图
Mat grayResult;
cvtColor(resultResized, grayResult, CV_BGR2GRAY);
blur(grayResult, grayResult, Size(, ));
grayResult = histeq(grayResult);
output.push_back(Plate(grayResult, minRect.boundingRect()));
}
} return output;
}

1.2 判断车牌是否存在

  1.2.1  训练 svm

    svm 会创建一个或多个超平面, 这些超级平面能判断数据属于那个类。

    训练数据: 所有训练数据存储再一个 N x M 的矩阵中, 其中 N 为样本数, M 为特征数(每个样本是该训练矩阵中的一行)。这些数据  所有数据存在  xml 文件中,

    标签数据:  每个样本的类别信息存储在另一个 N x 1 的矩阵中, 每行为一个样本标签。

    训练数据存放在本地 svm.xml 文件中。

    

    // TrainSvm.cpp 文件

    
#include <iostream>
#include <opencv2/opencv.hpp> #include "Preprocess.h" using namespace std;
using namespace cv;
using namespace cv::ml; int main(int argc, char** argv)
{
FileStorage fs;
fs.open("SVM.xml", FileStorage::READ);
Mat SVM_TrainingData;
Mat SVM_Classes;
fs["TrainingData"] >> SVM_TrainingData;
fs["classes"] >> SVM_Classes;
// Set SVM storage
Ptr<ml::SVM> model = ml::SVM::create();
model->setType(SVM::C_SVC);
model->setKernel(SVM::LINEAR); // 核函数
// 训练数据
Ptr<TrainData> tData = TrainData::create(SVM_TrainingData, ROW_SAMPLE, SVM_Classes);
// 训练分类器
model->train(tData);
model->save("model.xml"); // TODO: 测试
return ;

    // Preprocess.cpp    

    
#include <string>
#include <vector>
#include <fstream>
#include <algorithm> #include "Preprocess.h" using namespace cv; void Preprocess::getAllFiles(string path, vector<string> &files, string fileType)
{
long hFile = ;
struct _finddata_t fileInfo;
string p;
if ((hFile = _findfirst(p.assign(path).append("\\*" + fileType).c_str(), &fileInfo)) != -)
{
do
{
files.push_back(p.assign(path).append("\\").append(fileInfo.name));
} while (_findnext(hFile, &fileInfo) == );
_findclose(hFile); // 关闭句柄
} } void Preprocess::extract_img_data(string path_plates, string path_noPlates)
{
cout << "OpenCV Training SVM Automatic Number Plate Recognition\n"; int imgWidth = ;
int imgHeight = ;
int numPlates = ;
int numNoPlates = ;
Mat classes;
Mat trainingData; Mat trainingImages;
vector<int> trainingLabels; for (int i = ; i < numPlates; i++)
{
stringstream ss(stringstream::in | stringstream::out);
ss << path_plates << i << ".jpg";
Mat img = imread(ss.str(), );
resize(img, img, Size(imgWidth, imgWidth));
img = img.reshape(, );
trainingImages.push_back(img);
trainingLabels.push_back();
} for (int i = ; i < numNoPlates; i++)
{
stringstream ss;
ss << path_noPlates << i << ".jpg";
Mat img = imread(ss.str(), );
img = img.reshape(, );
trainingImages.push_back(img);
trainingLabels.push_back();
} Mat(trainingImages).copyTo(trainingData);
trainingData.convertTo(trainingData, CV_32FC1);
Mat(trainingLabels).copyTo(classes); FileStorage fs("SVM.xml", FileStorage::WRITE);
fs << "TrainingData" << trainingData;
fs << "classess" << classes;
fs.release();
}

  1.2.2  利用 svm 判断车牌是否存在

  
// load model
Ptr<ml::SVM> model = SVM::load("model.xml"); // For each possible plate, classify with svm if it's plate
vector<Plate> plates;
for (int i = ; i < posible_regions.size(); i++)
{
Mat img = posible_regions[i].plateImg;
Mat p = img.reshape(, );
p.convertTo(p, CV_32FC1);
int reponse = (int)model->predict(p);
if (reponse)
{
plates.push_back(posible_regions[i]);
//bool res = imwrite("test.jpg", img);
}
}

以上,已经找了存在车牌的区域,并保存到一个 vector 中。

下面使用 k 邻近算法, 来识别车牌图像中的车牌字符。

2.1 字符分割

  分割字符,并剔除不合格图像

vector<CharSegment> OCR::segment(Plate plate) {
Mat input = plate.plateImg;
vector<CharSegment> output;
//使字符为白色,背景为黑色
Mat img_threshold;
threshold(input, img_threshold, , , CV_THRESH_BINARY_INV); Mat img_contours;
img_threshold.copyTo(img_contours);
// 找到所有物体
vector< vector< Point> > contours;
findContours(img_contours,
contours, // a vector of contours
CV_RETR_EXTERNAL, // retrieve the external contours
CV_CHAIN_APPROX_NONE); // all pixels of each contours // Draw blue contours on a white image
cv::Mat result;
img_threshold.copyTo(result);
cvtColor(result, result, CV_GRAY2RGB);
cv::drawContours(result, contours,
-, // draw all contours
cv::Scalar(, , ), // in blue
); // with a thickness of 1 //Remove patch that are no inside limits of aspect ratio and area.
vector<vector<Point> >::iterator itc = contours.begin();
while (itc != contours.end()) { //Create bounding rect of object
Rect mr = boundingRect(Mat(*itc));
rectangle(result, mr, Scalar(, , ));
//提取合格图像区域
Mat auxRoi(img_threshold, mr);
if (verifySizes(auxRoi)) {
auxRoi = preprocessChar(auxRoi);
output.push_back(CharSegment(auxRoi, mr));
rectangle(result, mr, Scalar(, , ));
}
++itc;
} return output;
} Mat OCR::preprocessChar(Mat in) {
//Remap image
int h = in.rows;
int w = in.cols;
Mat transformMat = Mat::eye(, , CV_32F);
int m = max(w, h);
transformMat.at<float>(, ) = m / - w / ;
transformMat.at<float>(, ) = m / - h / ;
// 仿射变换,将图像投射到尺寸更大的图像上(使用偏移)
Mat warpImage(m, m, in.type());
warpAffine(in, warpImage, transformMat, warpImage.size(), INTER_LINEAR, BORDER_CONSTANT, Scalar());
Mat out;
resize(warpImage, out, Size(charSize, charSize)); return out;
}

2.2 字符识别

  2.2.1 训练 knn

    使用 opencv  自带的 digits.png 文件, 可以训练训练识别识别数字的 knn 。

    
#include <iostream>
#include <opencv2/opencv.hpp> using namespace cv;
using namespace std;
using namespace cv::ml; const int numFilesChars[] = { , , , , , , , , , , , , , , , , , , , , , , , , , , , , , }; int main()
{ std::cout << "OpenCV Training OCR Automatic Number Plate Recognition\n"; string path = "D:/Program Files (x86)/opencv_3.4.3/opencv/sources/samples/data/digits.png";
Mat img = imread(path);
Mat gray;
cvtColor(img, gray, CV_BGR2GRAY);
int b = ;
int m = gray.rows / b; // 将原图裁剪为 20 * 20 的小图块
int n = gray.cols / b; // 将原图裁剪为 20 * 20 的小图块 Mat data, labels; // 特征矩阵 // 按照列来读取数据, 每 5 个数据为一个类
for (int i = ; i < n; i++)
{
int offsetCol = i * b; // 列上的偏移量
for (int j = ; j < m; j++)
{
int offsetRow = j * b; // 行上的偏移量
Mat tmp;
gray(Range(offsetRow, offsetRow + b), Range(offsetCol, offsetCol + b)).copyTo(tmp);
data.push_back(tmp.reshape(, )); // 序列化后放入特征矩阵
labels.push_back((int)j / ); // 对应的标注
}
}
data.convertTo(data, CV_32F);
int samplesNum = data.rows;
int trainNum = ;
Mat trainData, trainLabels;
trainData = data(Range(, trainNum), Range::all()); // 前 3000 个为训练数据
trainLabels = labels(Range(, trainNum), Range::all()); // 使用k 邻近算法那(knn, k-nearest_neighbor) 算法
int K = ;
Ptr<cv::ml::TrainData> tData = cv::ml::TrainData::create(trainData, ROW_SAMPLE, trainLabels);
Ptr<KNearest> model = KNearest::create(); model->setDefaultK(K); // 设定查找时返回数量为 5
// 设置分类器为分类 或回归
// 分类问题:输出离散型变量(如 -1,1, 100), 为定性输出(如预测明天是下雨、天晴还是多云)
// 回归问题: 回归问题的输出为连续型变量,为定量输出(如明天温度为多少度)
model->setIsClassifier(true);
model->train(tData); // 预测分类
double train_hr = , test_hr = ;
Mat response;
// compute prediction error on train and test data
for (int i = ; i < samplesNum; i++)
{
Mat smaple = data.row(i);
float r = model->predict(smaple); // 对所有进行预测
// 预测结果与原结果对比,相等为 1, 不等为 0
r = std::abs(r - labels.at<int>(i)) <= FLT_EPSILON ? .f : .f; if (i < trainNum)
{
train_hr += r; // 累计正确数
}
else
{
test_hr += r;
}
} test_hr /= samplesNum - trainNum;
train_hr = trainNum > ? train_hr / trainNum : .;
cout << "train accuracy : " << train_hr * . << "\n";
cout << "test accuracy : " << test_hr * . << "\n"; // 保存 ocr 模型
string model_path = "ocr.xml";
model->save(model_path);
// 载入模型
// Ptr<KNearest> knn = KNearest::load<KNearest>(model_path); waitKey();
return ;
}

  2.2.2 使用 knn 识别字符

    
// Mat target_img  为目标图像矩阵
model->save(model_path);
// 载入模型
Ptr<KNearest> knn = KNearest::load<KNearest>(model_path);
float it_type = knn->predict(target_img)

    

以上就是车牌识别的核心代码了。

全部流程的代码我放到下面这个群里面了,欢迎来交流下载。

广州 OpenCV 学校交流群: 892083812

  

参考:

深入理解 OpenCV

https://www.cnblogs.com/denny402/p/5032839.html