TouchDesignerでOpenCVを使った簡単な顔認識について

# こんにちは
簡単に自己紹介をします。去年まで東京のデジハリで1年間デザインやプログラミングの勉強をしていました。今年の8月から[株式会社たき工房](https://www.taki.co.jp/) でプログラマーをしています。個人ではTouchDesignerで映像制作やVJをしています。（[Twitter](https://twitter.com/komakinex)）

入社してから初めて作ったコンテンツがこちらです！11/17に大阪で行われたdotFes2019というイベントに出展しました。当日はたくさんの人に楽しんでもらえて大盛況でした！（動画は制作途中のものです）
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">クスールさんと合同で<br>「ブロックに表示されている漢字を、ボードに書いてカメラに認識させると、そのままボードを動かしてブロックを引き抜くことができるゲーム」を作ってます！これは1週間前のデモ <a href="https://twitter.com/hashtag/dotFes?src=hash&amp;ref_src=twsrc%5Etfw">#dotFes</a> <a href="https://twitter.com/hashtag/dotFes2019?src=hash&amp;ref_src=twsrc%5Etfw">#dotFes2019</a> <a href="https://twitter.com/hashtag/madewithunity?src=hash&amp;ref_src=twsrc%5Etfw">#madewithunity</a> <a href="https://twitter.com/hashtag/TouchDesigner?src=hash&amp;ref_src=twsrc%5Etfw">#TouchDesigner</a> <a href="https://t.co/W3Q11eS3c6">pic.twitter.com/W3Q11eS3c6</a></p>&mdash; komakinex (@komakinex) <a href="https://twitter.com/komakinex/status/1194190757579460608?ref_src=twsrc%5Etfw">November 12, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

自分はディスプレイ画面のシステムと、プレイヤーが持つホワイトボードの検出し、文字が書かれた部分を画像で保存するという部分を担当しました。その過程で初めてPythonやOpenCVを触り、TouchDesignerで実装しました。
同じような工程で顔認識できそうだなと思い、やってみました。


# 出来上がり
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">顔認識！<a href="https://twitter.com/hashtag/TouchDesigner?src=hash&amp;ref_src=twsrc%5Etfw">#TouchDesigner</a> <a href="https://twitter.com/hashtag/OpenCV?src=hash&amp;ref_src=twsrc%5Etfw">#OpenCV</a> <a href="https://t.co/q1XvTdZH1y">pic.twitter.com/q1XvTdZH1y</a></p>&mdash; komakinex (@komakinex) <a href="https://twitter.com/komakinex/status/1200838757768818688?ref_src=twsrc%5Etfw">November 30, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

顔認識した部分にモザイクをかける処理をしています。


### サンプルファイル
↓ここにあります↓
https://github.com/komakinex/TD_AdventCalendar2019_facedetect

#### Perform Modeにすると顔検出が働かない事に関しての追記 2020/5/10
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">突然失礼します<br>こちらのbaseの顔認識した部分にピンク色の四角を表示させるものをPerform Modeで実行すると、初めに四角が表示された後、四角が顔を追従してくれないのですが、Perform Modeでずっと顔を追従させるにはどうしたらよいでしょうか<br>お時間あるときでよいので教えていただけると嬉しいです</p>&mdash; AIT (@AIT_iam) <a href="https://twitter.com/AIT_iam/status/1259005179258527744?ref_src=twsrc%5Etfw">May 9, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


`face_detect DAT`は、`PostCook`を使っているので、参照している`face_pre TOP`のCookに変更があった後に処理が走るようになっています。なので顔検出が働いていないのは、`face_pre TOP`のCookに変更がないからだと考えられます。
`face_pre TOP`にCookが走った時（`face_pre TOP`の映像に変化があった時）に、顔検出するようになっているのですが、階層が変わると`face_pre TOP`が動かなくなってしまうので、顔検出の処理が止まっている、ということです。

TouchDesignerのCookについては@kodai100 さんの記事がわかりやすいです。
https://qiita.com/kodai100/items/be672ea8978e443867b0

TouchDesignerの仕様で、表示していない階層の`TOP`は特定の`TOP`を除いて、Cookが止まっています。
サンプルファイルの`face_detectP.toe`を見てもらうと、`Opviewer TOP`で`face_pre TOP`を、Perform Modeのときに表示される`COMP`（`project1`）と同じ階層で見えるようにしています。
こうすることでPerform Modeにした時でも`face_pre TOP`のCookが走り続けるので顔検出が働くようになります。
![コメント 2020-05-10 131342.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144702/9073579a-9914-9ed2-edc8-e890df9d238e.png)


また、`CHOP`でのCookに対する処理だと@shiyuugo さんのこちらの記事が参考になります。
https://qiita.com/shiyuugo/items/cc043b3cafd594ee8aa4

#### 追記終わり


### 動作環境
Windows10
TouchDesigner 099 2019.19930
TouchDesigner 099 2020.22080

macOS 10.14.6
TouchDesigner 099 2019.20140
TouchDesigner 099 2020.22080

で確認済みです！


# 説明
OpenCVや顔認識についての詳細な説明は省きます！
大まかな流れを説明した後、サンプルファイルの、認識した顔のうち1つだけにマスクを被せる`base`、認識した顔全部にモザイクをかける`faces`の順に説明していきます。

## 参考にした記事
・PythonでOpenCVを使って顔を検出している記事
顔検出の基本的な部分を参考にしました。
https://note.nkmk.me/python-opencv-face-detection-haar-cascade/
https://qiita.com/FukuharaYohei/items/ec6dce7cc5ea21a51a82

・TouchDesignerでOpenCVを使って特徴点抽出している記事
抽出した点にジオメトリを当てていること、レンダリング結果を元の画像とうまく合わせている部分を参考にしました。
http://satoruhiga.com/post/extending-touchdesigner/

・TouchDesignerでOpenCVを使って動体追跡している記事
顔認識で使えるように、配列の変換を参考にしました。
https://www.velvet-number.com/atsushi/2019/06/21/150-touchdesigner%e3%81%aepython%e3%81%a7opencv%e3%82%92%e5%ae%9f%e8%a3%85%e3%81%97%e3%81%a6%e3%81%bf%e3%82%8b01/
配列の上下反転
https://www.velvet-number.com/atsushi/2019/06/28/152-touchdesigner%e3%81%aepython%e3%81%a7opencv%e3%82%92%e5%ae%9f%e8%a3%85%e3%81%97%e3%81%a6%e3%81%bf%e3%82%8b02/

## 用意するもの
・TouchDesigner
・顔認識したい画像、webカメラなど
・顔認識の検出器↓
https://github.com/opencv/opencv/tree/master/data/haarcascades
正面を向いた顔を検出したいので`haarcascade_frontalface_~`を使います。自分は`default`を使っています。`alt`などとは精度や顔認識の基準（判定の厳しさ？）の違いがあるみたいです。

## それぞれがしてること
### TouchDesigner
・画像の読み込み
・解像度やアスペクト比の調整
・マスクの生成

### Python
・顔の検出
・検出した場所を四角形で囲み、TouchDesignerにその四角形の座標と大きさを渡す

## 説明

`base`と`faces`の内容を、順に説明していきます。

### ① base
#### 検出器の準備
顔認識するには、顔を検出できる検出器が必要です。自分が使っている`haarcascade_frontalface_default.xml`は、以下のリンク先の`Raw`を押し、出てきた画面をCtrl+S、もしくは`右クリック` → `名前を付けて保存`でゲットできます。
https://github.com/opencv/opencv/blob/master/data/haarcascades/haarcascade_frontalface_default.xml
![コメント 2019-12-02 160922.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144702/02eec5e6-7227-a51c-3210-493f12a4edca.png)

#### ノードを組む
ポイントは、Pythonに渡す前に画像の解像度を1/4にしていることと、モノクロにしていることです。リアルタイムに動かすため、処理の高速化を図っています。
![コメント 2019-12-01 114218.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144702/86506809-d614-2841-4a5d-238f6fe31b09.png)
・`face_input`：顔認識したい画像や映像を受け取ります。
・`res1`：解像度を元画像の1/4にして、処理の負荷を軽くしています。
・`face_detect`：`face_pre`にCookが走った後にここに書いたPythonが動くようになっています。顔を検出する処理が書かれています。
・`face_pos`：検出された顔の位置と幅の情報を`face_detect`から受け取っています。`mask`の`sizex`や`centerx`に直接値を入れてもいいです。見やすいように`face_pos`で受けています。
・`mask`：顔の部分を覆う用の四角形を生成しています。
・`comp1`：元画像にマスクを被せています。

#### face_detect内

```python:face_detect.py

import cv2
import numpy as np

face_cascade = cv2.CascadeClassifier('./tools/haarcascade_frontalface_default.xml')

def onPostCook(changeOp):
    arr = changeOp.numpyArray(delayed=True)
    gray = arr[:, :, 0]
    gray = gray * 255.0
    gray = gray.astype(np.uint8)
    gray_invert = np.flipud(gray)

    faces = face_cascade.detectMultiScale(gray_invert)

    if len(faces) > 0:
        resX = op('face_pre').width
        resY = op('face_pre').height

        for (x, y, w, h) in faces:
            centerX = ((x + w/2) - resX/2) * 4
            centerY = (-(y + h/2) + resY/2) * 4

            op('face_pos').par.value0 = centerX
            op('face_pos').par.value1 = centerY
            op('face_pos').par.value2 = w * 4
            op('face_pos').par.value3 = h * 4
        pass
    return
```

##### 準備

```python

import cv2
import numpy as np

face_cascade = cv2.CascadeClassifier('./tools/haarcascade_frontalface_default.xml')

def onPostCook(changeOp):
    return
```

まずOpenCVと、配列を扱うライブラリNumpyをインポートします。
次に`cv2.CascadeClassifier`で検出器を生成します。サンプルファイルでは、`face_detect.toe`と同階層のフォルダtools内に`haarcascade_frontalface_default.xml`が置いてあるのでファイルの指定が`'./tools/haarcascade_frontalface_default.xml'`という書き方になっています。`.xml`ファイルを置いた場所によって適宜書き換えてください。
`onPostCook`の中にCookが走った後に起こってほしい処理を書きます。

また、`face_detect`のPost CookをONにするのを忘れないようにしてください。
![コメント 2019-12-02 163230.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144702/9821ad4e-1c62-42c0-3c13-de039eed4ed4.png)

##### onPostCook内

```python
arr = changeOp.numpyArray(delayed=True)
gray = arr[:, :, 0]
```
[参考にした記事](http://satoruhiga.com/post/extending-touchdesigner/)によると
> TOPのクラスの`.numpyArray(delayed=True)` のメソッドを呼び出すと`numpy.ndarray`の画像データが取得できます。
`gray = arr[:,:,0]`の部分で赤チャンネルだけ抜きだして、グレースケールにしています。`delayed`引数は、フレーム遅延を許容するかわりに処理を高速化するオプションです。

ということで、ここでは、入力した画像を配列として取得し、グレースケールにしています。

```python
gray = gray* 255.0
gray = gray.astype(np.uint8)
gray_invert = np.flipud(gray)
```

先程の処理で取得した配列をOpenCVに渡すため、Python単体のデータ形式に変換する必要があります。参考にした記事（[こちら](https://www.velvet-number.com/atsushi/2019/06/21/150-touchdesigner%e3%81%aepython%e3%81%a7opencv%e3%82%92%e5%ae%9f%e8%a3%85%e3%81%97%e3%81%a6%e3%81%bf%e3%82%8b01/)と[こちら](https://www.velvet-number.com/atsushi/2019/06/28/152-touchdesigner%e3%81%aepython%e3%81%a7opencv%e3%82%92%e5%ae%9f%e8%a3%85%e3%81%97%e3%81%a6%e3%81%bf%e3%82%8b02/)）によると
>ピクセル色情報の部分の内容が違っていて、
Python単体の場合
[Blue, Green, Red] （値の範囲は0〜255）
Touchdesigner + Pythonの場合
[Blue, Green, Red, Alpha] （値の範囲は0〜1）
ということでした。
　
`f_arr = np.flipud (f_arr)`
で配列の上下反転も必要でした。

ということで、ここでは、取得した画像の配列を、OpenCVに入るように値の範囲を0〜1から0〜255へ引き伸ばし、8ビット符号なし整数に変換し、配列を上下反転させています。


```python
faces = face_cascade.detectMultiScale(gray_invert)
if len(faces) > 0:
```
画像の配列を検出器に渡し、顔が1つでも検出されると以下の処理が走るようにします。

```python
for (x, y, w, h) in faces:
    centerX = ((x + w/2) - resX/2) * 4
    centerY = (-(y + h/2) + resY/2) * 4

    op('face_pos').par.value0 = centerX
    op('face_pos').par.value1 = centerY
    op('face_pos').par.value2 = w * 4
    op('face_pos').par.value3 = h * 4
pass
```
顔は四角形として検出されます。`faces`は四角形の左上のxy座標と四角形の幅と高さを情報として持っているので、それらを`x` `y` `w` `h` に代入します。
![iOS の画像 (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144702/6a39d76a-02aa-67e8-3810-1221459e50f2.jpeg)
OpenCVとTouchDesignerの`mask`とでは座標系がこのように異なります。
また、元画像の解像度に合わせるため最後に4倍します。

以上が`base`の説明です。


### ② faces

基本的な流れは`base`と同じです。`base`と異なる点を説明していきます。
#### 顔の検出
![コメント 2019-12-02 205141.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144702/419c891b-44e1-0f61-e100-2a46cfbe61f2.png)
・`position`：検出された顔の数、位置、四角形の幅の情報を`face_detect`から受け取っています。
・`cal_ratio`：Pythonから受け取った情報を、マスクするために必要な値に計算しています。
・`aspect_ratio`：入力した画像の幅と高さの比です。
・`face_pos`：マスクするために適切に計算された顔の位置です。

#### cal_ratio内
![コメント 2019-12-02 220308.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144702/78b3dff5-cdca-7bb4-26f5-6187f0b7115f.png)
・`info2`：元画像の解像度です。
・`math3`：元画像の比を算出しています。
・`info1`：`faec_pre`の解像度です。
・`math1`：`face_detect`から受け取った数値を正規化しています。
・`math2`：座標を0~1から-1~1に引き伸ばしています。
・`math4`：横方向の数値に`rename1`の数値をかけて、元画像と同じ比にしています。

#### face_detect内
```python:face_detect.py

import cv2
import numpy as np

face_cascade = cv2.CascadeClassifier('./tools/haarcascade_frontalface_default.xml')

def onPostCook(changeOp):
    arr = changeOp.numpyArray(delayed=True)
    gray = arr[:, :, 0]
    gray = gray * 255.0
    gray = gray.astype(np.uint8)
    gray_invert = np.flipud(gray)

    faces = face_cascade.detectMultiScale(gray_invert)

    position = op('position')
    position.clear()
    position.numSamples = len(faces)

    centerX = position.appendChan('centerX')
    centerY = position.appendChan('centerY')
    w = position.appendChan('w')
    h = position.appendChan('h')

    if len(faces) > 0:
        resX = op('face_pre').width
        resY = op('face_pre').height

        for i in range(position.numSamples):
            face = faces[i]

            w[i] = face[2]
            h[i] = face[3]

            centerX[i] = face[0] + face[2]/2
            centerY[i] = -(face[1] + face[3]/2) + resY
        pass
    return
```

`base`と異なる点を説明していきます。

```python

position = op('position')
position.clear()

position.numSamples = len(faces)

centerX = position.appendChan('centerX')
centerY = position.appendChan('centerY')
w = position.appendChan('w')
h = position.appendChan('h')
```
ここでは数値を渡す先の`position`の設定をしています。`len(faces)`をサンプル数にすることで、検出した顔の数だけ情報を持てるようになります。

```python

for i in range(position.numSamples):
    face = faces[i]

    w[i] = face[2]
    h[i] = face[3]

    centerX[i] = face[0] + face[2]/2
    centerY[i] = -(face[1] + face[3]/2) + resY
pass
```
`face`には四角形の左上のx座標, y座標, 幅, 高さの4つ数字が入っています。それらを`face[0]`や`face[1]`というように取り出し、座標や幅に代入します。
![iOS の画像.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144702/b7e3aa48-f5f6-dc81-5068-8122e22136ba.jpeg)
今度はこのような座標系なので、これに合うように計算します。
座標と四角形の大きさを、`position.numSamples`、つまり検出された顔の数だけ繰り返し取得します。

#### マスクの合成
![コメント 2019-12-02 214250.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144702/b2071261-1ac1-6bbc-2c31-6de937e154a0.png)
・`geo1`：`face_pos`の情報から、マスクの元となる四角形をインスタンシングしています。
・`cam1`：投影方法をOrthographicにして、物体の距離によって大きさが変わらないようにしています。（投影について詳しくは[この記事](http://mukaimame.blog111.fc2.com/blog-entry-1018.html)が参考になりました。）
・`render1`：インスタンシングした四角形を、元画像と同じ解像度でレンダリングしています。
・`res2`：モザイクがかかったように見せるため、マスクされた部分（検出された顔）の解像度を下げています。`mosaic_slinder`によって手軽にモザイクの目の粗さを変えられるようにしています。
・`res3`：マスク部分の解像度を元画像に合わせています。


以上が`faces`の説明です。

## 注意点
・`face_pre`のViewerをオフにするとPythonが止まって顔認識できなくなります。（Cookが走らなくなるから？ぽいです）
・解像度を小さくする処理をPythonの中でするとfpsがけっこう落ちます。Pythonに渡す前にTOPで処理したほうが良いです。
（`arr = cv2.resize(arr, (int(frame.shape[1] / 4), int(frame.shape[0] / 4)))`の部分）
![コメント 2019-12-01 111822.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144702/d61e88ca-ceba-796c-0cb1-3dc2986b4308.png)

# 最後に
検出した座標を受け取るだけなので、TouchDesignerのノードを繋ぐ部分は簡潔な仕組みになっています。Python内で画像を配列に変換したり、元の画像と合成するための計算がややこしいので、そのあたりをできるだけ丁寧に書きました。わからないところ、間違っているところがあれば連絡してください。

告知です！12/11にたき工房でTouchDesignerのもくもく会を開催します。今後も月1で行う予定です。次回はもっと多くの人に参加してもらえるように人数の枠を増やすつもりです。
また、今後はTouchDesignerで縛らず色々なツールでもくもくする会にしていきたいなと思っています。自分のTwitterアカウントからアナウンスするので、チェックしてみてください！
Peatixをフォローしてもらえると、チケットがオープンした時通知が行きます。
→https://techlab-tdmokumoku.peatix.com/

# おまけ
まったく関係ないですが、めちゃおもろかった動画を貼っておきます。

Aマッソのコント
https://youtu.be/ZxF3pdLJhiQ

藤井健太郎が千原ジュニアの過去の映像と声で、舞台にいる千原ジュニアと会話する企画です。これはその紹介動画です。
https://youtu.be/WQdy70xoGMk

ラランドの漫才
https://youtu.be/YBBjU1TtN-8

ぜひ見てみてください！