TouchDesignerでOpenCVを使った簡単なロゴ検出について

#Hello!!!!
What's up everybody guys!!
ということで
あまたのTouchdesignerユーザーが集うこのアドカレの`TOP`バッターを務めさせていただくことに感謝を申し上げます。

完全にタイトルが2年前の1日目の

[TouchDesignerでOpenCVを使った簡単な顔認識について](https://qiita.com/komakinex/items/5b84b88d537d393afc9)

に寄せていますが、これも何かの縁ですので**OpenCVでのロゴ検出**についてお話していきます

#今回の制作物
![ezgif.com-gif-maker (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704869/c0c76c76-c539-e386-05ea-1c7c7eb362b8.gif)

buttonを押すと、スタバのロゴ検出して、そこに　!LOGO!　がついてきますね。
今回は0からこれを作っていきます。

#チュートリアル
まず初めに、この記事には間違っていることやよりいい方法があるかもしれません。その際はこそっと教えてください！
個人的に、文字で説明するより[**Youtube**](https://www.youtube.com/watch?v=9rWMx2abh-A)でチュートリアルをやることが多くそっちの方がわかりやすいので、[**動画**](https://www.youtube.com/watch?v=9rWMx2abh-A)を作っています！
ぜひ[こちら](https://www.youtube.com/watch?v=9rWMx2abh-A)からご覧ください(少し前にqiitaでyoutubeを埋め込みできるようになりました)

<iframe width="560" height="455" src="https://www.youtube.com/embed/9rWMx2abh-A" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>


内容もりもりで少し長くなっちゃった(てへ)

#要点解説
動画の内容を大事なところに絞って解説していきます。
#今回使うロゴ
今回はこちらのスターバックスのロゴを使います
![starbacks_logo.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704869/8311e82c-53fb-9035-621a-ddf87fc889a7.jpeg)
[ネットで拾ってきたやつ](https://www.google.com/url?sa=i&url=https%3A%2F%2Flogos-download.com%2F1819-starbucks-logo-download.html&psig=AOvVaw0GgMeTd4Gc2RICO6vnaqfr&ust=1637755486755000&source=images&cd=vfe&ved=0CAwQjhxqFwoTCMi6ieK4rvQCFQAAAAAdAAAAABAD)ですが
ただ個人として使うので、勝手に使ったねーはい！逮捕！とか言わないでええ
###project file
そしてこちらのロゴの写真も入ったproject fileが[**github**](https://github.com/miwa-maroon/logo_detection.git)に上がっていますので
[**動画の概要欄のリンク**](https://www.youtube.com/watch?v=9rWMx2abh-A)からぜひダウンロードしてくださいまし

#基本セットアップ
![basic_setup.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704869/c5c7639d-324d-7a0f-f4b0-144a8a621082.png)

- `video device in` -> ロゴを見つけるTOP
- `movie file in` -> ロゴ参照

それぞれnullにつなげます。
そして検知する方を **train**
元のロゴを **query**
と名付けます。

- `script SOP` -> 結果確認用 「matching_result」
- `button COMP` -> ロゴ検出スタート
- `chop execute DAT` Off To Onで使います。**そしてここにコードを書いていきます！**

#全コード
少し説明することが多いので、まず全コードを公開します！
その後pointごとに解説します！

```python:chopexec1
import numpy
import cv2

def onOffToOn(channel, sampleIndex, val, prev):
	trainOp = op('train')
	queryOp = op('query')
	for i in range(2):
		train = trainOp.numpyArray(delayed = True)
		query = queryOp.numpyArray(delayed = True)
	

	input_list = []

	ratio = 0.65

	for item in [train, query]:
		item_255 = item * 255
		item_uint8 = item_255.astype('uint8')
		input_list.append(item_uint8)
	
	train = input_list[0]
	query = input_list[1]

	#feature detect
	detector = cv2.ORB_create()
	kp_query, des_query = detector.detectAndCompute(query,None)
	kp_train, des_train = detector.detectAndCompute(train,None)

	dst_query = cv2.drawKeypoints(query, kp_query, None)
	

	#matcher
	matcher = cv2.BFMatcher(cv2.NORM_HAMMING)

	try:
		#matching
		matches = matcher.knnMatch(des_query, des_train, k=2)

		#ratio test
                good_matches = []
		for q, t in matches:
			if q.distance < t.distance * ratio:
				good_matches.append(q)
		matches = good_matches

		num_goodmatches = len(matches)
		op('good_matches').par.value0 = num_goodmatches

		if num_goodmatches > 8:

			x_train_pt = [kp_train[m.trainIdx].pt[0] - (trainOp.width/2) for m in matches]
			y_train_pt = [kp_train[m.trainIdx].pt[1] - (trainOp.height/2) for m in matches]

			x_mean = sum(x_train_pt) / len(x_train_pt)
			y_mean = sum(y_train_pt) / len(y_train_pt)

			op('text_logo').par.fontalpha = 1
			op('transform_logo').par.tx = x_mean
			op('transform_logo').par.ty = y_mean

		else:
			op('text_logo').par.fontalpha = 0

		#draw_matching
		draw_params = dict(matchColor = (0, 255, 0),
						singlePointColor = (255, 0, 0),
						flags = cv2.DrawMatchesFlags_DEFAULT)
		matching_result = cv2.drawMatches(query, kp_query, train, kp_train, matches, None, **draw_params)


		op('matching_result').copyNumpyArray(matching_result)
	except cv2.error:
		op('matching_result').copyNumpyArray(dst_query)
	return

def whileOn(channel, sampleIndex, val, prev):
	return

def onOnToOff(channel, sampleIndex, val, prev):
	return

def whileOff(channel, sampleIndex, val, prev):
	return

def onValueChange(channel, sampleIndex, val, prev):
	return
	
```
そして残りのノードも全公開
![all_nodes.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704869/dfc3c362-3feb-20bb-888c-8e2b35c04eca.png)

#ロゴ検出までの一連の流れ
さあロゴ検出において、OpenCV使うことは予想できていると思います。
**じゃあ具体的にどうやんの？？**
これですよね。
ちょーざっくり言うと、

1.TOPをnumpyに変える
2.train, queryそれぞれの特徴点を検出する
3.それぞれの特徴点をマッチングさせる
4.マッチングした特徴点が多ければ～、ロゴだよね！！

って感じ！
###1.TOPをnumpyに変える
これ嘘かと思うかもしれないんですが、
**TOPってnumpyなんです**
**TOPって！numpyなんです！**
ごり押ししてすみません
あ、numpyってなに？って方のために
> NumPyはベクトルや行列の計算を高速に処理するためのライブラリ
です！

そしてTOPはnumpyの配列になっているので、OpenCVとかのライブラリが使えるわけですね！

今回で言うとここのコードでTOPをnumpyに変換

```python:chopexec1
for i in range(2):
	train = trainOp.numpyArray(delayed = True)
	query = queryOp.numpyArray(delayed = True)
```

numpyの配列(matching_result)をTOPに変換

```python:chopexec1
     op('matching_result').copyNumpyArray(matching_result)
```


####ちょっとした注意ポイント
`numpyArray()`だけだと、呼び出すタイミングによってはうまくnumpyに変換することができないことがあります。
それを防ぐのが`(delayed = True)`です。
**これでうまくいった！**
と思いがちなんですが、(delayed = True)にすると、
**前の処理の結果が返ってきます！！**
これは困る！！！！！
ってことで**for文で2回処理**を呼んであげてるわけですね！
詳しくは[TOP classのdocument](https://docs.derivative.ca/TOP_Class)にも記載があるので是非！！

###2.train, queryそれぞれの特徴点を検出する
特徴点を検出するには、**検出器**が必要になります。
フリーザーの戦闘力が知りたい！！ってなったらスカウターが必要ですよね。
そのスカウター自体が検出器
そんなイメージです

今回は`ORB`というアルゴリズムで検出器を作ります

```python:chopexec1
	detector = cv2.ORB_create()
```

そして実際に検出する！


```python:chopexec1
	kp_query, des_query = detector.detectAndCompute(query,None)
	kp_train, des_train = detector.detectAndCompute(train,None)
```


思ったよりシンプル。
返り値は、

- [keypoint](https://docs.opencv.org/3.4/d2/d29/classcv_1_1KeyPoint.html) -> 座標とかが入ってる
- descriptor -> マッチングの際に使う

って感じ！

検出された特徴点を確認したいときは、

```python:chopexec1
	dst_query = cv2.drawKeypoints(query, kp_query, None)
```

###3.それぞれの特徴点をマッチングさせる
特徴点同士をマッチングさせるときには、**マッチング器**が必要になります。
これはレアスニーカー鑑定士と一緒ですね。そのスニーカーの特徴が本物と同じであれば、それはレアものとして間違いない！！
となるわけです。

今回はBFMatcherというものを使います！

```python:chopexec1
	matcher = cv2.BFMatcher(cv2.NORM_HAMMING)
```

これまたシンプル！
そしたら、今回のメインディッシュ！
**特徴点マッチング！！**

```python:chopexec1
        #matching
		matches = matcher.knnMatch(des_query, des_train, k=2)

		#ratio test
                good_matches = []
		for q, t in matches:
			if q.distance < t.distance * ratio:
				good_matches.append(q)
		matches = good_matches
```

`matcher.knnMatch(des_query, des_train, k=2)` はknnMatchといって
それぞれをマッチングさせて上位k個の特徴点を探します

そして信頼度の低い結果を除くため
**ratio test**というものを行います！
ratioの値が1に近ければ、甘くなって、0に近いほど厳しくなります。

で最終的に、**matches**にマッチングできた特徴点のリストが入っている！
というわけでございます！

###4.マッチングした特徴点が多ければ～、ロゴだよね！！
さあこの**matches中身が多ければ多いほどマッチング率が高い**ので
それはロゴ！！
とみなすことができます。
今回はその閾値を**8**としてそれ以上`len(matches)`があれば検出！
ということでした。

```python:chopexec1
num_goodmatches = len(matches)
		op('good_matches').par.value0 = num_goodmatches

		if num_goodmatches > 8:
```

#特徴点の位置
特徴点の位置、ようはピクセルがどこか知りたいときありますよね！
それは、keypointの**pt**というアトリビュートに格納されています。
例えば、indexが1の特徴点の位置は、
`kp_train[1].pt`  -> (x,y)
です。
今回では、特徴点がいくつかあるので平均をその位置として、
値がxは0~1280、yは0~720で入ってくるので
それをTouchdesigner用に、
x -640~640
y -360~360
に変換しています。

transform_logoのTranslateをPixelsに変えるのをお忘れなく！！
![transform_pixel.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704869/b50a065e-aae9-46dd-307c-7f89333d7bbf.jpeg)

#そのほかは動画にて
だいたいのポイントはこちらにて解説しました！
が、例外処理やその他細かいところは[動画](https://www.youtube.com/watch?v=9rWMx2abh-A)にてご覧ください！

今回のロゴ検出以外にも、[L-system](https://youtu.be/5ghm-v1diaY)や[Hokuyo](https://youtu.be/opDVtyOQIeY)など**Touchdesigner初心者向けにチュートリアル**いくつか出してるので
是非ご覧いただければ嬉しい限りです！
僕の思いとしては、**少しでもTouchdesignerを触る人が増えて業界全体を盛り上げることができたらいいな！**
と思って、動画や記事を作っていますの今後も皆さんと一緒に歩みたい！

ちょっと最後熱くなってしまいましたが、最後まで読んでいただきありがとうございます！

それでは～～～～