コンテンツにスキップ

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/を参照して準備しましょう。

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

ソースコード

#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のバグなんでしょうか?

以上。