ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Python+MediaPipe+Unity虚拟人物驱动源码:从摄像头到骨骼动作

Python+MediaPipe+Unity虚拟人物驱动源码:从摄像头到骨骼动作 简介这份资源是一套基于Python与MediaPipe实现手部、面部关键点识别并通过Unity端驱动虚拟人物动作的完整项目源码面向计算机相关专业正在做毕业设计、课程设计或大作业的学生以及需要项目实战练习的开发者评审分达99分代码完整可运行零基础也能上手。压缩包共157个文件约85.91MB包含20个py脚本负责识别与数据处理、12个cs脚本承担Unity端控制逻辑、66个pickle与22个pyc用于模型与缓存、22个mp4演示素材另有xml配置、pdf说明及unitypackage资源包目录结构清晰。目前已有72人学习下载。项目覆盖MediaPipe手部与面部关键点提取、数据通信、虚拟人物驱动等核心环节读者可据此掌握从视觉识别到Unity动画联动的完整链路并直接用于毕业设计或课程答辩。1. 从摄像头到虚拟人物这套 Python MediaPipe Unity 源码到底能跑出什么很多人做虚拟形象驱动时第一反应是买动捕设备或者上昂贵的商业 SDK结果预算和时间全砸进去最后连一个能演示的 Demo 都没跑通。这套源码走的是另一条路用 Python 调 MediaPipe 做手部和面部的关键点识别把识别结果通过本地通信推给 Unity再由 Unity 端的控制器脚本驱动虚拟人物模型。整条链路不依赖专用硬件一个普通摄像头就能跑起来适合做毕业设计、课程大作业也适合想入门姿态驱动但不想被硬件门槛卡住的人。它解决的核心问题很具体——把「摄像头画面里的手和脸」翻译成「Unity 里虚拟人物的骨骼动作和表情参数」中间不需要你从零写通信协议和坐标映射。2. 拆开看这套源码Python 识别端与 Unity 驱动端怎么分工2.1 MediaPipe 在 Python 端到底输出了什么MediaPipe 的手部模型每帧会返回 21 个关键点面部模型返回 468 个关键点部分版本是 478含虹膜。这些点都是归一化坐标x、y 在 0 到 1 之间z 是相对深度。很多人第一次拿到这些数据不知道怎么用其实关键就一句话手部点用来算手指弯曲角度和手掌朝向面部点用来算眉毛高度、嘴巴开合和头部姿态。源码里 Python 端做的事情就是把这些点做一次筛选和归一化然后打包成固定格式发出去。import mediapipe as mp import cv2 mp_hands mp.solutions.hands mp_face mp.solutions.face_mesh hands mp_hands.Hands( static_image_modeFalse, max_num_hands2, min_detection_confidence0.6, min_tracking_confidence0.5 ) face mp_face.FaceMesh( static_image_modeFalse, max_num_faces1, refine_landmarksTrue, min_detection_confidence0.6 ) cap cv2.VideoCapture(0) while cap.isOpened(): ret, frame cap.read() if not ret: break rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) hand_result hands.process(rgb) face_result face.process(rgb) # 后续把 hand_result 和 face_result 里的关键点取出并发送这段代码里max_num_hands2表示最多检测两只手做单手驱动可以改成 1 降低开销。min_detection_confidence和min_tracking_confidence是两个最容易翻车的参数调高了画面稍微暗一点就丢检测调低了会频繁误检。我一般先在目标环境里用 0.6 和 0.5 跑一遍如果丢帧明显再往下调到 0.5 和 0.4。refine_landmarksTrue会额外输出虹膜点做眼球追踪才需要不做的话关掉能省一点算力。2.2 Unity 端控制器脚本的职责划分从文件名能看出 Unity 端分了几层HiyoriController.cs和UnityChanController.cs是两个不同虚拟人物的驱动控制器HiyoriPref.cs和UnityChanPref.cs是各自的预制体配置UISystem.cs管界面FileManager.cs和SaveDataManager.cs负责文件读写和存档。这种分层的好处是换模型时只需要改控制器和预制体通信层和 UI 不用动。控制器脚本的核心逻辑是接收 Python 端发来的关键点数据映射到 Unity 的骨骼旋转或 BlendShape 权重上。常见做法是在Update里读取一个线程安全的队列把最新一帧数据取出来然后逐骨骼赋值。这里有个血泪经验不要在Update里直接做网络接收网络线程和主线程抢数据会导致动作抖动甚至卡死。源码里如果用了ConcurrentQueue或者加锁的缓冲区那就是对的。using System.Collections.Concurrent; using UnityEngine; public class HiyoriController : MonoBehaviour { private ConcurrentQueuestring dataQueue new ConcurrentQueuestring(); public float smoothSpeed 12f; private float targetMouthOpen 0f; private float currentMouthOpen 0f; void Update() { if (dataQueue.TryDequeue(out string json)) { var data JsonUtility.FromJsonFaceData(json); targetMouthOpen data.mouthOpen; } // 平滑插值避免关键点抖动直接传到模型上 currentMouthOpen Mathf.Lerp(currentMouthOpen, targetMouthOpen, Time.deltaTime * smoothSpeed); ApplyMouthBlendShape(currentMouthOpen); } void ApplyMouthBlendShape(float value) { var skinnedMesh GetComponentInChildrenSkinnedMeshRenderer(); int index skinnedMesh.sharedMesh.GetBlendShapeIndex(MouthOpen); if (index 0) skinnedMesh.SetBlendShapeWeight(index, value * 100f); } }ConcurrentQueue保证跨线程读写安全Mathf.Lerp的smoothSpeed参数控制平滑程度设太大动作会延迟设太小会抖。我一般从 10 到 15 之间试面部表情用 12 左右比较自然。SetBlendShapeWeight的取值是 0 到 100所以传进去之前要乘 100。如果模型没有对应的 BlendShape 名字GetBlendShapeIndex会返回 -1这里必须判空否则直接报错。2.3 通信链路的选择与数据格式Python 和 Unity 之间常见的通信方式有三种本地 Socket、UDP 广播、共享内存。这套源码大概率用的是 Socket 或 UDP因为实现简单、跨平台。数据格式一般是 JSON 或自定义的紧凑二进制。JSON 可读性好但解析开销大二进制快但调试麻烦。做毕设演示用 JSON 足够了帧率要求高的话再换二进制。import socket import json sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) unity_addr (127.0.0.1, 5066) def send_landmarks(hand_points, face_points): payload { hand: hand_points, # [[x,y,z], ...] 21 个点 face: face_points # [[x,y,z], ...] 468 个点 } data json.dumps(payload).encode(utf-8) sock.sendto(data, unity_addr)UDP 不保证到达但延迟低适合这种每帧覆盖旧数据的场景。端口号 5066 是随便选的只要 Unity 端监听同一个端口就行。注意sendto的数据包大小468 个面部点加 21 个手部点用 JSON 序列化后可能超过 64KB某些系统上 UDP 包太大会被截断。稳妥做法是只发变化明显的点或者把面部点降采样到 68 个传统特征点。3. 从零跑通环境配置、参数调试与联调步骤3.1 Python 端环境安装与摄像头权限先确认 Python 版本MediaPipe 对 3.8 到 3.11 支持最好3.12 以上有些版本还没适配。安装用 pip 就行但要注意 opencv 和 mediapipe 的版本匹配。python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install mediapipe0.10.9 pip install opencv-python4.8.1.78 pip install numpy版本号不是随便写的mediapipe 0.10.x 和 opencv 4.8.x 搭配比较稳。如果装完 import 报DLL load failed大概率是 Visual C 运行库没装去微软官网下最新的 vc_redist 装上。摄像头权限在 Windows 上默认开启macOS 和 Linux 需要在系统设置里手动授权这个坑很多人第一次跑的时候会忽略程序不报错但就是黑屏。3.2 Unity 端项目导入与脚本挂载Unity 版本建议用 2021 LTS 或 2022 LTS太新的版本有些 API 变了会导致脚本编译不过。导入项目后先看Assets目录下脚本是否完整然后把HiyoriController或UnityChanController挂到场景里的虚拟人物根节点上。预制体HiyoriPref和UnityChanPref里应该已经配好了模型和骨骼引用如果报空引用检查SkinnedMeshRenderer有没有正确赋值。// 在 UISystem.cs 里通常会有一个启动按钮绑定这个函数 public void OnStartButtonClicked() { if (controller null) { Debug.LogError(Controller 未挂载检查场景根节点); return; } controller.StartReceiving(127.0.0.1, 5066); statusText.text 接收中...; }StartReceiving里一般会起一个后台线程做 UDP 接收把数据塞进队列。如果点了按钮没反应先看 Unity 控制台有没有报错再看防火墙是不是拦了 UDP 5066 端口。Windows 防火墙对本地回环一般不管但有些安全软件会拦临时关掉测试一下能快速定位。3.3 联调时先对齐坐标系再调平滑Python 端和 Unity 端的坐标系不一样。MediaPipe 的 y 轴向下Unity 的 y 轴向上x 轴方向也可能相反。联调第一步不是看动作像不像而是拿一个静止的手势看 Unity 里模型的手是不是也在对应位置。如果上下颠倒把 y 取反如果左右镜像把 x 取反。这一步没对齐后面调平滑参数全是白费。对齐之后调smoothSpeed和关键点滤波。面部 468 个点直接驱动模型会非常抖常见做法是对每个点做指数移动平均或者只取几个关键距离比如上下嘴唇距离、眉毛到眼睛距离来驱动 BlendShape。源码里如果直接用了原始点建议自己加一层滤波。class EMAFilter: def __init__(self, alpha0.4): self.alpha alpha self.value None def update(self, new_value): if self.value is None: self.value new_value else: self.value self.alpha * new_value (1 - self.alpha) * self.value return self.valuealpha越小越平滑但延迟越大0.3 到 0.5 之间比较平衡。每个关键点单独维护一个滤波器实例不要共用一个。4. 避坑与排查这套源码跑不起来时先查这五处4.1 现象Python 端能识别但 Unity 端完全没反应原因通常是端口不一致或防火墙拦截。Python 发到 5066Unity 监听 5067这种低级错误在改代码时经常发生。解决方法是两端都打印一下实际使用的端口号确认一致然后在 Unity 里用Debug.Log确认接收线程有没有启动如果线程都没起来检查StartReceiving是不是没被调用。4.2 现象虚拟人物动作延迟明显手已经动了模型半天才跟原因是平滑参数过大或者队列积压。smoothSpeed设成 5 以下会明显拖尾改成 12 到 15。另一个可能是接收线程每帧处理太慢队列里堆了几十帧旧数据。解决办法是在取数据时把队列清空只留最新一帧或者限制队列最大长度。4.3 现象面部表情抽搐嘴巴和眉毛乱跳原因是原始关键点抖动直接传给了 BlendShape。MediaPipe 的面部点在光线变化或遮挡时会跳变必须加滤波。解决方法是给每个驱动值加 EMA 滤波或者用滑动窗口取中值。另外检查refine_landmarks是否开启虹膜点在某些角度下噪声很大不做眼球追踪就关掉。4.4 现象Unity 编辑器里正常打包后就不行原因是打包后脚本执行顺序或权限变了。常见的是ConcurrentQueue在 IL2CPP 下行为差异或者 UDP 接收线程在打包后被系统限制。解决办法是把接收逻辑改成 Unity 的Thread加lock避免依赖ConcurrentQueue的隐式行为另外在打包设置里确认没有裁剪掉用到的网络相关代码。4.5 现象换了自己的虚拟人物模型后 BlendShape 不生效原因是模型没有对应的 BlendShape 名字或者名字大小写不匹配。GetBlendShapeIndex(MouthOpen)要求名字完全一致。解决办法是在建模软件里导出时确认 BlendShape 名称或者在 Unity 里用sharedMesh.blendShapeCount遍历打印所有名字找到实际名称再改代码。5. 进阶技巧把驱动精度再提一档的验证方法跑通之后如果想进一步提升效果我一般会做两件事一是用录制回放来验证二是把关键点映射从直接驱动改成角度驱动。录制回放的做法是 Python 端把每帧的关键点存成 JSON 文件Unity 端加一个回放模式读取文件而不是 UDP。这样调试时不用每次都对着摄像头做动作可以反复播放同一段数据来对比参数效果。具体就是在发送函数里加一个开关开启时写文件关闭时发 UDP。import json RECORD_MODE True record_buffer [] def send_landmarks(hand_points, face_points): payload {hand: hand_points, face: face_points} if RECORD_MODE: record_buffer.append(payload) if len(record_buffer) % 300 0: with open(record.json, w) as f: json.dump(record_buffer, f) else: sock.sendto(json.dumps(payload).encode(utf-8), unity_addr)每 300 帧存一次盘避免程序崩溃丢数据。回放时 Unity 端按固定帧率读record.json这样能精确复现同一段动作调平滑参数时特别有用。角度驱动是指不直接把关键点坐标映射到骨骼而是先算出关节角度再驱动。比如手指弯曲用三个连续关键点算夹角再把夹角映射到骨骼旋转。这样做的好处是角度对距离变化不敏感手离摄像头远近不影响弯曲程度。验证方法是把手从近到远移动看模型手指弯曲是否保持一致如果直接映射坐标手远了弯曲就变小角度驱动则不会。驱动方式对距离敏感实现难度适合场景坐标直接映射敏感低快速演示角度映射不敏感中需要稳定精度的场景坐标加滤波部分敏感低面部表情从那以后我每次拿到这类驱动项目都强制先跑一遍录制回放把参数在固定数据上调到满意再上实时摄像头省得反复对着镜头做同一个动作。希望帮到你。本文还有配套的精品资源点击获取
返回列表