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