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

アバター

はじめに

faceのblendshapeや、handのlandmarkを使用し、オブジェクトをイイ感じに動かせるようになりました。その方法を紹介します☺️

Python

前回からの変更点は以下の通りです。
・faceの輪郭4点の座標を送信用JSONに含める(顔の回転を検出するため)
・handの送信データを、ワールド座標からローカル座標に変更

handのワールド座標については、手の中心が原点になっているらしく、手全体の座標を動かすことはできませんでした。ローカル座標なら自由に動かすことができるようです。

ソースコード

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

PORT = 65500
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 send_udp():
    global th_udp_status, face_blendshapes_rcv, hand_landmarks_rcv, ADDRESS, PORT

    s = 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.sendto(face_json.encode('utf-8'), (ADDRESS, PORT))
        if (right_hand_json is not None):
            s.sendto(right_hand_json.encode('utf-8'), (ADDRESS, PORT))
        if (left_hand_json is not None):
            s.sendto(left_hand_json.encode('utf-8'), (ADDRESS, PORT))

    s.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 = threading.Thread(target=send_udp)
th_udp_status = True # UDP送信スレッド開始
th_udp.start()

# カメラが有効の場合のみ処理する
while cap.isOpened():
    # カメラから画像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.join()
print("face_hands_udp end")

get_face_blendshapes_json関数を変更しました。
顔のlandmarkは478点あり、その中の上端・下端・右端・左端・鼻先(顔の中心あたり)を送信します。
顔の左右回転は右端左端のzの差、顔の上下回転は上端下端のzの差、顔の傾きは上端下端のxの差から算出します。

Unity

こちらは顔のパーツに対応するオブジェクトと、オブジェクトを動かすためのスクリプト(ソースコード)を準備します。
今回動かすのは、簡易的なものとして以下パーツとします。
鼻は空のオブジェクトですが、顔全体の親パーツとするために用意します。
・鼻(空オブジェクト)
・右目の上瞼
・右目の下瞼
・右目
・左目の上瞼
・左目の下瞼
・左目
・上唇
・下唇

スクリプト

今回は以下4つのファイルに分割してみました。
・UdpManager.cs (UDP受信)
・FaceManager.cs (顔パーツ管理)
・RightHandManager.cs (右手パーツ管理)
・LeftHandManager.cs (左手パーツ管理)

UDP受信

Pythonから送信されたデータを、各Managerへ渡します。
ここでは特に何もせず渡すだけです。

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
{
    static int localPort = 65500;
    static UdpClient udpUnity;

    public Queue<string> rcvData_face;
    public Queue<string> rcvData_righthand;
    public Queue<string> rcvData_lefthand;

    void Start()
    {
        udpUnity = new UdpClient(localPort);
        udpUnity.BeginReceive(OnReceived, udpUnity);
        Debug.Log("start");
        rcvData_face = new Queue<string>();
        rcvData_righthand = new Queue<string>();
        rcvData_lefthand = new Queue<string>();
    }

    void Update()
    {
        if((rcvData_face != null) && (rcvData_face.Count != 0))
        {
            FaceManager.SetRcvData(rcvData_face.Dequeue());
        }
        if((rcvData_righthand != null) && (rcvData_righthand.Count != 0))
        {
            RightHandManager.SetRcvData(rcvData_righthand.Dequeue());
        }
        if((rcvData_lefthand != null) && (rcvData_lefthand.Count != 0))
        {
            LeftHandManager.SetRcvData(rcvData_lefthand.Dequeue());
        }
    }

    private void OnApplicationQuit()
    {
        if (udpUnity != null) udpUnity.Close();
    }

    private void OnDestroy()
    {
        if (udpUnity != null) udpUnity.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"))
        {
            rcvData_face.Enqueue(text);
        }
        else if (text.Contains("hand_right"))
        {
            rcvData_righthand.Enqueue(text);
        }
        else if (text.Contains("hand_left"))
        {
            rcvData_lefthand.Enqueue(text);
        }
        else{
            //処理なし
        }

        getUdp.BeginReceive(OnReceived, getUdp);
   }
}

ポート番号は今回は65500を使用しましたが、何らかの要因で使用できない場合、別の番号に変更すれば良いです。

顔パーツ管理

顔に属するオブジェクトを、受信データに従って動かします。
Mediapipeの座標とUnityの座標はピッタリ一致するわけではないので、受信データをUnityにあわせて調整します。

using System.Collections.Generic;
using UnityEngine;

namespace PartsData_face_ns{

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

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;
    [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;

    void Start()
    {
        rcvData_face = new Queue<string>();
        hidecnt = 0;
        RightEyeUp.GetComponent<Renderer>().enabled = false;
        RightEyeDown.GetComponent<Renderer>().enabled = false;
        LeftEyeUp.GetComponent<Renderer>().enabled = false;
        LeftEyeDown.GetComponent<Renderer>().enabled = false;
        RightEye.GetComponent<Renderer>().enabled = false;
        LeftEye.GetComponent<Renderer>().enabled = false;
        MouthUp.GetComponent<Renderer>().enabled = false;
        MouthDown.GetComponent<Renderer>().enabled = false;
        Nose.GetComponent<Renderer>().enabled = false;
    }

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

            hidecnt = 0;
            //処理順は固定
            NoseMove(face);
            EyeBlink(face);
            EyeMove(face);
            MouthMove(face);
            RightEyeUp.GetComponent<Renderer>().enabled = true;
            RightEyeDown.GetComponent<Renderer>().enabled = true;
            LeftEyeUp.GetComponent<Renderer>().enabled = true;
            LeftEyeDown.GetComponent<Renderer>().enabled = true;
            RightEye.GetComponent<Renderer>().enabled = true;
            LeftEye.GetComponent<Renderer>().enabled = true;
            MouthUp.GetComponent<Renderer>().enabled = true;
            MouthDown.GetComponent<Renderer>().enabled = true;
            Nose.GetComponent<Renderer>().enabled = true;
        }
        if(hidecnt > 20)
        {
            hidecnt = 0;
            for(int i=(int)FaceBlendshapeName.Neutral; i<(int)FaceBlendshapeName.FaceBlendshapeName_Max; i++)
            {
                RightEyeUp.GetComponent<Renderer>().enabled = false;
                RightEyeDown.GetComponent<Renderer>().enabled = false;
                LeftEyeUp.GetComponent<Renderer>().enabled = false;
                LeftEyeDown.GetComponent<Renderer>().enabled = false;
                RightEye.GetComponent<Renderer>().enabled = false;
                LeftEye.GetComponent<Renderer>().enabled = false;
                MouthUp.GetComponent<Renderer>().enabled = false;
                MouthDown.GetComponent<Renderer>().enabled = false;
                Nose.GetComponent<Renderer>().enabled = false;
            }
        }
    }

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

    private void OnDestroy()
    {
        rcvData_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 = ((face.nose[0]*6f) * (-1f)) + 3f;
        Nose_pos.y = ((face.nose[1]*3f) * (-1f)) + 2f;
        Nose_pos.z = (face.nose[2]*100f) + 1.5f;
        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;
        RightEyeDown_pos.x = 1.5f;
        RightEyeDown_pos.y = 1f;
        RightEyeDown_Transform.localPosition = RightEyeDown_pos;
        
        Transform RightEyeUp_Transform = RightEyeUp.transform;
        Vector3 RightEyeUp_pos = RightEyeUp_Transform.localPosition;
        Vector3 RightEyeUp_scale = RightEyeUp_Transform.localScale;
        RightEyeUp_pos.x = RightEyeDown_pos.x;
        RightEyeUp_pos.y = (RightEyeDown_pos.y+0.8f) - (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;
        LeftEyeDown_pos.x = -1.5f;
        LeftEyeDown_pos.y = 1f;
        LeftEyeDown_Transform.localPosition = LeftEyeDown_pos;

        Transform LeftEyeUp_Transform = LeftEyeUp.transform;
        Vector3 LeftEyeUp_pos = LeftEyeUp_Transform.localPosition;
        Vector3 LeftEyeUp_scale = LeftEyeUp_Transform.localScale;
        LeftEyeUp_pos.x = LeftEyeDown_pos.x;
        LeftEyeUp_pos.y = (LeftEyeDown_pos.y+0.8f) - (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 = RightEyeDown_pos.x + (eyemove_data_inout*0.5f);
        RightEye_pos.y = (RightEyeDown_pos.y+0.35f) + (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 = LeftEyeDown_pos.x + (eyemove_data_inout*0.5f);
        LeftEye_pos.y = (LeftEyeDown_pos.y+0.35f) + (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 = face.blendshape[(int)FaceBlendshapeName.JawOpen] - 1f;
        MouthDown_pos.y = ((face.blendshape[(int)FaceBlendshapeName.JawOpen]) * (-1f)) - 1f;

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

GetComponent().enabled は、オブジェクトを表示するかしないかを切り替えます。
Update関数でhidecntをカウントアップし、20回連続で更新データがなかった場合(画面外に行った時など)に非表示にします。

[SerializeField] GameObject オブジェクト変数名
Unity側は後ほど解説しますが、このように宣言した変数名がUnityに表示され、オブジェクトを割り当てることができます。

NoseMove関数で、傾き(回転)を計算します。
Unityの設定で、他のオブジェクトを鼻に追従するようにしているので、傾き計算はここだけで問題ありません。
他のオブジェクトは傾き計算をしないかわりに、座標取得にlocalPositionを使用します。

EyeBlink関数で、左右の目の瞬きをします。
上瞼は、下瞼の座標を基準にして、オブジェクトのサイズを変更しています。

EyeMove関数で、左右の目の移動をします。
Blendshapeの出力は、目の中心からの移動量を正数で表現します。
たとえば右目が上方向を向いている場合、EyeLookUpRightは正数側に大きくなりますが、EyeLookDownRightは0に近くなります。逆に下方向を向いている場合、EyeLookDownRightは正数側に大きくなりますが、EyeLookUpRightは0に近くなります。このため、値が大きい方が、実際に向いている方向であることになります。
右目の上下方向はEyeLookDownRightとEyeLookUpRightを比較、左右方向はEyeLookInRightとEyeLookOutRightを比較します。Inは顔の内側方向、Outは顔の外側方向にあたります。
左目も考え方は同じです。

各パーツの位置関係は、Unity側では設定せず、localPositionで調節しています。

右手パーツ管理

using System.Collections.Generic;
using UnityEngine;

namespace PartsData_righthand_ns{

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

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,
        RightHandLandmarkName_Max
    }

    public static Queue<string> rcvData_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;

    void Start()
    {
        rcvData_righthand = new Queue<string>();
        righthand_objs = new GameObject[(int)RightHandLandmarkName.RightHandLandmarkName_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;
        hidecnt = 0;
        for(int i=(int)RightHandLandmarkName.Wrist; i<(int)RightHandLandmarkName.RightHandLandmarkName_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 right_hand = JsonUtility.FromJson<PartsData_righthand>(rcvdata);

            hidecnt = 0;
            //右手に連動する
            for(int i=(int)RightHandLandmarkName.Wrist; i<(int)RightHandLandmarkName.RightHandLandmarkName_Max; i++)
            {
                SetTransformPos(right_hand, i);
                righthand_objs[i].GetComponent<Renderer>().enabled = true;
            }
        }
        if(hidecnt > 20)
        {
            hidecnt = 0;
            for(int i=(int)RightHandLandmarkName.Wrist; i<(int)RightHandLandmarkName.RightHandLandmarkName_Max; i++)
            {
                righthand_objs[i].GetComponent<Renderer>().enabled = false;
            }
        }
    }

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

    private void OnDestroy()
    {
        rcvData_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;
    }
}
}

基本的には顔パーツ管理と同様ですが、全ての部位を使用する分、for文で回せるので書き方は楽です。
顔同様、20回連続でデータ更新がなかった場合、非表示にします。

SetTransformPos関数で、Unity画面上の位置調整をします。
右手も左手も、Unity画面に対して左下に寄っているようなので、x+10fとy+7.5fで右上に来るようにします。

左手パーツ管理

using System.Collections.Generic;
using UnityEngine;

namespace PartsData_lefthand_ns{

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

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,
        LeftHandLandmarkName_Max
    }

    public static Queue<string> rcvData_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;

    void Start()
    {
        rcvData_lefthand = new Queue<string>();
        lefthand_objs = new GameObject[(int)LeftHandLandmarkName.LeftHandLandmarkName_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;
        hidecnt = 0;
        for(int i=(int)LeftHandLandmarkName.Wrist; i<(int)LeftHandLandmarkName.LeftHandLandmarkName_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 left_hand = JsonUtility.FromJson<PartsData_lefthand>(rcvdata);

            hidecnt = 0;
            //左手に連動する
            for(int i=(int)LeftHandLandmarkName.Wrist; i<(int)LeftHandLandmarkName.LeftHandLandmarkName_Max; i++)
            {
                SetTransformPos(left_hand, i);
                lefthand_objs[i].GetComponent<Renderer>().enabled = true;
            }
        }
        if(hidecnt > 20)
        {
            hidecnt = 0;
            for(int i=(int)LeftHandLandmarkName.Wrist; i<(int)LeftHandLandmarkName.LeftHandLandmarkName_Max; i++)
            {
                lefthand_objs[i].GetComponent<Renderer>().enabled = false;
            }
        }
    }

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

    private void OnDestroy()
    {
        rcvData_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;
    }
}
}

基本的には顔パーツ管理と同様ですが、全ての部位を使用する分、for文で回せるので書き方は楽です。
顔同様、20回連続でデータ更新がなかった場合、非表示にします。

SetTransformPos関数で、Unity画面上の位置調整をします。
右手も左手も、Unity画面に対して左下に寄っているようなので、x+10fとy+7.5fで右上に来るようにします。

エディタ設定

作成したC#スクリプトを、UnityのAssetsフォルダに置きます。


今回のトラッキング関連のオブジェクトをまとめるため、親オブジェクトとしてTrackingUDP(空のオブジェクト)を作成します。
その下に、Udp・Face・RightHand・LeftHandの空オブジェクトを作成します。

Udp

UDP受信として使用するオブジェクトです。
Unity画面のヒエラルキーのUdpに、AssetsのUdpManager.csをドラッグ&ドロップすることで、オブジェクトとスクリプトを関連付けます。

Face

顔関連のオブジェクトをまとめます。
Faceオブジェクトの下にNoseオブジェクトを作成します。これも空のオブジェクトです。
顔パーツの座標は鼻を中心とするため、パーツは全て鼻の子オブジェクトとします。
こうすることで、顔パーツは鼻の座標を中心とした座標系で動くようになります。(傾きや回転も含めて)
FaceとNose以外は以下のように作成します。座標は全て x:0 y:0 z:0 です。

オブジェクト名役割種類スケール
RightEyeUp右目の上瞼Cubex:1 y:0.1 z:0.5
RightEyeDown右目の下瞼Cubex:1 y:0.1 z:0.5
LeftEyeUp左目の上瞼Cubex:1 y:0.1 z:0.5
LeftEyeDown左目の下瞼Cubex:1 y:0.1 z:0.5
MouthUp上唇Cubex:1 y:0.3 z:0.5
MouthDown下唇Cubex:1 y:0.3 z:0.5
RightEye右目Spherex:0.3 y:0.3 z:0.3
LeftEye左目Spherex:0.3 y:0.3 z:0.3


Faceオブジェクトのインスペクターに、変数名が表示されています。
選択肢から、割り当てるオブジェクトを選択します。


Noseオブジェクトには、インスペクターでMeshRendererのコンポーネントを追加します。
パラメータは変更しません。
コンポーネントを追加 ➡︎ Mesh ➡︎ Mesh Renderer

右手パーツ管理

右手関連のオブジェクトをまとめます。
Noseオブジェクトには、インスペクターでMeshRendererのコンポーネントを追加します。
パラメータは変更しません。
コンポーネントを追加 ➡︎ Mesh ➡︎ Mesh Renderer

手のlandmarkは21点あるので、00〜20のSphereオブジェクトを作成します。
スケールは x:0.3 y:0.3 z:0.3 としました。

RightHandオブジェクトに、AssetsからRightHandManager.csをドラッグ&ドロップすると、インスペクターに変数名が表示されます。
選択肢から、割り当てるオブジェクトを選択します。

左手パーツ管理

基本的な設定は右手と同様です。
左手には左手用のオブジェクトを割り当てます。


動作確認

VSCodeで、face_hands_udp.pyを実行します。
Unityで、画面上側の再生ボタンを押して実行します。

すると
・顔が動く(移動と回転)
・上瞼が開閉する
・目が動く
・口が開閉する
・両手が動く
・画面外に行ったり隠れたりして、顔や手が認識できなくなると、オブジェクトが消える

多少カクカクしている感じはありますが、ちゃんと追従できています。

怖いて😂

さいごに

全体として、できる限り複雑な演算はしたくないので、簡易的な実装にしたつもりです。
MediaPipeのデータをほぼそのまま使用しているので、オブジェクトが細かくブレたり、時々消えたりします。それでもこれだけ動けているので一旦良しとします。
これからもう少し複雑なオブジェクトも作っていこうと思っています。
それと、今はMediaPipe側をVSCodeで実行していますが、Unityから自動実行できるようにしたいです。
ちょっと調べただけではよくわかりませんでした。どうやるんだろう🤔

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

コメント

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