1. 项目概述与核心价值最近在捣鼓一些体感交互和虚拟现实的原型发现手势识别是个绕不开的坎。传统的深度摄像头或者数据手套方案要么成本高得吓人要么部署起来麻烦得要命。直到我发现了Mediapipe这个宝藏配合Python和Unity居然只用一台普通的RGB摄像头就能实现一套实时、高精度的手势捕捉系统还能无缝驱动Unity里的虚拟手模型。这简直就是为独立开发者、交互艺术创作者和游戏原型设计者量身定做的“平民级”动捕方案。这个项目的核心就是打通从物理世界的手部动作到数字世界虚拟手模型的实时映射。你不需要任何特殊硬件只需要一个电脑摄像头写大概一百行左右的Python代码再在Unity里搭个简单的接收和驱动框架就能让屏幕里的虚拟手跟你自己的手同步起舞。无论是想做个隔空操控的演示、开发体感小游戏还是为数字人添加更自然的手部交互这套方案都能快速让你看到效果。下面我就把自己从环境搭建、代码编写、数据通信到Unity驱动实现的完整流程以及踩过的所有坑毫无保留地分享出来。2. 技术栈选型与思路拆解2.1 为什么是Python Mediapipe Unity这个技术组合不是随便选的每一环都有其不可替代的优势共同构成了一个高效、低成本的开发闭环。首先Mediapipe是谷歌开源的一个跨平台多媒体机器学习模型应用框架。它的手部关键点检测模型hands是经过大量数据训练的能够从单目RGB图像中实时检测出21个三维手部关节点坐标包括手腕、每个手指的指节等。精度足够高延迟也控制得非常好在普通CPU上都能跑到30FPS以上。最关键的是它省去了我们自己收集数据、训练模型的巨大成本开箱即用。其次Python在这里扮演了“感知层”的角色。Python拥有极其丰富和易用的科学计算与AI库生态如OpenCV, NumPy与Mediapipe的集成只需要几行代码。我们用Python脚本调用摄像头、运行Mediapipe模型、获取关键点数据这个过程非常轻快和灵活。相比用C直接集成Python脚本的快速迭代和调试优势在原型阶段是决定性的。最后Unity作为强大的实时3D内容创作引擎负责“呈现与交互层”。我们需要一个能实时渲染高质量虚拟手模型、并易于绑定骨骼进行驱动的环境。Unity的Mecanim动画系统和GameObject变换体系非常适合这件事。我们将Python端计算出的关节点数据通过某种通信方式如UDP/Socket实时发送给UnityUnity再根据这些数据去驱动虚拟手模型中对应骨骼的旋转从而实现实时运动。整个数据流是这样的摄像头画面 - Python(OpenCVMediapipe) - 21个3D关键点数据 - 网络通信 - Unity(C#脚本) - 虚拟手骨骼驱动。思路清晰各司其职。2.2 虚拟手模型的选择与处理驱动虚拟手首先得有个“手”。Unity Asset Store里有大量免费和付费的手部模型。对于这个项目我强烈建议选择那些已经做好骨骼绑定Rigging的模型。一个标准的、易于驱动的手部模型通常包含约15-20根骨骼结构大致为一根腕骨Wrist每根手指有3-4根指骨Metacarpal, Proximal, Intermediate, Distal。如果你找到的模型是带人形Avatar的那最好不过可以利用Unity的人形动画系统。但更通用的方法是直接操作骨骼的Transform。我们需要确保Python端计算的21个关键点能与模型上的骨骼关节大致对应起来。Mediapipe的21点模型定义是固定的我们需要在Unity脚本中建立一个映射关系比如Mediapipe的索引0点对应手腕骨骼索引4点对应大拇指指尖骨骼等等。注意不要追求关节点与骨骼点的绝对一一对应。Mediapipe输出的是“关节点”位置而我们要驱动的是“骨骼”旋转。更常见的做法是利用关节点计算骨骼的朝向Rotation。例如通过食指的根节点、中节点和尖节点三个点的位置可以计算出手指骨骼应该旋转的角度。这是实现的核心逻辑之一。3. Python端实时手势捕捉与数据流发送3.1 环境搭建与依赖安装工欲善其事必先利其器。首先确保你安装了Python 3.7或以上版本。我强烈建议使用Anaconda来创建独立的Python环境避免包冲突。# 创建一个新的conda环境可选但推荐 conda create -n mediapipe_hand python3.8 conda activate mediapipe_hand # 安装核心库 pip install opencv-python # 用于摄像头捕获和图像显示 pip install mediapipe # 核心的手势识别库 # 如果速度慢可以使用清华镜像-i https://pypi.tuna.tsinghua.edu.cn/simple安装过程通常很顺利。如果遇到Mediapipe安装问题多半是网络或依赖问题可以尝试升级pip或指定版本。3.2 核心代码逐行解析接下来是Python端的核心脚本。我将它分成几个功能块来讲解。第一部分初始化与摄像头捕获import cv2 import mediapipe as mp import numpy as np import json import socket import time # 初始化Mediapipe手部解决方案 mp_hands mp.solutions.hands mp_drawing mp.solutions.drawing_utils hands mp_hands.Hands( static_image_modeFalse, # 设为False用于视频流 max_num_hands1, # 最多检测一只手够用了 min_detection_confidence0.5, # 检测置信度阈值 min_tracking_confidence0.5 # 跟踪置信度阈值 ) # 初始化OpenCV摄像头 cap cv2.VideoCapture(0) # 0代表默认摄像头 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) # 初始化UDP socket用于向Unity发送数据 UDP_IP 127.0.0.1 # 本地回路地址Unity也运行在本机 UDP_PORT 8052 # 端口号需与Unity端一致 sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM)static_image_modeFalse这个参数很重要。设为False时Mediapipe会启用追踪器在视频连续帧间优化检测结果大幅提升流畅度和性能。置信度阈值min_detection_confidence和min_tracking_confidence可以调节。调高会降低误检但可能让识别变“迟钝”调低则更灵敏但也可能引入抖动。0.5是个不错的起点。UDP协议为什么用UDP而不是TCP因为手势数据是连续、高频每秒几十次、且允许少量丢失的。UDP无连接、开销小的特性非常适合这种实时流数据。丢失一帧数据对视觉影响微乎其微但延迟却是致命的。第二部分主循环与关键点提取while cap.isOpened(): success, image cap.read() if not success: print(忽略空摄像头帧。) continue # Mediapipe处理需要RGB图像但OpenCV读取的是BGR image_rgb cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 为了提高性能可以标记图像为不可写以跳过复制 image_rgb.flags.writeable False results hands.process(image_rgb) # 转换回BGR用于OpenCV显示 image_rgb.flags.writeable True image_bgr cv2.cvtColor(image_rgb, cv2.COLOR_RGB2BGR) hand_landmarks_list [] if results.multi_hand_landmarks: for hand_landmarks in results.multi_hand_landmarks: # 在图像上绘制手部关键点和连接线可选用于本地预览 mp_drawing.draw_landmarks( image_bgr, hand_landmarks, mp_hands.HAND_CONNECTIONS, mp_drawing.DrawingSpec(color(121, 22, 76), thickness2, circle_radius3), # 点样式 mp_drawing.DrawingSpec(color(250, 44, 250), thickness2, circle_radius2), # 线样式 ) # 提取21个关键点的归一化坐标 (x, y, z) landmarks [] for lm in hand_landmarks.landmark: # landmark.x, y, z 是相对于图像宽高的归一化坐标 (0~1) # z是深度值越小表示离摄像头越近 landmarks.append([lm.x, lm.y, lm.z]) hand_landmarks_list.append(landmarks) # 目前我们只处理一只手所以列表只有一个元素 # 显示带标注的图像 cv2.imshow(MediaPipe Hands, image_bgr) if cv2.waitKey(5) 0xFF 27: # 按ESC退出 breakhands.process()是核心函数它返回包含检测结果的results对象。results.multi_hand_landmarks是一个列表每个元素代表一只检测到的手包含21个landmark对象。每个landmark有x,y,z三个属性。这里需要注意x和y是归一化坐标0到1之间分别乘以图像的宽度和高度才能得到像素坐标。z表示相对深度原点在手腕附近值越小表示该点离摄像头越近。我们在循环里绘制关键点只是为了本地调试和可视化实际发给Unity的数据不需要这个步骤。第三部分数据打包与网络发送# 准备发送给Unity的数据 if hand_landmarks_list: # 如果检测到手 # 我们只取第一只手的数据 data_to_send hand_landmarks_list[0] # 将数据扁平化成一个长列表 [x1, y1, z1, x2, y2, z2, ...] flat_data [] for point in data_to_send: flat_data.extend(point) # 依次添加x, y, z # 将列表转换为JSON字符串方便解析 # 也可以直接发送二进制数据更高效但JSON更易调试 data_json json.dumps(flat_data) # 通过UDP发送 sock.sendto(data_json.encode(), (UDP_IP, UDP_PORT)) # 打印发送的数据长度调试用 # print(fSent {len(data_json)} bytes) else: # 如果没有检测到手可以发送一个空数据或特定标识 # 这里我们发送一个全零的数组Unity端收到后可以判断是否重置手势 empty_data [0.0] * 63 # 21个点 * 3维 63 sock.sendto(json.dumps(empty_data).encode(), (UDP_IP, UDP_PORT)) # 释放资源 cap.release() cv2.destroyAllWindows() hands.close() sock.close()数据格式我们将21个点的x, y, z坐标展平成一个长度为63的列表。选择JSON格式是因为它人类可读、跨语言支持好在原型阶段调试非常方便。缺点是效率比二进制格式如struct.pack低一些。如果后期追求极致性能可以改为发送二进制流。空数据处理当手离开摄像头视野hand_landmarks_list为空。此时发送全零数据Unity端可以据此判断“手部丢失”并让虚拟手回归默认姿势或隐藏避免虚拟手僵在最后一个位置。实操心得一性能与延迟的权衡在主循环中cv2.imshow()和绘制关键点mp_drawing.draw_landmarks是比较耗时的操作尤其是高分辨率下。如果你发现帧率上不去或者延迟明显可以尝试降低摄像头分辨率如320x240。注释掉cv2.imshow()这行代码。这是最有效的提帧手段因为GUI显示开销很大。将绘制关键点的代码放到一个条件判断里比如按某个键才显示。 我们的首要目标是保证数据计算和发送的帧率稳定本地预览可以牺牲。4. Unity端数据接收与虚拟手驱动4.1 Unity项目设置与虚拟手导入在Unity中新建一个3D项目。从Asset Store搜索“Hand Model”或“Robot Hand”找一个带骨骼的免费模型导入。我以“Robot Hand”为例它通常有清晰的骨骼层级。将手模型拖入场景。在Scene视图和Hierarchy中检查其骨骼结构。通常你会看到类似Armature-Wrist-Finger_Thumb_0-Finger_Thumb_1...这样的层级。记下这些关键骨骼的名字我们稍后需要用到。4.2 C#脚本UDP数据接收与解析在Unity中创建一个C#脚本命名为HandDataReceiver.cs。我们需要使用.NET的System.Net.Sockets来接收UDP数据。using UnityEngine; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Collections.Generic; public class HandDataReceiver : MonoBehaviour { public string receiveIP 127.0.0.1; public int receivePort 8052; private UdpClient udpClient; private Thread receiveThread; private bool isReceiving false; // 存储接收到的原始数据 private float[] receivedLandmarkData new float[63]; // 21 points * 3 private bool newDataAvailable false; private object dataLock new object(); // 用于线程安全地访问数据 void Start() { InitializeUDP(); } void InitializeUDP() { try { udpClient new UdpClient(receivePort); isReceiving true; receiveThread new Thread(new ThreadStart(ReceiveData)); receiveThread.IsBackground true; // 设为后台线程程序关闭时自动终止 receiveThread.Start(); Debug.Log($UDP接收器已启动监听 {receiveIP}:{receivePort}); } catch (System.Exception e) { Debug.LogError($初始化UDP失败: {e.Message}); } } void ReceiveData() { IPEndPoint remoteEndPoint new IPEndPoint(IPAddress.Any, 0); while (isReceiving udpClient ! null) { try { byte[] data udpClient.Receive(ref remoteEndPoint); string jsonString Encoding.UTF8.GetString(data); float[] tempData JsonUtility.FromJsonfloat[](jsonString); if (tempData ! null tempData.Length 63) { lock (dataLock) { System.Array.Copy(tempData, receivedLandmarkData, 63); newDataAvailable true; } } } catch (SocketException e) { // 通常是因为线程被关闭时Receive调用被中断 if (isReceiving) Debug.LogWarning($Socket异常: {e.Message}); } catch (System.Exception e) { Debug.LogError($接收数据异常: {e.Message}); } } } void Update() { // 在主线程中检查是否有新数据用于驱动模型 if (newDataAvailable) { lock (dataLock) { // 在这里处理 receivedLandmarkData驱动手部模型 ProcessHandData(receivedLandmarkData); newDataAvailable false; } } } void ProcessHandData(float[] data) { // 数据处理的逻辑将在这里实现 // 暂时留空下一节填充 } void OnDestroy() { isReceiving false; if (udpClient ! null) { udpClient.Close(); } if (receiveThread ! null receiveThread.IsAlive) { receiveThread.Join(100); // 等待线程结束最多100ms } Debug.Log(UDP接收器已关闭。); } }多线程警告网络接收udpClient.Receive()是阻塞调用如果放在Unity的主线程Update里会卡死整个游戏。因此必须开一个单独的线程来接收数据。线程安全接收线程和Unity主线程Update会同时访问receivedLandmarkData数组。使用lock关键字确保同一时间只有一个线程在读写避免数据错乱。JsonUtility.FromJsonfloat[]Unity自带的JSON解析工具要求数据格式必须是严格的JSON数组。我们的Python端发送的正是这种格式。OnDestroy务必在脚本销毁时关闭Socket和线程否则可能导致端口占用或线程无法退出。4.3 核心算法从关键点位置到骨骼旋转这是整个Unity端最核心、也最具挑战性的部分。Mediapipe给的是21个关节点在相机坐标系下的3D位置而我们要驱动的是骨骼的旋转。直接设置骨骼的位置是不对的因为骨骼有层级关系父骨骼动了子骨骼会跟着动。正确的思路是根据关节点位置计算每根骨骼应该指向的方向即旋转四元数然后将其赋予对应的骨骼Transform。以一根手指为例比如食指。它通常有三节骨骼近端指骨、中间指骨、远端指骨。对应Mediapipe的点是根节点(5)、中节点(6)、尖节点(7)和指尖节点(8)。实际上我们通常用三个点来计算一段骨骼的朝向。计算骨骼朝向的基本原理确定骨骼的“起点”和“终点”。例如近端指骨的起点是手腕点(0)到根节点(5)之间的某个点更准确地说是根节点(5)终点是中节点(6)。计算从起点指向终点的向量。将这个向量与骨骼的初始朝向T-Pose下的朝向进行比较计算出一个旋转差值四元数。将这个旋转应用到骨骼上。由于直接计算3D旋转涉及复杂的数学Unity提供了Quaternion.FromToRotation这个神器。它可以根据一个“从方向”旋转到“到方向”所需的旋转。我们为HandDataReceiver脚本添加一个字典来映射Mediapipe关节点索引到场景中具体的骨骼Transform并实现ProcessHandData方法。public class HandDataReceiver : MonoBehaviour { // ... 之前的变量和Start, ReceiveData等方法 ... // 手部骨骼Transform的引用字典在Inspector中赋值 public Transform wristBone; public Transform[] thumbBones new Transform[3]; // 拇指3节 public Transform[] indexBones new Transform[3]; // 食指3节 public Transform[] middleBones new Transform[3]; public Transform[] ringBones new Transform[3]; public Transform[] pinkyBones new Transform[3]; // 存储骨骼的初始朝向在T-Pose下 private DictionaryTransform, Vector3 boneInitialForward new DictionaryTransform, Vector3(); void Start() { InitializeUDP(); RecordInitialPose(); // 记录初始姿态 } void RecordInitialPose() { // 假设在游戏开始时手模型处于一个标准的伸展姿态如T-Pose // 记录下每根骨骼此时的“前向”向量通常是其自身的Transform.forward // 这个向量将作为计算旋转的参考基准。 RecordBoneForward(wristBone); foreach (var bone in thumbBones) if (bone ! null) RecordBoneForward(bone); foreach (var bone in indexBones) if (bone ! null) RecordBoneForward(bone); // ... 记录其他手指 } void RecordBoneForward(Transform bone) { if (bone ! null) { boneInitialForward[bone] bone.forward; } } void ProcessHandData(float[] data) { // 1. 将扁平化的数据重新组织成21个Vector3 Vector3[] landmarks new Vector3[21]; for (int i 0; i 21; i) { // 注意Mediapipe的y坐标原点在图像顶部Unity中通常Y轴向上。 // 我们需要进行坐标系转换。一个简单的映射是 // Unity X Mediapipe X // Unity Y 1 - Mediapipe Y (翻转Y轴) // Unity Z Mediapipe Z (或取负取决于你想要的深度方向) float x data[i * 3]; float y 1.0f - data[i * 3 1]; // 翻转Y轴 float z data[i * 3 2]; landmarks[i] new Vector3(x, y, z); } // 2. 驱动手腕简单示例手腕的位置和粗略朝向 if (wristBone ! null) { // 将归一化坐标转换到Unity的世界坐标空间需要根据场景缩放 float scale 10.0f; // 缩放因子根据你的场景调整 Vector3 wristPos new Vector3( (landmarks[0].x - 0.5f) * scale, // 以屏幕中心为原点 (landmarks[0].y - 0.5f) * scale, landmarks[0].z * scale ); wristBone.position wristPos; // 计算手腕的大致朝向从手腕点(0)指向中指根节点(9) Vector3 wristToMid landmarks[9] - landmarks[0]; if (wristToMid.sqrMagnitude 0.001f) { Quaternion targetRot Quaternion.LookRotation(wristToMid.normalized, Vector3.up); wristBone.rotation targetRot; } } // 3. 驱动手指骨骼以食指为例 DriveFingerBone(indexBones[0], landmarks[5], landmarks[6]); // 食指近端点5-点6 DriveFingerBone(indexBones[1], landmarks[6], landmarks[7]); // 食指中间点6-点7 DriveFingerBone(indexBones[2], landmarks[7], landmarks[8]); // 食指远端点7-点8 // 同理驱动拇指、中指、无名指、小指... // DriveFingerBone(thumbBones[0], landmarks[1], landmarks[2]); // ... 其他手指 } void DriveFingerBone(Transform bone, Vector3 startLandmark, Vector3 endLandmark) { if (bone null || !boneInitialForward.ContainsKey(bone)) return; Vector3 targetDirection (endLandmark - startLandmark).normalized; // 获取该骨骼在初始姿态下的前向方向 Vector3 initialForward boneInitialForward[bone]; // 计算从初始方向旋转到目标方向所需的四元数 // 注意FromToRotation要求两个方向向量都已归一化 if (targetDirection.sqrMagnitude 0.001f initialForward.sqrMagnitude 0.001f) { Quaternion rotationFromInitialToTarget Quaternion.FromToRotation(initialForward, targetDirection); // 将计算出的旋转应用到骨骼上 bone.rotation rotationFromInitialToTarget * bone.parent.rotation; // 注意旋转的空间这里假设是局部旋转 // 更严谨的做法可能需要考虑骨骼的本地旋转空间这里做了简化。 } } }坐标系转换这是最大的坑之一。Mediapipe的坐标系原点在图像左上角Y轴向下。Unity通常是Y轴向上原点在中心。代码中y 1.0f - data[i*31]实现了Y轴翻转。X轴方向一致Z轴深度方向可能需要根据模型调整正负。缩放因子scale归一化坐标(0~1)需要乘以一个系数才能变成有意义的Unity世界坐标。这个系数需要你根据场景中虚拟手的大小和摄像头视野来反复调试。骨骼旋转的层级上面的DriveFingerBone方法是一个简化版本。在真实的骨骼层级中子骨骼的旋转是相对于父骨骼的。更准确的做法是计算骨骼在其父节点局部空间下的旋转并使用bone.localRotation进行赋值。bone.rotation rotationFromInitialToTarget * bone.parent.rotation;这个写法试图在世界空间中进行计算可能在某些层级下产生错误。一个更健壮的方法是使用Quaternion.FromToRotation计算出旋转后将其转换为相对于父骨骼初始状态的本地旋转增量。手指弯曲的简化上述方法只考虑了骨骼的“指向”没有完美模拟手指关节的弯曲约束比如指关节通常不能侧向弯曲太多。对于更逼真的效果可能需要使用逆运动学IK。一个更简单实用的替代方案是直接使用Mediapipe提供的角度信息。实际上我们可以用三个点例如指根、指中、指尖计算两个向量之间的夹角然后将这个角度映射到骨骼的某个轴如X轴的旋转上。这种方法虽然物理上不精确但实现简单视觉效果往往可以接受。实操心得二骨骼驱动的简化与优化完全精确的逆运动学驱动实现复杂且对模型骨骼要求严格。在原型阶段我强烈推荐一种“混合驱动”方案手腕用位置和粗略朝向驱动。手指根部骨骼控制手指的整体朝向用FromToRotation方法计算。手指的中间和末端关节主要控制弯曲使用关节点计算的角度来驱动。例如计算食指近端指骨点5-6向量与中间指骨点6-7向量的夹角将这个角度线性映射到对应骨骼绕其本地X轴的旋转上。 这样做的好处是逻辑清晰调试方便并且能产生比较自然的手指弯曲效果。你可以在DriveFingerBone函数中加入角度计算和单轴旋转的逻辑。4.4 场景搭建与调试技巧将写好的HandDataReceiver脚本挂载到场景中的一个空GameObject上。在Inspector面板中将手部模型的骨骼Transform拖拽到脚本对应的公共变量中如wristBone,indexBones数组等。你需要根据你的模型骨骼结构一一对应地赋值。调试步骤先运行Python脚本确保摄像头打开并能看到手部关键点绘制。在Unity中运行游戏。观察Console窗口应该看到“UDP接收器已启动”的日志。将手放在摄像头前观察虚拟手的运动。常见问题与排查虚拟手不动检查Unity Console是否有错误。检查UDP端口号是否与Python脚本一致。检查Python脚本是否真的在发送数据可以取消打印数据长度的注释。在Unity的ProcessHandData函数开头加Debug.Log(“收到数据: ” data[0])看是否持续有数据进来。虚拟手乱飞或旋转诡异大概率是坐标系转换或缩放因子问题。尝试调整scale值或修改landmarks[i]的构造公式如尝试z -data[i*32]。可以先将wristBone.position的驱动注释只测试手指旋转。手指弯曲方向反了在角度映射时尝试取反角度值或者旋转不同的轴本地Y轴或Z轴。延迟很高首先在Python端关闭图像显示(cv2.imshow)。其次检查Unity的帧率Stats窗口如果Game视图分辨率太高可以调低。确保没有在Update中做非常耗时的操作。5. 项目优化与扩展方向5.1 性能优化与数据平滑实时手势驱动对延迟非常敏感。除了之前提到的关闭预览还有以下优化手段数据平滑滤波Mediapipe的输出本身会有微小抖动。在Unity端对接收到的关节点位置进行平滑处理能显著提升视觉稳定性。最简单有效的方法是使用指数移动平均EMA。private Vector3[] smoothedLandmarks new Vector3[21]; public float smoothFactor 0.5f; // 平滑因子0~1越大越平滑但延迟也越大 void ProcessHandData(float[] rawData) { // ... 将rawData转换为landmarks ... for (int i 0; i 21; i) { // 第一次初始化 if (smoothedLandmarks[i] Vector3.zero) { smoothedLandmarks[i] landmarks[i]; } else { // EMA滤波: new α * current (1-α) * old smoothedLandmarks[i] smoothFactor * landmarks[i] (1 - smoothFactor) * smoothedLandmarks[i]; } } // 使用 smoothedLandmarks 代替 landmarks 去驱动骨骼 }smoothFactor取值0.2到0.5之间效果较好能在平滑度和响应速度间取得平衡。数据压缩如前所述将JSON传输改为二进制传输。Python端使用struct.packUnity端使用System.BitConverter和Buffer.BlockCopy进行解析能减少数据包大小和序列化/反序列化开销。降低发送频率不一定需要每帧Python数据都对应Unity一帧。可以在Python端或Unity端设置一个固定发送/更新频率如30Hz即使Python计算是60Hz也只按30Hz发送减轻网络和Unity更新压力。5.2 功能扩展手势识别与交互仅仅驱动模型还不够酷。我们可以基于这21个关键点轻松实现手势识别从而在Unity中触发交互。原理通过计算特定关节点之间的距离、角度或相对位置关系来定义手势。示例识别“捏合”Pinch手势“捏合”通常指拇指尖索引4和食指尖索引8距离很近。bool IsPinching(Vector3[] landmarks) { Vector3 thumbTip landmarks[4]; Vector3 indexTip landmarks[8]; float distance Vector3.Distance(thumbTip, indexTip); // 设定一个阈值距离小于阈值则认为正在捏合 float pinchThreshold 0.05f; // 这个值需要根据你的坐标缩放来调整 return distance pinchThreshold; } void Update() { if (newDataAvailable) { // ... 处理数据 ... if (IsPinching(smoothedLandmarks)) { // 触发捏合事件例如抓取一个虚拟物体 Debug.Log(检测到捏合手势); // 可以在这里调用抓取物体的函数 } } }示例识别“张开手掌”可以计算所有指尖点到手腕点(0)的平均距离。当手张开时这个平均距离会比较大当手握拳时这个距离会变小。float GetHandOpenness(Vector3[] landmarks) { Vector3 wrist landmarks[0]; float totalDistance 0f; int[] fingertipIndices {4, 8, 12, 16, 20}; // 拇指、食、中、无名、小指的指尖索引 foreach (int idx in fingertipIndices) { totalDistance Vector3.Distance(wrist, landmarks[idx]); } return totalDistance / fingertipIndices.Length; }通过设定一个“张开”阈值就可以判断手掌是否张开。你可以发挥创意定义“点赞”、“OK”、“摇滚”等各种手势并映射到游戏中的不同操作。5.3 部署与跨设备通信目前我们使用的是本地回路地址127.0.0.1意味着Python和Unity必须运行在同一台电脑上。如果想实现手机摄像头捕捉电脑Unity显示就需要跨设备通信。在同一局域网内找到运行Python脚本的设备如手机或另一台电脑的局域网IP地址如192.168.1.xxx。将Unity脚本中的receiveIP改为这个IP地址。确保防火墙允许UDP端口如8052通信。Python在手机运行在安卓或iOS上运行Python需要一些额外工作比如使用Pydroid 3安卓或PythonistaiOS这类APP。Mediapipe有移动端优化版本但集成起来比桌面端复杂。一个更简单的替代方案是使用IP摄像头APP。在手机上安装一个IP摄像头APP将手机摄像头变成网络视频流。然后在电脑的Python脚本中将cv2.VideoCapture(0)改为cv2.VideoCapture(‘http://手机IP:端口/video’)这样就能获取手机摄像头画面进行处理实现跨设备。这套PythonMediapipeUnity的实时手势驱动方案从零到一的搭建过程确实会遇到不少坐标转换、数据同步、骨骼驱动上的小麻烦但一旦跑通其扩展潜力是非常巨大的。它为你打开了一扇低成本体感交互的大门。你可以用它来做VR/AR的虚拟手交互原型、体感音乐游戏、远程协作中的手势指示甚至结合语音识别做更复杂的多模态交互。