个人云服务器应用参考

拥有云服务器和软件开发基础的个人用户,可以从以下方向探索实用且有学习价值的应用场景。这些方向既能提升技术能力,又能优化日常生活或工作效率:

一、生产力与效率工具 私有云存储与同步

工具:Nextcloud、Seafile 用途:替代公有云盘,同步文件、日历、联系人,支持插件扩展(如在线文档编辑、密码管理)。 技术点:文件加密、WebDAV协议、Docker部署。 自动化工作流

场景:自动备份数据、爬取网页信息、监控价格变动、定时发送邮件/消息。 工具:Python脚本 + Cron任务、Zapier替代方案(如n8n自托管版)。 示例:每日抓取新闻摘要并推送至Telegram,自动下载YouTube订阅内容。 知识管理与文档系统

工具:Wiki.js(知识库)、BookStack(文档管理)、Trilium(笔记软件)。 用途:整理个人知识、技术文档,支持Markdown和多人协作。 二、开发与学习环境 代码托管与CI/CD

工具:自建Git服务(Gitea/GitLab)、Jenkins或Drone实现自动化测试与部署。 用途:管理私有代码库,自动化构建个人项目。 开发沙盒环境

方案:用Docker/Kubernetes创建隔离的开发环境,支持多语言(Python/Node.js等)。 扩展:部署Jupyter Notebook或VS Code Server,实现远程编码。 API服务与微服务实验

项目:开发个人天气查询API、短链接服务,或尝试Serverless框架(如OpenFaaS)。 三、媒体与娱乐 家庭媒体中心

工具:Plex、Jellyfin、Emby 用途:搭建私人影视库,支持跨设备串流、自动刮削影片信息。 游戏服务器

热门选项:Minecraft、Terraria、Valheim私有服务器。 技术点:端口配置、性能优化(针对游戏需求调整云服务器规格)。 音乐与播客

工具:Navidrome(音乐流媒体)、Audiobookshelf(有声书管理)。 四、智能家居与物联网 智能家居中枢

工具:Home Assistant(支持接入小米/HomeKit等设备)。 功能:远程控制家电、自动化场景(如离家关闭灯光)。 环境监控

方案:通过传感器收集温湿度数据,用Grafana可视化展示。 五、安全与隐私 自建VPN

工具:WireGuard、OpenVPN 用途:加密网络流量,访问受限资源,提升公共Wi-Fi安全性。 密码管理

工具:Vaultwarden(Bitwarden开源版) 优势:完全掌控密码数据,支持TOTP两步验证。 网络监控与防护

工具:AdGuard Home(去广告+DNS过滤)、Fail2ban(防暴力破解)。 六、数据分析与可视化 个人数据看板

场景:统计健康数据(如Apple Health导出)、消费记录、学习进度。 工具:Metabase、Grafana + PostgreSQL/InfluxDB。 爬虫与数据分析

项目:抓取社交媒体/电商数据,分析趋势并生成报告(Python + Pandas)。 七、其他创意方向 区块链节点

运行:比特币轻节点、IPFS节点,参与去中心化网络。 AI实验平台

用途:部署开源模型(如Stable Diffusion、LLAMA),实现本地AI绘图/聊天。 匿名服务

工具:Tor中继节点(需注意法律风险),或自建隐私搜索引擎(SearXNG)。 注意事项 成本与性能:根据服务器配置选择应用,低配机器可优先考虑轻量级工具(如SQLite替代MySQL)。 安全加固:定期更新系统、配置防火墙、禁用SSH密码登录、使用SSL加密。 备份策略:通过脚本自动备份数据到对象存储(如AWS S3、MinIO)。 以上方向可根据兴趣自由组合,例如将Home Assistant与自动化脚本结合,实现离家时自动关闭媒体服务器以节省资源。通过实践这些项目,不仅能提升全栈开发能力,还能打造高度定制的个人数字生态系统。

发表在 工具 | 个人云服务器应用参考已关闭评论

身体关键点+avatar处理

写一段代码,将mediapipe捕捉的人体关键点数据转换成unity中avatar支持的形式 以下是一个将MediaPipe人体关键点数据转换为Unity Avatar控制的示例代码方案,分为Python数据采集和C# Unity控制两部分:

# Python部分 - MediaPipe数据处理(示例)
import mediapipe as mp
import socket
import json

mp_pose = mp.solutions.pose

# Unity连接配置
HOST = '127.0.0.1'  # Unity所在IP
PORT = 65432        # 通信端口

# 关键点索引映射(MediaPipe Pose定义的33个关键点)
JOINT_INDICES = {
    "LEFT_HIP": 23,
    "RIGHT_HIP": 24,
    "SPINE": 11,  # 肩部中点近似为脊柱上端
    "LEFT_KNEE": 25,
    "RIGHT_KNEE": 26,
    "LEFT_SHOULDER": 11,
    "RIGHT_SHOULDER": 12,
    "NECK": 0,     # 鼻部近似为颈部
}

# 坐标系转换函数
def convert_coordinates(mediapipe_point):
    """将MediaPipe坐标系转换为Unity坐标系"""
    # MediaPipe: 右手坐标系,Y轴向下
    # Unity: 左手坐标系,Y轴向上,Z轴向前
    return {
        'x': mediapipe_point.x,
        'y': -mediapipe_point.y,  # 反转Y轴
        'z': -mediapipe_point.z   # 反转Z轴
    }

# 数据发送函数
def send_to_unity(sock, landmarks):
    joint_data = {}
    for joint_name, index in JOINT_INDICES.items():
        if landmarks[index].visibility < 0.5:  # 过滤低可见度关键点
            continue
        joint_data[joint_name] = convert_coordinates(landmarks[index])

    # 计算臀部中心点
    if "LEFT_HIP" in joint_data and "RIGHT_HIP" in joint_data:
        lh = joint_data["LEFT_HIP"]
        rh = joint_data["RIGHT_HIP"]
        joint_data["HIP_CENTER"] = {
            'x': (lh['x'] + rh['x']) / 2,
            'y': (lh['y'] + rh['y']) / 2,
            'z': (lh['z'] + rh['z']) / 2
        }

    sock.sendto(json.dumps(joint_data).encode(), (HOST, PORT))

# MediaPipe处理主循环
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
    with mp_pose.Pose(
        min_detection_confidence=0.7,
        min_tracking_confidence=0.7) as pose:

        cap = cv2.VideoCapture(0)
        while cap.isOpened():
            success, image = cap.read()
            if not success:
                continue

            results = pose.process(image)
            if results.pose_landmarks:
                send_to_unity(s, results.pose_landmarks.landmark)
// C#部分 - Unity控制脚本
using UnityEngine;
using System.Net.Sockets;
using System.Text;
using System;

public class MotionReceiver : MonoBehaviour
{
    public Transform hipCenter;
    public Transform spine;
    public Transform leftLeg, rightLeg;
    public Transform leftArm, rightArm;
    public Transform neck;

    private UdpClient udpClient;
    private const int port = 65432;

    void Start()
    {
        udpClient = new UdpClient(port);
        udpClient.Client.ReceiveTimeout = 1000;
    }

    void Update()
    {
        try {
            IPEndPoint remoteEP = null;
            byte[] data = udpClient.Receive(ref remoteEP);
            string json = Encoding.UTF8.GetString(data);
            ProcessPoseData(JsonUtility.FromJson<PoseData>(json));
        } catch (Exception e) {
            Debug.LogWarning(e.ToString());
        }
    }

    void ProcessPoseData(PoseData data)
    {
        // 臀部位置控制
        if (data.HIP_CENTER != null)
        {
            Vector3 hipPos = new Vector3(
                data.HIP_CENTER.x * 2,  // 缩放系数根据实际调整
                data.HIP_CENTER.y * 2,
                data.HIP_CENTER.z * 2
            );
            hipCenter.localPosition = hipPos;
        }

        // 腿部旋转计算
        if (data.LEFT_HIP != null && data.LEFT_KNEE != null)
        {
            Vector3 hip = ToVector3(data.LEFT_HIP);
            Vector3 knee = ToVector3(data.LEFT_KNEE);
            leftLeg.rotation = Quaternion.LookRotation(knee - hip, Vector3.up);
        }

        // 脊柱旋转
        if (data.SPINE != null && data.NECK != null)
        {
            Vector3 spinePos = ToVector3(data.SPINE);
            Vector3 neckPos = ToVector3(data.NECK);
            spine.rotation = Quaternion.LookRotation(neckPos - spinePos, Vector3.up);
        }
    }

    Vector3 ToVector3(JointPoint point)
    {
        return new Vector3(point.x, point.y, point.z);
    }

    [System.Serializable]
    public class PoseData
    {
        public JointPoint HIP_CENTER;
        public JointPoint LEFT_HIP;
        public JointPoint RIGHT_HIP;
        public JointPoint LEFT_KNEE;
        // 其他需要的关键点...
    }

    [System.Serializable]
    public class JointPoint
    {
        public float x;
        public float y;
        public float z;
    }
}

关键实现细节说明:

  1. 坐标系转换:
  2. MediaPipe使用右手坐标系(Y轴向下)
  3. Unity使用左手坐标系(Y轴向上,Z轴向前)
  4. 转换公式:y = -mediapipe.y, z = -mediapipe.z
  5. 数据优化:
  6. 使用UDP协议实现低延迟传输
  7. 仅传输必要关键点数据
  8. 添加可见性过滤(visibility < 0.5)
  9. Unity端处理:
  10. 使用Quaternion.LookRotation计算骨骼朝向
  11. 根据实际场景调整位置缩放系数
  12. 建议的骨骼层级关系:
    Hips (root)
    ├─ LeftUpLeg
    ├─ RightUpLeg
    └─ Spine
    ├─ Neck
    ├─ LeftArm
    └─ RightArm
  13. 校准建议:
  14. 在Unity中创建校准场景
  15. 添加位置/旋转偏移量补偿
  16. 实现动态缩放系数调整

实际使用时需要: 1. 在Unity Avatar中正确设置Humanoid骨骼映射 2. 根据摄像头分辨率调整坐标缩放参数 3. 添加数据平滑滤波器(移动平均/Kalman滤波) 4. 实现断线重连机制 5. 添加调试可视化(显示原始关键点数据)

Q:unity中怎样直接控制avatar做动作 在Unity中直接控制Avatar动作主要通过操作骨骼的Transform来实现,以下是详细的实现方法和步骤:


1. 基础准备

1.1 配置Humanoid Avatar

  1. 导入模型后,在模型导入设置中:
  2. 选择 Rig 标签
  3. Animation Type: Humanoid
  4. 点击 Configure 检查骨骼映射
  5. 确保所有关键骨骼(如Hips、Spine、Legs等)正确映射

1.2 创建空物体控制器

public class AvatarController : MonoBehaviour
{
    // 通过Unity Inspector绑定骨骼
    public Transform hips;
    public Transform spine;
    public Transform leftUpperLeg;
    public Transform rightUpperLeg;
    public Transform neck;
    // 其他需要控制的骨骼...
}

2. 直接骨骼控制核心逻辑

2.1 位置控制(适用于根节点)

void UpdateHipsPosition(Vector3 newPosition)
{
    // 保持Y轴高度(根据需求调整)
    Vector3 adjustedPos = new Vector3(
        newPosition.x * positionScale,
        Mathf.Clamp(newPosition.y, minHeight, maxHeight),
        newPosition.z * positionScale
    );
    hips.localPosition = Vector3.Lerp(hips.localPosition, adjustedPos, 0.2f);
}

2.2 旋转控制(关节控制关键)

void UpdateLimbRotation(Transform upperJoint, Transform lowerJoint, Vector3 jointPos, Vector3 nextJointPos)
{
    // 计算骨骼方向
    Vector3 boneDirection = (nextJointPos - jointPos).normalized;

    // 计算旋转(使用LookRotation+轴调整)
    if(boneDirection != Vector3.zero)
    {
        Quaternion targetRot = Quaternion.LookRotation(boneDirection, Vector3.up);

        // 调整轴向(根据模型骨骼方向)
        targetRot *= Quaternion.Euler(90, 0, 0); // 示例修正

        upperJoint.rotation = Quaternion.Slerp(
            upperJoint.rotation,
            targetRot,
            rotationSmoothness * Time.deltaTime
        );
    }
}

2.3 脊柱链式控制

void UpdateSpine(Vector3 hipPos, Vector3 spinePos, Vector3 neckPos)
{
    // 臀部到脊柱的旋转
    UpdateLimbRotation(hips, spine, hipPos, spinePos);

    // 脊柱到脖子的旋转
    UpdateLimbRotation(spine, neck, spinePos, neckPos);
}

3. 完整数据驱动示例

public class MotionCaptureDriver : MonoBehaviour
{
    // 参数调整
    [Header(&quot;Settings&quot;)]
    [Range(0.1f, 2f)] public float positionScale = 1.5f;
    [Range(0.1f, 5f)] public float rotationSmoothness = 2f;

    void ProcessMotionData(PoseData data)
    {
        // 根节点位置
        UpdateHipsPosition(data.hipCenter);

        // 左腿
        UpdateLimbRotation(
            leftUpperLeg, 
            leftLowerLeg, 
            data.leftHip, 
            data.leftKnee
        );

        // 右腿
        UpdateLimbRotation(
            rightUpperLeg,
            rightLowerLeg,
            data.rightHip,
            data.rightKnee
        );

        // 脊柱链
        UpdateSpineChain(
            data.hipCenter,
            data.spine,
            data.neck
        );

        // 手臂控制(同理)
        // ...
    }
}

4. 高级控制技巧

4.1 逆向动力学(IK)控制

// 在Animator上添加IK控制
void OnAnimatorIK(int layerIndex)
{
    if(useFootIK)
    {
        // 左脚IK
        animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, 1);
        animator.SetIKRotationWeight(AvatarIKGoal.LeftFoot, 1);
        animator.SetIKPosition(AvatarIKGoal.LeftFoot, leftFootTarget.position);
        animator.SetIKRotation(AvatarIKGoal.LeftFoot, leftFootTarget.rotation);

        // 同样处理右脚...
    }
}

4.2 数据平滑处理

// 使用低通滤波器
Vector3 LowPassFilter(Vector3 newValue, Vector3 previousValue)
{
    return Vector3.Lerp(previousValue, newValue, 0.3f);
}

// 使用在关键数据上
smoothedHipPosition = LowPassFilter(rawHipPosition, smoothedHipPosition);

4.3 运动重定向校准

IEnumerator CalibrationRoutine()
{
    // 记录初始偏移
    Vector3 initialHipOffset = hips.position - rawData.hipCenter;

    // 等待3秒校准时间
    yield return new WaitForSeconds(3);

    // 应用永久性偏移
    permanentOffset = initialHipOffset;
}

5. 调试与优化

5.1 可视化调试

void OnDrawGizmos()
{
    // 绘制骨骼连线
    Gizmos.color = Color.red;
    Gizmos.DrawLine(hips.position, spine.position);
    Gizmos.DrawLine(spine.position, neck.position);

    // 绘制关键点
    if(showRawData)
    {
        Gizmos.color = Color.green;
        Gizmos.DrawSphere(rawHipPosition, 0.1f);
    }
}

5.2 性能优化

  1. 使用 Job System + Burst Compiler 处理数据
  2. 控制Update频率: csharp [SerializeField] int updateInterval = 2; // 每2帧更新一次 void Update() { if(Time.frameCount % updateInterval == 0) { UpdateAvatar(); } }

6. 常见问题解决

  1. 骨骼翻转问题
  2. LookRotation后添加轴向修正: csharp targetRotation *= Quaternion.Euler(90, 0, 0); // 根据实际模型调整
  3. 动作抖动csharp // 添加阈值过滤 if((newPosition - currentPosition).magnitude > 0.01f) { ApplyPosition(newPosition); }
  4. 比例不匹配csharp // 动态缩放计算 float CalculateScaleRatio(Vector3 realWorldShoulderWidth) { float modelWidth = Vector3.Distance(leftShoulder.position, rightShoulder.position); return modelWidth / realWorldShoulderWidth.magnitude; }

通过以上方法,可以实现从MediaPipe数据到Unity Avatar的精准动作控制。实际开发时需要根据具体模型比例和动作需求调整参数,建议配合Unity的Humanoid重定向功能实现更好的跨模型兼容性。

Q:将动作数据应用到ik系统,实现平滑自然的效果,有什么要点方法

将动作数据应用到IK系统实现平滑自然效果的要点与方法

在Unity中通过IK系统实现平滑自然的动作效果,需要结合数据预处理、IK参数调节、运动学协调等多个环节。以下是关键步骤与实施方法:


1. 选择合适的IK系统

  • Unity内置Animator IK
  • 适用于Humanoid Avatar,通过OnAnimatorIK回调控制。
  • 支持设置AvatarIKGoal(如手、脚)和AvatarIKHint(如肘、膝)的权重与目标位置。
  • 优点:集成方便,适合基础需求。
  • 缺点:功能较为基础,复杂动作需结合其他逻辑。
  • 第三方插件(如Final IK)
  • 提供更强大的IK解算器(如CCD、FABRIK)。
  • 支持全身IK、骨骼链约束等高级功能。
  • 适用场景:复杂动作(攀爬、抓取)或高精度需求。

2. 数据预处理:减少抖动与噪声

  • 低通滤波csharp Vector3 LowPassFilter(Vector3 rawData, Vector3 prevData, float smoothFactor = 0.2f) { return Vector3.Lerp(prevData, rawData, smoothFactor); }
  • 对关节位置和旋转进行平滑处理,避免数据突变。
  • 可见性过滤csharp if (landmark.visibility < 0.5f) return; // 忽略低置信度数据
  • 速度限制csharp Vector3 ClampVelocity(Vector3 target, Vector3 current, float maxSpeed) { Vector3 direction = target - current; if (direction.magnitude > maxSpeed) { return current + direction.normalized * maxSpeed; } return target; }

3. 坐标系与比例校准

  • 坐标系对齐
  • MediaPipe使用右手坐标系(Y轴向下),Unity为左手坐标系(Y轴向上,Z轴向前)。
  • 转换公式: csharp Vector3 ConvertToUnitySpace(Vector3 mediapipePoint) { return new Vector3( mediapipePoint.x, -mediapipePoint.y, // Y轴反转 -mediapipePoint.z // Z轴反转 ) * scaleFactor; // 根据场景调整缩放系数 }
  • 动态比例校准csharp // 校准步骤:用户摆出T-Pose时计算缩放系数 float CalculateScaleFactor(Vector3 realWorldShoulderWidth) { Vector3 modelShoulderWidth = rightShoulder.position - leftShoulder.position; return modelShoulderWidth.magnitude / realWorldShoulderWidth.magnitude; }

4. IK权重与过渡控制

  • 渐进式权重调整csharp float ikWeight = Mathf.Clamp01(Vector3.Distance(currentPos, targetPos) * 0.5f); animator.SetIKPositionWeight(AvatarIKGoal.LeftHand, ikWeight); animator.SetIKPosition(AvatarIKGoal.LeftHand, targetPos);
  • 根据目标距离动态调整权重,避免动作突变。
  • 平滑插值csharp currentPos = Vector3.Lerp(currentPos, targetPos, 0.1f); currentRot = Quaternion.Slerp(currentRot, targetRot, 0.1f);

5. 身体协调与约束

  • 脊柱与头部跟随 void UpdateBodyOrientation(Vector3 headPosition) { // 身体朝向跟随头部 Vector3 lookDirection = headPosition - spine.position; spine.rotation = Quaternion.LookRotation(lookDirection, Vector3.up);</p> <p>// 头部微调 neck.rotation = Quaternion.Lerp(neck.rotation, targetHeadRot, 0.3f); }
  • 关节旋转约束csharp // 限制肘部弯曲角度(示例) float ClampElbowAngle(float angle) { return Mathf.Clamp(angle, 20f, 160f); // 限制在20°~160° }

6. 高级技巧:逆向动力学与正向动力学结合

  • 混合使用IK与FK
  • 使用IK控制手脚位置,FK控制躯干旋转。
  • 示例代码: csharp void UpdateArm(bool useIK) { if (useIK) { // IK模式:控制手部位置 animator.SetIKPosition(AvatarIKGoal.RightHand, rightHandTarget.position); } else { // FK模式:直接设置关节旋转 upperArm.rotation = CalculateArmRotation(); } }
  • 脚部地面适配csharp void AdjustFootToGround(Transform foot, Vector3 targetPos) { if (Physics.Raycast(targetPos + Vector3.up, Vector3.down, out RaycastHit hit, 2f)) { foot.position = hit.point + Vector3.up * 0.1f; // 保持轻微悬空 foot.rotation = Quaternion.FromToRotation(foot.up, hit.normal) * foot.rotation; } }

7. 调试与优化

  • 可视化调试csharp void OnDrawGizmos() { Gizmos.color = Color.blue; Gizmos.DrawSphere(leftHandTarget.position, 0.1f); Gizmos.DrawLine(leftShoulder.position, leftHandTarget.position); }
  • 性能优化
  • 降低更新频率csharp [SerializeField] int updateInterval = 2; // 每2帧更新一次 void Update() { if (Time.frameCount % updateInterval == 0) { UpdateIK(); } }
  • 使用Job System并行处理csharp // 定义IJobParallelFor处理多关节计算 public struct IKJob : IJobParallelFor { public NativeArray<Vector3> positions; public void Execute(int index) { // 并行计算每个关节的目标位置 } }

8. 常见问题解决

  • 动作抖动
  • 增加数据平滑系数,或添加移动平均滤波。
  • 限制最小移动距离(如位移小于0.01单位时不更新)。
  • 骨骼扭曲
  • 调整轴向修正参数: csharp targetRotation *= Quaternion.Euler(90, 0, 0); // 根据模型调整
  • 穿透问题
  • 添加碰撞检测,限制关节位置: csharp if (Physics.CheckSphere(targetPos, 0.1f)) { targetPos = FindNearestValidPosition(targetPos); }

总结

实现平滑自然的IK动作需要: 1. 数据清洗:滤波与校准消除噪声。 2. 权重控制:渐进式过渡避免突变。 3. 运动协调:结合IK与FK保持身体平衡。 4. 约束优化:限制关节范围与物理检测。 5. 高效计算:通过并行处理与频率控制优化性能。

通过逐步调试参数(如平滑系数、权重过渡速度)并配合可视化工具,最终可实现接近真实人体运动的自然效果。

发表在 Media Pipe | 身体关键点+avatar处理已关闭评论

肢体动作识别和数据发送

用python写个程序,调用本机摄像头,使用mediapipe库识别我的肢体动作,通过udp发送获得的动作数据 以下是增加关键点可视化功能的改进代码,包含详细的注释说明:

import cv2
import mediapipe as mp
import socket
import json

# 初始化MediaPipe组件
mp_pose = mp.solutions.pose
mp_drawing = mp.solutions.drawing_utils  # 新增绘图工具
pose = mp_pose.Pose(
    static_image_mode=False,
    model_complexity=1,
    enable_segmentation=False,
    min_detection_confidence=0.5
)

# 配置UDP
UDP_IP = "127.0.0.1"
UDP_PORT = 5005
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 自定义绘图样式
landmark_drawing_spec = mp_drawing.DrawingSpec(
    color=(0, 255, 0),   # 关键点颜色 (BGR)
    thickness=2,         # 关键点粗细
    circle_radius=2      # 关键点半径
)
connection_drawing_spec = mp_drawing.DrawingSpec(
    color=(255, 0, 0),   # 连接线颜色
    thickness=2          # 连接线粗细
)

cap = cv2.VideoCapture(0)

while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break

    # 转换为RGB格式进行检测
    rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    results = pose.process(rgb_frame)

    if results.pose_landmarks:
        # 发送数据逻辑保持不变
        landmarks = []
        for idx, landmark in enumerate(results.pose_landmarks.landmark):
            landmarks.append({
                "id": idx,
                "x": landmark.x,
                "y": landmark.y,
                "z": landmark.z,
                "visibility": landmark.visibility
            })
        data = json.dumps(landmarks).encode('utf-8')
        sock.sendto(data, (UDP_IP, UDP_PORT))

        # 新增可视化部分 ---------------------------
        # 绘制关键点和连接线
        mp_drawing.draw_landmarks(
            frame,
            results.pose_landmarks,
            mp_pose.POSE_CONNECTIONS,
            landmark_drawing_spec=landmark_drawing_spec,
            connection_drawing_spec=connection_drawing_spec
        )

        # 显示关键点编号
        h, w, _ = frame.shape  # 获取画面尺寸
        for idx, lm in enumerate(results.pose_landmarks.landmark):
            # 将归一化坐标转换为实际像素坐标
            cx, cy = int(lm.x * w), int(lm.y * h)
            # 在关键点旁显示编号
            cv2.putText(frame, 
                       str(idx), 
                       (cx+5, cy+5), 
                       cv2.FONT_HERSHEY_SIMPLEX, 
                       0.4, 
                       (0, 0, 255),  # 红色编号
                       1)

    # 显示处理后的画面
    cv2.imshow('Pose Detection with Landmarks', frame)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()
pose.close()
sock.close()

新增功能说明:

  1. 关键点可视化
    • 使用mp_drawing.draw_landmarks绘制:
      • 绿色关键点(可自定义颜色/大小)
      • 蓝色连接线(显示身体部位之间的连接)
    • 在关键点右侧显示红色编号(0-32)
  2. 自定义样式
    • 通过DrawingSpec对象可调节:
      landmark_drawing_spec = mp_drawing.DrawingSpec(
      color=(0, 255, 0),  # BGR颜色格式
      thickness=2,        # 线宽
      circle_radius=2     # 关键点半径
      )
  3. 坐标转换
    cx, cy = int(lm.x * w), int(lm.y * h)  # 将归一化坐标转换为实际像素坐标

效果增强建议:

  1. 按可见性过滤关键点
    if lm.visibility > 0.5:  # 只显示可见性大于50%的关键点
       cv2.putText(...)
  2. 不同部位使用不同颜色
    # 为上半身设置不同颜色
    if idx in [11,12,13,14,15,16]:  # 肩部、手肘等
       color = (0, 165, 255)  # 橙色
  3. 显示坐标信息
    cv2.putText(frame, 
              f"{cx},{cy}", 
              (cx+15, cy-5), 
              cv2.FONT_HERSHEY_PLAIN,
              0.8,
              (255,255,255))

各关键点对应身体部位:

编号 身体部位 编号 身体部位
0 鼻子 16 右手腕
11 左肩 17 右手小指根部
12 右肩 18 右手无名指根部
13 左手肘 19 右手中指根部
14 右手肘 20 右手食指根部
15 左手腕 21 右手拇指根部

完整对应表建议参考 https://ai.google.dev/edge/mediapipe/solutions/guide?hl=zh-cn

运行效果:

  • 实时视频窗口中将显示:
    • 带编号的身体关键点
    • 蓝色连接线表示骨骼结构
    • 红色数字标识具体关键点编号
    • 原始UDP数据发送功能保持不变

可以通过调整circle_radiusthickness参数来优化显示效果以适应不同分辨率的摄像头。

发表在 Media Pipe | 肢体动作识别和数据发送已关闭评论

面部识别相关

用python的mediapipe工具识别人脸,控制unity中3d角色的表情 以下是使用Python的MediaPipe和Unity实现面部表情控制的逐步指南:

步骤1:安装所需库

pip install mediapipe opencv-python

步骤2:Python面部识别脚本

import cv2
import mediapipe as mp
import socket
import json

# MediaPipe初始化
mp_face_mesh = mp.solutions.face_mesh
face_mesh = mp_face_mesh.FaceMesh(
    static_image_mode=False,
    max_num_faces=1,
    min_detection_confidence=0.5,
    min_tracking_confidence=0.5)

# 关键点索引(根据官方文档调整)
MOUTH_TOP = 13    # 上唇中心
MOUTH_BOTTOM = 14 # 下唇中心
LEFT_EYE_TOP = 159
LEFT_EYE_BOTTOM = 145

# Socket服务器设置
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('127.0.0.1', 12345))
server_socket.listen(1)
print("等待Unity连接...")
conn, addr = server_socket.accept()
print("已连接:", addr)

cap = cv2.VideoCapture(0)

while cap.isOpened():
    success, frame = cap.read()
    if not success:
        break

    # 转换为RGB并处理
    image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    results = face_mesh.process(image)

    if results.multi_face_landmarks:
        landmarks = results.multi_face_landmarks[0].landmark

        # 计算嘴巴开合程度
        mouth_top = landmarks[MOUTH_TOP]
        mouth_bottom = landmarks[MOUTH_BOTTOM]
        mouth_open = mouth_bottom.y - mouth_top.y

        # 计算左眼闭合程度
        eye_top = landmarks[LEFT_EYE_TOP]
        eye_bottom = landmarks[LEFT_EYE_BOTTOM]
        eye_close = eye_bottom.y - eye_top.y

        # 构造数据包
        data = {
            "mouth_open": float(mouth_open * 100),  # 放大数值便于观察
            "eye_close": float(eye_close * 100)
        }

        # 发送JSON数据
        conn.send(json.dumps(data).encode() + b'n')

    # 显示画面(可选)
    cv2.imshow('MediaPipe FaceMesh', cv2.flip(frame, 1))
    if cv2.waitKey(5) & 0xFF == 27:
        break

# 清理资源
cap.release()
cv2.destroyAllWindows()
conn.close()
server_socket.close()

步骤3:Unity设置

  1. 创建3D角色并设置Blend Shapes

    • 确保角色有对应的Blend Shapes(如”MouthOpen”, “EyeClose”)
  2. 创建C#脚本FaceController.cs

    using UnityEngine;
    using System.Net.Sockets;
    using System.Text;
    using System.Threading;
    using System.Collections.Concurrent;
    public class FaceController : MonoBehaviour
    {
    public SkinnedMeshRenderer faceMesh;
    public string mouthBlendShapeName = "MouthOpen";
    public string eyeBlendShapeName = "EyeClose";
    
    private TcpClient client;
    private NetworkStream stream;
    private Thread receiveThread;
    private bool isRunning;
    
    private ConcurrentQueue<string> dataQueue = new ConcurrentQueue<string>();
    private float mouthValue;
    private float eyeValue;
    
    void Start()
    {
        ConnectToServer();
    }
    
    void ConnectToServer()
    {
        try
        {
            client = new TcpClient("127.0.0.1", 12345);
            stream = client.GetStream();
            isRunning = true;
    
            receiveThread = new Thread(() =>
            {
                byte[] buffer = new byte[1024];
                while (isRunning)
                {
                    try
                    {
                        int bytesRead = stream.Read(buffer, 0, buffer.Length);
                        if (bytesRead > 0)
                        {
                            string data = Encoding.UTF8.GetString(buffer, 0, bytesRead);
                            dataQueue.Enqueue(data);
                        }
                    }
                    catch { break; }
                }
            });
    
            receiveThread.Start();
        }
        catch (System.Exception e)
        {
            Debug.LogError("Connection error: " + e.Message);
        }
    }
    
    void Update()
    {
        // 处理所有排队的数据
        while (dataQueue.TryDequeue(out string rawData))
        {
            foreach (var json in rawData.Split('n'))
            {
                if (string.IsNullOrEmpty(json)) continue;
    
                try
                {
                    var data = JsonUtility.FromJson<FaceData>(json);
                    mouthValue = data.mouth_open;
                    eyeValue = data.eye_close;
                }
                catch (System.Exception e)
                {
                    Debug.LogWarning("Parse error: " + e.Message);
                }
            }
        }
    
        // 应用Blend Shapes
        if (faceMesh != null)
        {
            int mouthIndex = faceMesh.sharedMesh.GetBlendShapeIndex(mouthBlendShapeName);
            int eyeIndex = faceMesh.sharedMesh.GetBlendShapeIndex(eyeBlendShapeName);
    
            if (mouthIndex != -1) 
                faceMesh.SetBlendShapeWeight(mouthIndex, mouthValue);
            if (eyeIndex != -1)
                faceMesh.SetBlendShapeWeight(eyeIndex, eyeValue);
        }
    }
    
    void OnDestroy()
    {
        isRunning = false;
        stream?.Close();
        client?.Close();
    }
    
    [System.Serializable]
    class FaceData
    {
        public float mouth_open;
        public float eye_close;
    }
    }

步骤4:Unity配置

  1. 将脚本附加到角色对象
  2. 在Inspector中:
    • 将角色的SkinnedMeshRenderer拖入faceMesh字段
    • 输入正确的Blend Shape名称

步骤5:运行流程

  1. 先运行Python脚本
  2. 等待Unity连接提示后运行Unity项目
  3. 对摄像头做出表情观察角色变化

增强功能建议

  1. 添加更多表情参数:
    
    # 添加眉毛控制
    EYEBROW_LEFT = 105
    EYEBROW_RIGHT = 334

在数据中添加眉毛位置

eyebrow_left = landmarks[EYEBROW_LEFT].y data[“eyebrow_left”] = float(eyebrow_left * 100)


2. 在Unity中添加对应的Blend Shape控制

3. 添加校准功能(在Python中添加空格键校准):
```python
calibration = {}

if cv2.waitKey(5) & 0xFF == 32:  # 空格键
    calibration["mouth_neutral"] = mouth_open
    calibration["eye_neutral"] = eye_close
  1. 使用归一化计算:
    mouth_open = (current_value - calibration["mouth_neutral"]) * sensitivity

故障排除

  1. 确保防火墙允许本地连接
  2. 检查Blend Shape名称匹配
  3. 确认摄像头权限已开启
  4. 调整Python脚本中的关键点索引
  5. 使用Debug.Log显示接收数值验证数据流

这个方案实现了实时面部捕捉到虚拟角色驱动的完整流程,可根据需要扩展更多面部特征控制。

发表在 开发技巧记录 | 面部识别相关已关闭评论

几种人体动作识别技术比较

嗯,用户让我帮忙整理现在可用于PC电脑上的人体关键点识别技术,还要做对比图,分析优劣和适用场景。首先,我需要明确用户的需求是什么。他们可能在开发一个需要人体姿态识别的应用,比如健身指导、安防监控或者游戏互动之类的,所以需要了解不同的技术选项,选择最适合自己项目的方案。

首先,我得列举目前主流的人体关键点识别技术。常见的库和框架包括OpenPose、MediaPipe、MMPose、AlphaPose、PoseNet,可能还有DeepLabCut或者TensorFlow.js的某些实现,不过用户提到是PC上的,所以可能更倾向于本地运行而不是Web端的,所以PoseNet可能不太合适?或者是否有PC版本?

接下来,我需要确认每个技术的特点。比如OpenPose应该支持多人,准确度高,但速度可能较慢。MediaPipe是谷歌的,应该轻量级,适合实时应用,但精度可能稍低。MMPose是商汤的,基于Pytorch,可能灵活但需要更多计算资源。AlphaPose可能专注于跟踪,适合视频分析。PoseNet是基于TensorFlow的,可能适合简单的应用,但可能在PC上也有实现方式。

然后需要考虑对比的维度,比如支持的骨架模型(比如关节点数量,如18、25、33等)、多人支持、速度(FPS)、准确度、依赖库、硬件要求、易用性、适用场景等。这些维度能帮助用户比较不同技术的优缺点。

另外,用户可能需要知道各个技术的安装和配置难度,比如是否需要GPU,依赖的框架是TensorFlow还是PyTorch,或者是否独立。比如OpenPose需要编译,可能对新手不太友好,而MediaPipe可能通过pip安装更简单。

适用场景方面,比如实时应用需要高FPS,可能选MediaPipe;需要高精度或多人场景选OpenPose;研究或定制模型可能用MMPose;视频中的姿态跟踪用AlphaPose;轻量级或Web集成用PoseNet。

可能需要检查每个技术的最新情况,比如OpenPose是否还在维护,或者是否有新的版本。比如MediaPipe的更新情况,支持的模型是否有改进。例如,MediaPipe现在的BlazePose可能有33个关键点,比之前的版本更精确。

还要考虑硬件兼容性,比如是否支持CPU和GPU,比如OpenPose在GPU上运行更快,而MediaPipe可能在CPU上也能实时运行,这对于没有高端显卡的用户来说很重要。

还有准确度的指标,比如使用COCO或MPII数据集上的AP值,但用户可能需要更直观的比较,比如高、中、低等级别。

可能还需要考虑是否支持3D姿态估计,比如某些库如MediaPipe可能有3D的支持,而其他可能只有2D。不过用户可能只需要2D,但如果有3D需求也需要提到。

然后,我需要把这些信息整理成表格,然后分析每个的优劣和适用场景。可能需要确认每个技术的文档和社区支持情况,比如OpenPose有较多的教程,但安装复杂;MediaPipe文档完善,易用性好。

最后,用户可能需要一个总结,根据不同的需求推荐合适的技术,比如实时性、精度、多人、开发难度等因素。

以下是对当前PC端人体关键点识别技术的对比分析表,以及各技术的优劣势和适用场景总结:


人体关键点识别技术对比表

技术名称 开发者/机构 骨架模型 多人支持 速度(FPS) 准确度 依赖框架 硬件要求 易用性 主要特点
OpenPose CMU 25/18点(BODY_25) ✔️ 5-10(GPU) Caffe/OpenCV NVIDIA GPU 高精度多人检测,支持手部/面部
MediaPipe Google 33点(BlazePose) ✔️ 30+ (CPU) 中高 独立/Python CPU/低端GPU 轻量级实时处理,跨平台
MMPose 商汤科技 多种可选 ✔️ 15-20(GPU) 极高 PyTorch NVIDIA GPU 模块化设计,支持前沿算法
AlphaPose 上海交大 17点(COCO) ✔️ 10-15(GPU) PyTorch NVIDIA GPU 精准跟踪,适合视频分析
PoseNet TensorFlow 17点(COCO) 20+ (CPU) TensorFlow.js CPU/Web浏览器 轻量级,适合Web集成
DeepLabCut 社区开源 自定义模型 5-10(GPU) TensorFlow/PyTorch NVIDIA GPU 支持动物姿态,需定制训练

技术优劣势与适用场景分析

1. OpenPose

  • 优势
  • 支持多人、手部、面部联合检测,精度高。
  • 社区资源丰富,适合学术研究。
  • 劣势
  • 依赖GPU且安装复杂,速度较慢。
  • 适用场景
  • 多人场景(如舞蹈动作分析)、高精度需求(医疗康复评估)。

2. MediaPipe

  • 优势
  • 轻量级,CPU实时运行,跨平台支持(Windows/Linux/macOS)。
  • 提供Python API,集成简单。
  • 劣势
  • 复杂姿态(如遮挡)下精度下降。
  • 适用场景
  • 实时应用(健身指导、视频会议虚拟背景)。

3. MMPose

  • 优势
  • 提供多种SOTA模型(如HRNet、ViTPose),灵活性高。
  • 支持3D姿态估计和自定义数据集训练。
  • 劣势
  • 配置复杂,需PyTorch和GPU支持。
  • 适用场景
  • 研究或工业级高精度需求(动画捕捉、运动生物力学分析)。

4. AlphaPose

  • 优势
  • 结合目标检测(YOLO)与姿态估计,视频跟踪稳定。
  • 适合长序列动作分析。
  • 劣势
  • 依赖GPU,速度中等。
  • 适用场景
  • 视频监控、体育赛事动作分析。

5. PoseNet

  • 优势
  • 轻量级,可通过TensorFlow.js在浏览器运行。
  • 适合快速原型开发。
  • 劣势
  • 仅支持单人,精度较低。
  • 适用场景
  • Web应用(互动艺术、简易健身指导)。

6. DeepLabCut

  • 优势
  • 支持动物和自定义关键点训练,科研友好。
  • 劣势
  • 需自行标注数据,学习曲线陡峭。
  • 适用场景
  • 动物行为研究、实验室定制化分析。

总结推荐

  • 实时性优先:MediaPipe(CPU)、PoseNet(Web)。
  • 高精度需求:OpenPose、MMPose。
  • 视频分析:AlphaPose。
  • 科研定制:DeepLabCut、MMPose。
  • 快速部署:MediaPipe(Python)、PoseNet(JavaScript)。

可根据项目需求(精度/速度/硬件/开发周期)灵活选择技术方案。

发表在 Media Pipe | 几种人体动作识别技术比较已关闭评论

MMPose实现

好的,用户现在问是否可以用Python调用MMPose来实现人体识别。首先,我需要回顾之前对话的内容。用户之前让我整理PC上的人体关键点识别技术,我列出了包括MMPose在内的几个方案,并提到了MMPose的优劣势,比如高精度、模块化设计,但配置复杂。现在用户可能对MMPose感兴趣,想实际操作。

接下来要确定用户的需求。用户可能是一个开发者,或者是研究人员,想要在项目中实现人体姿态估计。他们的技术背景可能中等,因为MMPose需要一定的PyTorch和深度学习框架的知识。可能他们已经有了Python的基础,但可能对MMPose的具体使用流程不太熟悉。

需要回答的主要问题是:能否用Python调用MMPose,以及如何实现。根据之前整理的信息,MMPose是基于PyTorch的,支持Python,所以答案是肯定的。但要给出具体的步骤,可能需要指导安装、配置环境、编写代码示例,以及常见问题处理。

用户可能的深层需求是快速上手MMPose,而不是仅仅知道理论上的可行性。所以需要提供明确的步骤,例如安装命令、代码片段,甚至注意事项,比如依赖项、模型选择、硬件要求(GPU)。此外,用户可能在安装过程中遇到问题,比如CUDA版本不兼容,或者依赖库冲突,所以需要提示可能的解决方法,比如参考官方文档或社区支持。

还要考虑用户的使用场景。MMPose适用于高精度需求,比如动画捕捉或运动分析,用户可能是在做类似的项目。可能需要提醒他们在不同场景下选择适合的模型,比如HRNet用于2D,ViTPose用于更高精度或3D估计。

另外,用户可能对性能有要求,需要说明MMPose在GPU上的表现,以及是否需要实时处理,可能需要调整模型大小或在速度和精度之间权衡。例如,使用轻量级模型如mobilenetv2提升速度,但牺牲一些精度。

最后,要确保提供的代码示例尽可能简单明了,让用户能快速测试,比如使用预训练模型进行推理,并可视化结果。同时,提醒用户处理视频或摄像头输入时的循环结构和性能优化。

总结思考步骤:确认MMPose的Python支持,整理安装和使用的步骤,提供代码示例,提示常见问题,考虑用户的实际应用场景和可能的需求扩展。

是的,可以通过Python调用MMPose实现人体关键点识别。MMPose是基于PyTorch的开源姿态估计工具库,支持2D/3D人体、动物等多类姿态估计模型。以下是具体实现步骤和示例代码:


1. 环境准备

安装依赖

# 安装PyTorch(需与CUDA版本匹配,此处以CUDA 11.3为例)
conda install pytorch==1.12.1 torchvision==0.13.1 torchaudio==0.12.1 cudatoolkit=11.3 -c pytorch

# 安装MMCV和MMPose
pip install openmim
mim install mmcv-full
mim install mmpose

验证安装

import mmpose
print(mmpose.__version__) # 应输出如0.30.0

2. 快速调用预训练模型

示例代码:人体关键点检测

import cv2
import numpy as np
from mmpose.apis import inference_topdown, init_model
from mmpose.utils import register_all_modules

# 注册所有模块(加载配置文件)
register_all_modules()

# 初始化模型(以HRNet为例)
config_file = 'configs/body_2d_keypoint/topdown_heatmap/coco/td-hm_hrnet-w48_8xb64-210e_coco-256x192.py'
checkpoint_file = 'https://download.openmmlab.com/mmpose/top_down/hrnet/hrnet_w48_coco_256x192-7e65c15d_20200708.pth'
model = init_model(config_file, checkpoint_file, device='cuda:0') # 使用GPU

# 读取图像
image_path = 'test.jpg'
image = cv2.imread(image_path)

# 推理(支持多人检测,需配合目标检测模型如YOLO)
results = inference_topdown(model, image)

# 可视化结果
from mmpose.utils.visualize import visualize_keypoints
vis_image = visualize_keypoints(
image=image,
keypoints=results[0].pred_instances.keypoints, # 关键点坐标
keypoint_scores=results[0].pred_instances.keypoint_scores, # 置信度
skeleton=model.dataset_meta.skeleton, # 骨架连线规则
show_kpt_idx=False, # 是否显示关键点索引
radius=4, # 点半径
thickness=2 # 线宽
)

# 保存结果
cv2.imwrite('output.jpg', vis_image)

3. 关键功能扩展

(1) 结合目标检测(YOLO)实现端到端多人姿态估计

from mmdet.apis import inference_detector, init_detector

# 初始化YOLO检测模型
det_config = 'mmdetection/configs/yolo/yolov3_d53_320_273e_coco.py'
det_checkpoint = 'https://download.openmmlab.com/mmdetection/v2.0/yolo/yolov3_d53_320_273e_coco/yolov3_d53_320_273e_coco-421362b6.pth'
det_model = init_detector(det_config, det_checkpoint, device='cuda:0')

# 检测人体边界框
det_results = inference_detector(det_model, image)
person_bboxes = det_results.pred_instances.bboxes.cpu().numpy() # 提取人体框

# 将检测框输入MMPose进行关键点估计
pose_results = inference_topdown(model, image, person_bboxes)

(2) 视频流处理(摄像头/视频文件)

cap = cv2.VideoCapture(0) # 摄像头
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break

# 推理
results = inference_topdown(model, frame)
vis_frame = visualize_keypoints(frame, results[0].pred_instances.keypoints)

cv2.imshow('Pose Estimation', vis_frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break

cap.release()
cv2.destroyAllWindows()

4. 常见问题解决

Q1: 模型加载报错?

  • 检查CUDA与PyTorch版本是否匹配,可通过 nvidia-smi 查看CUDA版本。
  • 确保MMCV版本兼容,推荐使用 mim install mmcv-full 自动匹配版本。

Q2: 运行速度慢?

  • 改用轻量模型(如 mobilenetv2 配置):
    config_file = 'configs/body_2d_keypoint/topdown_heatmap/coco/td-hm_mobilenetv2_8xb64-210e_coco-256x192.py'

Q3: 如何自定义训练?

  • 参考官方文档准备COCO格式数据集,修改配置文件中的 data_rootann_file 路径。

5. 适用场景建议

  • 高精度场景:使用HRNet或ViTPose模型(需GPU)。
  • 实时性要求:选择MobileNetV2等轻量模型,或降低输入分辨率(如128×96)。
  • 3D姿态估计:使用 configs/body_3d_keypoint 下的3D模型配置。

通过Python调用MMPose,可快速实现从简单图像处理到复杂视频分析的人体姿态识别任务。

发表在 Media Pipe | MMPose实现已关闭评论

Final IK介绍

Final IK 是 Unity 中一款强大的逆向运动学(IK)插件,用于实现角色骨骼的自然运动。以下是其核心用法详解:


1. 安装与基础设置

  • 安装:通过 Unity Asset Store 导入 Final IK 资源包。
  • 创建 IK 解算器
    1. 在角色骨骼链的末端(如手腕)创建空物体作为 目标(Target)
    2. 选中骨骼链的根节点(如肩膀),添加对应的 IK 组件(如 CCDIKFABRIK)。
    3. 将 Target 拖入解算器的 Target 字段,并指定 Effector(末端效应器)。

2. 常用解算器类型与配置

CCD IK (循环坐标下降)

  • 适用场景:手臂、触手等链状结构。
  • 设置
    • 指定骨骼链的 Bones 数组(从根到末端依次添加)。
    • 调整 Iterations(迭代次数,通常 5-10)和 Tolerance(容差,如 0.001)

FABRIK

  • 特点:更高效处理长链,避免关节扭曲。
  • 参数:类似 CCD IK,但通常需要设置 Pole(用于控制关节弯曲方向)。

LookAt IK

  • 用途:头部/眼睛跟随目标。
  • 配置
    • 添加 LookAtIK 组件,指定 spinehead 等骨骼。
    • 设置 TargetEye Weight(眼睛权重)。

Limb IK

  • 适用:手臂/腿部关节(如肘部、膝盖)。
  • 设置
    • 指定 Goal(目标位置)和 Bend Goal(弯曲方向参考点)。
    • 调整 Position/Rotation Weight 控制权重。

3. Grounder 功能(脚部贴合地面)

  • 作用:自动调整脚部位置以适应地形。
  • 步骤
    1. 添加 Grounder 组件(如 GrounderFBBIK)。
    2. 指定脚部效应器(Feet 数组)和地面检测的 Raycasters
    3. 调整 Pelvis Speed 控制骨盆移动平滑度。

4. 与动画系统集成

  • Animator 控制
    • 在 Animator Controller 中,通过 SetIKPositionWeight 控制 IK 权重。
      animator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1.0f);
      animator.SetIKPosition(AvatarIKGoal.RightHand, target.position);
  • 层权重:使用不同动画层混合 IK 与关键帧动画。

5. 示例:实现手部跟随移动目标

  1. 为角色右手添加 CCDIK 组件。
  2. 创建空物体 HandTarget 并拖入解算器的 Target
  3. Update 中更新目标位置:
    void Update() {
       CCDIK ik = GetComponent<CCDIK>();
       ik.solver.IKPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
    }

6. 常见问题

  • 抖动/不稳定:增加 Iterations 或降低 Tolerance
  • 骨骼翻转:设置 Pole 方向或调整 Bend Goal
  • 性能优化:减少高频率的 IK 计算,或限制骨骼链长度。

7. 学习资源推荐

  • 官方文档:查看 FinalIK/Documentation 文件夹。
  • 示例场景:参考 FinalIK/Samples 中的预制体。
  • 视频教程:YouTube 搜索 “Final IK Tutorial”。

通过以上步骤,你能够快速掌握 Final IK 的核心功能,实现复杂的角色动画效果!

发表在 Media Pipe | Final IK介绍已关闭评论

简易版log程序

以下是一个简易的Unity日志工具实现,包含分类显示、基础过滤和手机摇动触发功能:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;

public enum LogTypeFilter
{
    Info = 1,
    Warning = 2,
    Error = 4,
    All = Info | Warning | Error
}

public class SimpleLogTool : MonoBehaviour
{
    private static SimpleLogTool _instance;
    public static SimpleLogTool Instance => _instance;

    [Header("UI References")]
    [SerializeField] private GameObject logPanel;
    [SerializeField] private Text logText;
    [SerializeField] private ScrollRect scrollRect;
    [SerializeField] private Toggle infoToggle;
    [SerializeField] private Toggle warningToggle;
    [SerializeField] private Toggle errorToggle;
    [SerializeField] private InputField searchInput;

    [Header("Settings")]
    [SerializeField] private bool enableShakeToShow = true;
    [SerializeField] private float shakeDetectionThreshold = 2f;
    [SerializeField] private Color infoColor = Color.white;
    [SerializeField] private Color warningColor = Color.yellow;
    [SerializeField] private Color errorColor = Color.red;

    private List<LogEntry> logEntries = new List<LogEntry>();
    private LogTypeFilter currentFilter = LogTypeFilter.All;
    private string searchKeyword = "";
    private Vector3 acceleration;

    private void Awake()
    {
        if (_instance != null && _instance != this)
        {
            Destroy(gameObject);
            return;
        }
        _instance = this;

        Application.logMessageReceived += HandleLog;
    }

    private void Update()
    {
        if (enableShakeToShow && Input.acceleration.sqrMagnitude >= shakeDetectionThreshold)
        {
            ToggleLogPanel();
        }
    }

    private void HandleLog(string logString, string stackTrace, LogType type)
    {
        LogEntry entry = new LogEntry
        {
            content = logString,
            type = ConvertToLogType(type),
            timestamp = System.DateTime.Now.ToString("HH:mm:ss")
        };

        logEntries.Add(entry);
        UpdateLogDisplay();
    }

    private LogTypeFilter ConvertToLogType(LogType unityLogType)
    {
        switch (unityLogType)
        {
            case LogType.Warning:
                return LogTypeFilter.Warning;
            case LogType.Error:
            case LogType.Assert:
            case LogType.Exception:
                return LogTypeFilter.Error;
            default:
                return LogTypeFilter.Info;
        }
    }

    public void ToggleLogPanel()
    {
        logPanel.SetActive(!logPanel.activeSelf);
        if (logPanel.activeSelf)
        {
            UpdateLogDisplay();
        }
    }

    public void OnFilterChanged()
    {
        currentFilter = (LogTypeFilter)(
            (infoToggle.isOn ? (int)LogTypeFilter.Info : 0) |
            (warningToggle.isOn ? (int)LogTypeFilter.Warning : 0) |
            (errorToggle.isOn ? (int)LogTypeFilter.Error : 0)
        );
        UpdateLogDisplay();
    }

    public void OnSearchInputChanged()
    {
        searchKeyword = searchInput.text.ToLower();
        UpdateLogDisplay();
    }

    public void ClearLogs()
    {
        logEntries.Clear();
        UpdateLogDisplay();
    }

    private void UpdateLogDisplay()
    {
        System.Text.StringBuilder sb = new System.Text.StringBuilder();
        foreach (var entry in logEntries)
        {
            if ((currentFilter & entry.type) == 0) continue;
            if (!string.IsNullOrEmpty(searchKeyword) && 
                !entry.content.ToLower().Contains(searchKeyword)) continue;

            sb.AppendLine($"<color={GetColorHex(entry.type)}>[{entry.type}] {entry.timestamp} - {entry.content}</color>");
        }
        logText.text = sb.ToString();
        Canvas.ForceUpdateCanvases();
        scrollRect.verticalNormalizedPosition = 0;
    }

    private string GetColorHex(LogTypeFilter type)
    {
        switch (type)
        {
            case LogTypeFilter.Info: return $"#{ColorUtility.ToHtmlStringRGB(infoColor)}";
            case LogTypeFilter.Warning: return $"#{ColorUtility.ToHtmlStringRGB(warningColor)}";
            case LogTypeFilter.Error: return $"#{ColorUtility.ToHtmlStringRGB(errorColor)}";
            default: return "white";
        }
    }

    private struct LogEntry
    {
        public string content;
        public LogTypeFilter type;
        public string timestamp;
    }
}

使用步骤:

  1. 创建一个Canvas并添加以下UI元素:

    • Panel(作为日志窗口)
    • Scroll View包含Text用于显示日志
    • 三个Toggle分别对应Info/Warning/Error过滤
    • InputField用于搜索
    • 一个清除按钮
  2. 将脚本挂载到场景中的空物体上

  3. 在Inspector中关联各个UI引用

功能特点:

  • 自动捕获Unity日志(包括异常)
  • 分类显示(Info/Warning/Error)
  • 颜色区分不同类型日志
  • 关键词搜索过滤
  • 摇动手机切换显示/隐藏(移动端)
  • 实时更新日志显示
  • 手动清除日志功能
  • 类型过滤组合功能

扩展建议:

  1. 添加日志上限限制防止内存溢出
  2. 增加日志导出功能
  3. 添加对象池优化大量日志的性能
  4. 增加堆栈跟踪显示
  5. 添加日志文件保存功能
  6. 增加屏幕日志打印功能(类似手机端的悬浮窗)

注意事项:

  1. 需要在Unity中设置好UI层级关系
  2. 根据项目需求调整颜色和阈值参数
  3. 移动端使用时建议适当调高shakeDetectionThreshold值
  4. 长时间运行建议添加日志数量限制

这个工具可以帮助开发者在真机调试时快速查看日志信息,并通过过滤功能快速定位问题,比Unity默认的Console窗口更适合移动端调试使用。

发表在 工具 | 简易版log程序已关闭评论

手势识别转TUIO

  1. TUIO发送脚本 TUIO_Sender.cs
using UnityEngine;
using System;
using System.Net;
using System.Net.Sockets;
using System.Collections.Generic;
using System.Text;

public class TUIO_Sender : MonoBehaviour
{
    [Header("Network Settings")]
    public int receivePort = 5005;
    public string sendIP = "127.0.0.1";
    public int sendPort = 3333;

    [Header("TUIO Settings")]
    public float updateRate = 60f;
    public float clickThreshold = 0.05f;

    private UdpClient receiver;
    private UdpClient sender;
    private Thread receiveThread;
    private Dictionary<string, HandCursor> hands = new Dictionary<string, HandCursor>();
    private float timeSinceLastUpdate;

    class HandCursor
    {
        public uint sessionId;
        public Vector2 position;
        public bool isLeftClick;
        public bool isRightClick;
        public float lastUpdate;
    }

    void Start()
    {
        InitializeNetwork();
    }

    void InitializeNetwork()
    {
        receiver = new UdpClient(receivePort);
        sender = new UdpClient();
        sender.Connect(sendIP, sendPort);

        receiveThread = new Thread(ReceiveData);
        receiveThread.IsBackground = true;
        receiveThread.Start();
    }

    void ReceiveData()
    {
        while (true)
        {
            try
            {
                IPEndPoint anyIP = new IPEndPoint(IPAddress.Any, 0);
                byte[] data = receiver.Receive(ref anyIP);
                ProcessHandData(data);
            }
            catch (Exception e)
            {
                Debug.LogError($"接收错误: {e.Message}");
            }
        }
    }

    void ProcessHandData(byte[] data)
    {
        string json = Encoding.UTF8.GetString(data);
        var handData = JsonUtility.FromJson<HandData>(json);

        foreach (var hand in handData.hands)
        {
            string key = $"{hand.handedness}_{hand.hand_index}";

            if (!hands.ContainsKey(key))
            {
                hands[key] = new HandCursor
                {
                    sessionId = (uint)hands.Count + 1,
                    position = GetPalmPosition(hand)
                };
            }

            var cursor = hands[key];
            cursor.position = GetPalmPosition(hand);
            cursor.isLeftClick = CheckPinch(hand);
            cursor.isRightClick = CheckFist(hand);
            cursor.lastUpdate = Time.time;
        }
    }

    Vector2 GetPalmPosition(HandInfo hand)
    {
        var palm = hand.landmarks[0];
        return new Vector2(palm.x, 1 - palm.y);
    }

    bool CheckPinch(HandInfo hand)
    {
        var thumb = hand.landmarks[4];
        var index = hand.landmarks[8];
        return Vector2.Distance(new Vector2(thumb.x, thumb.y), 
                              new Vector2(index.x, index.y)) < clickThreshold;
    }

    bool CheckFist(HandInfo hand)
    {
        var palm = hand.landmarks[0];
        return Vector2.Distance(palm, hand.landmarks[8]) < clickThreshold * 1.5f &&
               Vector2.Distance(palm, hand.landmarks[12]) < clickThreshold * 1.5f;
    }

    void Update()
    {
        timeSinceLastUpdate += Time.deltaTime;
        if (timeSinceLastUpdate >= 1f / updateRate)
        {
            SendTUIOData();
            CleanupOldHands();
            timeSinceLastUpdate = 0f;
        }
    }

    void SendTUIOData()
    {
        var bundle = new List<byte>();
        bundle.AddRange(Encoding.ASCII.GetBytes("#bundle "));
        bundle.AddRange(BitConverter.GetBytes(DateTime.Now.Ticks));

        // Alive message
        var aliveData = EncodeOSC("/tuio/2Dcur", "alive", hands.Values);
        bundle.AddRange(BitConverter.GetBytes(aliveData.Length));
        bundle.AddRange(aliveData);

        // Set messages
        foreach (var hand in hands.Values)
        {
            var setData = EncodeOSC("/tuio/2Dcur", "set", hand);
            bundle.AddRange(BitConverter.GetBytes(setData.Length));
            bundle.AddRange(setData);
        }

        // Click events
        foreach (var hand in hands.Values)
        {
            if (hand.isLeftClick)
                SendClickEvent(hand, "left");
            if (hand.isRightClick)
                SendClickEvent(hand, "right");
        }

        sender.Send(bundle.ToArray(), bundle.Length);
    }

    byte[] EncodeOSC(string address, string cmd, IEnumerable<HandCursor> cursors)
    {
        // OSC encoding implementation
        // ...
    }

    void SendClickEvent(HandCursor hand, string clickType)
    {
        var clickData = Encoding.UTF8.GetBytes($"{clickType},{hand.sessionId},{hand.position.x},{hand.position.y}");
        sender.Send(clickData, clickData.Length);
    }

    void CleanupOldHands()
    {
        var expired = new List<string>();
        foreach (var pair in hands)
            if (Time.time - pair.Value.lastUpdate > 0.5f)
                expired.Add(pair.Key);

        foreach (var key in expired)
            hands.Remove(key);
    }

    void OnDestroy()
    {
        receiver.Close();
        sender.Close();
        receiveThread.Abort();
    }

    [System.Serializable]
    class HandData
    {
        public List<HandInfo> hands;
    }

    [System.Serializable]
    class HandInfo
    {
        public int hand_index;
        public string handedness;
        public List<Landmark> landmarks;
    }

    [System.Serializable]
    class Landmark
    {
        public float x;
        public float y;
    }
}
  1. TUIO可视化脚本 TUIO_Visualizer.cs
using UnityEngine;
using System;
using System.Net;
using System.Net.Sockets;
using System.Collections.Generic;
using System.Threading;

public class TUIO_Visualizer : MonoBehaviour
{
    public int listenPort = 3333;
    public GameObject cursorPrefab;
    public Color normalColor = Color.blue;
    public Color clickColor = Color.red;

    private UdpClient receiver;
    private Thread receiveThread;
    private Dictionary<uint, CursorObject> cursors = new Dictionary<uint, CursorObject>();
    private Queue<Action> mainThreadActions = new Queue<Action>();

    class CursorObject
    {
        public GameObject obj;
        public Renderer renderer;
        public Vector2 position;
        public float lastUpdate;
    }

    void Start()
    {
        InitializeReceiver();
    }

    void InitializeReceiver()
    {
        receiver = new UdpClient(listenPort);
        receiveThread = new Thread(ReceiveData);
        receiveThread.IsBackground = true;
        receiveThread.Start();
    }

    void ReceiveData()
    {
        while (true)
        {
            try
            {
                IPEndPoint anyIP = new IPEndPoint(IPAddress.Any, 0);
                byte[] data = receiver.Receive(ref anyIP);
                ProcessTUIOData(data);
            }
            catch (Exception e)
            {
                Debug.LogError($"TUIO接收错误: {e.Message}");
            }
        }
    }

    void ProcessTUIOData(byte[] data)
    {
        // 解析TUIO Bundle数据
        // 这里需要实现OSC协议解析器
        // 伪代码示例:
        var cursorId = BitConverter.ToUInt32(data, 16);
        var posX = BitConverter.ToSingle(data, 20);
        var posY = BitConverter.ToSingle(data, 24);

        mainThreadActions.Enqueue(() => {
            UpdateCursor(cursorId, new Vector2(posX, posY));
        });
    }

    void UpdateCursor(uint id, Vector2 position)
    {
        if (!cursors.ContainsKey(id))
        {
            var newObj = Instantiate(cursorPrefab);
            cursors[id] = new CursorObject
            {
                obj = newObj,
                renderer = newObj.GetComponent<Renderer>(),
                position = position
            };
        }

        var cursor = cursors[id];
        cursor.position = position;
        cursor.obj.transform.position = Camera.main.ViewportToWorldPoint(
            new Vector3(position.x, position.y, 10f));
        cursor.lastUpdate = Time.time;
    }

    void Update()
    {
        // 执行主线程操作
        while (mainThreadActions.Count > 0)
            mainThreadActions.Dequeue().Invoke();

        // 清理过期光标
        CleanupExpiredCursors();
    }

    void CleanupExpiredCursors()
    {
        var expired = new List<uint>();
        foreach (var pair in cursors)
            if (Time.time - pair.Value.lastUpdate > 1f)
                expired.Add(pair.Key);

        foreach (var id in expired)
        {
            Destroy(cursors[id].obj);
            cursors.Remove(id);
        }
    }

    void OnDestroy()
    {
        receiver.Close();
        receiveThread.Abort();
    }
}
  1. Cube控制脚本 CubeController.cs
using UnityEngine;
using System.Collections.Generic;

public class CubeController : MonoBehaviour
{
    [Header("Control Settings")]
    public float moveSpeed = 5f;
    public float rotateSpeed = 90f;
    public float maxHeight = 3f;
    public float minHeight = 0.5f;

    [Header("TUIO Settings")]
    public TUIO_Visualizer tuioVisualizer;

    void Update()
    {
        if (tuioVisualizer == null) return;

        foreach (var cursor in tuioVisualizer.cursors.Values)
        {
            Vector3 worldPos = Camera.main.ViewportToWorldPoint(
                new Vector3(cursor.position.x, cursor.position.y, 10f));

            // 左右移动控制
            if (cursor.position.x < 0.5f)
                MoveLeft(worldPos);
            else
                MoveRight(worldPos);

            // 高度控制
            ControlHeight(cursor.position.y);
        }
    }

    void MoveLeft(Vector3 position)
    {
        float delta = position.x - transform.position.x;
        transform.Translate(Vector3.left * delta * moveSpeed * Time.deltaTime);
    }

    void MoveRight(Vector3 position)
    {
        float delta = position.x - transform.position.x;
        transform.Translate(Vector3.right * delta * moveSpeed * Time.deltaTime);
    }

    void ControlHeight(float inputY)
    {
        float targetY = Mathf.Lerp(minHeight, maxHeight, inputY);
        Vector3 newPos = new Vector3(
            transform.position.x,
            Mathf.Lerp(transform.position.y, targetY, 0.1f),
            transform.position.z
        );
        transform.position = newPos;
    }

    public void OnTUIOClick(string clickType, Vector2 position)
    {
        if (clickType == "right")
        {
            transform.Rotate(Vector3.up, rotateSpeed * Time.deltaTime);
        }
    }
}

场景配置步骤 TUIO发送器配置:

创建空对象TUIO_Sender

添加TUIO_Sender脚本

设置接收端口(Python发送端口)和发送地址

可视化控制器配置:

创建空对象TUIO_Visualizer

添加TUIO_Visualizer脚本

指定光标预制体(建议使用带球体碰撞器的小球预制体)

设置监听端口与发送器一致

Cube控制器配置:

创建Cube对象

添加CubeController脚本

将TUIO_Visualizer组件拖拽到脚本引用字段

预制体创建:

创建CursorPrefab预制体:

使用Sphere对象

添加材质和颜色

添加碰撞器组件

协议数据流说明

sequenceDiagram
    participant Python as Python手势程序
    participant Sender as TUIO发送器
    participant Visualizer as TUIO可视化器
    participant Cube as Cube控制器

    Python->>Sender: 手势数据(JSON)
    Sender->>Visualizer: TUIO协议数据(UDP)
    Visualizer->>Cube: 转换后的3D坐标
    Note right of Cube: 根据坐标控制移动/旋转

高级功能扩展建议 坐标校准系统:

public class CalibrationSystem : MonoBehaviour
{
    public Transform[] calibrationPoints;
    private Vector2[] screenPoints;

    void StartCalibration()
    {
        screenPoints = new Vector2[calibrationPoints.Length];
        for (int i = 0; i < calibrationPoints.Length; i++)
        {
            screenPoints[i] = Camera.main.WorldToViewportPoint(
                calibrationPoints[i].position);
        }
    }

    public Vector2 CalibratePosition(Vector2 rawPos)
    {
        // 实现双线性插值校准
        // ...
        return calibratedPos;
    }
}

手势组合识别:

bool CheckTwoHandGesture()
{
    if (tuioVisualizer.cursors.Count != 2) return false;

    var cursors = tuioVisualizer.cursors.Values.ToList();
    float distance = Vector3.Distance(
        cursors[0].obj.transform.position,
        cursors[1].obj.transform.position);

    return distance < 1f; // 双手靠近触发特殊操作
}

网络延迟补偿:

void UpdateCursorPosition()
{
    // 使用速度预测未来位置
    Vector3 predictedPos = currentPos + velocity * estimatedLatency;
    // 使用插值平滑移动
    transform.position = Vector3.Lerp(transform.position, predictedPos, 0.2f);
}

此实现方案提供了完整的端到端手势控制解决方案,具有以下特点:

模块化设计:三个脚本各司其职,便于维护和扩展

网络优化:使用独立线程处理网络通信

可视化调试:实时显示TUIO数据点

手势反馈:支持点击事件识别

物理映射:将2D手势自然映射到3D空间控制

实际部署时建议根据具体硬件性能调整更新频率和移动插值参数,并通过校准流程确保坐标映射的准确性。

发表在 Media Pipe | 手势识别转TUIO已关闭评论

Hello world!

Welcome to WordPress. This is your first post. Edit or delete it, then start writing!

发表在 Uncategorized | 留下评论