MediaPipeとUnityをUDPで繋ぐ -その3-

アバター

はじめに

UnityからPythonファイルを実行する方法と、少し滑らかに動かす仕組みを作ったので紹介します☺️
顔や手が認識できなくなったとき、オブジェクトが残っていると違和感があるので、消すようにもしました。
UnityからPythonを実行することに伴い、UDP通信は双方向にしています。

Python

前回からの変更点は以下です。
・cv2での画面表示を削除
・UDP受信処理を追加

ソースコード

import time
import cv2
import mediapipe as mp
import numpy as np
from socket import socket, AF_INET, SOCK_DGRAM
import json
import queue
import threading

HOST = ''
PORT_UNITY = 65500
PORT_MEDIAPIPE = 65501
ADDRESS = "127.0.0.1"

face_model_path = './face_landmarker.task'
hands_model_path = './hand_landmarker.task'

BaseOptions = mp.tasks.BaseOptions
FaceLandmarker = mp.tasks.vision.FaceLandmarker
FaceLandmarkerOptions = mp.tasks.vision.FaceLandmarkerOptions
FaceLandmarkerResult = mp.tasks.vision.FaceLandmarkerResult
HandLandmarker = mp.tasks.vision.HandLandmarker
HandLandmarkerOptions = mp.tasks.vision.HandLandmarkerOptions
HandLandmarkerResult = mp.tasks.vision.HandLandmarkerResult
VisionRunningMode = mp.tasks.vision.RunningMode

face_blendshapes_rcv = queue.Queue()
hand_landmarks_rcv = queue.Queue()
th_udp_status = False # スレッド停止

#顔ランドマーク情報からJSONデータ作成
def get_face_blendshapes_json(detection_result):
    face_json = None

    face_json_tmp = {}
    face_data = []
    # 顔を認識できなくなるとエラー終了してしまうので、捉えているか判定する
    if detection_result.face_blendshapes:
        face_blendshapes_list = detection_result.face_blendshapes
        #検出する顔は1つであることが前提
        for face_bs in face_blendshapes_list[0]:
            face_data.append(face_bs.score) # 配列face_dataにscoreを追加する

        face_landmarks_list = detection_result.face_landmarks
        # 鼻の位置
        nose_lm_data = []
        nose_lm_data.append(face_landmarks_list[0][5].x)
        nose_lm_data.append(face_landmarks_list[0][5].y)
        nose_lm_data.append(face_landmarks_list[0][5].z)

        # 顔の上部の位置
        face_top_lm_data = []
        face_top_lm_data.append(face_landmarks_list[0][10].x)
        face_top_lm_data.append(face_landmarks_list[0][10].y)
        face_top_lm_data.append(face_landmarks_list[0][10].z)

        # 顔の下部の位置
        face_bottom_lm_data = []
        face_bottom_lm_data.append(face_landmarks_list[0][152].x)
        face_bottom_lm_data.append(face_landmarks_list[0][152].y)
        face_bottom_lm_data.append(face_landmarks_list[0][152].z)

        # 顔の右側の位置
        face_right_lm_data = []
        face_right_lm_data.append(face_landmarks_list[0][454].x)
        face_right_lm_data.append(face_landmarks_list[0][454].y)
        face_right_lm_data.append(face_landmarks_list[0][454].z)

        # 顔の左側の位置
        face_left_lm_data = []
        face_left_lm_data.append(face_landmarks_list[0][234].x)
        face_left_lm_data.append(face_landmarks_list[0][234].y)
        face_left_lm_data.append(face_landmarks_list[0][234].z)

        face_json_tmp['parts'] = "face"
        face_json_tmp['nose'] = nose_lm_data
        face_json_tmp['top'] = face_top_lm_data
        face_json_tmp['bottom'] = face_bottom_lm_data
        face_json_tmp['right'] = face_right_lm_data
        face_json_tmp['left'] = face_left_lm_data
        face_json_tmp['blendshape'] = face_data
        face_json = json.dumps(face_json_tmp) #jsonデータに変換する

    return face_json


#手ランドマーク情報からJSONデータ作成
def get_hands_landmarks_json(detection_result):
    right_hand_json = None
    left_hand_json = None

    right_hand_json_tmp = {}
    left_hand_json_tmp = {}
    # 手を認識できなくなるとエラー終了してしまうので、捉えているか判定する
    if detection_result.hand_landmarks:
        hand_landmarks_list = detection_result.hand_landmarks
        handedness_list = detection_result.handedness

        right_hand_json_tmp['parts'] = "hand_right"
        left_hand_json_tmp['parts'] = "hand_left"
        # Loop through the detected hands to visualize.
        for idx in range(len(hand_landmarks_list)):
            hand_landmarks = hand_landmarks_list[idx]
            handedness = handedness_list[idx]
            if handedness[0].category_name == "Right":
                right_hand_data_x = []
                right_hand_data_y = []
                right_hand_data_z = []

                # ランドマークのxyz座標を取得
                for landmark in hand_landmarks:
                    right_hand_data_x.append(landmark.x)
                    right_hand_data_y.append(landmark.y)
                    right_hand_data_z.append(landmark.z)

                # xyz座標をまとめてキーに設定する
                right_hand_json_tmp['x'] = right_hand_data_x
                right_hand_json_tmp['y'] = right_hand_data_y
                right_hand_json_tmp['z'] = right_hand_data_z
                right_hand_json = json.dumps(right_hand_json_tmp) #jsonデータに変換する

            elif handedness[0].category_name == "Left":
                left_hand_data_x = []
                left_hand_data_y = []
                left_hand_data_z = []

                # ランドマークのxyz座標を取得
                for landmark in hand_landmarks:
                    left_hand_data_x.append(landmark.x)
                    left_hand_data_y.append(landmark.y)
                    left_hand_data_z.append(landmark.z)

                # xyz座標をまとめてキーに設定する
                left_hand_json_tmp['x'] = left_hand_data_x
                left_hand_json_tmp['y'] = left_hand_data_y
                left_hand_json_tmp['z'] = left_hand_data_z
                left_hand_json = json.dumps(left_hand_json_tmp) #jsonデータに変換する

            else:
                pass

    return right_hand_json, left_hand_json


# Create a face landmarker instance with the live stream mode:
def print_result_face(result: FaceLandmarkerResult, output_image: mp.Image, timestamp_ms: int):
    global face_blendshapes_rcv
    # 受信したランドマーク情報をキューにためる
    face_blendshapes_rcv.put(result)


# Create a hand landmarker instance with the live stream mode:
def print_result_hands(result: HandLandmarkerResult, output_image: mp.Image, timestamp_ms: int):
    global hand_landmarks_rcv
    # 受信したランドマーク情報をキューにためる
    hand_landmarks_rcv.put(result)


# UDP送信するスレッド
def udp_send():
    global th_udp_status, face_blendshapes_rcv, hand_landmarks_rcv, ADDRESS, PORT_UNITY

    s_send = socket(AF_INET, SOCK_DGRAM)

    while th_udp_status:
        face_json = None
        right_hand_json = None
        left_hand_json = None

        if (face_blendshapes_rcv.qsize() != 0):
            # 顔はblendshapeデータをjson形式にするだけ
            face_json = get_face_blendshapes_json(face_blendshapes_rcv.get())
        if (hand_landmarks_rcv.qsize() != 0):
            # 両手はworld landmarkデータをjson形式にするだけ
            right_hand_json, left_hand_json = get_hands_landmarks_json(hand_landmarks_rcv.get())

        if (face_json is not None):
            s_send.sendto(face_json.encode('utf-8'), (ADDRESS, PORT_UNITY))
        if (right_hand_json is not None):
            s_send.sendto(right_hand_json.encode('utf-8'), (ADDRESS, PORT_UNITY))
        if (left_hand_json is not None):
            s_send.sendto(left_hand_json.encode('utf-8'), (ADDRESS, PORT_UNITY))

    s_send.close()


# UDP受信するスレッド
def udp_rcv():
    global th_udp_status

    s_rcv = socket(AF_INET, SOCK_DGRAM)
    s_rcv.bind((HOST, PORT_MEDIAPIPE))

    while th_udp_status:
        msg, address = s_rcv.recvfrom(64)
        print(msg.decode("utf-8"))
        if msg.decode("utf-8") == "end":
            th_udp_status = False

    s_rcv.close()


face_options = FaceLandmarkerOptions(
    base_options=BaseOptions(model_asset_path=face_model_path),
    running_mode=VisionRunningMode.LIVE_STREAM,
    num_faces=1,
    min_face_detection_confidence=0.5,
    min_tracking_confidence=0.5,
    output_face_blendshapes=True,
    result_callback=print_result_face)

hands_options = HandLandmarkerOptions(
    base_options=BaseOptions(model_asset_path=hands_model_path),
    running_mode=VisionRunningMode.LIVE_STREAM,
    num_hands=2,
    min_hand_detection_confidence=0.2,
    min_tracking_confidence=0.2,
    result_callback=print_result_hands)

face_landmarker = FaceLandmarker.create_from_options(face_options)
hands_landmarker = HandLandmarker.create_from_options(hands_options)

print("face_hands_udp start")

cap = cv2.VideoCapture(0)
th_udp_send = threading.Thread(target=udp_send)
th_udp_rcv = threading.Thread(target=udp_rcv)
th_udp_status = True # UDPスレッド開始
th_udp_send.start()
th_udp_rcv.start()

# カメラが有効の場合のみ処理する
while cap.isOpened() and th_udp_status:
    # カメラから画像1枚取得
    success, image = cap.read()
    if not success:
        print("Ignoring empty camera frame.")
        continue

    mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=image)
    frame_timestamp_ms = int(time.time() * 1000)
    face_landmarker.detect_async(mp_image, frame_timestamp_ms)
    hands_landmarker.detect_async(mp_image, frame_timestamp_ms)

    # 黒画表示
#    img_h = 100
#    img_w = 200
#    blank = np.zeros((img_h, img_w, 3))
#    cv2.imshow('MediaPipe Send UDP', blank)

    #終了判定 ESCで終了する
#    if cv2.waitKey(5) & 0xFF == 27:
#      th_udp_status = False # UDPスレッド停止
#      break

cap.release()
th_udp_send.join()
th_udp_rcv.join()
print("face_hands_udp end")


変更点を見ていきます。

    # 黒画表示
#    img_h = 100
#    img_w = 200
#    blank = np.zeros((img_h, img_w, 3))
#    cv2.imshow('MediaPipe Send UDP', blank)

    #終了判定 ESCで終了する
#    if cv2.waitKey(5) & 0xFF == 27:
#      th_udp_status = False # UDPスレッド停止
#      break

Unityから起動と終了を制御してもらうので、Python単独で終了する手段は削除しました。
実はcv2.imshow()は、たとえ表示サイズを小さくしたとしてもけっこうな処理負荷がかかっています。
ただ、何らかの要因でUnityからの終了処理が失敗した場合、Python側だけが動作したまま(カメラ起動したまま)になってしまう恐れがあります。そのような心配がある場合はコメントアウトを解除してください。

# UDP受信するスレッド
def udp_rcv():
    global th_udp_status

    s_rcv = socket(AF_INET, SOCK_DGRAM)
    s_rcv.bind((HOST, PORT_MEDIAPIPE))

    while th_udp_status:
        msg, address = s_rcv.recvfrom(64)
        print(msg.decode("utf-8"))
        if msg.decode("utf-8") == "end":
            th_udp_status = False

    s_rcv.close()
・・・
th_udp_rcv = threading.Thread(target=udp_rcv)
・・・
th_udp_rcv.start()

UnityからPythonを制御するのもUDP通信を使います。なのでPython側にも受信処理を追加しました。
今は大きなデータを受信する必要はないので、最大64byteで待機しています。
Unityから”end”の文字列を受信したとき、Python処理を終了するようにしています。

Unity

パーツの種類は変更ありませんが、将来的にアバターモデルを使うときのことを想定して、オブジェクトの初期配置をすることにしました。
それに伴い、各パーツの座標オフセットを変更しています。

スクリプト

今回新たに、MediaPipe管理を追加しました。Pythonの起動と終了を管理します。

MediaPipe管理

Pythonの起動と終了を管理します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.IO;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Text;

public class MediaPipeManager : MonoBehaviour
{
    private string fileName = "face_hands_udp.py";
    public Process mp;

    IEnumerator Start()
    {
        if(Application.HasUserAuthorization (UserAuthorization.WebCam) != true)
        {
            yield return Application.RequestUserAuthorization(UserAuthorization.WebCam);
        }
        MediaPipeThread();
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Escape))
        {
            byte[] data = Encoding.UTF8.GetBytes("end");
            UdpManager.SendUdp(data);

#if UNITY_EDITOR
            UnityEditor.EditorApplication.isPlaying = false; //ゲームプレイ終了
#else
            Application.Quit(); //ゲームプレイ終了
#endif
        }
    }

    private void OnApplicationQuit()
    {
        byte[] data = Encoding.UTF8.GetBytes("end");
        UdpManager.SendUdp(data);

#if UNITY_EDITOR
        UnityEditor.EditorApplication.isPlaying = false; //ゲームプレイ終了
#else
        Application.Quit(); //ゲームプレイ終了
#endif
    }

    private void OnDestroy()
    {
        if(mp != null) mp.Close();
    }

    public void MediaPipeThread()
    {
        Task.Run(() =>
        {
            //string fullPath = Path.GetFullPath(fileName);
            //string filePath = Directory.GetCurrentDirectory() + "/Assets/StreamingAssets";
            string filePath = Application.streamingAssetsPath;
            mp = new Process();
            mp.StartInfo.WorkingDirectory = filePath;
            mp.StartInfo.FileName = "/Users/" + Environment.UserName + "/.pyenv/shims/python";
            mp.StartInfo.Arguments = fileName;
            mp.StartInfo.UseShellExecute = false;
            mp.StartInfo.CreateNoWindow = true;
            mp.StartInfo.RedirectStandardOutput = false;
            mp.StartInfo.RedirectStandardInput = false;
            mp.StartInfo.RedirectStandardError = false;
            mp.Start();
        });
    }
}

変更点を見ていきます。

    IEnumerator Start()
    {
        if(Application.HasUserAuthorization (UserAuthorization.WebCam) != true)
        {
            yield return Application.RequestUserAuthorization(UserAuthorization.WebCam);
        }
        MediaPipeThread();
    }

MediaPipeでWebカメラを使用するため、許可ダイアログを出すようにします。
この状態だと、初回起動時に出るダイアログで許可したあとは、システム設定で拒否設定に変えてもダイアログが出ません。改善の余地あり🤔

    public void MediaPipeThread()
    {
        Task.Run(() =>
        {
            string filePath = Application.streamingAssetsPath;
            mp = new Process();
            mp.StartInfo.WorkingDirectory = filePath;
            mp.StartInfo.FileName = "/Users/" + Environment.UserName + "/.pyenv/shims/python";
            mp.StartInfo.Arguments = fileName;
            mp.StartInfo.UseShellExecute = false;
            mp.StartInfo.CreateNoWindow = true;
            mp.StartInfo.RedirectStandardOutput = false;
            mp.StartInfo.RedirectStandardInput = false;
            mp.StartInfo.RedirectStandardError = false;
            mp.Start();
        });
    }

今回、ここが一番悩みました。
Processを使うことで、Pythonに限らず様々なファイルを実行することができます。
なのですが・・・eightの環境では、ググった方法ではなかなかうまくいかず。
FileNameでコマンド名を、Argumentsに実行対象ファイルのフルパスを指定するのですが、「ファイルが見つからない」と言われ🥺
最終的に、WorkingDirectoryの指定と、pythonコマンドのフルパス指定することで実行可能になりました。

Task.Run()によって、中の処理を別スレッドで実行します。
pythonコマンドはユーザごとのディレクトリにあるので、ユーザ名をEnvironment.UserNameから取得します。
Task.Run()では無限ループしていませんが、起動したPythonコードのほうで無限ループしています。

        if (Input.GetKeyDown(KeyCode.Escape))
        {
            byte[] data = Encoding.UTF8.GetBytes("end");
            UdpManager.SendUdp(data);

#if UNITY_EDITOR
            UnityEditor.EditorApplication.isPlaying = false; //ゲームプレイ終了
#else
            Application.Quit(); //ゲームプレイ終了
#endif
        }

ESCキーが押されたら、Pythonへ文字列”end”を送信した上で、Unityアプリも終了させます。
UDP送信はUdpManagerを通します。

UDP管理

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Text;
using UnityEngine.UI;
using PartsData_face_ns;
using PartsData_righthand_ns;
using PartsData_lefthand_ns;

public class UdpManager : MonoBehaviour
{
    private static string host = "127.0.0.1";
    private int port_unity = 65500;
    private static int port_mediapipe = 65501;
    private UdpClient udpUnity_rcv;
    private static UdpClient udpUnity_send;

    void Start()
    {
        udpUnity_rcv = new UdpClient(port_unity);
        udpUnity_send = new UdpClient();

        udpUnity_rcv.BeginReceive(OnReceived, udpUnity_rcv);
    }

    private void OnDestroy()
    {
        if (udpUnity_rcv != null) udpUnity_rcv.Close();
    }

    private void OnReceived(System.IAsyncResult result) {
        UdpClient getUdp = (UdpClient) result.AsyncState;
        IPEndPoint ipEnd = null;

        byte[] getByte = getUdp.EndReceive(result, ref ipEnd);
        string text = Encoding.UTF8.GetString(getByte);
        
        if (text.Contains("face"))
        {
            FaceManager.SetRcvData(text);
        }
        else if (text.Contains("hand_right"))
        {
            RightHandManager.SetRcvData(text);
        }
        else if (text.Contains("hand_left"))
        {
            LeftHandManager.SetRcvData(text);
        }
        else{
            //処理なし
        }

        getUdp.BeginReceive(OnReceived, getUdp);
   }

   public static void SendUdp(byte[] data)
   {
        if(udpUnity_send != null)
        {
            udpUnity_send.Send(data, data.Length, host, port_mediapipe);
        }
   }
}


変更点を見ていきます。

    void Start()
    {
        udpUnity_rcv = new UdpClient(port_unity);
        udpUnity_send = new UdpClient();

        udpUnity_rcv.BeginReceive(OnReceived, udpUnity_rcv);
    }

UDP送信用のクライアントも作成します。
送信側はUdpClientの引数にポート番号を指定すると、「アドレスが使用中です」というエラーが出て動きませんでした。送信先のポートは送信開始時に指定できるので、ここでは指定無しにしました。

    private void OnReceived(System.IAsyncResult result) {
        UdpClient getUdp = (UdpClient) result.AsyncState;
        IPEndPoint ipEnd = null;

        byte[] getByte = getUdp.EndReceive(result, ref ipEnd);
        string text = Encoding.UTF8.GetString(getByte);
        
        if (text.Contains("face"))
        {
            FaceManager.SetRcvData(text);
        }
        else if (text.Contains("hand_right"))
        {
            RightHandManager.SetRcvData(text);
        }
        else if (text.Contains("hand_left"))
        {
            LeftHandManager.SetRcvData(text);
        }
        else{
            //処理なし
        }

        getUdp.BeginReceive(OnReceived, getUdp);
   }

受信データを振り分けるとき、以前はキューに格納していました。
しかし実際には、各パーツ管理のSetRcvData()をコールした先でもキューへ格納するだけであり、大した処理負荷はないため、ここからSetRcvData()をコールするようにしました。

   public static void SendUdp(byte[] data)
   {
        if(udpUnity_send != null)
        {
            udpUnity_send.Send(data, data.Length, host, port_mediapipe);
        }
   }

UDP送信用のメソッドです。
受け取ったbyte配列をそのまま送信するだけです。

顔パーツ管理

using System.Collections.Generic;
using UnityEngine;

namespace PartsData_face_ns{

[System.Serializable]
public class PartsData_face
{
    public string parts = "";
    public float[] nose = new float[3];
    public float[] top = new float[3];
    public float[] bottom = new float[3];
    public float[] right = new float[3];
    public float[] left = new float[3];
    public float[] blendshape = new float[52];
}

public class FaceManager : MonoBehaviour
{
    public enum FaceBlendshapeName
    {
        Neutral = 0,
        BrowDownLeft,
        BrowDownRight,
        BrowInnerUp,
        BrowOuterUpLeft,
        BrowOuterUpRight,
        CheekPuff,
        CheekSquintLeft,
        CheekSquintRight,
        EyeBlinkLeft,
        EyeBlinkRight,
        EyeLookDownLeft,
        EyeLookDownRight,
        EyeLookInLeft,
        EyeLookInRight,
        EyeLookOutLeft,
        EyeLookOutRight,
        EyeLookUpLeft,
        EyeLookUpRight,
        EyeSquintLeft,
        EyeSquintRight,
        EyeWideLeft,
        EyeWideRight,
        JawForward,
        JawLeft,
        JawOpen,
        JawRight,
        MouthClose,
        MouthDimpleLeft,
        MouthDimpleRight,
        MouthFrownLeft,
        MouthFrownRight,
        MouthFunnel,
        MouthLeft,
        MouthLowerDownLeft,
        MouthLowerDownRight,
        MouthPressLeft,
        MouthPressRight,
        MouthPucker,
        MouthRight,
        MouthRollLower,
        MouthRollUpper,
        MouthShrugLower,
        MouthShrugUpper,
        MouthSmileLeft,
        MouthSmileRight,
        MouthStretchLeft,
        MouthStretchRight,
        MouthUpperUpLeft,
        MouthUpperUpRight,
        NoseSneerLeft,
        NoseSneerRight,
        FaceBlendshapeName_Max
    }

    public static Queue<string> rcvData_face;
    public Queue<PartsData_face> moveData_face;
    [SerializeField] GameObject RightEyeUp;
    [SerializeField] GameObject RightEyeDown;
    [SerializeField] GameObject LeftEyeUp;
    [SerializeField] GameObject LeftEyeDown;
    [SerializeField] GameObject RightEye;
    [SerializeField] GameObject LeftEye;
    [SerializeField] GameObject MouthUp;
    [SerializeField] GameObject MouthDown;
    [SerializeField] GameObject Nose;

    private int hidecnt;
    private Vector3 RightEyeUp_pos_base;
    private Vector3 RightEyeDown_pos_base;
    private Vector3 LeftEyeUp_pos_base;
    private Vector3 LeftEyeDown_pos_base;
    private Vector3 RightEye_pos_base;
    private Vector3 LeftEye_pos_base;
    private Vector3 MouthUp_pos_base;
    private Vector3 MouthDown_pos_base;
    private Vector3 Nose_pos_base;

    private PartsData_face prev_moveData_face;
    private int rcvdata_devide; //受信データを分割して滑らかにする

    void Start()
    {
        hidecnt = 0;
        rcvdata_devide = 2;
        rcvData_face = new Queue<string>();
        moveData_face = new Queue<PartsData_face>();
        prev_moveData_face = null;
        ChangeVisibility(false);
        RightEyeUp_pos_base = RightEyeUp.transform.localPosition;
        RightEyeDown_pos_base = RightEyeDown.transform.localPosition;
        LeftEyeUp_pos_base = LeftEyeUp.transform.localPosition;
        LeftEyeDown_pos_base = LeftEyeDown.transform.localPosition;
        RightEye_pos_base = RightEye.transform.localPosition;
        LeftEye_pos_base = LeftEye.transform.localPosition;
        MouthUp_pos_base = MouthUp.transform.localPosition;
        MouthDown_pos_base = MouthDown.transform.localPosition;
        Nose_pos_base = Nose.transform.localPosition;
    }

    void Update()
    {
        hidecnt++;
        if((rcvData_face != null) && (rcvData_face.Count != 0))
        {
            string rcvdata = rcvData_face.Dequeue();
            PartsData_face rcv_face = JsonUtility.FromJson<PartsData_face>(rcvdata);

            //今回値を前回値の差を、今回値(目標)に向かって数回に分けて移動させる
            if(prev_moveData_face != null)
            {
                PartsData_face div_face = new PartsData_face();
                
                //前回値を開始地点とする
                div_face.parts = prev_moveData_face.parts;
                for(int idx=0; idx<3; idx++)
                {
                    div_face.nose[idx] = prev_moveData_face.nose[idx];
                    div_face.top[idx] = prev_moveData_face.top[idx];
                    div_face.bottom[idx] = prev_moveData_face.bottom[idx];
                    div_face.right[idx] = prev_moveData_face.right[idx];
                    div_face.left[idx] = prev_moveData_face.left[idx];
                }
                for(int idx=(int)FaceBlendshapeName.Neutral; idx<(int)FaceBlendshapeName.FaceBlendshapeName_Max; idx++)
                {
                    div_face.blendshape[idx] = prev_moveData_face.blendshape[idx];
                }

                //今回値と前回値の差分を分割し、キューへ格納する
                for(int div=0; div<rcvdata_devide-1; div++)
                {
                    for(int idx=0; idx<3; idx++)
                    {
                        div_face.nose[idx] += (rcv_face.nose[idx] - prev_moveData_face.nose[idx]) / rcvdata_devide;
                        div_face.top[idx] += (rcv_face.top[idx] - prev_moveData_face.top[idx]) / rcvdata_devide;
                        div_face.bottom[idx] += (rcv_face.bottom[idx] - prev_moveData_face.bottom[idx]) / rcvdata_devide;
                        div_face.right[idx] += (rcv_face.right[idx] - prev_moveData_face.right[idx]) / rcvdata_devide;
                        div_face.left[idx] += (rcv_face.left[idx] - prev_moveData_face.left[idx]) / rcvdata_devide;
                    }
                    for(int idx=(int)FaceBlendshapeName.Neutral; idx<(int)FaceBlendshapeName.FaceBlendshapeName_Max; idx++)
                    {
                        div_face.blendshape[idx] += (rcv_face.blendshape[idx] - prev_moveData_face.blendshape[idx]) / rcvdata_devide;
                    }
                    moveData_face.Enqueue(div_face);
                }
            }
            else
            {
                //ここを実行するのは初回のみ
                prev_moveData_face = new PartsData_face();
            }
            //分割数によっては最終値が中途半端になるため、最後は今回値ぴったりで終わらせる
            moveData_face.Enqueue(rcv_face);
            
            //今回値を前回値として保持
            prev_moveData_face.parts = rcv_face.parts;
            for(int idx=0; idx<3; idx++)
            {
                prev_moveData_face.nose[idx] = rcv_face.nose[idx];
                prev_moveData_face.top[idx] = rcv_face.top[idx];
                prev_moveData_face.bottom[idx] = rcv_face.bottom[idx];
                prev_moveData_face.right[idx] = rcv_face.right[idx];
                prev_moveData_face.left[idx] = rcv_face.left[idx];
            }
            for(int idx=(int)FaceBlendshapeName.Neutral; idx<(int)FaceBlendshapeName.FaceBlendshapeName_Max; idx++)
            {
                prev_moveData_face.blendshape[idx] = rcv_face.blendshape[idx];
            }
        }

        if((moveData_face != null) && (moveData_face.Count != 0))
        {
            PartsData_face face = moveData_face.Dequeue();

            hidecnt = 0;
            //処理順は固定
            NoseMove(face);
            EyeBlink(face);
            EyeMove(face);
            MouthMove(face);
            ChangeVisibility(true);
        }
        if(hidecnt > 100)
        {
            hidecnt = 0;
            for(int i=(int)FaceBlendshapeName.Neutral; i<(int)FaceBlendshapeName.FaceBlendshapeName_Max; i++)
            {
                ChangeVisibility(false);
            }
            rcvData_face.Clear();
            moveData_face.Clear();
        }
    }

    private void OnApplicationQuit()
    {
        rcvData_face.Clear();
        moveData_face.Clear();
    }

    private void OnDestroy()
    {
        rcvData_face.Clear();
        moveData_face.Clear();
    }

    public static void SetRcvData(string rcvdata)
    {
        rcvData_face.Enqueue(rcvdata);
    }

    private void NoseMove(PartsData_face face)
    {
        Transform Nose_Transform = Nose.transform;

        //鼻の位置を計算する
        Vector3 Nose_pos = Nose_Transform.position;
        Nose_pos.x = Nose_pos_base.x + (face.nose[0]*6f) * (-1f);
        Nose_pos.y = Nose_pos_base.y + (face.nose[1]*3f) * (-1f);
        Nose_pos.z = Nose_pos_base.z + (face.nose[2]*100f);
        Nose_Transform.position = Nose_pos;

        //顔の上下左右端の座標から、顔全体の傾きを計算する
        Vector3 Nose_angle = Nose_Transform.eulerAngles;
        Nose_angle.x = (face.top[2] - face.bottom[2]) * 250f;
        Nose_angle.y = (face.right[2] - face.left[2]) * 250f;
        Nose_angle.z = (face.top[0] - face.bottom[0]) * 250f;
        Nose_Transform.eulerAngles = Nose_angle;
    }

    private void EyeBlink(PartsData_face face)
    {
        //右目の瞬き
        Transform RightEyeDown_Transform = RightEyeDown.transform;
        Vector3 RightEyeDown_pos = RightEyeDown_Transform.localPosition;
        
        Transform RightEyeUp_Transform = RightEyeUp.transform;
        Vector3 RightEyeUp_pos = RightEyeUp_Transform.localPosition;
        Vector3 RightEyeUp_scale = RightEyeUp_Transform.localScale;
        RightEyeUp_pos.y = RightEyeUp_pos_base.y - (face.blendshape[(int)FaceBlendshapeName.EyeBlinkRight]/2f);
        RightEyeUp_scale.y = face.blendshape[(int)FaceBlendshapeName.EyeBlinkRight];
        RightEyeUp_Transform.localPosition = RightEyeUp_pos;
        RightEyeUp_Transform.localScale = RightEyeUp_scale;

        //左目の瞬き
        Transform LeftEyeDown_Transform = LeftEyeDown.transform;
        Vector3 LeftEyeDown_pos = LeftEyeDown_Transform.localPosition;

        Transform LeftEyeUp_Transform = LeftEyeUp.transform;
        Vector3 LeftEyeUp_pos = LeftEyeUp_Transform.localPosition;
        Vector3 LeftEyeUp_scale = LeftEyeUp_Transform.localScale;
        LeftEyeUp_pos.y = LeftEyeUp_pos_base.y - (face.blendshape[(int)FaceBlendshapeName.EyeBlinkLeft]/2f);
        LeftEyeUp_scale.y = face.blendshape[(int)FaceBlendshapeName.EyeBlinkLeft];
        LeftEyeUp_Transform.localPosition = LeftEyeUp_pos;
        LeftEyeUp_Transform.localScale = LeftEyeUp_scale;
    }

    private void EyeMove(PartsData_face face)
    {
        EyeMove_Right(face);
        EyeMove_Left(face);
    }

    private void EyeMove_Right(PartsData_face face)
    {
        float eyemove_data_updown;
        float eyemove_data_inout;

        //上下方向に大きい値の方向へ動かす
        if(face.blendshape[(int)FaceBlendshapeName.EyeLookDownRight] > face.blendshape[(int)FaceBlendshapeName.EyeLookUpRight])
        {
            eyemove_data_updown = face.blendshape[(int)FaceBlendshapeName.EyeLookDownRight] * (-1f);
        }
        else
        {
            eyemove_data_updown = face.blendshape[(int)FaceBlendshapeName.EyeLookUpRight];
        }

        //左右方向に大きい値の方向へ動かす
        if(face.blendshape[(int)FaceBlendshapeName.EyeLookInRight] > face.blendshape[(int)FaceBlendshapeName.EyeLookOutRight])
        {
            eyemove_data_inout = face.blendshape[(int)FaceBlendshapeName.EyeLookInRight] * (-1f);
        }
        else
        {
            eyemove_data_inout = face.blendshape[(int)FaceBlendshapeName.EyeLookOutRight];
        }

        Transform RightEyeDown_Transform = RightEyeDown.transform;
        Transform RightEye_Transform = RightEye.transform;
        Vector3 RightEyeDown_pos = RightEyeDown_Transform.localPosition;
        Vector3 RightEye_pos = RightEye_Transform.localPosition;

        RightEye_pos.x = RightEye_pos_base.x + (eyemove_data_inout*0.5f);
        RightEye_pos.y = RightEye_pos_base.y + (eyemove_data_updown*0.5f);

        RightEye_Transform.localPosition = RightEye_pos;
    }

    private void EyeMove_Left(PartsData_face face)
    {
        float eyemove_data_updown;
        float eyemove_data_inout;

        //上下方向に大きい値の方向へ動かす
        if(face.blendshape[(int)FaceBlendshapeName.EyeLookDownLeft] > face.blendshape[(int)FaceBlendshapeName.EyeLookUpLeft])
        {
            eyemove_data_updown = face.blendshape[(int)FaceBlendshapeName.EyeLookDownLeft] * (-1f);
        }
        else
        {
            eyemove_data_updown = face.blendshape[(int)FaceBlendshapeName.EyeLookUpLeft];
        }

        //左右方向に大きい値の方向へ動かす
        if(face.blendshape[(int)FaceBlendshapeName.EyeLookInLeft] > face.blendshape[(int)FaceBlendshapeName.EyeLookOutLeft])
        {
            eyemove_data_inout = face.blendshape[(int)FaceBlendshapeName.EyeLookInLeft];
        }
        else
        {
            eyemove_data_inout = face.blendshape[(int)FaceBlendshapeName.EyeLookOutLeft] * (-1f);
        }

        Transform LeftEyeDown_Transform = LeftEyeDown.transform;
        Transform LeftEye_Transform = LeftEye.transform;
        Vector3 LeftEyeDown_pos = LeftEyeDown_Transform.localPosition;
        Vector3 LeftEye_pos = LeftEye_Transform.localPosition;

        LeftEye_pos.x = LeftEye_pos_base.x + (eyemove_data_inout*0.5f);
        LeftEye_pos.y = LeftEye_pos_base.y + (eyemove_data_updown*0.5f);

        LeftEye_Transform.localPosition = LeftEye_pos;
    }

    private void MouthMove(PartsData_face face)
    {
        Transform MouthUp_Transform = MouthUp.transform;
        Transform MouthDown_Transform = MouthDown.transform;
        Vector3 MouthUp_pos = MouthUp_Transform.localPosition;
        Vector3 MouthDown_pos = MouthDown_Transform.localPosition;

        // 口は上下に動かすため、顎の開き具合を上下とも適用する
        MouthUp_pos.y = MouthUp_pos_base.y + face.blendshape[(int)FaceBlendshapeName.JawOpen];
        MouthDown_pos.y = MouthDown_pos_base.y - face.blendshape[(int)FaceBlendshapeName.JawOpen];

        MouthUp_Transform.localPosition = MouthUp_pos;
        MouthDown_Transform.localPosition = MouthDown_pos;
    }

    private void ChangeVisibility(bool vis)
    {
        RightEyeUp.GetComponent<Renderer>().enabled = vis;
        RightEyeDown.GetComponent<Renderer>().enabled = vis;
        LeftEyeUp.GetComponent<Renderer>().enabled = vis;
        LeftEyeDown.GetComponent<Renderer>().enabled = vis;
        RightEye.GetComponent<Renderer>().enabled = vis;
        LeftEye.GetComponent<Renderer>().enabled = vis;
        MouthUp.GetComponent<Renderer>().enabled = vis;
        MouthDown.GetComponent<Renderer>().enabled = vis;
        Nose.GetComponent<Renderer>().enabled = vis;
    }
}
}


変更点を見ていきます。

        if((rcvData_face != null) && (rcvData_face.Count != 0))
        {
            string rcvdata = rcvData_face.Dequeue();
            PartsData_face rcv_face = JsonUtility.FromJson<PartsData_face>(rcvdata);

            //今回値を前回値の差を、今回値(目標)に向かって数回に分けて移動させる
            if(prev_moveData_face != null)
            {
                PartsData_face div_face = new PartsData_face();
                
                //前回値を開始地点とする
                div_face.parts = prev_moveData_face.parts;
                for(int idx=0; idx<3; idx++)
                {
                    div_face.nose[idx] = prev_moveData_face.nose[idx];
                    div_face.top[idx] = prev_moveData_face.top[idx];
                    div_face.bottom[idx] = prev_moveData_face.bottom[idx];
                    div_face.right[idx] = prev_moveData_face.right[idx];
                    div_face.left[idx] = prev_moveData_face.left[idx];
                }
                for(int idx=(int)FaceBlendshapeName.Neutral; idx<(int)FaceBlendshapeName.FaceBlendshapeName_Max; idx++)
                {
                    div_face.blendshape[idx] = prev_moveData_face.blendshape[idx];
                }

                //今回値と前回値の差分を分割し、キューへ格納する
                for(int div=0; div<rcvdata_devide-1; div++)
                {
                    for(int idx=0; idx<3; idx++)
                    {
                        div_face.nose[idx] += (rcv_face.nose[idx] - prev_moveData_face.nose[idx]) / rcvdata_devide;
                        div_face.top[idx] += (rcv_face.top[idx] - prev_moveData_face.top[idx]) / rcvdata_devide;
                        div_face.bottom[idx] += (rcv_face.bottom[idx] - prev_moveData_face.bottom[idx]) / rcvdata_devide;
                        div_face.right[idx] += (rcv_face.right[idx] - prev_moveData_face.right[idx]) / rcvdata_devide;
                        div_face.left[idx] += (rcv_face.left[idx] - prev_moveData_face.left[idx]) / rcvdata_devide;
                    }
                    for(int idx=(int)FaceBlendshapeName.Neutral; idx<(int)FaceBlendshapeName.FaceBlendshapeName_Max; idx++)
                    {
                        div_face.blendshape[idx] += (rcv_face.blendshape[idx] - prev_moveData_face.blendshape[idx]) / rcvdata_devide;
                    }
                    moveData_face.Enqueue(div_face);
                }
            }
            else
            {
                //ここを実行するのは初回のみ
                prev_moveData_face = new PartsData_face();
            }
            //分割数によっては最終値が中途半端になるため、最後は今回値ぴったりで終わらせる
            moveData_face.Enqueue(rcv_face);
            
            //今回値を前回値として保持
            prev_moveData_face.parts = rcv_face.parts;
            for(int idx=0; idx<3; idx++)
            {
                prev_moveData_face.nose[idx] = rcv_face.nose[idx];
                prev_moveData_face.top[idx] = rcv_face.top[idx];
                prev_moveData_face.bottom[idx] = rcv_face.bottom[idx];
                prev_moveData_face.right[idx] = rcv_face.right[idx];
                prev_moveData_face.left[idx] = rcv_face.left[idx];
            }
            for(int idx=(int)FaceBlendshapeName.Neutral; idx<(int)FaceBlendshapeName.FaceBlendshapeName_Max; idx++)
            {
                prev_moveData_face.blendshape[idx] = rcv_face.blendshape[idx];
            }
        }

アバターの動きを滑らかにする処理を追加しました。
エディタで実行時のhidecntの変化を見ていると、6〜8程度までカウントしているようでした。(前回受信から今回受信までの間に約8回の空ループが回っている)
時間的にはごく僅かですが、その間アバターは止まった状態になります。この待ち時間を利用し、前回値と今回値の差分を数回にわけて描画していけば、より滑らかに動くように見えるはずです。
今は2回にわけて動かす設定にしています。

        if(hidecnt > 100)
        {
            hidecnt = 0;
            for(int i=(int)FaceBlendshapeName.Neutral; i<(int)FaceBlendshapeName.FaceBlendshapeName_Max; i++)
            {
                ChangeVisibility(false);
            }
            rcvData_face.Clear();
            moveData_face.Clear();
        }

hidecntが100を超えたらオブジェクトを消すようにしました。
エディタは20で問題ありませんが、ビルドして.appで動かすと頻繁に消えてしまい、点滅しているように見えます。おそらく.appにすると最適化されて、エディタ時より遥かに高速に動作してるためだと思います。

右手パーツ管理

using System.Collections.Generic;
using UnityEngine;

namespace PartsData_righthand_ns{

[System.Serializable]
public class PartsData_righthand
{
    public string parts = "";
    public float[] x = new float[21];
    public float[] y = new float[21];
    public float[] z = new float[21];
}

public class RightHandManager : MonoBehaviour
{
    public enum RightHandLandmarkName
    {
        Wrist = 0,
        Thumb_Cmc,
        Thumb_Mcp,
        Thumb_Ip,
        Thumb_Tip,
        Index_Finger_Mcp,
        Index_Finger_Pip,
        Index_Finger_Dip,
        Index_Finger_Tip,
        Middle_Finger_Mcp,
        Middle_Finger_Pip,
        Middle_Finger_Dip,
        Middle_Finger_Tip,
        Ring_Finger_Mcp,
        Ring_Finger_Pip,
        Ring_Finger_Dip,
        Ring_Finger_Tip,
        Pinky_Finger_Mcp,
        Pinky_Finger_Pip,
        Pinky_Finger_Dip,
        Pinky_Finger_Tip,
        HandLandmarkName_Max
    }

    public static Queue<string> rcvData_righthand;
    public Queue<PartsData_righthand> moveData_righthand;
    [SerializeField] GameObject righthand_00;
    [SerializeField] GameObject righthand_01;
    [SerializeField] GameObject righthand_02;
    [SerializeField] GameObject righthand_03;
    [SerializeField] GameObject righthand_04;
    [SerializeField] GameObject righthand_05;
    [SerializeField] GameObject righthand_06;
    [SerializeField] GameObject righthand_07;
    [SerializeField] GameObject righthand_08;
    [SerializeField] GameObject righthand_09;
    [SerializeField] GameObject righthand_10;
    [SerializeField] GameObject righthand_11;
    [SerializeField] GameObject righthand_12;
    [SerializeField] GameObject righthand_13;
    [SerializeField] GameObject righthand_14;
    [SerializeField] GameObject righthand_15;
    [SerializeField] GameObject righthand_16;
    [SerializeField] GameObject righthand_17;
    [SerializeField] GameObject righthand_18;
    [SerializeField] GameObject righthand_19;
    [SerializeField] GameObject righthand_20;

    GameObject[] righthand_objs;

    private int hidecnt;

    PartsData_righthand prev_moveData_righthand;
    private int rcvdata_devide; //受信データを分割して滑らかにする

    void Start()
    {
        hidecnt = 0;
        rcvdata_devide = 2;
        rcvData_righthand = new Queue<string>();
        moveData_righthand = new Queue<PartsData_righthand>();
        prev_moveData_righthand = null;
        righthand_objs = new GameObject[(int)RightHandLandmarkName.HandLandmarkName_Max];
        righthand_objs[(int)RightHandLandmarkName.Wrist] = righthand_00;
        righthand_objs[(int)RightHandLandmarkName.Thumb_Cmc] = righthand_01;
        righthand_objs[(int)RightHandLandmarkName.Thumb_Mcp] = righthand_02;
        righthand_objs[(int)RightHandLandmarkName.Thumb_Ip] = righthand_03;
        righthand_objs[(int)RightHandLandmarkName.Thumb_Tip] = righthand_04;
        righthand_objs[(int)RightHandLandmarkName.Index_Finger_Mcp] = righthand_05;
        righthand_objs[(int)RightHandLandmarkName.Index_Finger_Pip] = righthand_06;
        righthand_objs[(int)RightHandLandmarkName.Index_Finger_Dip] = righthand_07;
        righthand_objs[(int)RightHandLandmarkName.Index_Finger_Tip] = righthand_08;
        righthand_objs[(int)RightHandLandmarkName.Middle_Finger_Mcp] = righthand_09;
        righthand_objs[(int)RightHandLandmarkName.Middle_Finger_Pip] = righthand_10;
        righthand_objs[(int)RightHandLandmarkName.Middle_Finger_Dip] = righthand_11;
        righthand_objs[(int)RightHandLandmarkName.Middle_Finger_Tip] = righthand_12;
        righthand_objs[(int)RightHandLandmarkName.Ring_Finger_Mcp] = righthand_13;
        righthand_objs[(int)RightHandLandmarkName.Ring_Finger_Pip] = righthand_14;
        righthand_objs[(int)RightHandLandmarkName.Ring_Finger_Dip] = righthand_15;
        righthand_objs[(int)RightHandLandmarkName.Ring_Finger_Tip] = righthand_16;
        righthand_objs[(int)RightHandLandmarkName.Pinky_Finger_Mcp] = righthand_17;
        righthand_objs[(int)RightHandLandmarkName.Pinky_Finger_Pip] = righthand_18;
        righthand_objs[(int)RightHandLandmarkName.Pinky_Finger_Dip] = righthand_19;
        righthand_objs[(int)RightHandLandmarkName.Pinky_Finger_Tip] = righthand_20;
        for(int i=(int)RightHandLandmarkName.Wrist; i<(int)RightHandLandmarkName.HandLandmarkName_Max; i++)
        {
            righthand_objs[i].GetComponent<Renderer>().enabled = false;
        }
    }

    void Update()
    {
        hidecnt++;
        if((rcvData_righthand != null) && (rcvData_righthand.Count != 0))
        {
            string rcvdata = rcvData_righthand.Dequeue();
            PartsData_righthand rcv_righthand = JsonUtility.FromJson<PartsData_righthand>(rcvdata);

            //今回値を前回値の差を、今回値(目標)に向かって数回に分けて移動させる
            if(prev_moveData_righthand != null)
            {
                PartsData_righthand div_righthand = new PartsData_righthand();

                //前回値を開始地点とする
                div_righthand.parts = prev_moveData_righthand.parts;
                for(int idx=(int)RightHandLandmarkName.Wrist; idx<(int)RightHandLandmarkName.HandLandmarkName_Max; idx++)
                {
                    div_righthand.x[idx] = prev_moveData_righthand.x[idx];
                    div_righthand.y[idx] = prev_moveData_righthand.y[idx];
                    div_righthand.z[idx] = prev_moveData_righthand.z[idx];
                }

                //今回値と前回値の差分を分割し、キューへ格納する
                for(int div=0; div<rcvdata_devide-1; div++)
                {
                    for(int idx=(int)RightHandLandmarkName.Wrist; idx<(int)RightHandLandmarkName.HandLandmarkName_Max; idx++)
                    {
                        div_righthand.x[idx] += (rcv_righthand.x[idx] - prev_moveData_righthand.x[idx]) / rcvdata_devide;
                        div_righthand.y[idx] += (rcv_righthand.y[idx] - prev_moveData_righthand.y[idx]) / rcvdata_devide;
                        div_righthand.z[idx] += (rcv_righthand.z[idx] - prev_moveData_righthand.z[idx]) / rcvdata_devide;
                    }
                    moveData_righthand.Enqueue(div_righthand);
                }
            }
            else
            {
                //ここを実行するのは初回のみ
                prev_moveData_righthand = new PartsData_righthand();
            }
            //分割数によっては最終値が中途半端になるため、最後は今回値ぴったりで終わらせる
            moveData_righthand.Enqueue(rcv_righthand);

            //今回値を前回値として保持
            prev_moveData_righthand.parts = rcv_righthand.parts;
            for(int idx=(int)RightHandLandmarkName.Wrist; idx<(int)RightHandLandmarkName.HandLandmarkName_Max; idx++)
            {
                prev_moveData_righthand.x[idx] = rcv_righthand.x[idx];
                prev_moveData_righthand.y[idx] = rcv_righthand.y[idx];
                prev_moveData_righthand.z[idx] = rcv_righthand.z[idx];
            }
        }

        if((moveData_righthand != null) && (moveData_righthand.Count != 0))
        {
            PartsData_righthand rcv_righthand = moveData_righthand.Dequeue();

            hidecnt = 0;
            //右手に連動する
            for(int idx=(int)RightHandLandmarkName.Wrist; idx<(int)RightHandLandmarkName.HandLandmarkName_Max; idx++)
            {
                SetTransformPos(rcv_righthand, idx);
                righthand_objs[idx].GetComponent<Renderer>().enabled = true;
            }
        }
        if(hidecnt > 100)
        {
            hidecnt = 0;
            for(int idx=(int)RightHandLandmarkName.Wrist; idx<(int)RightHandLandmarkName.HandLandmarkName_Max; idx++)
            {
                righthand_objs[idx].GetComponent<Renderer>().enabled = false;
            }
            rcvData_righthand.Clear();
            moveData_righthand.Clear();
        }
    }

    private void OnApplicationQuit()
    {
        rcvData_righthand.Clear();
        moveData_righthand.Clear();
    }

    private void OnDestroy()
    {
        rcvData_righthand.Clear();
        moveData_righthand.Clear();
    }

    public static void SetRcvData(string rcvdata)
    {
        rcvData_righthand.Enqueue(rcvdata);
    }

    private void SetTransformPos(PartsData_righthand righthand, int idx)
    {
        Transform myTransform;
        myTransform = righthand_objs[idx].transform;
        Vector3 pos = myTransform.position;
        pos.x = righthand.x[idx]*(-20f)+10f;
        pos.y = righthand.y[idx]*(-12f)+7.5f;
        pos.z = righthand.z[idx]*(20f);
        myTransform.position = pos;
    }
}
}

変更点を見ていきます。

        if((rcvData_righthand != null) && (rcvData_righthand.Count != 0))
        {
            string rcvdata = rcvData_righthand.Dequeue();
            PartsData_righthand rcv_righthand = JsonUtility.FromJson<PartsData_righthand>(rcvdata);

            //今回値を前回値の差を、今回値(目標)に向かって数回に分けて移動させる
            if(prev_moveData_righthand != null)
            {
                PartsData_righthand div_righthand = new PartsData_righthand();

                //前回値を開始地点とする
                div_righthand.parts = prev_moveData_righthand.parts;
                for(int idx=(int)RightHandLandmarkName.Wrist; idx<(int)RightHandLandmarkName.HandLandmarkName_Max; idx++)
                {
                    div_righthand.x[idx] = prev_moveData_righthand.x[idx];
                    div_righthand.y[idx] = prev_moveData_righthand.y[idx];
                    div_righthand.z[idx] = prev_moveData_righthand.z[idx];
                }

                //今回値と前回値の差分を分割し、キューへ格納する
                for(int div=0; div<rcvdata_devide-1; div++)
                {
                    for(int idx=(int)RightHandLandmarkName.Wrist; idx<(int)RightHandLandmarkName.HandLandmarkName_Max; idx++)
                    {
                        div_righthand.x[idx] += (rcv_righthand.x[idx] - prev_moveData_righthand.x[idx]) / rcvdata_devide;
                        div_righthand.y[idx] += (rcv_righthand.y[idx] - prev_moveData_righthand.y[idx]) / rcvdata_devide;
                        div_righthand.z[idx] += (rcv_righthand.z[idx] - prev_moveData_righthand.z[idx]) / rcvdata_devide;
                    }
                    moveData_righthand.Enqueue(div_righthand);
                }
            }
            else
            {
                //ここを実行するのは初回のみ
                prev_moveData_righthand = new PartsData_righthand();
            }
            //分割数によっては最終値が中途半端になるため、最後は今回値ぴったりで終わらせる
            moveData_righthand.Enqueue(rcv_righthand);

            //今回値を前回値として保持
            prev_moveData_righthand.parts = rcv_righthand.parts;
            for(int idx=(int)RightHandLandmarkName.Wrist; idx<(int)RightHandLandmarkName.HandLandmarkName_Max; idx++)
            {
                prev_moveData_righthand.x[idx] = rcv_righthand.x[idx];
                prev_moveData_righthand.y[idx] = rcv_righthand.y[idx];
                prev_moveData_righthand.z[idx] = rcv_righthand.z[idx];
            }
        }

顔同様、アバターの動きを滑らかにする処理を追加しました。
今は2回にわけて動かす設定にしています。

左手パーツ管理

using System.Collections.Generic;
using UnityEngine;

namespace PartsData_lefthand_ns{

[System.Serializable]
public class PartsData_lefthand
{
    public string parts = "";
    public float[] x= new float[21];
    public float[] y= new float[21];
    public float[] z= new float[21];
}

public class LeftHandManager : MonoBehaviour
{
    public enum LeftHandLandmarkName
    {
        Wrist = 0,
        Thumb_Cmc,
        Thumb_Mcp,
        Thumb_Ip,
        Thumb_Tip,
        Index_Finger_Mcp,
        Index_Finger_Pip,
        Index_Finger_Dip,
        Index_Finger_Tip,
        Middle_Finger_Mcp,
        Middle_Finger_Pip,
        Middle_Finger_Dip,
        Middle_Finger_Tip,
        Ring_Finger_Mcp,
        Ring_Finger_Pip,
        Ring_Finger_Dip,
        Ring_Finger_Tip,
        Pinky_Finger_Mcp,
        Pinky_Finger_Pip,
        Pinky_Finger_Dip,
        Pinky_Finger_Tip,
        HandLandmarkName_Max
    }

    public static Queue<string> rcvData_lefthand;
    public Queue<PartsData_lefthand> moveData_lefthand;
    [SerializeField] GameObject lefthand_00;
    [SerializeField] GameObject lefthand_01;
    [SerializeField] GameObject lefthand_02;
    [SerializeField] GameObject lefthand_03;
    [SerializeField] GameObject lefthand_04;
    [SerializeField] GameObject lefthand_05;
    [SerializeField] GameObject lefthand_06;
    [SerializeField] GameObject lefthand_07;
    [SerializeField] GameObject lefthand_08;
    [SerializeField] GameObject lefthand_09;
    [SerializeField] GameObject lefthand_10;
    [SerializeField] GameObject lefthand_11;
    [SerializeField] GameObject lefthand_12;
    [SerializeField] GameObject lefthand_13;
    [SerializeField] GameObject lefthand_14;
    [SerializeField] GameObject lefthand_15;
    [SerializeField] GameObject lefthand_16;
    [SerializeField] GameObject lefthand_17;
    [SerializeField] GameObject lefthand_18;
    [SerializeField] GameObject lefthand_19;
    [SerializeField] GameObject lefthand_20;

    GameObject[] lefthand_objs;

    private int hidecnt;

    PartsData_lefthand prev_moveData_lefthand;
    private int rcvdata_devide; //受信データを分割して滑らかにする

    void Start()
    {
        hidecnt = 0;
        rcvdata_devide = 2;
        rcvData_lefthand = new Queue<string>();
        moveData_lefthand = new Queue<PartsData_lefthand>();
        prev_moveData_lefthand = null;
        lefthand_objs = new GameObject[(int)LeftHandLandmarkName.HandLandmarkName_Max];
        lefthand_objs[(int)LeftHandLandmarkName.Wrist] = lefthand_00;
        lefthand_objs[(int)LeftHandLandmarkName.Thumb_Cmc] = lefthand_01;
        lefthand_objs[(int)LeftHandLandmarkName.Thumb_Mcp] = lefthand_02;
        lefthand_objs[(int)LeftHandLandmarkName.Thumb_Ip] = lefthand_03;
        lefthand_objs[(int)LeftHandLandmarkName.Thumb_Tip] = lefthand_04;
        lefthand_objs[(int)LeftHandLandmarkName.Index_Finger_Mcp] = lefthand_05;
        lefthand_objs[(int)LeftHandLandmarkName.Index_Finger_Pip] = lefthand_06;
        lefthand_objs[(int)LeftHandLandmarkName.Index_Finger_Dip] = lefthand_07;
        lefthand_objs[(int)LeftHandLandmarkName.Index_Finger_Tip] = lefthand_08;
        lefthand_objs[(int)LeftHandLandmarkName.Middle_Finger_Mcp] = lefthand_09;
        lefthand_objs[(int)LeftHandLandmarkName.Middle_Finger_Pip] = lefthand_10;
        lefthand_objs[(int)LeftHandLandmarkName.Middle_Finger_Dip] = lefthand_11;
        lefthand_objs[(int)LeftHandLandmarkName.Middle_Finger_Tip] = lefthand_12;
        lefthand_objs[(int)LeftHandLandmarkName.Ring_Finger_Mcp] = lefthand_13;
        lefthand_objs[(int)LeftHandLandmarkName.Ring_Finger_Pip] = lefthand_14;
        lefthand_objs[(int)LeftHandLandmarkName.Ring_Finger_Dip] = lefthand_15;
        lefthand_objs[(int)LeftHandLandmarkName.Ring_Finger_Tip] = lefthand_16;
        lefthand_objs[(int)LeftHandLandmarkName.Pinky_Finger_Mcp] = lefthand_17;
        lefthand_objs[(int)LeftHandLandmarkName.Pinky_Finger_Pip] = lefthand_18;
        lefthand_objs[(int)LeftHandLandmarkName.Pinky_Finger_Dip] = lefthand_19;
        lefthand_objs[(int)LeftHandLandmarkName.Pinky_Finger_Tip] = lefthand_20;
        for(int i=(int)LeftHandLandmarkName.Wrist; i<(int)LeftHandLandmarkName.HandLandmarkName_Max; i++)
        {
            lefthand_objs[i].GetComponent<Renderer>().enabled = false;
        }
    }

    void Update()
    {
        hidecnt++;
        if((rcvData_lefthand != null) && (rcvData_lefthand.Count != 0))
        {
            string rcvdata = rcvData_lefthand.Dequeue();
            PartsData_lefthand rcv_lefthand = JsonUtility.FromJson<PartsData_lefthand>(rcvdata);

            //今回値を前回値の差を、今回値(目標)に向かって数回に分けて移動させる
            if(prev_moveData_lefthand != null)
            {
                PartsData_lefthand div_lefthand = new PartsData_lefthand();

                //前回値を開始地点とする
                div_lefthand.parts = prev_moveData_lefthand.parts;
                for(int idx=(int)LeftHandLandmarkName.Wrist; idx<(int)LeftHandLandmarkName.HandLandmarkName_Max; idx++)
                {
                    div_lefthand.x[idx] = prev_moveData_lefthand.x[idx];
                    div_lefthand.y[idx] = prev_moveData_lefthand.y[idx];
                    div_lefthand.z[idx] = prev_moveData_lefthand.z[idx];
                }

                //今回値と前回値の差分を分割し、キューへ格納する
                for(int div=0; div<rcvdata_devide-1; div++)
                {
                    for(int idx=(int)LeftHandLandmarkName.Wrist; idx<(int)LeftHandLandmarkName.HandLandmarkName_Max; idx++)
                    {
                        div_lefthand.x[idx] += (rcv_lefthand.x[idx] - prev_moveData_lefthand.x[idx]) / rcvdata_devide;
                        div_lefthand.y[idx] += (rcv_lefthand.y[idx] - prev_moveData_lefthand.y[idx]) / rcvdata_devide;
                        div_lefthand.z[idx] += (rcv_lefthand.z[idx] - prev_moveData_lefthand.z[idx]) / rcvdata_devide;
                    }
                    moveData_lefthand.Enqueue(div_lefthand);
                }
            }
            else
            {
                //ここを実行するのは初回のみ
                prev_moveData_lefthand = new PartsData_lefthand();
            }
            //分割数によっては最終値が中途半端になるため、最後は今回値ぴったりで終わらせる
            moveData_lefthand.Enqueue(rcv_lefthand);
            
            //今回値を前回値として保持
            prev_moveData_lefthand.parts = rcv_lefthand.parts;
            for(int idx=(int)LeftHandLandmarkName.Wrist; idx<(int)LeftHandLandmarkName.HandLandmarkName_Max; idx++)
            {
                prev_moveData_lefthand.x[idx] = rcv_lefthand.x[idx];
                prev_moveData_lefthand.y[idx] = rcv_lefthand.y[idx];
                prev_moveData_lefthand.z[idx] = rcv_lefthand.z[idx];
            }
        }

        if((moveData_lefthand != null) && (moveData_lefthand.Count != 0))
        {
            PartsData_lefthand left_hand = moveData_lefthand.Dequeue();
            hidecnt = 0;
            //左手に連動する
            for(int idx=(int)LeftHandLandmarkName.Wrist; idx<(int)LeftHandLandmarkName.HandLandmarkName_Max; idx++)
            {
                SetTransformPos(left_hand, idx);
                lefthand_objs[idx].GetComponent<Renderer>().enabled = true;
            }
        }
        if(hidecnt > 100)
        {
            hidecnt = 0;
            for(int idx=(int)LeftHandLandmarkName.Wrist; idx<(int)LeftHandLandmarkName.HandLandmarkName_Max; idx++)
            {
                lefthand_objs[idx].GetComponent<Renderer>().enabled = false;
            }
            rcvData_lefthand.Clear();
            moveData_lefthand.Clear();
        }
    }

    private void OnApplicationQuit()
    {
        rcvData_lefthand.Clear();
        moveData_lefthand.Clear();
    }

    private void OnDestroy()
    {
        rcvData_lefthand.Clear();
        moveData_lefthand.Clear();
    }

    public static void SetRcvData(string rcvdata)
    {
        rcvData_lefthand.Enqueue(rcvdata);
    }

    private void SetTransformPos(PartsData_lefthand lefthand, int idx)
    {
        Transform myTransform;
        myTransform = lefthand_objs[idx].transform;
        Vector3 pos = myTransform.position;
        pos.x = lefthand.x[idx]*(-20f)+10f;
        pos.y = lefthand.y[idx]*(-12f)+7.5f;
        pos.z = lefthand.z[idx]*(20f);
        myTransform.position = pos;
    }
}
}

変更点を見ていきます。

        if((rcvData_lefthand != null) && (rcvData_lefthand.Count != 0))
        {
            string rcvdata = rcvData_lefthand.Dequeue();
            PartsData_lefthand rcv_lefthand = JsonUtility.FromJson<PartsData_lefthand>(rcvdata);

            //今回値を前回値の差を、今回値(目標)に向かって数回に分けて移動させる
            if(prev_moveData_lefthand != null)
            {
                PartsData_lefthand div_lefthand = new PartsData_lefthand();

                //前回値を開始地点とする
                div_lefthand.parts = prev_moveData_lefthand.parts;
                for(int idx=(int)LeftHandLandmarkName.Wrist; idx<(int)LeftHandLandmarkName.HandLandmarkName_Max; idx++)
                {
                    div_lefthand.x[idx] = prev_moveData_lefthand.x[idx];
                    div_lefthand.y[idx] = prev_moveData_lefthand.y[idx];
                    div_lefthand.z[idx] = prev_moveData_lefthand.z[idx];
                }

                //今回値と前回値の差分を分割し、キューへ格納する
                for(int div=0; div<rcvdata_devide-1; div++)
                {
                    for(int idx=(int)LeftHandLandmarkName.Wrist; idx<(int)LeftHandLandmarkName.HandLandmarkName_Max; idx++)
                    {
                        div_lefthand.x[idx] += (rcv_lefthand.x[idx] - prev_moveData_lefthand.x[idx]) / rcvdata_devide;
                        div_lefthand.y[idx] += (rcv_lefthand.y[idx] - prev_moveData_lefthand.y[idx]) / rcvdata_devide;
                        div_lefthand.z[idx] += (rcv_lefthand.z[idx] - prev_moveData_lefthand.z[idx]) / rcvdata_devide;
                    }
                    moveData_lefthand.Enqueue(div_lefthand);
                }
            }
            else
            {
                //ここを実行するのは初回のみ
                prev_moveData_lefthand = new PartsData_lefthand();
            }
            //分割数によっては最終値が中途半端になるため、最後は今回値ぴったりで終わらせる
            moveData_lefthand.Enqueue(rcv_lefthand);
            
            //今回値を前回値として保持
            prev_moveData_lefthand.parts = rcv_lefthand.parts;
            for(int idx=(int)LeftHandLandmarkName.Wrist; idx<(int)LeftHandLandmarkName.HandLandmarkName_Max; idx++)
            {
                prev_moveData_lefthand.x[idx] = rcv_lefthand.x[idx];
                prev_moveData_lefthand.y[idx] = rcv_lefthand.y[idx];
                prev_moveData_lefthand.z[idx] = rcv_lefthand.z[idx];
            }
        }

顔同様、アバターの動きを滑らかにする処理を追加しました。
今は2回にわけて動かす設定にしています。

エディタ設定

オブジェクトは好みの配置にします。
目や口などの可動部分は、人によって移動量が異なるので、動かしてみて調整すると良いでしょう。

Scene

MediaPipe管理(MediaPipeManager.cs)はAssets配下に置きます。


Assetsフォルダの直下にStreamingAssetsフォルダを作成し、以下3ファイルを置きます。
face_hands_udp.py
face_landmarker.task
hand_landmarker.task


ヒエラルキーでMediaPipe用の空オブジェクトを作成し、MediaPipeManager.csをアタッチします。

Project Settings

ビルドするときの設定を行います。


「バックグラウンドで実行」にチェック
ウィンドウモード
解像度は1920 x 1080


「カメラの使用説明」に何らかの文言を入力
これを入れないと、カメラのダイアログが出たときにアプリがクラッシュするので注意

動作確認

今回はビルドした.appで確認してみます。

アプリ実行すると、カメラの許可を求めるダイアログが出ます。


許可すればアプリが起動し、顔と手のトラッキングを開始します。


バックグラウンドになっても追従は継続します。
ESCキーで終了します。

GIFだとよく分からないかもしれませんが、比べてみると滑らかになっています。
分割数をもっと増やせば、さらに滑らかになります。

課題:処理負荷が高い

今回初めて.appで動かしてみたのですが、エディタで動かすより滑らかに動いています。
おそらく最適化によって高速動作するようになっているのだと思いますが、そのせいか処理負荷が高く、開始数秒でMacBookProのCPUファンが回り始めました。
どこかで余計なループが回っている影響かもしれません。
CPU使用率はエディタと.appでそう変わりませんが、エディタならファンは回りません。ただ.appに比べると若干カクカクです。
要調査😩

さいごに

PythonのMediaPipeとUnityを、Unityからの制御で一本化できました。
利便性が上がってよかったです😁
ただ、Unity(C#)のProcessでbashを実行できなかったのが気がかりです。
これができれば、外部の実行ファイルやスクリプト起動など、ほぼ何でもきるんですが🤔

それでは、今回はここまで。
ありがとうございました😊





コメント

タイトルとURLをコピーしました