モーダルを閉じる工作HardwareHub ロゴ画像

工作HardwareHubは、ロボット工作や電子工作に関する情報やモノが行き交うコミュニティサイトです。さらに詳しく

利用規約プライバシーポリシー に同意したうえでログインしてください。

OpenCV3 C++ を用いた手書き数字の認識 (サポートベクタマシン)

モーダルを閉じる

ステッカーを選択してください

お支払い手続きへ
モーダルを閉じる

お支払い内容をご確認ください

購入商品
」ステッカーの表示権
メッセージ
料金
(税込)
決済方法
GooglePayマーク
決済プラットフォーム
確認事項

利用規約をご確認のうえお支払いください

※カード情報はGoogleアカウント内に保存されます。本サイトやStripeには保存されません

※記事の執筆者は購入者のユーザー名を知ることができます

※購入後のキャンセルはできません

作成日作成日
2020/01/08
最終更新最終更新
2024/07/12
記事区分記事区分
一般公開

目次

    C/C++やアルゴリズムに注目し、実践的な知識を発信しています!

    サポートベクタマシン (SVM; Support Vector Machine) は分類アルゴリズムの一つです。二つのクラスに分類されたデータをもとに分類器を構成します。その分類器を用いると、未知のデータを二つのクラスに分類できます。OpenCV3 C++ に実装されている SVM アルゴリズムを利用して、手書き数字を 0-9 のいずれかに分類してみます。

    digits.png

    簡単な例

    四つの点それぞれに 1 または -1 のラベルを付与します。これらを分類する直線を学習して分類器を構成します。平面内の点を分類器で塗り分けると以下のようになります。

    #include <opencv2/opencv.hpp>
    
    int main() {
    
        // 分類器を作成するためのデータを用意します。
        int labels[4] = {1, -1, -1, -1};
        float trainingData[4][2] = { {501, 10}, {255, 10}, {501, 255}, {10, 501} };
        cv::Mat trainingDataMat(4, 2, CV_32F, trainingData);
        cv::Mat labelsMat(4, 1, CV_32S, labels);
    
        // [SVM] 分類器を作成します。
        cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
        svm->setType(cv::ml::SVM::C_SVC);
        svm->setKernel(cv::ml::SVM::LINEAR);
        svm->setTermCriteria(cv::TermCriteria(cv::TermCriteria::MAX_ITER, 100, 1e-6));
        svm->train(trainingDataMat, cv::ml::ROW_SAMPLE, labelsMat);
    
        // 分類器で 512 x 512 平面内の点を分類してみます。
        int width = 512, height = 512;
        cv::Mat image = cv::Mat::zeros(height, width, CV_8UC3);
    
        // 分類結果によって「緑」または「青」で色付けして可視化します。
        cv::Vec3b green(0,255,0), blue(255,0,0);
        for (int i = 0; i < image.rows; i++) {
            for (int j = 0; j < image.cols; j++) {
                cv::Mat sampleMat = (cv::Mat_<float>(1,2) << j,i);
    
                // [SVM] 分類
                float response = svm->predict(sampleMat);
    
                if (response == 1) {
                    image.at<cv::Vec3b>(i,j) = green;
                }
                else if (response == -1) {
                    image.at<cv::Vec3b>(i,j) = blue;
                }
            }
        }
    
        // 分かりやすさのため、訓練用のデータを描画します。
        int thickness = -1;
        cv::circle( image, cv::Point(501,  10), 5, cv::Scalar(  0,   0,   0), thickness );
        cv::circle( image, cv::Point(255,  10), 5, cv::Scalar(255, 255, 255), thickness );
        cv::circle( image, cv::Point(501, 255), 5, cv::Scalar(255, 255, 255), thickness );
        cv::circle( image, cv::Point( 10, 501), 5, cv::Scalar(255, 255, 255), thickness );
    
        // [SVM] サポートベクタを描画します。
        thickness = 2;
        cv::Mat sv = svm->getUncompressedSupportVectors();
        for (int i = 0; i < sv.rows; i++) {
            const float* v = sv.ptr<float>(i);
            cv::circle(image, cv::Point( (int) v[0], (int) v[1]), 6, cv::Scalar(128, 128, 128), thickness);
        }
    
        cv::imshow("SVM Simple Example", image);
        cv::waitKey(0);
        return 0;
    }
    

    cv::Ptr はスマートポインタです。C++11 スマートポインタと同様です。cv::TermCriteria は終了基準を表現するクラスです。<<cv::Mat を以下のように初期化します。

    cv::Mat m = (cv::Mat_<float>(2, 2) << 1, 2, 3, 4);
    std::cout << m << std::endl;
    
    [1, 2;
     3, 4]
    

    高次元の空間に写像して線形の分類器を見つける

    訓練用のデータと同じ次元では線形の分類器を見つけられない場合は、カーネル空間とよばれる高次元空間に写像することを考えます。カーネル空間における線形分類器は、もとの低次元空間では線形であるとは限りません。以下に簡単な例を示します。

    #include <opencv2/opencv.hpp>
    
    int main() {
    
        // 分類器を作成するためのデータを用意します。
        int labels[4] = {1, -1, -1, 1};
        float trainingData[4] = {-200, -100, 150, 190};
        cv::Mat trainingDataMat(4, 1, CV_32F, trainingData);
        cv::Mat labelsMat(4, 1, CV_32S, labels);
    
        // [SVM] 分類器を作成します。
        cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
        svm->setType(cv::ml::SVM::C_SVC);
        svm->setKernel(cv::ml::SVM::POLY);
        svm->setDegree(2);
        svm->setGamma(1.0);
        svm->setCoef0(0.0);
        svm->setTermCriteria(cv::TermCriteria(cv::TermCriteria::MAX_ITER, 100, 1e-6));
        svm->train(trainingDataMat, cv::ml::ROW_SAMPLE, labelsMat);
    
        // 分類器で 512 x 1 直線上の点を分類してみます。
        int width = 512, height = 512;
        cv::Mat image = cv::Mat::zeros(height, width, CV_8UC3);
    
        // 分類結果によって「緑」または「青」で色付けして可視化します。
        cv::Vec3b green(0,255,0), blue(255,0,0), black(0,0,0);
        for (int i = 0; i < image.rows; i++) {
            for (int j = 0; j < image.cols; j++) {
    
                if (i != 256) {
                    image.at<cv::Vec3b>(i,j) = black;
                    continue;
                }
    
                cv::Mat sampleMat = (cv::Mat_<float>(1,1) << j - 256);
    
                // [SVM] 分類
                float response = svm->predict(sampleMat);
                if (response == 1) {
                    image.at<cv::Vec3b>(i,j) = green;
                }
                else if (response == -1) {
                    image.at<cv::Vec3b>(i,j) = blue;
                }
            }
        }
    
        // 分かりやすさのため、訓練用のデータを描画します。
        int thickness = -1;
        cv::circle( image, cv::Point(-200 + 256, 256), 5, cv::Scalar(0, 0, 255), thickness );
        cv::circle( image, cv::Point(-100 + 256, 256), 5, cv::Scalar(255, 255, 255), thickness );
        cv::circle( image, cv::Point(150 + 256, 256), 5, cv::Scalar(255, 255, 255), thickness );
        cv::circle( image, cv::Point(190 + 256, 256), 5, cv::Scalar(0, 0, 255), thickness );
    
        // [SVM] サポートベクタを描画します。
        thickness = 2;
        cv::Mat sv = svm->getSupportVectors();
        std::cout << sv << std::endl;
        for (int i = 0; i < sv.rows; i++) {
            const float v = sv.at<float>(i);
            cv::circle(image, cv::Point( (int) v + 256, 256), 6, cv::Scalar(256, 0, 0), thickness);
        }
    
        cv::imshow("SVM Simple Example", image);
        cv::waitKey(0);
        return 0;
    }
    

    上記例では、多項式カーネルで二次元空間に写像しています。cv::ml::SVM::POLY

    svm->setKernel(cv::ml::SVM::POLY);
    svm->setDegree(2);
    svm->setGamma(1.0);
    svm->setCoef0(0.0);
    

    例えば (x,x2)(x, x^2) に写像して、{-3, 3}{-1, 1}{(-3, 9), (3, 9}{(-1, 1), (1, 1)} に変換すれば、二次元空間における線形の分類器が見つけられます。線形カーネルを用いると以下のようになり、うまく分類できません。

    svm->setKernel(cv::ml::SVM::LINEAR);
    

    手書き数字の分類

    画像から分類に必要となる情報をうまく抽出して N 次元のベクトルで表現できれば、画像の分類を SVM で行うことができます。こちらのページに倣って、手書き数字の分類を行ってみます。

    画像の情報として HOG (Histogram of Oriented Gradients) を利用しています。Sobel 微分で各点の x軸方向、y軸方向の勾配 Gradients を計算して、各方向 Oriented についてヒストグラムを計算します。このヒストグラムを画像の特徴量として扱い、SVM で利用します。HOG を計算するにあたり、事前に画像の傾きを補正しておきます。

    CMakeLists.txt

    cmake_minimum_required (VERSION 3.1)
    find_package(OpenCV REQUIRED)
    add_executable(main main.cpp)
    target_link_libraries(main ${OpenCV_LIBS})
    

    main.cpp

    #include <opencv2/opencv.hpp>
    #include <iostream>
    
    int SZ = 20;
    
    cv::Mat deskew(cv::Mat& img){
        cv::Moments m = cv::moments(img);
        if(abs(m.mu02) < 1e-2) {
            return img.clone();
        }
        float skew = m.mu11 / m.mu02;
        cv::Mat warpMat = (cv::Mat_<float>(2,3) << 1, skew, -0.5 * SZ * skew, 0, 1, 0);
        cv::Mat imgOut = cv::Mat::zeros(img.rows, img.cols, img.type());
        cv::warpAffine(img, imgOut, warpMat, imgOut.size(), cv::WARP_INVERSE_MAP|cv::INTER_LINEAR);
        return imgOut;
    }
    
    void loadTrainTestLabel(std::string &pathName,
                            std::vector<cv::Mat> &trainCells,
                            std::vector<cv::Mat> &testCells,
                            std::vector<int> &trainLabels,
                            std::vector<int> &testLabels) {
        cv::Mat img = cv::imread(pathName, cv::IMREAD_GRAYSCALE);
        int ImgCount = 0;
        for(int i = 0; i < img.rows; i = i + SZ) {
            for(int j = 0; j < img.cols; j = j + SZ) {
                cv::Mat digitImg = (img.colRange(j, j + SZ).rowRange(i, i + SZ)).clone();
                if(j < int(0.9 * img.cols)) {
                    trainCells.push_back(digitImg);
                }
                else {
                    testCells.push_back(digitImg);
                }
                ImgCount++;
            }
        }
        std::cout << "Image Count : " << ImgCount << std::endl;
        float digitClassNumber = 0;
        for(int z = 0; z < int(0.9 * ImgCount); z++) {
            if(z % 450 == 0 && z != 0) {
                digitClassNumber = digitClassNumber + 1;
            }
            trainLabels.push_back(digitClassNumber);
        }
        digitClassNumber = 0;
        for(int z = 0; z < int(0.1*ImgCount); z++) {
            if(z % 50 == 0 && z != 0) {
                digitClassNumber = digitClassNumber + 1;
            }
            testLabels.push_back(digitClassNumber);
        }
    }
    
    void CreateDeskewedTrainTest(std::vector<cv::Mat> &deskewedTrainCells,
                                 std::vector<cv::Mat> &deskewedTestCells,
                                 std::vector<cv::Mat> &trainCells,
                                 std::vector<cv::Mat> &testCells){
        for(int i = 0; i < (int)trainCells.size(); i++) {
            cv::Mat deskewedImg = deskew(trainCells[i]);
            deskewedTrainCells.push_back(deskewedImg);
        }
        for(int i = 0; i < (int)testCells.size(); i++) {
            cv::Mat deskewedImg = deskew(testCells[i]);
            deskewedTestCells.push_back(deskewedImg);
        }
    }
    
    cv::HOGDescriptor hog(
            cv::Size(20,20), //winSize
            cv::Size(10,10), //blocksize
            cv::Size(5,5), //blockStride,
            cv::Size(10,10), //cellSize,
                     9, //nbins,
                      1, //derivAper,
                     -1, //winSigma,
                      0, //histogramNormType,
                    0.2, //L2HysThresh,
                      0,//gammal correction,
                      64,//nlevels=64
                      1);
    
    void CreateTrainTestHOG(std::vector<std::vector<float> > &trainHOG,
                            std::vector<std::vector<float> > &testHOG,
                            std::vector<cv::Mat> &deskewedtrainCells,
                            std::vector<cv::Mat> &deskewedtestCells) {
        for(int y = 0; y < (int)deskewedtrainCells.size(); y++) {
            std::vector<float> descriptors;
            hog.compute(deskewedtrainCells[y], descriptors);
            trainHOG.push_back(descriptors);
        }
        for(int y = 0; y < (int)deskewedtestCells.size(); y++) {
            std::vector<float> descriptors;
            hog.compute(deskewedtestCells[y], descriptors);
            testHOG.push_back(descriptors);
        }
    }
    
    void ConvertVectortoMatrix(std::vector<std::vector<float> > &trainHOG,
                               std::vector<std::vector<float> > &testHOG,
                               cv::Mat &trainMat,
                               cv::Mat &testMat) {
        int descriptor_size = trainHOG[0].size();
        for(int i = 0; i < (int)trainHOG.size(); i++){
            for(int j = 0;j<descriptor_size;j++){
                trainMat.at<float>(i,j) = trainHOG[i][j];
            }
        }
        for(int i = 0; i < (int)testHOG.size(); i++) {
            for(int j = 0;j<descriptor_size;j++){
                testMat.at<float>(i,j) = testHOG[i][j];
            }
        }
    }
    
    void getSVMParams(cv::Ptr<cv::ml::SVM> svm) {
        std::cout << "Kernel type     : " << svm->getKernelType() << std::endl;
        std::cout << "Type            : " << svm->getType() << std::endl;
        std::cout << "C               : " << svm->getC() << std::endl;
        std::cout << "Degree          : " << svm->getDegree() << std::endl;
        std::cout << "Nu              : " << svm->getNu() << std::endl;
        std::cout << "Gamma           : " << svm->getGamma() << std::endl;
    }
    
    cv::Ptr<cv::ml::SVM> SVMtrain(cv::Mat &trainMat, std::vector<int> &trainLabels) {
        cv::Ptr<cv::ml::TrainData> td = cv::ml::TrainData::create(trainMat, cv::ml::ROW_SAMPLE, trainLabels);
        cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
    
        svm->setGamma(0.50625);
        svm->setC(12.5);
        svm->setKernel(cv::ml::SVM::RBF);
        svm->setType(cv::ml::SVM::C_SVC);
        svm->train(td);
    
        // 上記パラメータを自動で計算させることもできます。時間がかかります。
        // svm->trainAuto(td);
    
        svm->save("model.yml");
        getSVMParams(svm);
        return svm;
    }
    
    void SVMevaluate(cv::Ptr<cv::ml::SVM> svm, cv::Mat &testMat, std::vector<int> &testLabels, std::vector<cv::Mat> &deskewedTestCells){
        cv::Mat testResponse;
        svm->predict(testMat, testResponse);
        int count = 0;
        for(int i = 0; i < testResponse.rows; i++) {
            if(testResponse.at<float>(i, 0) == testLabels[i]) {
                count = count + 1;
            }
            else {
                std::cout << testResponse.at<float>(i,0) << " != " << testLabels[i] << std::endl;
                cv::imshow("prediction failed", deskewedTestCells[i]);
                cv::waitKey(0);
            }
        }
        float accuracy = (count / (float)testResponse.rows) * 100;
        std::cout << "Accuracy        : " << accuracy << "%" << std::endl;
    }
    
    int main() {
    
        // 手書き数字の画像
        std::string pathName = "digits.png";
    
        // 分類器を学習するためのデータと、分類器を検証するためのデータ
        std::vector<cv::Mat> trainCells;
        std::vector<cv::Mat> testCells;
        std::vector<int> trainLabels;
        std::vector<int> testLabels;
        loadTrainTestLabel(pathName, trainCells, testCells, trainLabels, testLabels);
    
        // 画像の傾きを補正します。
        std::vector<cv::Mat> deskewedTrainCells;
        std::vector<cv::Mat> deskewedTestCells;
        CreateDeskewedTrainTest(deskewedTrainCells, deskewedTestCells, trainCells, testCells);
    
        // 特徴量として HOG を計算します。
        std::vector<std::vector<float> > trainHOG;
        std::vector<std::vector<float> > testHOG;
        CreateTrainTestHOG(trainHOG, testHOG, deskewedTrainCells,deskewedTestCells);
        int descriptor_size = trainHOG[0].size();
        std::cout << "Descriptor Size : " << descriptor_size << std::endl;
    
        cv::Mat trainMat(trainHOG.size(), descriptor_size,CV_32FC1);
        cv::Mat testMat(testHOG.size(), descriptor_size,CV_32FC1);
        ConvertVectortoMatrix(trainHOG, testHOG, trainMat, testMat);
    
        // SVM 分類器を構成します。
        cv::Ptr<cv::ml::SVM> svm;
        svm = SVMtrain(trainMat, trainLabels);
    
        // SVM 分類器を評価します。
        SVMevaluate(svm, testMat, testLabels, deskewedTestCells);
        return 0;
    }
    

    実行例

    Image Count : 5000
    Descriptor Size : 81
    Kernel type     : 2
    Type            : 100
    C               : 12.5
    Degree          : 0
    Nu              : 0
    Gamma           : 0.50625
    7 != 1
    3 != 2
    2 != 7
    1 != 7
    0 != 9
    3 != 9
    7 != 9
    Accuracy        : 98.6%
    

    7 と認識 (正しくは 1)

    3 と認識 (正しくは 2)

    2 と認識 (正しくは 7)

    1 と認識 (正しくは 7)

    0 と認識 (正しくは 9)

    3 と認識 (正しくは 9)

    7 と認識 (正しくは 9)

    Likeボタン(off)0
    詳細設定を開く/閉じる
    アカウント プロフィール画像

    C/C++やアルゴリズムに注目し、実践的な知識を発信しています!

    記事の執筆者にステッカーを贈る

    有益な情報に対するお礼として、またはコメント欄における質問への返答に対するお礼として、 記事の読者は、執筆者に有料のステッカーを贈ることができます。

    >>さらに詳しくステッカーを贈る
    ステッカーを贈る コンセプト画像

    Feedbacks

    Feedbacks コンセプト画像

      ログインするとコメントを投稿できます。

      ログインする

      関連記事