Mediapipe+Opencv+Unity实现手势识别及虚拟手

效果图:

因为在unity端是通过生成Object的方式实现关节点,因此较为粗糙

步骤1:

py端安装必要的库:网上都有安装地址方法,自行搜一下

import cv2
import mediapipe as mp
import time
import json
import socket

步骤2:

用cv创建视频流,由于我的电脑没有自带摄像头,因此我使用了DroidCam,即将手机摄像头作为视频流。

stream_url = 'http://192.168.1.104:4747/video'
# 创建一个VideoCapture对象,并指定视频流URL
cap = cv2.VideoCapture(stream_url)
# 设置摄像头帧率
cap.set(cv2.CAP_PROP_FPS, 30)

步骤3:

直接调用mediapipe的模型进行手部识别

mp_hands = mp.solutions.hands
hands = mp_hands.Hands(
    max_num_hands=2,
    min_detection_confidence=0.8,
    min_tracking_confidence=0.5)

# 初始化MediaPipe姿态估计(如果需要)
mp_drawing = mp.solutions.drawing_utils
mp_pose = mp.solutions.pose
success, image = cap.read()
if not success:
   print("Ignoring empty camera frame.")
   continue
# 转换BGR到RGB
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
results = hands.process(image)

result中存储了识别到的手的数据,如果为空则表示没有识别到手

 if results.multi_hand_landmarks:
    # 绘制手部标记
    for hand_landmarks in results.multi_hand_landmarks:  #单只手的数据
        for i,hand in enumerate(hand_landmarks.landmark): #每一个关节数据
    mp_drawing.draw_landmarks(image, hand_landmarks, mp_hands.HAND_CONNECTIONS) #绘制关节点

步骤4:

用数组保存每个关节点的信息,存完21个关节点后打包成json格式的数据直接发送给unity端脚本

同一台电脑,ip直接为'127.0.0.1',port自己设置

for i,hand in enumerate(hand_landmarks.landmark): #每一个关节数据
   landmark_info={'id':i, 'x':hand.x , 'y':hand.y, 'z':hand.z}
   landmarks_data.append(landmark_info)
   message_data={"landmarks":landmarks_data}
   message = json.dumps(message_data)#要发送的数据包
   try:
      sock.sendto(message.encode(), (ip, port))
      print("数据已发送:", message)  # 确认数据已发送
   except Exception as e:
      print(f"发送数据时出错: {e}")  # 捕捉并打印错误信息

步骤5:

Unity端创建空对象并挂上脚本

公用变量名,其中的LineRender是用来绘制关节点之间的线条的

 public GameObject jointPrefab;  // 用于显示关节点的球体预制体
 private Transform[] handJoints; // 存储每个关节点的 Transform
 private List<LineRenderer> lineRenderers;  // 用于绘制骨架的 LineRenderer
 private UdpClient udpClient;
 public int port = 2346;
 private IPEndPoint endPoint;
 public GameObject lineRendererPrefab;

 // JSON 数据结构
 [System.Serializable]

 初始化对象,获取到py的数据包,并将json格式转换成自建类landmark

public class Landmark
{
    public int id;
    public float x;
    public float y;
    public float z;
} //自建类

[System.Serializable]
public class LandmarkWrapper
{
    public List<Landmark> landmarks;
} //有21个关节点,故为数组形式


void Start()
{
    // 初始化 UDP 接收
    udpClient = new UdpClient(port);
    endPoint = new IPEndPoint(IPAddress.Any, port);

    // 初始化 21 个关节点球体
    handJoints = new Transform[21];
    lineRenderers = new List<LineRenderer>();
    for (int i = 0; i < 21; i++)
    {
        GameObject joint = Instantiate(jointPrefab, Vector3.zero, Quaternion.identity);
        handJoints[i] = joint.transform;
    }
    for (int i = 0; i < 5; i++)
    {
        GameObject lineObj = Instantiate(lineRendererPrefab);
        LineRenderer lineRenderer = lineObj.GetComponent<LineRenderer>();
        lineRenderer.startWidth = 0.02f;
        lineRenderer.endWidth = 0.02f;
        lineRenderer.useWorldSpace = true;
        lineRenderers.Add(lineRenderer);
    }
}

步骤6:

写update函数

y的坐标更新和x,z不一样:是因为mediapipe识别时给的坐标是按照从左上角出发,该点到左上角距离占据整个图像的比例,所以y越往下反而越大,故我们需要将其反转

void Update()
{
    // 检查是否有数据可接收
    if (udpClient.Available > 0)
    {
        // 接收并解析 JSON 数据
        byte[] data = udpClient.Receive(ref endPoint);
        string jsonString = Encoding.UTF8.GetString(data);
        Debug.Log("接收到的数据: " + jsonString);  // 打印接收到的数据,便于调试

        // 尝试解析 JSON 数据
        try
        {
            LandmarkWrapper wrapper = JsonUtility.FromJson<LandmarkWrapper>(jsonString);
            List<Landmark> landmarks = wrapper.landmarks;

            // 更新每个关节点的坐标
            for (int i = 0; i < landmarks.Count; i++)
            {
                Vector3 newPosition = new Vector3(
                    landmarks[i].x * 10,  // 放大坐标以适应 Unity 场景
                    (1 - landmarks[i].y) * 10,
                    landmarks[i].z * 10
                );
                handJoints[i].localPosition = newPosition;
            }
            SetFingerPositions();
        }
        catch (System.Exception e)
        {
            Debug.LogError("JSON 解析出错: " + e.Message);
        }
    }
}

最后是关节点画线函数

void SetFingerPositions()
{
    // 定义每根手指的关节点索引
    int[][] fingerJoints = new int[][]
    {
        new int[] {0, 1, 2, 3, 4},       // 拇指
        new int[] {0, 5, 6, 7, 8},       // 食指
        new int[] {0, 9, 10, 11, 12},    // 中指
        new int[] {0, 13, 14, 15, 16},   // 无名指
        new int[] {0, 17, 18, 19, 20}    // 小指
    };

    // 为每根手指设置 LineRenderer 的位置
    for (int i = 0; i < fingerJoints.Length; i++)
    {
        int[] joints = fingerJoints[i];
        LineRenderer lineRenderer = lineRenderers[i];
        lineRenderer.positionCount = joints.Length;

        for (int j = 0; j < joints.Length; j++)
        {
            lineRenderer.SetPosition(j, handJoints[joints[j]].position);
        }
    }
}

额外工作:

创建一个空对象,添加LineRenderer组件,并将该对象变成预制体;

创建一个小球,并变成预制体;

在主场景创建一个空对象,并挂载脚本;

其中的Joint变量为预制小球,liner变量为预制的画线

因为是一个比较简陋的实现方式,并且没有太多的原创思路,所以不留太多文字。我是在网上看到了别人用mediapipe+unity实现虚拟手,但是卖钱,故自己实现。

unity的完整可执行代码

using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Collections.Generic;

public class UDPReceiver : MonoBehaviour
{
    public GameObject jointPrefab;  // 用于显示关节点的球体预制体
    private Transform[] handJoints; // 存储每个关节点的 Transform
    private List<LineRenderer> lineRenderers;  // 用于绘制骨架的 LineRenderer
    private UdpClient udpClient;
    public int port = 2346;
    private IPEndPoint endPoint;
    public GameObject lineRendererPrefab;

    // JSON 数据结构
    [System.Serializable]
    public class Landmark
    {
        public int id;
        public float x;
        public float y;
        public float z;
    }

    [System.Serializable]
    public class LandmarkWrapper
    {
        public List<Landmark> landmarks;
    }

    void Start()
    {
        // 初始化 UDP 接收
        udpClient = new UdpClient(port);
        endPoint = new IPEndPoint(IPAddress.Any, port);

        // 初始化 21 个关节点球体
        handJoints = new Transform[21];
        lineRenderers = new List<LineRenderer>();
        for (int i = 0; i < 21; i++)
        {
            GameObject joint = Instantiate(jointPrefab, Vector3.zero, Quaternion.identity);
            handJoints[i] = joint.transform;
        }
        for (int i = 0; i < 5; i++)
        {
            GameObject lineObj = Instantiate(lineRendererPrefab);
            LineRenderer lineRenderer = lineObj.GetComponent<LineRenderer>();
            lineRenderer.startWidth = 0.02f;
            lineRenderer.endWidth = 0.02f;
            lineRenderer.useWorldSpace = true;
            lineRenderers.Add(lineRenderer);
        }
    }

    void Update()
    {
        // 检查是否有数据可接收
        if (udpClient.Available > 0)
        {
            // 接收并解析 JSON 数据
            byte[] data = udpClient.Receive(ref endPoint);
            string jsonString = Encoding.UTF8.GetString(data);
            Debug.Log("接收到的数据: " + jsonString);  // 打印接收到的数据,便于调试

            // 尝试解析 JSON 数据
            try
            {
                LandmarkWrapper wrapper = JsonUtility.FromJson<LandmarkWrapper>(jsonString);
                List<Landmark> landmarks = wrapper.landmarks;

                // 更新每个关节点的坐标
                for (int i = 0; i < landmarks.Count; i++)
                {
                    Vector3 newPosition = new Vector3(
                        landmarks[i].x * 10,  // 放大坐标以适应 Unity 场景
                        (1 - landmarks[i].y) * 10,
                        landmarks[i].z * 10
                    );
                    handJoints[i].localPosition = newPosition;
                }
                SetFingerPositions();
            }
            catch (System.Exception e)
            {
                Debug.LogError("JSON 解析出错: " + e.Message);
            }
        }
    }
    void SetFingerPositions()
    {
        // 定义每根手指的关节点索引
        int[][] fingerJoints = new int[][]
        {
            new int[] {0, 1, 2, 3, 4},       // 拇指
            new int[] {0, 5, 6, 7, 8},       // 食指
            new int[] {0, 9, 10, 11, 12},    // 中指
            new int[] {0, 13, 14, 15, 16},   // 无名指
            new int[] {0, 17, 18, 19, 20}    // 小指
        };

        // 为每根手指设置 LineRenderer 的位置
        for (int i = 0; i < fingerJoints.Length; i++)
        {
            int[] joints = fingerJoints[i];
            LineRenderer lineRenderer = lineRenderers[i];
            lineRenderer.positionCount = joints.Length;

            for (int j = 0; j < joints.Length; j++)
            {
                lineRenderer.SetPosition(j, handJoints[joints[j]].position);
            }
        }
    }
}

py完整可执行代码

import cv2
import mediapipe as mp
import time
import json
import socket



sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setblocking(False)
ip='127.0.0.1'
port = 2346
# 初始化MediaPipe手势识别
mp_hands = mp.solutions.hands
hands = mp_hands.Hands(
    max_num_hands=2,
    min_detection_confidence=0.8,
    min_tracking_confidence=0.5)

# 初始化MediaPipe姿态估计(如果需要)
mp_drawing = mp.solutions.drawing_utils
mp_pose = mp.solutions.pose

stream_url = 'http://192.168.1.104:4747/video'

# 创建一个VideoCapture对象,并指定视频流URL
cap = cv2.VideoCapture(stream_url)

# 设置摄像头帧率
cap.set(cv2.CAP_PROP_FPS, 30)
result_hand=[[]]
while cap.isOpened():
    success, image = cap.read()
    if not success:
        print("Ignoring empty camera frame.")
        continue
    # 转换BGR到RGB
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    results = hands.process(image)
    # 如果检测到手势
    if results.multi_hand_landmarks:
        # 绘制手部标记
        for hand_landmarks in results.multi_hand_landmarks:  #单只手的数据
            landmarks_data=[]
            for i,hand in enumerate(hand_landmarks.landmark): #每一个关节数据
                landmark_info={
                    'id':i, 'x':hand.x , 'y':hand.y, 'z':hand.z
                }
                landmarks_data.append(landmark_info)
            message_data={"landmarks":landmarks_data}
            message = json.dumps(message_data)#要发送的数据包
            try:
                sock.sendto(message.encode(), (ip, port))
                print("数据已发送:", message)  # 确认数据已发送
            except Exception as e:
                print(f"发送数据时出错: {e}")  # 捕捉并打印错误信息
            mp_drawing.draw_landmarks(image, hand_landmarks, mp_hands.HAND_CONNECTIONS)
    # 显示图像
    cv2.imshow('MediaPipe Hands', image)

    # 按'q'退出
    if cv2.waitKey(5) & 0xFF == 27:
        break

# 释放摄像头
cap.release()
# 关闭所有窗口
cv2.destroyAllWindows()

猜你喜欢

转载自blog.csdn.net/snaijggid/article/details/143302279
今日推荐