效果图:
因为在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()