コンテンツにスキップ

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/を参照して準備をしてください。

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

ソースコード

 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
#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を見ると確かに右下が白いです。
なんでかは知らん。

以上。