🕌

Unityにdepthai_hand_trackerを導入する方法

2023/02/09に公開

概要

本記事では、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プロジェクトの作成

  1. 3D Coreを使って、新規プロジェクトを作成します。
  2. スクリプト2つ「DepthAIHandTrackingClient.cs」と「HandTracking.cs」をプロジェクトに配置します。
  3. 空のGameObjectを追加します。名前を「HandTrackingController」に変更しても構いません(任意)。
  4. 「HandTrackingController」に「HandTracking.cs」をAdd Componentします。
  5. SphereのPrefabを作成します。具体的な方法については、Unityに関連する記事や書籍を参照してください。
  6. 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にお願いしました。

参考文献

geaxgx/depth_hand_tracker
DepthAIカメラ OAK-Dで遊んでみる
depthAI

Discussion