なるように、なる

徒然なつぶやき、備忘録です。

TouchDesignerでOpenCVのDNNモジュールを使う

WHY?

すごく調子の良いNoise Glitchが手に入ったのと、SquarepusherのMVに触発されたことで、openCVオンリーでお手軽にSemanticSegmentationが出来ないかと調べ始めました。
するとEnetという手法・モデルがヒットし、試してみることにしました。
できました。

本稿では、dnnについては何も語りません(語れません)。
TouchDesignerでdnnを使う前後の部分の学びの共有になります。

サンプルはこちら↓
https://github.com/thinpedelica/touchdesigner_sample/tree/master/Segmentation

Items

  • EnetのモデルをTouchDesignerで動かす
  • DATにTOPの画像を入力する
  • DATからTOPに画像を出力する(Spout for Python)
  • 性能について

Environment

  • OS: windows10
  • CPU: core-i7 9750H
  • GPU: GTX 1660Ti
  • TouchDesigner: 2020.22080

References

EnetのモデルをTouchDesignerで動かす

Enetモデルは、TouchDesignerに同梱されているopenCVだけで動作しました。
pyimagesearchからサンプルコード+学習済みモデルをダウンロードできます (メールアドレス登録するとリンクを送ってくれます) 。

ローカルのPythonで動くことを確認出来たら、あとをText DATに貼り付けるだけでした。

※なお、pyimagesearchはライセンスが不明だったので、gitに登録したコードとモデルは、本家のものに変更しました。

動作確認が出来たら、実際に使用するかたちにします。
今回はOP Execute DATに実装しました。

初期化処理

net = cv2.dnn.readNet(os.path.join(ENET_BASE_PATH, MODEL_FILE))

Cookごとの処理

blob = cv2.dnn.blobFromImage(image, 1 / 255.0, (WIDTH, HEIGHT), 0,
    swapRB=True, crop=False)

net.setInput(blob)
output = net.forward()

classMap = np.argmax(output[0], axis=0)
mask = COLORS[classMap]
mask = cv2.resize(mask, (image.shape[1], image.shape[0]),
    interpolation=cv2.INTER_NEAREST)

すごく短いですね。機械学習すごい。

DATにTOPの画像を入力する

過去のTouchDesignerは、DATでTOPの画像を参照することができなかったらしいですが、今はできます。
@satoruhigaさん、@komakinexさん、@v_ohjiさんの記事を参考にしました。感謝!

TOPから画像を抜く

def onPostCook(changeOp):
    frame = changeOp.numpyArray(delayed = True)

TouchDesignerの画像フォーマットをopenCVのフォーマットに変換する

f_arr = frame[:, :, 0:3]
f_arr = f_arr * 255.0
image_invert = f_arr.astype (np.uint8)
image = np.flipud(image_invert)

なお、移植元のコードではnumPyで画像のリサイズを行っていたのですが、TDSWで聞いたところ、TOPで変換(リサイズなど)したほうが速いようなので、事前にTOPで実施するようしました。

DatからTOPに画像を出力する

2020.04時点で調べた限り、DATから直接TOPに画像を出力する方法はありませんでした。
そのため、DATからSpoutで送信し、Syphon Spout In TOPで受信するという方法で、Segmentation領域の画像をTOPに渡しました。
※ただ、これみんなやりたいことだと思うので、今後いい感じの方法が追加される気がします。

調べたらSpout-for-Pythonがbuild済のpyd(dll)を提供してくれていたり、サンプルにTouchDesignerを使っていたりですごく良い感じだったのですが、私の環境ではなぜかモジュールをimportする際にエラーがでてしまいました。

諦めて、こちらを参考に自分でビルドしました。
これが思いのほか一発で出来たので、Spout-for-Pythonがコケた人は、諦めてビルドしなおしましょう!

思い出せる限りの注意点

  • ローカルのPythonのバージョンをTouchDesignerと合わせておきましょう(私は3.7.2)

  • boostのバージョンはTouchDesignerと合ってなくても大丈夫だった(私は1.72)

  • 「create user-config.jam file and add this:」というところでは、ユーザフォルダ直下に「user-config.jam」を作成する
    (C:\Users\shampagne\user-config.jam)

  • Spout for Pythonをビルドする際に指定するincludeとlibのディレクトリは、ローカルのPythonのものでOK

これで無事にSpout for Pythonをimportできるようになりました。
サンプルコードから送信に必要な処理を抜いてくると以下のようになりました。

# setup the texture so we can load the output into it
glBindTexture(GL_TEXTURE_2D, textureSendID)
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
# copy output into texture
glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB, WIDTH, HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, output )

# Send texture to spout...
# Its signature in C++ looks like this: bool SendTexture(GLuint TextureID, GLuint TextureTarget, unsigned int width, unsigned int height, bool bInvert=true, GLuint HostFBO = 0);
if sys.version_info[1] == 5:
    spoutSender.SendTexture(textureSendID, GL_TEXTURE_2D, spoutSenderWidth, spoutSenderHeight, False, 0)
else:
    spoutSender.SendTexture(textureSendID.item(), GL_TEXTURE_2D, spoutSenderWidth, spoutSenderHeight, False, 0)

これで、Enetで生成したSegmentationされた領域の画像をTOPに戻すことができました。

性能について

EnetモデルによるSegmentation部分の速度は、私の環境では以下のようになりました。
ただ、これバックエンドがCPUになっているようです。。

  • 512x256 : 73ms (13FPS)
  • 1024x512 : 354ms ( 3FPS)

精度は、後者の高解像度のほうが、見た目で分かるレベルでよかったです。
-> 2020.04.27 バックエンドをCUDAにしたところ、1024x512が80msくらいになりました。

おわりに

今回も多くの先輩たちのおかげで、調べたらすべての情報があり、
わりと難なくやりたいことができました。大変ありがたいことです。

この記事も、誰かの役に立ちますように。