物体検出の出力が遅かったのでグリッドコンピューティングで実装した話
以前作成したこの記事では物体検出の精度はまあまあいいものの、それにかかる時間が遅く、プレイ体験としては悪かった。
原因はCPU使用率が常に100%に張り付いていることだった。
新しくPCを組む時間も予算もないので、既存のものを使用して、なんとかCPU使用率100%を回避しつつ、1秒間にある程度の回数、物体検出をできるようにしたいというのが今回の目標です。
実際に使用していたPCのスペック
- Core i7 9700K
- RTX2080 Super
- メモリ 32GB
原因究明
家の環境での物体検出は特に遅くなく、問題ありませんでした。
そこで、それぞれの環境の違いを見比べていった結果、今回書いたコードではスレッド数が足りてないという結論に至りました。
具体的には家のPCはAMD Ryzen 7 5700X 8-Core Processor
でスレッド数が16あり、実際に使用していたPCでCore i7 9700K
でスレッド数が8で、家のPCより少なかったです。
解決策
幸い、このPCは何台か確保しており、同時に動かすことで対応できそうという結論に至りました。
そこで、まず以下のような構成でパソコンを配置して通信することにしました。
MasterPCではKinectの画像処理をしてClientPCにUDPで送信します。
ClientPCでは送られてきた画像に対して物体検出をし、その結果をMasterPCにUDPで返します。
というようなことをすれば、なんとなく動きそうな予感がしたので、実際に作成してみました。
通信は以下の順で実行することで動くと予想しました。 ※()内の数字はポート番号
ただし、通信周りは高校の頃勉強していた基本情報技術者の知識と2023年(今年)の3月頃から作っていたUnity上でUDPとTCP/IPの通信を楽にするアセットUnityEasyNet
の知識しかなかったので、実際に動くかわからない状態でスタートしました。
github.com
問題点
最初は上の画像のようにTCP/IPで通信する処理で実装していましたが、UDPで受け取りを開始し、最初の受信をした際エラーで落ちるという問題が起きました。
今回は時間がなく解決策を聞けそうな人もいなかったので、UDPに置き換えて実装することにしました。
TCP/IPでエラーが出ること以外は既存のコードもあるので特に問題なく実装(移植)できました。
グリッドコンピューティング
どのあたりがグリッドコンピューティングかという話
MasterPC
からClientPC
に画像データをUDPで送信するときに送信済みのClientPC
のIPを保持しておき、物体検出の結果が返ってくるまでは再度画像を送信しないという処理を入れています。
これによりClientPC
は同時に複数の画像の物体検出をすることはなくなり、ClientPC 1
の物体検出結果を待っている間にClientPC 2
に画像を送り物体検出をすると言うのを繰り返すことになります。
そしてこのあとClientPC
1台あたりの物体検出にかかる時間を割り出し、連続した画像をClientPC
に送るのではなく、ClientPC 1
に画像を送り返ってくるまでの中間でClientPC 2
に画像を送り物体検出の結果をなめらかにする処理を入れようとしましたが、ClientPC
を2台接続した時点でほぼ毎フレーム物体検出の結果が返ってくるようになったので未実装で終わりました。
結果
最終的に30FPS以上出せるようになりました。
以下のリポジトリにそれぞれのコードがあります。 github.com github.com
Azure Kinect DKで入力処理を作成した時の話
導入
今回、制作中のゲームのInput周りの開発をしました。
ゲーム自体の内容には触れませんが、イメージでは4人のプレイヤーが机を囲むように座り、デジタルボードゲームを現実でプレイするゲームです。
この時プレイヤーは入力機器としてそれぞれ違う道具を持っておりInput情報として、その道具が存在するデジタルボードゲーム上の座標とその種類が必要になってきます。
また、今回のゲームを作るときに決まっていることは以下の3点でした。
- プリジェクターを使用してプロジェクションマッピングのような形で作ること
- Azure Kinect DK を使用すること
- Unityでゲームを作ること
この時点で座標を取る方法として考えたものは以前、HoloDiveというゲームで使用していたvuforiaというサービスでした。 しかし、HoloDiveでは会場のライトが強くカメラの映像が白飛びしてうまく物体検出ができない問題がありました。 今回プロジェクションマッピングで投影している机の上に存在するオブジェクトを認識するのはとても不可能に近いと考えました。
Azure Kinect DKには通常のRGBカメラ以外にToFで測定する深度カメラと近赤外線のNIRが実装されています。 learn.microsoft.com
そこで今回、会場のライトやプロジェクターの光量に影響しない方法として深度カメラと近赤外線カメラを使用して物体検出することにしました。
計画
①NIRの最大距離より外側にプレイヤーが持つ道具が来るようにAzure Kinect DKを配置する
②プレイヤーが持つ道具に識別できる固有のマークを反射材で付ける
③近赤外線の光を②で付けた反射材で反射させ、IR画像にプレイヤーがマークのみが映るようにする このとき画像を白黒にする
④Depth画像から机よりカメラ側の物体を白黒で映す
⑤③と④の画像でBitwiseAnd
をかけ余計なものが映ることを極力少なくする
以下の画像を参考に作成する
マスクの処理
DepthとIRの白黒画像を使用してOpenCVのBitwiseAndをかけてResultの画像を生成します。 実際にはこの段階で物体検出モデルにかけます。
使用する机がちょうど正方形の机なので上下左右の方向から一定の値だった場合物体検出で返された値は使用しないというようなMaskの計算をします。 ただし、実際に調整する際に直感的ではないのでResultにはMaskの値が大体どのくらいかを表示するために乗算した画像を表示しています。
このMaskのみOpenCVではなくGraphicsのFillRectangleを使用していますが勉強のために使用してます。
以下の記事はUnityでこのMaskの処理をしようとしていたときの記事ですがやっていることは一緒です。 qiita.com
CustomVisionでONNXモデルを作成する
今回ONNXモデルを作成するのに使用したのはCustomVisionです。 www.customvision.ai 使用理由の前に使用する条件として - 物体検出モデルが作成できる - WindowsのPC上で実行できる というものがありました。 CustomVisionでは物体検出モデルが作成でき、そのモデルをONNXなど複数の形式で書き出すことができました。 また、今回モデルの作成を個人でしていたらとても時間がかかるので比較的時間のあったプランナーに手伝ってもらうためにも何かしらのサービスを使用することは必要でした。 結果条件にあったCustomVisionを使用することになりました。 ただし、書き出されるモデルがどのYOLOモデルなのかが分からずとても時間を消費しました。
書き出したONNXモデルをNetronで表示したときに入力が3x416x416だったので試しに以下のリポジトリのコードを使用して実行したときに多少修正は必要ですが実行できたのでこのリポジトリを使用して作成していくことにしました。
UIとかこだわり
実際にゲームをプレイしてもらう会場での準備時間はとても少ないので、その場でコードの修正することはできる限り避けてすべてUIにある数値を微調整することで調整を終わらせたいというのが理想でした。
この結果を実現するためにしたことが以下の3つです。 - 実際にKinectで描画している画面の表示 - 物体検出の出力結果の表示 - 値の保存と読み込み
これを反映させたものがこの画像になります。 左側のウィンドウがMaskの値を入力や通信の開始をするもので、右のコンソールに物体検出の結果が表示されます。 また、ソフトの起動と終了時に値の保存と読み込みをするようにしました。
使用方法としては最初にSendType
でKinectRun
を押し、MaskとPositionOffsetの値で実際の座標と合うよう調整します。
ConnectIP
に送信先のIPアドレスを追加し、UDPConnectStart
を押すことで物体検出で返された座標とラベルを送信します。
DebugSender
では通信のテスト時に適当な値が欲しい時が多々あったので円を描くように座標を送信するようにしました。
以下に実際のコードがあります。 実行にはAzureKinectDKが必要です。 github.com
FBXに含まれるAnimation Clipの一括変換 (Unity)
以前以下の記事を書いたのだが、一つ一つ同じ設定をしてたら時間が足りないので一括で設定を変更できるコードを書いた qiita.com
使用条件
- MixamoでダウンロードしたアニメーションのFBXであること
Assets/Resources/Avatar/
直下にMixamoでダウンロードしたキャラクターのFBXが存在すること、またファイル名がcharacterであること- characterの設定を以下のように変更すること
今回MixamoのアニメーションをVRMで再生するときに足の向きがおかしくなる現象に遭遇したのでそれの対応も含めている 以下のサイトを参考にさせていただきました
MixamoアニメーションがUnityで動かない?足がおかしくなる?時の対処法【アバターの適用が重要】 | YouDoYou Blog ~スマートかつ快適に創造を楽しむ~
解決法としてはMixamoでダウンロードしたアニメーションのFBXのRigに対してMixamoで出力したキャラクターについているAvaterを設定することで正しく動く
使用方法
設定を変更したいFBXを選択してメニューバーにあるAssets/Set Animation Options
を選択することで変更可能
Assets/Set Loop
ではアニメーションクリップのLoop Timeにチェックを入れることができる
using UnityEngine; using UnityEditor; public class AnimationProcessor : AssetPostprocessor { static void SetAnimationImporterSettings(ModelImporter importer) { //rigの設定 importer.animationType = ModelImporterAnimationType.Human; importer.avatarSetup = ModelImporterAvatarSetup.CopyFromOther; var avatar = Resources.Load<GameObject>("Avatar/character"); if (avatar != null) { importer.sourceAvatar = avatar.GetComponent<Animator>().avatar; } //animationClipの設定 var clips = importer.clipAnimations; if (clips.Length == 0) clips = importer.defaultClipAnimations; foreach (var clip in clips) { clip.name = System.IO.Path.GetFileNameWithoutExtension(importer.assetPath); clip.keepOriginalOrientation = true; clip.keepOriginalPositionY = true; clip.keepOriginalPositionXZ = true; clip.lockRootRotation = true; clip.lockRootHeightY = true; clip.lockRootPositionXZ = false; clip.lockRootHeightY = true; clip.heightFromFeet = false; clip.keepOriginalPositionY = true; clip.heightOffset = 0f; clip.cycleOffset = 0f; clip.rotationOffset = 0f; } importer.clipAnimations = clips; } static void SetAnimationLoopSettings(ModelImporter importer) { //animationClipの設定 var clips = importer.clipAnimations; if (clips.Length == 0) clips = importer.defaultClipAnimations; foreach (var clip in clips) { clip.loop = true; } importer.clipAnimations = clips; } void OnPreprocessAnimation() { // 自動的に実行 //SetAnimationImporterSettings(assetImporter as ModelImporter); } [MenuItem("Assets/Set Animation Options", true)] static bool SetAnimationOptionsValidate() { return Selection.GetFiltered(typeof(GameObject), SelectionMode.Assets).Length > 0; } [MenuItem("Assets/Set Animation Options")] static void SetAnimationOptions() { var filtered = Selection.GetFiltered(typeof(GameObject), SelectionMode.Assets); foreach (var go in filtered) { var path = AssetDatabase.GetAssetPath(go); var importer = AssetImporter.GetAtPath(path); SetAnimationImporterSettings(importer as ModelImporter); AssetDatabase.ImportAsset(path); } Selection.activeObject = null; } [MenuItem("Assets/Set Loop", true)] static bool SetLoopValidate() { return Selection.GetFiltered(typeof(GameObject), SelectionMode.Assets).Length > 0; } [MenuItem("Assets/Set Loop")] static void SetLoop() { var filtered = Selection.GetFiltered(typeof(GameObject), SelectionMode.Assets); foreach (var go in filtered) { var path = AssetDatabase.GetAssetPath(go); var importer = AssetImporter.GetAtPath(path); SetAnimationLoopSettings(importer as ModelImporter); AssetDatabase.ImportAsset(path); } Selection.activeObject = null; } }