cv::matchShapesによる形状マッチングを試してみた

# 形状マッチングの特徴
輝度情報を用いたテンプレートマッチングと異なり、形状情報を用いたマッチングには以下の利点があるとされています。
	1. 回転やスケール変化に対応できる
	2. 輝度値の変化に強い
	3. 特徴的な形状を持つモノ(角がある、など)のマッチング精度が高い
都合よくOpenCVにはcv::matchShapesという関数があるので、それを使ってチャレンジしてみました。

# 例題
以下の入力画像からテンプレートを探します。
**入力画像**
![src.jpg](https://qiita-image-store.s3.amazonaws.com/0/111593/863ff86d-8249-a7f1-5940-5dbe520a2fbf.jpeg)

**テンプレート**
![temp.jpg](https://qiita-image-store.s3.amazonaws.com/0/111593/02de596c-6379-0883-be2e-086e75a199c4.jpeg)

# 具体的な手順
cv::matchTemplateと異なり、cv::matchShapesは画像同士の類似度を求めるだけの関数です。したがって、「マッチングしたい対象物がどこにあるか」は別に探す必要があります。
今回は以下の手順をとりました。
	1. 入力画像とテンプレート画像をグレースケール化→二値化
	2. モルフォロジー演算でノイズ除去
	3. 入力画像にラベリングを行い、各ブロブの情報を取得(OpenCV3.0使用)
	4. 各ブロブに対してcv::matchShapesを行い、閾値以下ならマッチング

二値化+モルフォロジー演算後のテンプレートは以下のようになりました。元々黒かった部分が白になるように反転しています。実用的には二値化閾値の決定という重要な問題があるのですが、今回は決め打ちです。
**前処理後のテンプレート**
![temp.jpg](https://qiita-image-store.s3.amazonaws.com/0/111593/26998891-08a5-7f7c-0219-92b79e9d83b7.jpeg)

ブロブは以下の画像のように取得できました。ビンの絵の部分だけマッチングしてくれれば成功ですが…。
**ラベリングによって得られたブロブ**
![match.jpg](https://qiita-image-store.s3.amazonaws.com/0/111593/e7e79c0a-922e-79c3-7323-64d975bb97af.jpeg)

# 結果
**すいません、ここまで偉そうに話してきましたが100点満点にはなりませんでした**
以下の画像のように、机の端っこがマッチングしてしまっています。しかも困ったことに、ここの類似度が一番高くなっています。cv::matchShapesはHuモーメントを使っているそうなので、そのあたりについて一度深く勉強する必要がありそうです。

（まあ、このテンプレートだったらもっと複雑な特徴量を使った方がいいよね…)
![dst.jpg](https://qiita-image-store.s3.amazonaws.com/0/111593/2c0146ce-b1bf-8002-e6f9-b5a952465fd5.jpeg)

# コード
おまけ。OpenCV3.0は必須。ファイル入出力なんかはかなり適当です。

```Cpp:ShapeMatching.cpp
#include <iostream>
#include "opencv2\\opencv.hpp"
#include "opencv2\\opencv_lib.hpp"

// 定数
const int tempThres = 48;			// テンプレート二値化閾値
const int srcThres  = 36;			// 入力画像二値化閾値
const double huMomentThres = 0.01;	// 形状マッチングの閾値

int main()
{
	cv::Mat temp = cv::imread("./image/temp.jpg", 0); // ニッカのおじさんの画像
	if (temp.empty()){
		std::cout << "テンプレート読み込みエラー" << std::endl;
		return -1;
	}

	cv::Mat src = cv::imread("./image/src.jpg"); // 撮影された画像
	if (src.empty()){
		std::cout << "入力画像読み込みエラー" << std::endl;
		return -1;
	}
	cv::Mat src_gray;
	cv::cvtColor(src, src_gray, CV_BGR2GRAY);

	// 二値化
	cv::Mat temp_bin, src_bin;
	cv::threshold(temp, temp_bin, tempThres, 255, cv::THRESH_BINARY_INV);
	cv::threshold(src_gray, src_bin, srcThres, 255, cv::THRESH_BINARY_INV);

	// ノイズ除去
	cv::morphologyEx(temp_bin, temp_bin, cv::MORPH_OPEN, cv::Mat(), cv::Point(-1, -1), 2);
	cv::morphologyEx(src_bin,  src_bin,  cv::MORPH_OPEN, cv::Mat(), cv::Point(-1, -1), 2);

	// ラベリング
	cv::Mat labelsImg;
	cv::Mat stats;
	cv::Mat centroids;
	int nLabels = cv::connectedComponentsWithStats(src_bin, labelsImg, stats, centroids);

	// ラベルごとのROIを得る(0番目は背景なので無視)
	cv::Mat roiImg;
	cv::cvtColor(src_bin, roiImg, CV_GRAY2BGR);
	std::vector<cv::Rect> roiRects;
	for (int i = 1; i < nLabels; i++) {
		int *param = stats.ptr<int>(i);

		int x = param[cv::ConnectedComponentsTypes::CC_STAT_LEFT];
		int y = param[cv::ConnectedComponentsTypes::CC_STAT_TOP];
		int height = param[cv::ConnectedComponentsTypes::CC_STAT_HEIGHT];
		int width = param[cv::ConnectedComponentsTypes::CC_STAT_WIDTH];
		roiRects.push_back(cv::Rect(x, y, width, height));

		cv::rectangle(roiImg, roiRects.at(i-1), cv::Scalar(0, 255, 0), 2);
	}

	// huモーメントによる形状マッチングを行う
	cv::Mat dst = src.clone();
	for (int i = 1; i < nLabels; i++){
		cv::Mat roi = src_bin(roiRects.at(i-1));	// 対象とするブロブの領域取り出し
		double similarity = cv::matchShapes(temp_bin, roi, CV_CONTOURS_MATCH_I1, 0);	// huモーメントによるマッチング

		if (similarity < huMomentThres){
			cv::rectangle(dst, roiRects.at(i - 1), cv::Scalar(0, 255, 0), 2);
		}
	}

	cv::imshow("template", temp);
	cv::imshow("src", src);
	cv::imshow("dst", dst);
	cv::waitKey();

	cv::imwrite("dst.jpg", dst);

	return 0;
}
```