Unityにdepthai_hand_trackerを導入する方法
概要
本記事では、depthai_hand_trackerが取得するハンドトラッキング情報をUnityで使用する方法を紹介します。depthai_hand_trackerは、LuxonisのDepthAIハードウェア (OAK-D、OAK-D lite、OAK-1など) で動作するGoogle Mediapipeハンドトラッキングモデルを実行するPythonスクリプトです。
取得したトラッキング情報を使用して、Unity上で手を操作することができます。例えば、手の情報に基づいて3Dオブジェクトを動かすことや、手の動きに合わせてアニメーションを再生することができます。また、手の情報を使用して手に関連するイベントをトリガーすることもできます。例えば、手を指差したタイミングで特定のアクションを起こすといったようなことができます。
各プロジェクトによって実装方法は異なりますが、トラッキング情報を使用することで多彩な操作が可能になります。
構成
ハードウェア
- Mac Book Pro(M1)
- OAK-D
ソフトウェア
- python 3.9.12
- Unity 2021.3.16f1
事前準備
depthapiのインストール
参考になる記事「DepthAIカメラ OAK-Dで遊んでみる」を参考、インストールします。
depthai_hand_trackerのクローン
下記のコマンドを実行して、depthai_hand_trackerをクローンします。
git clone https://github.com/geaxgx/depthai_hand_tracker
cd depthai_hand_tracker
その後、以下のコマンドを実行して、ハンドトラッキングのデモを動かします。
python3 demo.py
HandTrackerの最小構成の確認
最小構成のコードを実行することで、HandTrackerの最低限の機能を確認することができます。このコードは、FPSを最大化するためにEdgeモードを使用しています。また、手の情報をより多く取得するために、HandTrackerの生成パラメータを設定しています。
from HandTrackerRenderer import HandTrackerRenderer
from HandTrackerEdge import HandTracker
tracker = HandTracker(
use_gesture=True,
xyz=True,
use_lm='full',
use_world_landmarks=True,
solo=False)
renderer = HandTrackerRenderer(tracker=tracker)
while True:
frame, hands, bag = tracker.next_frame()
if frame is None: break
frame = renderer.draw(frame, hands, bag)
key = renderer.waitKey(delay=1)
if key == 27 or key == ord('q'):
break
Unityでトラッキング情報を取得する
このような通信を行うことで、Hand Tracker側で得たトラッキング情報をUnity側で利用することができます。
Hand Tracker側(サーバー)
import socket
hand_tracking_info = {...} # シリアライズ可能なトラッキング情報
ipAddress = "127.0.0.1";
port = 7010;
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((ip_address, port))
s.listen(1)
conn, addr = s.accept()
conn.sendto(json.dumps(hand_tracking_info).encode(), addr)
Unity側
string m_ipAddress = "127.0.0.1";
int m_port = 7010;
var m_tcpClient = new TcpClient(m_ipAddress, m_port);
var m_networkStream = m_tcpClient.GetStream();
Debug.LogFormat( "接続成功" );
var buffer = new byte[20480]; //バッファサイズは大きめにとってます
var count = m_networkStream.Read(buffer, 0, buffer.Length);
var message = Encoding.UTF8.GetString( buffer, 0, count );
var handTrackingInfo = JsonUtility.FromJson<トラッキング情報格納クラス>(message);
トラッキング情報のJSONシリアライズ
このトラッキング情報をJSON形式にシリアライズすることで、PythonからC#にデータを送信することができます。
frame, hands, bag = tracker.next_frame()
シリアライズ時のポイント
シリアライズのポイントとして、ndarray(NumPy配列)をx,y,zの要素に分解して辞書型に変換することが挙げられます。また、トラッキングの状況に応じて、パラメータにNoneが含まれる場合があるということも注意する必要があります。
def numlist2xyz(items):
dim = ['x', 'y', 'z']
return {k:v for k, v in zip(dim, items)}
tracking_info = {"hands":[]}
for hand in hands:
hand_dict = {
'gesture':hand.gesture,
'handedness':hand.handedness,
'index_state':hand.index_state,
'label':hand.label,
'landmarks': [numlist2xyz(x) for x in hand.landmarks.tolist()],
'little_state':hand.little_state,
'lm_score':hand.lm_score,
'middle_state':hand.middle_state,
'norm_landmarks':[numlist2xyz(x) for x in hand.norm_landmarks.tolist()],
'pd_box':hand.pd_box.tolist() if hand.pd_box is not None else None,
'pd_kps':[numlist2xyz(x) for x in hand.pd_kps] if hand.pd_kps is not None else None,
'pd_score':hand.pd_score,
'rect_h_a':hand.rect_h_a,
'rect_points':[numlist2xyz(x) for x in hand.rect_points],
'rect_w_a':hand.rect_w_a,
'rect_x_center_a':hand.rect_x_center_a,
'rect_y_center_a':hand.rect_y_center_a,
'ring_state':hand.ring_state,
'rotation':hand.rotation,
'thumb_angle':hand.thumb_angle,
'thumb_state':hand.thumb_state,
'rota':[numlist2xyz(x) for x in hand.world_landmarks.tolist()],
'rotated_world_landmarks':[numlist2xyz(x) for x in hand.get_rotated_world_landmarks()],
'world_landmarks':[numlist2xyz(x) for x in hand.world_landmarks.tolist()],
'xyz':numlist2xyz(hand.xyz.tolist()),
'xyz_zone':numlist2xyz( hand.xyz_zone)
}
tracking_info['hands'].append(hand_dict)
C#側では、JsonUtilityを使用するためにSerializable属性を付けたクラスを用意する必要があります。
[Serializable]
public class HandTrackingInfo
{
public Hand[] hands;
}
[Serializable]
public class Hand
{
public string gesture;
public float handedness;
public int index_state;
public string label;
public Vector2[] landmarks;
public int little_state;
public float lm_score;
public int middle_state;
public Vector3[] norm_landmarks;
public float rect_h_a;
public Vector2[] rect_points;
public float rect_w_a;
public float rect_x_center_a;
public float rect_y_center_a;
public int ring_state;
public float rotation;
public Vector3[] rotated_world_landmarks;
public float thumb_angle;
public int thumb_state;
public Vector3[] world_landmarks;
public Vector3 xyz;
public Vector3 xyz_zone;
}
実装
HandTracker(サーバー)の説明
のHandTracker(サーバー)は、手の検出を行い、その結果をUnityアプリケーションに送信するためのサーバーアプリケーションです。手の情報を取得するために、毎フレームごとに手の検出を行います。取得した情報は辞書オブジェクトに格納され、JSON形式の文字列に変換されてUnityアプリケーションに送信されます。
このスクリプトは、TCP/IPソケットを使用してUnityアプリケーションと通信する設定がされており、「ESC」キーまたは「q」キーが入力された場合に終了するようになっています。
README = """
このDemoは、HandTracking情報をTCP/IPで通信するものです。
Unity用のScriptをfor Unityフォルダに置いてあります。
"""
import sys
sys.path.append("../..")
import numpy as np
from HandTrackerRenderer import HandTrackerRenderer
from HandTracker import HandTracker
import json
import socket
from concurrent.futures import ThreadPoolExecutor
tracker = HandTracker(use_gesture=True, xyz=True, use_lm='full', use_world_landmarks=True, solo=False)
renderer = HandTrackerRenderer(
tracker=tracker)
STATUS={'hand':{}, 'running':True}
ip_address = '127.0.0.1'
port = 7010
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((ip_address, port))
s.listen(1)
def loop():
while STATUS['running']:
try:
conn, addr = s.accept()
hand = STATUS['hand']
conn.sendto(json.dumps(hand).encode(), addr)
except Exception as e:
pass
socket_worker = ThreadPoolExecutor(1)
ft = socket_worker.submit(loop)
def numlist2xyz(items):
dim = ['x', 'y', 'z']
return {k:v for k, v in zip(dim, items)}
while True:
# Run hand tracker on next frame
# 'bag' contains some information related to the frame
# and not related to a particular hand like body keypoints in Body Pre Focusing mode
# Currently 'bag' contains meaningful information only when Body Pre Focusing is used
frame, hands, bag = tracker.next_frame()
# conn, addr = s.accept()
if frame is None: break
data = {'hands':[]}
# print hands info
for hand in hands:
hand_dict = {
'gesture':hand.gesture,
'handedness':hand.handedness,
'index_state':hand.index_state,
'label':hand.label,
'landmarks': [numlist2xyz(x) for x in hand.landmarks.tolist()],
'little_state':hand.little_state,
'lm_score':hand.lm_score,
'middle_state':hand.middle_state,
'norm_landmarks':[numlist2xyz(x) for x in hand.norm_landmarks.tolist()],
'pd_box':hand.pd_box.tolist() if hand.pd_box is not None else None,
'pd_kps':[numlist2xyz(x) for x in hand.pd_kps] if hand.pd_kps is not None else None,
'pd_score':hand.pd_score,
'rect_h_a':hand.rect_h_a,
'rect_points':[numlist2xyz(x) for x in hand.rect_points],
'rect_w_a':hand.rect_w_a,
'rect_x_center_a':hand.rect_x_center_a,
'rect_y_center_a':hand.rect_y_center_a,
'ring_state':hand.ring_state,
'rotation':hand.rotation,
'thumb_angle':hand.thumb_angle,
'thumb_state':hand.thumb_state,
'rota':[numlist2xyz(x) for x in hand.world_landmarks.tolist()],
'rotated_world_landmarks':[numlist2xyz(x) for x in hand.get_rotated_world_landmarks()],
'world_landmarks':[numlist2xyz(x) for x in hand.world_landmarks.tolist()],
'xyz':numlist2xyz(hand.xyz.tolist()),
'xyz_zone':numlist2xyz( hand.xyz_zone)
}
data['hands'].append(hand_dict)
if len(data['hands']) > 1:
break
STATUS['hand'] = data
# Draw hands
frame = renderer.draw(frame, hands, bag)
# frame = renderer.draw(frame,[])
key = renderer.waitKey(delay=1)
if key == 27 or key == ord('q'):
break
STATUS['running'] = False
print('abort connection')
s.close()
print('wait terminate loop')
ft.result()
renderer.exit()
tracker.exit()
Unityプロジェクトの作成
- 3D Coreを使って、新規プロジェクトを作成します。
- スクリプト2つ「DepthAIHandTrackingClient.cs」と「HandTracking.cs」をプロジェクトに配置します。
- 空のGameObjectを追加します。名前を「HandTrackingController」に変更しても構いません(任意)。
- 「HandTrackingController」に「HandTracking.cs」をAdd Componentします。
- SphereのPrefabを作成します。具体的な方法については、Unityに関連する記事や書籍を参照してください。
- Prefabを「HandTrackingController」の「Landmark Prefab」に設定します。
「DepthAIHandTrackingClient.cs」はトラッキングの共通部分のスクリプトです。
「HandTracking.cs」は手ぽいものを表示するスクリプトです。
DepthAIHandTrackingClient.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Net.Sockets;
using System.Text;
[Serializable]
public class HandTrackingInfo
{
public Hand[] hands;
}
[Serializable]
public class Hand
{
public string gesture;
public float handedness;
public int index_state;
public string label;
public Vector2[] landmarks;
public int little_state;
public float lm_score;
public int middle_state;
public Vector3[] norm_landmarks;
public float rect_h_a;
public Vector2[] rect_points;
public float rect_w_a;
public float rect_x_center_a;
public float rect_y_center_a;
public int ring_state;
public float rotation;
public Vector3[] rotated_world_landmarks;
public float thumb_angle;
public int thumb_state;
public Vector3[] world_landmarks;
public Vector3 xyz;
public Vector3 xyz_zone;
public override string ToString()
{
return string.Format("[{0}, {1}]", gesture, xyz);
}
}
public class DepthAIHandTrackingClient
{
public string m_ipAddress = "127.0.0.1";
public int m_port = 7010;
private TcpClient m_tcpClient;
private NetworkStream m_networkStream;
public DepthAIHandTrackingClient()
{
}
public HandTrackingInfo getHandTrackingInfo()
{
try
{
m_tcpClient = new TcpClient(m_ipAddress, m_port);
m_networkStream = m_tcpClient.GetStream();
// Debug.LogFormat( "接続成功" );
var buffer = new byte[20480];
var count = m_networkStream.Read(buffer, 0, buffer.Length);
var message = Encoding.UTF8.GetString( buffer, 0, count );
var handTrackingInfo = JsonUtility.FromJson<HandTrackingInfo>(message);
return handTrackingInfo;
}
catch( SocketException)
{
Debug.LogError("接続失敗");
}
catch
{
Debug.LogError("不明なエラー");
}
return new HandTrackingInfo();
}
}
HandTracking.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Net.Sockets;
using System.Text;
public class HandTracking : MonoBehaviour
{
public int hand_index = 0;
private DepthAIHandTrackingClient htc;
private GameObject[] landmarks;
public GameObject landmarkPrefab;
public Vector3 landmarks_scale = new Vector3(-50, -50, 50);
public Vector3 xyz_scale = new Vector3(0.05f, 0.05f, 0.05f);
private bool running = true;
// Start is called before the first frame update
void Start()
{
htc = new DepthAIHandTrackingClient();
landmarks = new GameObject[21];
for(int i=0; i < 21; i++)
{
landmarks[i] = Instantiate(landmarkPrefab, new Vector3(0,0,0), Quaternion.identity);
landmarks[i].transform.parent = this.transform;
}
StartCoroutine("trackingLoop");
}
IEnumerator trackingLoop()
{
while(running){
var hti = htc.getHandTrackingInfo();
if(hti != null && hti.hands != null && hti.hands.Length > hand_index)
{
var hand = hti.hands[hand_index];
this.transform.position = Vector3.Scale(hand.xyz, new Vector3(-0.05f,0.05f,0.05f));
for(int i=0; i < 21; i++)
{
landmarks[i].transform.localPosition = Vector3.Scale(hand.rotated_world_landmarks[i], landmarks_scale);
}
}
else
{
for(int i=0; i < 21; i++)
{
landmarks[i].transform.localPosition = new Vector3(0,0,0);
}
}
yield return new WaitForSeconds(0.01f);
}
yield break;
}
// Update is called once per frame
void Update()
{
}
void OnDestroy()
{
running = false;
}
}
実行
以下のコマンドを実行して、demo_server.pyを起動します。
python3 demo_server.py
次に、Unityプロジェクトを「Play」します。手のようなものが表示されたら成功です。
この作成したスクリプトは、元のリポジトリをフォークしたGitHubに保存されています。
Githubリポジトリ
この文章について
文章の校正は、ChatGPTにお願いしました。
Discussion