コンテンツにスキップ

Top

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

OpenCVのテンプレートマッチング関数を用いて物体を検出します。

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

OpenCVは6種類のテンプレートマッチング手法を提供します。

大きくは3種類で、それぞれに正規化の有無があり、計6種類となっています。

・二乗差分法 cv::TM_SQDIFF

テンプレート画像と入力画像の画素の差分を二乗したものの総和
 (値が0に近いほど似ていると判断できる)
 SSD(Sum of Squared Difference)とも呼ばれる

・正規化二乗差分法 cv::TM_SQDIFF_NORMED

二乗差分法を正規化したもの

・相互相関法 cv::TM_CCORR

テンプレート画像と入力画像の画素同士の内積の総和
 (値が1に近いほど似ていると判断できる)

・正規化相互相関法 cv::TM_CCORR_NORMED

相互相関法を正規化したもの
 NCC(Normalized Cross-Correlation)とも呼ばれる

・相関係数法 cv::TM_CCOEFF

テンプレート画像の平均値と入力画像の平均値画素同士の内積の総和
 (値が1に近いほど似ていて、-1に近いほど不一致と判断できる。なお、値が0に近いものは無相関である)

・正規化相関係数法 cv::TM_CCOEFF_NORMED

相関係数法を正規化したもの (ZNCC(Zero-means Normalized Cross-Correlation)とも呼ばれる)

大まかには、

計算量(処理時間)

(正規化)相関係数法 > (正規化)相互相関法 > (正規化)二乗差分法

精密さ

(正規化)相関係数法 > (正規化)相互相関法 > (正規化)二乗差分法

という感じ。

処理速度お求めるなら二乗差分法。精密さを求めるなら正規化相関係数法を選択します。

注意が必要なのは、(正規化)二乗差分法では0に近いほどマッチング率が高く、そのほかの手法は1に近いほどマッチング率が高くなります。

テンプレートマッチングサンプルプログラム(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("messi5.jpg", cv::IMREAD_COLOR);
    cv::Mat templ = cv::imread("messi_face.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];

        // 類似度MAP格納領域の確保(結果MAPのサイズは入力画像からテンプレート画像を引いて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);

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

        // 最後のファイル出力用に類似度MAPのバックアップ取っとく。この行為はテンプレートマッチングには必要ない。結果出力のための処理
        cv::Mat resultBackup = result.clone();

        // 結果を正規化する
        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);
        cv::rectangle(tmp, matchLoc, cv::Point(matchLoc.x + templ.cols, matchLoc.y + templ.rows), cv::Scalar::all(255), 2, 8, 0);

        // 結果MAPにも枠を書く。ただし、MAPは縦横がそれぞれ入力画像からテンプレート画像を引いて1足したサイズになるので、そのままの座標では当然うまくいかない。その比率分小さくする。
        double ratio_w = (double)result_cols / (double)image.cols;
        double ratio_h = (double)result_rows / (double)image.rows;
        double _x = ratio_w * (double)matchLoc.x;
        double _y = ratio_h * (double)matchLoc.y;
        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), 2, 8, 0);

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

        // ファイルとして保存
        cv::imwrite(matchMethodName[i] + ".png", tmp);
        // cv::imshow()と異なり、floatの値のままではpng保存したときに真っ黒になる。ので、0 - 255の範囲に正規化してから保存する。なお、すでに0 - 1で正規化したものを正規化してもダメなので注意。大元のresultを正規化する。
        cv::normalize(resultBackup, resultBackup, 0, 255.0, cv::NORM_MINMAX, -1, cv::Mat());
        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), 2, 8, 0);
        cv::imwrite(matchMethodName[i] + "_result.png", resultBackup);
    }

    cv::waitKey();
    return 0;
}

実行結果

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

相互相関法(cv::TM_CCORR)のみ外れていますね。
MAPを見ると確かに右下が白いです。
なんでかは知らん。

以上。