1. 程式人生 > >opencv 視覺項目學習筆記(二): 基於 svm 和 knn 車牌識別

opencv 視覺項目學習筆記(二): 基於 svm 和 knn 車牌識別

its ++ eas -a rect() repr poi obj std

車牌識別的屬於常見的 模式識別 ,其基本流程為下面三個步驟:

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);

    
// 閾值化過濾像素 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; }
View Code

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

opencv 視覺項目學習筆記(二): 基於 svm 和 knn 車牌識別