opencv 視覺專案學習筆記(二): 基於 svm 和 knn 車牌識別
車牌識別的屬於常見的 模式識別 ,其基本流程為下面三個步驟:
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(5, 5)); //找垂直邊 Mat img_sobel; Sobel(img_gray, img_sobel, CV_8U, 1, 0, 3, 1, 0, BORDER_DEFAULT);View Code// 閾值化過濾畫素 Mat img_threshold; threshold(img_sobel, img_threshold, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY); // 開運算 Mat element = getStructuringElement(MORPH_RECT, Size(17, 3)); 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, -1, cv::Scalar(255, 0, 0), 1); for (int i = 0; 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, 3, Scalar(0, 255, 0), -1); //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 + 2, input.cols + 2, CV_8UC1); mask = Scalar::all(0); int loDiff = 30; int upDiff = 30; int connectivity = 4; int newMaskVal = 255; int NumSeeds = 10; Rect ccomp; int flags = connectivity + (newMaskVal << 8) + CV_FLOODFILL_FIXED_RANGE + CV_FLOODFILL_MASK_ONLY; for (int j = 0; j < NumSeeds; j++) { Point seed; seed.x = rects[i].center.x + rand() % (int)minSize - (minSize / 2); seed.y = rects[i].center.y + rand() % (int)minSize - (minSize / 2); circle(result, seed, 1, Scalar(0, 255, 255), -1); int area = floodFill(input, mask, seed, Scalar(255, 0, 0), &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 == 255) pointsInterest.push_back(itMask.pos()); RotatedRect minRect = minAreaRect(pointsInterest); if (verifySizes(minRect)) { // rotated rectangle drawing Point2f rect_points[4]; minRect.points(rect_points); for (int j = 0; j < 4; j++) line(result, rect_points[j], rect_points[(j + 1) % 4], Scalar(0, 0, 255), 1, 8); // 獲取旋轉矩陣 float r = (float)minRect.size.width / (float)minRect.size.height; float angle = minRect.angle; if (r < 1) angle = 90 + angle; Mat rotmat = getRotationMatrix2D(minRect.center, angle, 1); // 獲取對映影象 Mat img_rotated; warpAffine(input, img_rotated, rotmat, input.size(), CV_INTER_CUBIC); // Crop image Size rect_size = minRect.size; if (r < 1) swap(rect_size.width, rect_size.height); Mat img_crop; getRectSubPix(img_rotated, rect_size, minRect.center, img_crop); Mat resultResized; resultResized.create(33, 144, CV_8UC3); resize(img_crop, resultResized, resultResized.size(), 0, 0, INTER_CUBIC); // 直方圖 Mat grayResult; cvtColor(resultResized, grayResult, CV_BGR2GRAY); blur(grayResult, grayResult, Size(3, 3)); 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 0;View Code
// 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 = 0; struct _finddata_t fileInfo; string p; if ((hFile = _findfirst(p.assign(path).append("\\*" + fileType).c_str(), &fileInfo)) != -1) { do { files.push_back(p.assign(path).append("\\").append(fileInfo.name)); } while (_findnext(hFile, &fileInfo) == 0); _findclose(hFile); // 關閉控制代碼 } } void Preprocess::extract_img_data(string path_plates, string path_noPlates) { cout << "OpenCV Training SVM Automatic Number Plate Recognition\n"; int imgWidth = 144; int imgHeight = 33; int numPlates = 100; int numNoPlates = 100; Mat classes; Mat trainingData; Mat trainingImages; vector<int> trainingLabels; for (int i = 0; i < numPlates; i++) { stringstream ss(stringstream::in | stringstream::out); ss << path_plates << i << ".jpg"; Mat img = imread(ss.str(), 0); resize(img, img, Size(imgWidth, imgWidth)); img = img.reshape(1, 1); trainingImages.push_back(img); trainingLabels.push_back(1); } for (int i = 0; i < numNoPlates; i++) { stringstream ss; ss << path_noPlates << i << ".jpg"; Mat img = imread(ss.str(), 0); img = img.reshape(1, 1); trainingImages.push_back(img); trainingLabels.push_back(0); } 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(); }View Code
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 = 0; i < posible_regions.size(); i++) { Mat img = posible_regions[i].plateImg; Mat p = img.reshape(1, 1); 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); } }View Code
以上,已經找了存在車牌的區域,並儲存到一個 vector 中。
下面使用 k 鄰近演算法, 來識別車牌影象中的車牌字元。
2.1 字元分割
分割字元,並剔除不合格影象
vector<CharSegment> OCR::segment(Plate plate) { Mat input = plate.plateImg; vector<CharSegment> output; //使字元為白色,背景為黑色 Mat img_threshold; threshold(input, img_threshold, 60, 255, 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, -1, // draw all contours cv::Scalar(255, 0, 0), // in blue 1); // 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(0, 255, 0)); //提取合格影象區域 Mat auxRoi(img_threshold, mr); if (verifySizes(auxRoi)) { auxRoi = preprocessChar(auxRoi); output.push_back(CharSegment(auxRoi, mr)); rectangle(result, mr, Scalar(0, 125, 255)); } ++itc; } return output; } Mat OCR::preprocessChar(Mat in) { //Remap image int h = in.rows; int w = in.cols; Mat transformMat = Mat::eye(2, 3, CV_32F); int m = max(w, h); transformMat.at<float>(0, 2) = m / 2 - w / 2; transformMat.at<float>(1, 2) = m / 2 - h / 2; // 仿射變換,將影象投射到尺寸更大的影象上(使用偏移) Mat warpImage(m, m, in.type()); warpAffine(in, warpImage, transformMat, warpImage.size(), INTER_LINEAR, BORDER_CONSTANT, Scalar(0)); Mat out; resize(warpImage, out, Size(charSize, charSize)); return out; }View Code
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[] = { 35, 40, 42, 41, 42, 33, 30, 31, 49, 44, 30, 24, 21, 20, 34, 9, 10, 3, 11, 3, 15, 4, 9, 12, 10, 21, 18, 8, 15, 7 }; 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 = 20; int m = gray.rows / b; // 將原圖裁剪為 20 * 20 的小圖塊 int n = gray.cols / b; // 將原圖裁剪為 20 * 20 的小圖塊 Mat data, labels; // 特徵矩陣 // 按照列來讀取資料, 每 5 個數據為一個類 for (int i = 0; i < n; i++) { int offsetCol = i * b; // 列上的偏移量 for (int j = 0; 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(0, 1)); // 序列化後放入特徵矩陣 labels.push_back((int)j / 5); // 對應的標註 } } data.convertTo(data, CV_32F); int samplesNum = data.rows; int trainNum = 3000; Mat trainData, trainLabels; trainData = data(Range(0, trainNum), Range::all()); // 前 3000 個為訓練資料 trainLabels = labels(Range(0, trainNum), Range::all()); // 使用k 鄰近演算法那(knn, k-nearest_neighbor) 演算法 int K = 5; 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 = 0, test_hr = 0; Mat response; // compute prediction error on train and test data for (int i = 0; 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 ? 1.f : 0.f; if (i < trainNum) { train_hr += r; // 累計正確數 } else { test_hr += r; } } test_hr /= samplesNum - trainNum; train_hr = trainNum > 0 ? train_hr / trainNum : 1.; cout << "train accuracy : " << train_hr * 100. << "\n"; cout << "test accuracy : " << test_hr * 100. << "\n"; // 儲存 ocr 模型 string model_path = "ocr.xml"; model->save(model_path); // 載入模型 // Ptr<KNearest> knn = KNearest::load<KNearest>(model_path); waitKey(1); return 0; }View Code
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)View Code
以上就是車牌識別的核心程式碼了。
全部流程的程式碼我放到下面這個群裡面了,歡迎來交流下載。
廣州 OpenCV 學校交流群: 892083812
參考:
深入理解 OpenCV
https://www.cnblogs.com/denny402/p/5032839.html