はじめに
モーショントラッキングというのをご存じでしょうか?ゲーム業界ではしばしばリアルさを追求するために利用される技術です。
そんな多岐にわたる分野でリアルな動きをデジタル空間に反映させるために活用されている技術ですが、「もし、もっと手軽にモーショントラッキングができたら、個人でももっと面白いものが作れるんじゃないか?」と思いました。高価な機材や専門的な知識なしに、誰でも気軽に自分の動きをデジタル空間に反映させられるような仕組みがあれば、ゲームだけでなく、フィットネスアプリやインタラクティブアート、果ては医療にまで生かせるのではと、さまざまな分野で新しい表現が生まれるはずだと考えていました。
そんな中、画像処理を大学で学ぶうちにこれを使えば高価な機材なしに誰でもモーショントラッキングを利用できるのではと感じました。そして、目に留まったのがGoogleの開発したMoveNetでした。このAIモデルは、Webカメラの映像から人物の姿勢をリアルタイムで高精度に推定できるというものです。しかも、軽量で動作が速い。これなら「手軽に」という私の理想に近づけるのではないかと直感しました。
しかし、MoveNetはそのままではUnityでは使えません。そこで次に注目したのが、Unityが提供するSentisというパッケージです。これは、AIモデルをUnityプロジェクト内で直接実行できるという画期的なものでした。MoveNetで姿勢推定を行い、SentisでUnityに取り込む。この組み合わせが、まさに私の求めていた「手軽なモーショントラッキング」を実現できるのではと感じました。
Sentisは2.0で破壊的変更を含んでいます。Qiitaなどの既存のコードはほとんど動きません。GeminiやChatGPTなどもほぼ役に立ちません。また、Sentis2.1.3では公式のサンプルが警告を量産します。私が浅いのかもしれませんが公式ドキュメント通りに書いてエラー出ることもありました。
UnityCommunityで質問して何とかなった部分もありましたが、Sensitを使うときは十分に覚悟して使いましょう。
開発環境
- ソフト等
- Unity6.0(6000.0.49f1)
- Sentis 2.1.3
- MoveNet 4
- Python 3.11.3
- pip 25.1.1
- ハード
- Intel🄬 Core™i5-13400
- NVIDIA GeForce RTX3060
- Memory 16GBx2(32GB)
- GooglePixel 6a(今回はWebカメラとして利用)
MoveNetとは
MoveNetは、Googleが開発したオープンソースの姿勢推定モデルです。簡単に言えば、カメラに映った人物の動きを認識し、その体の各関節(腕、足、首など)の位置、つまり「キーポイント」を検出してくれるAIです。
MoveNetの最大の特徴は、その軽量性と高精度を両立している点にあります。一般的なWebカメラの映像からでも、リアルタイムに滑らかに人物の姿勢を追跡できるため、ゲームやアプリケーションへの組み込みが非常にしやすい設計になっています。
Sentisとは
Sentisは、Unityが提供するオンデバイスAI推論ライブラリで、一言でいえば「Unityの中でAIモデルを動かすためのツール」です。通常、AIモデルをアプリケーションで利用するには、専用のフレームワークを導入したり、外部のAPIサービスと連携したりといった手間がかかります。
Sentisを使えば、AIモデルをまるでUnityのアセットのようにプロジェクトにインポートし、C#スクリプトから直接推論を実行できます。
利用したMoveNetモデル
MoveNetにはいくつかのモデルバリエーションが存在します。今回は、その中でも代表的な「SinglePose」モデルを使用します。これは、画像内に単一の人物が写っている場合に特化したモデルで、シンプルかつ効率的に姿勢推定を行うことができます。モーショントラッキングにピッタリですね。
MoveNet SinglePose Lightning(V4)
特徴: 高速な推論に特化しており、比較的低い精度で動作します。Webカメラからのリアルタイム処理に非常に適しています。
MoevNet SinglePose Thunder(V4)
特徴: Lightningよりも高い精度を提供しますが、その分推論速度は若干遅くなります。より正確なキーポイント検出が必要な場合に有効です。
ONNX変換
上記のURLで取得するだけだと実は動きません。SentisはONNX形式をサポートしているので、変換が必要です。
また、floatに変換していますが、いかんせん使い始めで私もあまり詳しくないので、取りあえず動けばいいやの精神でやります。なので今回使用するAIモデルのinputを変換floatに変換してあげないといけません。
以下のコマンドで変換用ライブラリをインストールしておきます
pip install tensorflow tensorflow-hub tf2onnx onnx
変換用Pythonコード
Lightningは通常192x192の入力に対して、Thunderは256x256です。なので、URLとshapeだけ変えれば他は同じです。(出力される名前も違うけどご自身でわかりやすいやつに変えてください。)
MoveNet SinglePose Lightning (V4)
import tensorflow as tf
import tensorflow_hub as hub
import tf2onnx
import onnx
import numpy as np
print(f"TensorFlow version: {tf.__version__}")
print(f"tf2onnx version: {tf2onnx.__version__}")
print(f"ONNX version: {onnx.__version__}")
# 1. TensorFlow Hub から MoveNet モデルをロード
model_url = "https://tfhub.dev/google/movenet/singlepose/lightning/4"
print(f"Loading MoveNet model from: {model_url}")
movenet_loaded_model = hub.load(model_url)
movenet_infer = movenet_loaded_model.signatures['serving_default']
# 2. float32 入力を持つ新しいKerasモデルラッパーを作成
def create_float_input_movenet_model(movenet_signature):
input_shape = (192, 192, 3)
input_tensor_float = tf.keras.layers.Input(shape=input_shape, dtype=tf.float32, name='input_image_float')
# 前処理レイヤー:
# 1. float [0,1] のピクセル値を float [0,255] にスケール
scaled_input = input_tensor_float * 255.0
# 2. float [0,255] を int32 にキャスト (Lambda レイヤーでラップ)
casted_input = tf.keras.layers.Lambda(lambda x: tf.cast(x, dtype=tf.int32), name='cast_to_int32')(scaled_input)
# 元のMoveNetモデルにキャストした入力を渡して推論を実行
# tf.function (movenet_signature) の呼び出しを Lambda レイヤーでラップする
outputs = tf.keras.layers.Lambda(
lambda x: movenet_signature(input=x)['output_0'], # ここでキーワード引数 'input' を使用
name='movenet_inference_layer'
)(casted_input) # casted_input をこのLambdaレイヤーに渡す
model = tf.keras.Model(inputs=input_tensor_float, outputs=outputs)
return model
print("Creating Keras wrapper model for float32 input...")
keras_movenet_float_input = create_float_input_movenet_model(movenet_infer)
print("Keras model created.")
# 3. KerasモデルをONNX形式に変換
onnx_model_path = "movenet_lightning_float_input.onnx"
input_spec = [tf.TensorSpec(shape=[1, 192, 192, 3], dtype=tf.float32, name='input_image_float')]
print(f"Converting Keras model to ONNX: {onnx_model_path}")
model_proto, _ = tf2onnx.convert.from_keras(
keras_movenet_float_input,
input_signature=input_spec,
opset=13,
output_path=onnx_model_path
)
print(f"ONNX model saved to: {onnx_model_path}")
# 4. Optional: Verify the ONNX model
try:
onnx_model = onnx.load(onnx_model_path)
print("\n--- ONNX Model Input Verification ---")
for input_node in onnx_model.graph.input:
print(f"Input Name: {input_node.name}")
print(f"Input Type: {onnx.helper.tensor_dtype_to_string(input_node.type.tensor_type.elem_type)}")
shape_dims = [d.dim_value for d in input_node.type.tensor_type.shape.dim]
print(f"Input Shape: {shape_dims}")
except Exception as e:
print(f"Error verifying ONNX model: {e}")
MoveNet SinglePose Thunder (V4)
import tensorflow as tf
import tensorflow_hub as hub
import tf2onnx
import onnx
import numpy as np
print(f"TensorFlow version: {tf.__version__}")
print(f"tf2onnx version: {tf2onnx.__version__}")
print(f"ONNX version: {onnx.__version__}")
# 1. TensorFlow Hub から MoveNet モデルをロード
model_url = "https://tfhub.dev/google/movenet/singlepose/thunder/4"
print(f"Loading MoveNet model from: {model_url}")
movenet_loaded_model = hub.load(model_url)
movenet_infer = movenet_loaded_model.signatures['serving_default']
# 2. float32 入力を持つ新しいKerasモデルラッパーを作成
def create_float_input_movenet_model(movenet_signature):
input_shape = (256, 256, 3)
input_tensor_float = tf.keras.layers.Input(shape=input_shape, dtype=tf.float32, name='input_image_float')
# 前処理レイヤー:
# 1. float [0,1] のピクセル値を float [0,255] にスケール
scaled_input = input_tensor_float * 255.0
# 2. float [0,255] を int32 にキャスト (Lambda レイヤーでラップ)
casted_input = tf.keras.layers.Lambda(lambda x: tf.cast(x, dtype=tf.int32), name='cast_to_int32')(scaled_input)
# 元のMoveNetモデルにキャストした入力を渡して推論を実行
# tf.function (movenet_signature) の呼び出しを Lambda レイヤーでラップする
outputs = tf.keras.layers.Lambda(
lambda x: movenet_signature(input=x)['output_0'], # ここでキーワード引数 'input' を使用
name='movenet_inference_layer'
)(casted_input) # casted_input をこのLambdaレイヤーに渡す
model = tf.keras.Model(inputs=input_tensor_float, outputs=outputs)
return model
print("Creating Keras wrapper model for float32 input...")
keras_movenet_float_input = create_float_input_movenet_model(movenet_infer)
print("Keras model created.")
# 3. KerasモデルをONNX形式に変換
onnx_model_path = "movenet_thunder_float_input.onnx"
# input_spec の変更
input_spec = [tf.TensorSpec(shape=[1, 256, 256, 3], dtype=tf.float32, name='input_image_float')] # 192 -> 256
print(f"Converting Keras model to ONNX: {onnx_model_path}")
model_proto, _ = tf2onnx.convert.from_keras(
keras_movenet_float_input,
input_signature=input_spec,
opset=13,
output_path=onnx_model_path
)
print(f"ONNX model saved to: {onnx_model_path}")
# 4. Optional: Verify the ONNX model
try:
onnx_model = onnx.load(onnx_model_path)
print("\n--- ONNX Model Input Verification ---")
for input_node in onnx_model.graph.input:
print(f"Input Name: {input_node.name}")
print(f"Input Type: {onnx.helper.tensor_dtype_to_string(input_node.type.tensor_type.elem_type)}")
shape_dims = [d.dim_value for d in input_node.type.tensor_type.shape.dim]
print(f"Input Shape: {shape_dims}")
except Exception as e:
print(f"Error verifying ONNX model: {e}")
終わりに
まあまあ長くなるので、今回はここまでにします。
また次回を書いていますので、UPされていなかった場合は気長にお待ちください。