コンテンツにスキップ

Top

OpenCVのテンプレートマッチングを使って複数の物体検出する

OpenCVのテンプレートマッチング関数を用いて1枚の入力画像の中に複数存在している物体をすべて検出します。

OpenCVが提供する物体検出の種類

1枚の画像の中に1つだけ物体検出対象があればいいのですが、実際には複数存在している場合があります。

今まではmatchTemplate()を用いて作成した類似度MAPの最大(もしくは最小)となる点を元に検出してきました。

例えばpythonのmatchTemplate()であれば、戻り値が類似度だけでなく、座標もセットで配列に入って返ってきてくれますので簡単です。

しかしながら、C++のmatchTemplateは類似度だけで作られたマップしか返ってきません。

それを元にminMaxLoc()で場所を求めるのですが、これでは2番目、3番目の場所がわかりません。

どうしましょう?

答えは簡単で、見つかるたびに類似度MAPの点を「塗りつぶして」しまえばよいのです。

テンプレートマッチングサンプルプログラム(VC++)

すでに、VC++でOpenCVを実行するための準備は済んでいるものとします。
できていない場合は、https://jitaku.work/opencv/vc/opencv/を参照して準備しましょう。

テンプレート画像
入力画像

ソースコード

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
#include <iostream>
#include <math.h>

int main(int argc, char** argv)
{
    cv::Mat image = cv::imread("flower.jpg", cv::IMREAD_COLOR);
    cv::Mat templ = cv::imread("flower_templ.jpg", cv::IMREAD_COLOR);

    if (image.empty() || templ.empty())
    {
        std::cout << "Can't read one of the images" << std::endl;
        return EXIT_FAILURE;
    }

    int matchMethodEnum[] = { cv::TM_SQDIFF, cv::TM_SQDIFF_NORMED, cv::TM_CCORR, cv::TM_CCORR_NORMED, cv::TM_CCOEFF, cv::TM_CCOEFF_NORMED };
    std::string matchMethodName[] = { "01_TM_SQDIFF", "02_TM_SQDIFF_NORMED", "03_TM_CCORR", "04_TM_CCORR_NORMED", "05_TM_CCOEFF", "06_TM_CCOEFF_NORMED" };

    for (int i = 0; i < 6; i++) {
        int match_method = matchMethodEnum[i];

        // 類似度マップ結果格納領域の確保(入力画像からテンプレート画像を引いて1足したサイズになるので注意)
        int result_cols = image.cols - templ.cols + 1;
        int result_rows = image.rows - templ.rows + 1;
        cv::Mat result;
        result.create(result_rows, result_cols, CV_32FC1);

        // 入力画像と類似度マップの高さ幅の割合を出しておく
        double ratio_w = (double)result_cols / (double)image.cols;
        double ratio_h = (double)result_rows / (double)image.rows;

        // マッチング結果の画像を作成する
        cv::matchTemplate(image, templ, result, match_method);

        // pngファイル保存用に0-255で正規化したものも準備している(この処理は物体検出処理と関係ない)
        cv::Mat resultBackup = result.clone();
        cv::normalize(resultBackup, resultBackup, 0, 255.0, cv::NORM_MINMAX, -1, cv::Mat());

        // 結果を正規化する
        if (match_method == cv::TM_SQDIFF || match_method == cv::TM_CCORR || match_method == cv::TM_CCOEFF || match_method == cv::TM_CCOEFF_NORMED) {
            cv::normalize(result, result, 0.0, 1.0, cv::NORM_MINMAX, -1, cv::Mat());
        }

        // 結果内の最大値最小値それらのポジションを取得する
        double minVal;
        double maxVal;
        cv::Point minLoc;
        cv::Point maxLoc;
        cv::Point matchLoc;
        cv::minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc, cv::Mat());

        // 差の二乗和は0に近いほうが一致度が高い他は逆
        if (match_method == cv::TM_SQDIFF || match_method == cv::TM_SQDIFF_NORMED)
        {
            matchLoc = minLoc;
        }
        else
        {
            matchLoc = maxLoc;
        }

        // 入力画像を出力用にコピーしておく(枠などを書くため)
        cv::Mat tmp;
        image.copyTo(tmp);

        // 念のため10回ぐらいでbreak
        for (int j = 0; j < 10; j++) {
            double minVal = 0.0;
            double maxVal = 0.0;
            cv::Point minLoc;
            cv::Point maxLoc;
            cv::Point matchLoc;
            minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc, cv::Mat());

            if (match_method == cv::TM_SQDIFF || match_method == cv::TM_SQDIFF_NORMED) {
                // TM_SQDIFFとTM_SQDIFF_NORMEDは値が0に近いほど精度が高いのでminValが正解
                if (minVal > 0.05) {
                    break;
                }
                matchLoc = minLoc;

                // 座標はあくまで入力画像における座標であり一回り小さい類似度マップの座標とは違う
                // よってその割合の分だけ座標をずらす必要がある
                double _x = ratio_w * (double)matchLoc.x;
                double _y = ratio_h * (double)matchLoc.y;

                // 塗りつぶすこの時注意しないといけないのはTM_SQDIFFやTM_SQDIFF_NORMEDは値が0に近いほど正解色でいうと黒に近いほど正解なので白で塗りつぶす点
        // なお実際にはちょっとはみ出るように塗りつぶしといたほうが良い最も良い点の周辺も当然類似度が高いので
                cv::rectangle(result, cv::Point((int)_x, (int)_y), cv::Point((int)(_x + (templ.cols * ratio_w)), (int)(_y + (templ.rows * ratio_h))), cv::Scalar::all(255), -1);

                // 以下はpngファイル用物体検出処理そのものとは無関係
                cv::rectangle(resultBackup, cv::Point((int)_x, (int)_y), cv::Point((int)(_x + (templ.cols * ratio_w)), (int)(_y + (templ.rows * ratio_h))), cv::Scalar::all(255), -1);
            }
            else {
                // TM_CCORRやTM_CCOEFFは値が1に近いほど精度が高いのでmaxValが正解
                if (maxVal < 0.95) {
                    break;
                }
                matchLoc = maxLoc;

                // 座標はあくまで入力画像における座標であり一回り小さい類似度マップの座標とは違う
                // よってその割合の分だけ座標をずらす必要がある
                double _x = ratio_w * (double)matchLoc.x;
                double _y = ratio_h * (double)matchLoc.y;

                // 塗りつぶすこの時注意しないといけないのはTM_CCORRやTM_CCOEFFは値が1に近いほど正解色でいうと白に近いほど正解なので黒で塗りつぶす点
        // なお実際にはちょっとはみ出るように塗りつぶしといたほうが良い最も良い点の周辺も当然類似度が高いので
                cv::rectangle(result, cv::Point((int)_x, (int)_y), cv::Point((int)(_x + (templ.cols * ratio_w)), (int)(_y + (templ.rows * ratio_h))), cv::Scalar::all(0), -1);

                // 以下はpngファイル用物体検出処理そのものとは無関係
                cv::rectangle(resultBackup, cv::Point((int)_x, (int)_y), cv::Point((int)(_x + (templ.cols * ratio_w)), (int)(_y + (templ.rows * ratio_h))), cv::Scalar::all(0), -1);
            }

            cv::rectangle(tmp, matchLoc, cv::Point(matchLoc.x + templ.cols, matchLoc.y + templ.rows), cv::Scalar(0, 0, 255), 5, 8, 0);

            // 表示
            cv::imshow(matchMethodName[i], tmp);
            cv::imshow(matchMethodName[i] + "_result", result);

            // ファイルとして保存
            cv::imwrite(matchMethodName[i] + "_flower.png", tmp);
            cv::imwrite(matchMethodName[i] + "_flower_result.png", resultBackup); // resultを用いると正しく絵が出ない
        }
    }

    cv::waitKey();

    return 0;
}

実行結果

物体検出手法 結果画像 類似度MAP
二乗差分法 TM_SQDIFF
正規化二乗差分法 TM_SQDIFF_NORMED
相互相関法 TM_CCORR
正規化相互相関法 TM_CCORR_NORMED
相関係数法 TM_CCOEFF
正規化相関係数法 TM_CCOEFF_NORMED

また相互相関法(cv::TM_CCORR)のみ外れていますね。
なんなんでしょうかね?こいつ。OpenCVのバグなんでしょうか?

以上。