第47节:机器学习:3D姿态估计与动画驱动

概述

机器学习正在革命性地改变3D交互方式。本节将探索如何集成TensorFlow.js进行实时3D姿态估计,并使用估计结果驱动Three.js中的骨骼动画,实现从2D视频到3D角色的自然映射。

ML-3D系统架构:
摄像头输入 姿态估计模型 2D关键点检测 3D姿态重建 骨骼映射 动画驱动 运动重定向 3D角色动画 实时渲染 动作捕捉

核心原理

姿态估计算法对比

模型 精度 速度 3D支持 适用场景
PoseNet 中等 实时应用
MoveNet 很快 运动分析
MediaPipe Pose 很高 专业应用
BlazePose 极高 中等 医疗健身

骨骼映射原理

javascript 复制代码
// 关键点映射配置
class PoseMapper {
    static BODY_CONNECTIONS = [
        // 身体主干
        ['left_shoulder', 'right_shoulder'],
        ['left_shoulder', 'left_hip'],
        ['right_shoulder', 'right_hip'],
        ['left_hip', 'right_hip'],
        
        // 左臂
        ['left_shoulder', 'left_elbow'],
        ['left_elbow', 'left_wrist'],
        
        // 右臂
        ['right_shoulder', 'right_elbow'],
        ['right_elbow', 'right_wrist'],
        
        // 左腿
        ['left_hip', 'left_knee'],
        ['left_knee', 'left_ankle'],
        
        // 右腿
        ['right_hip', 'right_knee'],
        ['right_knee', 'right_ankle']
    ];

    static mapToBoneRotations(keypoints) {
        const rotations = {};
        
        for (const [start, end] of this.BODY_CONNECTIONS) {
            const startPoint = keypoints.find(k => k.name === start);
            const endPoint = keypoints.find(k => k.name === end);
            
            if (startPoint && endPoint) {
                const direction = new THREE.Vector3()
                    .subVectors(endPoint.position, startPoint.position)
                    .normalize();
                
                rotations[`${start}_to_${end}`] = this.vectorToQuaternion(direction);
            }
        }
        
        return rotations;
    }
    
    static vectorToQuaternion(direction) {
        // 将方向向量转换为四元数
        const up = new THREE.Vector3(0, 1, 0);
        const quaternion = new THREE.Quaternion();
        quaternion.setFromUnitVectors(up, direction);
        return quaternion;
    }
}

完整代码实现

实时姿态估计系统

vue 复制代码
<template>
  <div class="pose-estimation-container">
    <!-- 视频输入和3D输出 -->
    <div class="main-view">
      <!-- 视频源 -->
      <div class="video-section">
        <video ref="videoElement" 
               class="video-feed" 
               autoplay 
               playsinline
               @loadedmetadata="onVideoReady">
        </video>
        <canvas ref="poseCanvas" class="pose-overlay"></canvas>
        
        <!-- 视频控制 -->
        <div class="video-controls">
          <button @click="toggleCamera" class="control-button">
            {{ isCameraActive ? '📷 停止' : '📷 开始' }}
          </button>
          <button @click="toggleMirror" class="control-button">
            {{ mirrorMode ? '🔁 关闭镜像' : '🔁 开启镜像' }}
          </button>
          <button @click="takeSnapshot" class="control-button">
            📸 截图
          </button>
        </div>
      </div>
      
      <!-- 3D角色 -->
      <div class="character-section">
        <canvas ref="characterCanvas" class="character-canvas"></canvas>
        
        <!-- 角色控制 -->
        <div class="character-controls">
          <button @click="toggleCharacter" class="control-button">
            {{ showCharacter ? '👤 隐藏角色' : '👤 显示角色' }}
          </button>
          <button @click="resetPose" class="control-button">
            🔄 重置姿势
          </button>
          <button @click="calibratePose" class="control-button">
            🎯 校准
          </button>
        </div>
      </div>
    </div>

    <!-- 控制面板 -->
    <div class="control-panel">
      <div class="panel-section">
        <h3>🤖 模型设置</h3>
        
        <div class="model-config">
          <div class="config-group">
            <label>姿态估计模型</label>
            <select v-model="selectedModel" @change="loadModel">
              <option value="movenet">MoveNet (轻量级)</option>
              <option value="posenet">PoseNet (平衡)</option>
              <option value="blazepose">BlazePose (高精度)</option>
            </select>
          </div>
          
          <div class="config-group">
            <label>模型复杂度</label>
            <select v-model="modelComplexity">
              <option value="lite">Lite (最快)</option>
              <option value="full">Full (平衡)</option>
              <option value="heavy">Heavy (最准)</option>
            </select>
          </div>
          
          <div class="config-group">
            <label>检测置信度</label>
            <input type="range" v-model="minPoseConfidence" min="0.1" max="1" step="0.1">
            <span>{{ minPoseConfidence }}</span>
          </div>
          
          <div class="config-group">
            <label>关键点置信度</label>
            <input type="range" v-model="minKeypointConfidence" min="0.1" max="1" step="0.1">
            <span>{{ minKeypointConfidence }}</span>
          </div>
        </div>
      </div>

      <div class="panel-section">
        <h3>🎭 角色设置</h3>
        
        <div class="character-config">
          <div class="config-group">
            <label>角色类型</label>
            <select v-model="characterType" @change="loadCharacter">
              <option value="stickman">简笔画人</option>
              <option value="humanoid">人形角色</option>
              <option value="robot">机器人</option>
              <option value="custom">自定义</option>
            </select>
          </div>
          
          <div class="config-group">
            <label>骨骼粗细</label>
            <input type="range" v-model="boneThickness" min="0.1" max="2" step="0.1">
            <span>{{ boneThickness }}</span>
          </div>
          
          <div class="config-group">
            <label>关节大小</label>
            <input type="range" v-model="jointSize" min="0.1" max="1" step="0.1">
            <span>{{ jointSize }}</span>
          </div>
          
          <div class="config-group">
            <label>平滑系数</label>
            <input type="range" v-model="smoothingFactor" min="0" max="1" step="0.1">
            <span>{{ smoothingFactor }}</span>
          </div>
        </div>
      </div>

      <div class="panel-section">
        <h3>📊 性能监控</h3>
        
        <div class="performance-monitor">
          <div class="performance-item">
            <span>检测帧率:</span>
            <span>{{ detectionFPS }} FPS</span>
          </div>
          <div class="performance-item">
            <span>渲染帧率:</span>
            <span>{{ renderFPS }} FPS</span>
          </div>
          <div class="performance-item">
            <span>检测延迟:</span>
            <span>{{ detectionTime }}ms</span>
          </div>
          <div class="performance-item">
            <span>关键点数量:</span>
            <span>{{ keypointCount }}</span>
          </div>
          <div class="performance-item">
            <span>跟踪质量:</span>
            <span :class="trackingQualityClass">{{ trackingQuality }}</span>
          </div>
        </div>
      </div>

      <div class="panel-section">
        <h3>🎮 动作捕捉</h3>
        
        <div class="mocap-controls">
          <div class="control-group">
            <label>
              <input type="checkbox" v-model="enableRecording">
              录制动作
            </label>
          </div>
          
          <div class="control-group">
            <label>
              <input type="checkbox" v-model="enablePlayback">
              回放动作
            </label>
          </div>
          
          <div class="recording-controls" v-if="enableRecording">
            <button @click="startRecording" class="control-button" :disabled="isRecording">
              ● 开始录制
            </button>
            <button @click="stopRecording" class="control-button" :disabled="!isRecording">
              ■ 停止录制
            </button>
            <button @click="saveRecording" class="control-button" :disabled="!recordedData.length">
              💾 保存数据
            </button>
          </div>
          
          <div class="playback-controls" v-if="enablePlayback">
            <button @click="loadRecording" class="control-button">
              📁 加载数据
            </button>
            <button @click="playRecording" class="control-button" :disabled="isPlaying">
              ▶️ 播放
            </button>
            <button @click="pauseRecording" class="control-button" :disabled="!isPlaying">
              ⏸️ 暂停
            </button>
          </div>
        </div>
      </div>

      <div class="panel-section">
        <h3>🔧 高级设置</h3>
        
        <div class="advanced-settings">
          <div class="setting-group">
            <label>
              <input type="checkbox" v-model="enable3DReconstruction">
              3D姿态重建
            </label>
          </div>
          
          <div class="setting-group">
            <label>
              <input type="checkbox" v-model="enableSkeletonSmoothing">
              骨骼平滑
            </label>
          </div>
          
          <div class="setting-group">
            <label>
              <input type="checkbox" v-model="showKeypoints">
              显示关键点
            </label>
          </div>
          
          <div class="setting-group">
            <label>
              <input type="checkbox" v-model="showSkeleton">
              显示骨骼
            </label>
          </div>
          
          <div class="setting-group">
            <label>镜像模式</label>
            <select v-model="mirrorMode">
              <option value="none">无镜像</option>
              <option value="horizontal">水平镜像</option>
              <option value="vertical">垂直镜像</option>
            </select>
          </div>
        </div>
      </div>
    </div>

    <!-- 关键点信息面板 -->
    <div v-if="showKeypointPanel" class="keypoint-panel">
      <div class="panel-header">
        <h4>关键点信息</h4>
        <button @click="showKeypointPanel = false" class="close-button">×</button>
      </div>
      <div class="keypoint-list">
        <div v-for="keypoint in currentKeypoints" 
             :key="keypoint.name"
             :class="['keypoint-item', { active: keypoint.score >= minKeypointConfidence }]">
          <span class="keypoint-name">{{ keypoint.name }}</span>
          <span class="keypoint-score">{{ (keypoint.score * 100).toFixed(1) }}%</span>
          <span class="keypoint-position">
            ({{ keypoint.position.x.toFixed(1) }}, {{ keypoint.position.y.toFixed(1) }})
          </span>
        </div>
      </div>
    </div>

    <!-- 加载状态 -->
    <div v-if="isLoading" class="loading-overlay">
      <div class="loading-content">
        <div class="spinner"></div>
        <h3>加载模型中...</h3>
        <p>{{ loadingProgress }}%</p>
      </div>
    </div>
  </div>
</template>

<script>
import { onMounted, onUnmounted, ref, reactive, computed } from 'vue';
import * as THREE from 'three';
import * as poseDetection from '@tensorflow-models/pose-detection';
import '@tensorflow/tfjs-backend-webgl';

// 姿态估计管理器
class PoseEstimationManager {
  constructor() {
    this.detector = null;
    this.isInitialized = false;
    this.lastDetectionTime = 0;
  }

  async initialize(modelType = 'movenet', complexity = 'lite') {
    try {
      // 设置TensorFlow.js后端
      await tf.setBackend('webgl');
      
      // 创建检测器配置
      const detectorConfig = this.getDetectorConfig(modelType, complexity);
      
      // 加载模型
      this.detector = await poseDetection.createDetector(
        modelType === 'blazepose' ? poseDetection.SupportedModels.BlazePose : 
        modelType === 'posenet' ? poseDetection.SupportedModels.PoseNet :
        poseDetection.SupportedModels.MoveNet,
        detectorConfig
      );
      
      this.isInitialized = true;
      console.log('姿态估计模型加载成功');
    } catch (error) {
      console.error('模型加载失败:', error);
      throw error;
    }
  }

  getDetectorConfig(modelType, complexity) {
    const baseConfig = {
      modelType: complexity.toUpperCase(),
      enableSmoothing: true,
      minPoseScore: 0.25
    };

    switch (modelType) {
      case 'movenet':
        return {
          ...baseConfig,
          modelType: complexity === 'heavy' ? 'THUNDER' : 'LIGHTNING'
        };
        
      case 'blazepose':
        return {
          ...baseConfig,
          runtime: 'tfjs',
          enableSmoothing: true,
          modelType: complexity === 'heavy' ? 'full' : 'lite'
        };
        
      case 'posenet':
        return {
          ...baseConfig,
          architecture: 'MobileNetV1',
          outputStride: 16,
          inputResolution: { width: 640, height: 480 },
          multiplier: complexity === 'heavy' ? 1.0 : 0.75
        };
        
      default:
        return baseConfig;
    }
  }

  async estimatePoses(videoElement, minScore = 0.3) {
    if (!this.detector || !this.isInitialized) {
      throw new Error('检测器未初始化');
    }

    const startTime = performance.now();
    
    try {
      const poses = await this.detector.estimatePoses(videoElement, {
        maxPoses: 1,
        flipHorizontal: false
      });
      
      const detectionTime = performance.now() - startTime;
      
      // 过滤低置信度的姿态
      const validPoses = poses.filter(pose => pose.score >= minScore);
      
      return {
        poses: validPoses,
        detectionTime,
        keypointCount: validPoses.length > 0 ? validPoses[0].keypoints.length : 0
      };
    } catch (error) {
      console.error('姿态估计失败:', error);
      return { poses: [], detectionTime: 0, keypointCount: 0 };
    }
  }

  dispose() {
    if (this.detector) {
      this.detector.dispose();
    }
    this.isInitialized = false;
  }
}

// 3D角色管理器
class CharacterManager {
  constructor(renderer, scene, camera) {
    this.renderer = renderer;
    this.scene = scene;
    this.camera = camera;
    this.character = null;
    this.bones = new Map();
    this.joints = new Map();
    
    this.setupScene();
  }

  setupScene() {
    // 基础照明
    const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
    this.scene.add(ambientLight);
    
    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
    directionalLight.position.set(10, 10, 5);
    this.scene.add(directionalLight);
    
    // 网格地面
    const gridHelper = new THREE.GridHelper(10, 10);
    this.scene.add(gridHelper);
  }

  // 创建简笔画角色
  createStickFigure() {
    this.clearCharacter();
    
    const characterGroup = new THREE.Group();
    characterGroup.name = 'stick_figure';
    
    // 创建骨骼材质
    const boneMaterial = new THREE.MeshBasicMaterial({ 
      color: 0x00ff00,
      transparent: true,
      opacity: 0.8
    });
    
    const jointMaterial = new THREE.MeshBasicMaterial({ 
      color: 0xff0000,
      transparent: true,
      opacity: 0.9
    });
    
    // 定义骨骼连接
    const boneConnections = [
      // 身体主干
      { name: 'spine', start: 'hip', end: 'shoulder_center', radius: 0.1 },
      
      // 左臂
      { name: 'left_upper_arm', start: 'shoulder_center', end: 'left_elbow', radius: 0.08 },
      { name: 'left_lower_arm', start: 'left_elbow', end: 'left_wrist', radius: 0.06 },
      
      // 右臂
      { name: 'right_upper_arm', start: 'shoulder_center', end: 'right_elbow', radius: 0.08 },
      { name: 'right_lower_arm', start: 'right_elbow', end: 'right_wrist', radius: 0.06 },
      
      // 左腿
      { name: 'left_upper_leg', start: 'hip', end: 'left_knee', radius: 0.1 },
      { name: 'left_lower_leg', start: 'left_knee', end: 'left_ankle', radius: 0.08 },
      
      // 右腿
      { name: 'right_upper_leg', start: 'hip', end: 'right_knee', radius: 0.1 },
      { name: 'right_lower_leg', start: 'right_knee', end: 'right_ankle', radius: 0.08 }
    ];
    
    // 创建骨骼
    boneConnections.forEach(connection => {
      const bone = this.createBone(connection.start, connection.end, connection.radius, boneMaterial);
      characterGroup.add(bone);
      this.bones.set(connection.name, bone);
    });
    
    // 创建关节
    const jointPoints = [
      'hip', 'shoulder_center', 'left_elbow', 'left_wrist', 
      'right_elbow', 'right_wrist', 'left_knee', 'left_ankle', 
      'right_knee', 'right_ankle'
    ];
    
    jointPoints.forEach(jointName => {
      const joint = this.createJoint(jointName, 0.15, jointMaterial);
      characterGroup.add(joint);
      this.joints.set(jointName, joint);
    });
    
    this.scene.add(characterGroup);
    this.character = characterGroup;
    
    return characterGroup;
  }

  createBone(startJoint, endJoint, radius, material) {
    // 创建圆柱体作为骨骼
    const boneGeometry = new THREE.CylinderGeometry(radius, radius, 1, 8);
    const bone = new THREE.Mesh(boneGeometry, material);
    
    // 初始位置和方向
    bone.userData = { startJoint, endJoint };
    bone.rotation.order = 'YXZ';
    
    return bone;
  }

  createJoint(name, radius, material) {
    const jointGeometry = new THREE.SphereGeometry(radius, 8, 6);
    const joint = new THREE.Mesh(jointGeometry, material);
    joint.name = name;
    return joint;
  }

  // 更新角色姿态
  updatePose(keypoints, smoothing = 0.5) {
    if (!this.character || !keypoints || keypoints.length === 0) return;
    
    // 将2D关键点映射到3D空间
    const jointPositions = this.mapKeypointsTo3D(keypoints);
    
    // 更新关节位置
    for (const [jointName, position] of Object.entries(jointPositions)) {
      const joint = this.joints.get(jointName);
      if (joint) {
        // 应用平滑
        if (smoothing > 0) {
          position.lerp(joint.position, 1 - smoothing);
        }
        joint.position.copy(position);
      }
    }
    
    // 更新骨骼方向和长度
    this.updateBones();
  }

  mapKeypointsTo3D(keypoints) {
    const positions = {};
    const scale = 0.1; // 缩放因子
    
    // 关键点映射表
    const keypointMapping = {
      'nose': 'head',
      'left_shoulder': 'left_shoulder',
      'right_shoulder': 'right_shoulder', 
      'left_elbow': 'left_elbow',
      'right_elbow': 'right_elbow',
      'left_wrist': 'left_wrist',
      'right_wrist': 'right_wrist',
      'left_hip': 'left_hip',
      'right_hip': 'right_hip',
      'left_knee': 'left_knee',
      'right_knee': 'right_knee',
      'left_ankle': 'left_ankle',
      'right_ankle': 'right_ankle'
    };
    
    // 计算中心点(臀部)
    const leftHip = keypoints.find(k => k.name === 'left_hip');
    const rightHip = keypoints.find(k => k.name === 'right_hip');
    
    if (leftHip && rightHip) {
      const hipCenter = {
        x: (leftHip.x + rightHip.x) / 2,
        y: (leftHip.y + rightHip.y) / 2
      };
      
      // 设置臀部位置为原点
      positions.hip = new THREE.Vector3(0, 0, 0);
      
      // 计算其他关节的相对位置
      for (const [keypointName, jointName] of Object.entries(keypointMapping)) {
        const keypoint = keypoints.find(k => k.name === keypointName);
        if (keypoint && keypoint.score >= 0.3) {
          const x = (keypoint.x - hipCenter.x) * scale;
          const y = -(keypoint.y - hipCenter.y) * scale; // Y轴翻转
          const z = 0; // 初始深度为0
          
          positions[jointName] = new THREE.Vector3(x, y, z);
        }
      }
      
      // 计算肩部中心
      const leftShoulder = positions.left_shoulder;
      const rightShoulder = positions.right_shoulder;
      if (leftShoulder && rightShoulder) {
        positions.shoulder_center = new THREE.Vector3()
          .addVectors(leftShoulder, rightShoulder)
          .multiplyScalar(0.5);
      }
    }
    
    return positions;
  }

  updateBones() {
    for (const [boneName, bone] of this.bones) {
      const startJoint = this.joints.get(bone.userData.startJoint);
      const endJoint = this.joints.get(bone.userData.endJoint);
      
      if (startJoint && endJoint) {
        // 计算骨骼方向
        const direction = new THREE.Vector3()
          .subVectors(endJoint.position, startJoint.position);
        
        const length = direction.length();
        
        if (length > 0) {
          // 设置骨骼位置(起点和终点的中点)
          bone.position.copy(startJoint.position)
            .add(endJoint.position)
            .multiplyScalar(0.5);
          
          // 设置骨骼方向
          bone.lookAt(endJoint.position);
          
          // 调整圆柱体方向(默认朝向Y轴)
          bone.rotateX(Math.PI / 2);
          
          // 设置骨骼长度
          bone.scale.set(1, length, 1);
        }
      }
    }
  }

  clearCharacter() {
    if (this.character) {
      this.scene.remove(this.character);
      
      // 清理几何体和材质
      this.bones.forEach(bone => {
        bone.geometry.dispose();
        bone.material.dispose();
      });
      
      this.joints.forEach(joint => {
        joint.geometry.dispose();
        joint.material.dispose();
      });
      
      this.bones.clear();
      this.joints.clear();
      this.character = null;
    }
  }

  // 重置角色姿态
  resetPose() {
    this.joints.forEach(joint => {
      joint.position.set(0, 0, 0);
    });
    this.updateBones();
  }
}

// 姿态可视化器
class PoseVisualizer {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.keypointRadius = 4;
    this.skeletonColor = '#00ff00';
    this.keypointColor = '#ff0000';
  }

  drawPose(pose, videoWidth, videoHeight) {
    if (!pose || !pose.keypoints) return;
    
    // 清除画布
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    
    // 设置画布尺寸匹配视频
    this.canvas.width = videoWidth;
    this.canvas.height = videoHeight;
    
    // 绘制骨骼
    this.drawSkeleton(pose.keypoints);
    
    // 绘制关键点
    this.drawKeypoints(pose.keypoints);
  }

  drawSkeleton(keypoints) {
    const connections = [
      // 身体主干
      ['left_shoulder', 'right_shoulder'],
      ['left_shoulder', 'left_hip'],
      ['right_shoulder', 'right_hip'],
      ['left_hip', 'right_hip'],
      
      // 左臂
      ['left_shoulder', 'left_elbow'],
      ['left_elbow', 'left_wrist'],
      
      // 右臂
      ['right_shoulder', 'right_elbow'],
      ['right_elbow', 'right_wrist'],
      
      // 左腿
      ['left_hip', 'left_knee'],
      ['left_knee', 'left_ankle'],
      
      // 右腿
      ['right_hip', 'right_knee'],
      ['right_knee', 'right_ankle'],
      
      // 面部(简化)
      ['nose', 'left_eye'],
      ['nose', 'right_eye'],
      ['left_eye', 'left_ear'],
      ['right_eye', 'right_ear']
    ];

    this.ctx.strokeStyle = this.skeletonColor;
    this.ctx.lineWidth = 2;

    connections.forEach(([start, end]) => {
      const startPoint = keypoints.find(k => k.name === start);
      const endPoint = keypoints.find(k => k.name === end);

      if (startPoint && endPoint && startPoint.score > 0.3 && endPoint.score > 0.3) {
        this.ctx.beginPath();
        this.ctx.moveTo(startPoint.x, startPoint.y);
        this.ctx.lineTo(endPoint.x, endPoint.y);
        this.ctx.stroke();
      }
    });
  }

  drawKeypoints(keypoints) {
    keypoints.forEach(keypoint => {
      if (keypoint.score > 0.3) {
        // 绘制关键点
        this.ctx.fillStyle = this.keypointColor;
        this.ctx.beginPath();
        this.ctx.arc(keypoint.x, keypoint.y, this.keypointRadius, 0, 2 * Math.PI);
        this.ctx.fill();
        
        // 绘制置信度圆环
        const ringRadius = this.keypointRadius * (1 + keypoint.score);
        this.ctx.strokeStyle = `rgba(255, 255, 255, ${keypoint.score})`;
        this.ctx.lineWidth = 1;
        this.ctx.beginPath();
        this.ctx.arc(keypoint.x, keypoint.y, ringRadius, 0, 2 * Math.PI);
        this.ctx.stroke();
      }
    });
  }
}

export default {
  name: 'PoseEstimation',
  setup() {
    // 响应式状态
    const videoElement = ref(null);
    const poseCanvas = ref(null);
    const characterCanvas = ref(null);
    
    const isCameraActive = ref(false);
    const isLoading = ref(false);
    const loadingProgress = ref(0);
    const selectedModel = ref('movenet');
    const modelComplexity = ref('lite');
    const minPoseConfidence = ref(0.3);
    const minKeypointConfidence = ref(0.3);
    const characterType = ref('stickman');
    const boneThickness = ref(0.1);
    const jointSize = ref(0.15);
    const smoothingFactor = ref(0.5);
    const showCharacter = ref(true);
    const enableRecording = ref(false);
    const isRecording = ref(false);
    const enablePlayback = ref(false);
    const isPlaying = ref(false);
    const enable3DReconstruction = ref(false);
    const enableSkeletonSmoothing = ref(true);
    const showKeypoints = ref(true);
    const showSkeleton = ref(true);
    const mirrorMode = ref('horizontal');
    const showKeypointPanel = ref(false);
    
    // 性能统计
    const detectionFPS = ref(0);
    const renderFPS = ref(0);
    const detectionTime = ref(0);
    const keypointCount = ref(0);
    const trackingQuality = ref('未知');
    
    // 当前关键点数据
    const currentKeypoints = ref([]);

    // 计算属性
    const trackingQualityClass = computed(() => {
      const quality = trackingQuality.value;
      if (quality === '优秀') return 'excellent';
      if (quality === '良好') return 'good';
      if (quality === '一般') return 'fair';
      return 'poor';
    });

    // 管理器实例
    let poseManager, characterManager, poseVisualizer;
    let renderer, scene, camera;
    let animationFrameId;
    let detectionFrameCount = 0;
    let renderFrameCount = 0;
    let lastFpsUpdate = 0;
    let videoStream = null;

    // 初始化
    const init = async () => {
      isLoading.value = true;
      
      try {
        await initPoseEstimation();
        await init3DRenderer();
        initPoseVisualizer();
        
        isLoading.value = false;
      } catch (error) {
        console.error('初始化失败:', error);
        isLoading.value = false;
      }
    };

    // 初始化姿态估计
    const initPoseEstimation = async () => {
      poseManager = new PoseEstimationManager();
      loadingProgress.value = 50;
      await poseManager.initialize(selectedModel.value, modelComplexity.value);
      loadingProgress.value = 100;
    };

    // 初始化3D渲染器
    const init3DRenderer = () => {
      renderer = new THREE.WebGLRenderer({
        canvas: characterCanvas.value,
        antialias: true
      });
      
      renderer.setSize(characterCanvas.value.clientWidth, characterCanvas.value.clientHeight);
      renderer.setClearColor(0x222222);
      
      scene = new THREE.Scene();
      camera = new THREE.PerspectiveCamera(75, 
        characterCanvas.value.clientWidth / characterCanvas.value.clientHeight, 
        0.1, 1000
      );
      
      camera.position.set(0, 0, 5);
      camera.lookAt(0, 0, 0);
      
      characterManager = new CharacterManager(renderer, scene, camera);
      characterManager.createStickFigure();
    };

    // 初始化姿态可视化
    const initPoseVisualizer = () => {
      poseVisualizer = new PoseVisualizer(poseCanvas.value);
    };

    // 切换摄像头
    const toggleCamera = async () => {
      if (isCameraActive.value) {
        stopCamera();
      } else {
        await startCamera();
      }
    };

    // 启动摄像头
    const startCamera = async () => {
      try {
        const constraints = {
          video: {
            width: { ideal: 640 },
            height: { ideal: 480 },
            facingMode: 'user'
          }
        };
        
        videoStream = await navigator.mediaDevices.getUserMedia(constraints);
        videoElement.value.srcObject = videoStream;
        isCameraActive.value = true;
        
        // 开始姿态估计循环
        startPoseEstimationLoop();
        
      } catch (error) {
        console.error('摄像头访问失败:', error);
        alert('无法访问摄像头,请检查权限设置');
      }
    };

    // 停止摄像头
    const stopCamera = () => {
      if (videoStream) {
        videoStream.getTracks().forEach(track => track.stop());
        videoStream = null;
      }
      
      videoElement.value.srcObject = null;
      isCameraActive.value = false;
      
      if (animationFrameId) {
        cancelAnimationFrame(animationFrameId);
      }
    };

    // 视频准备就绪
    const onVideoReady = () => {
      // 设置画布尺寸匹配视频
      if (poseCanvas.value) {
        poseCanvas.value.width = videoElement.value.videoWidth;
        poseCanvas.value.height = videoElement.value.videoHeight;
      }
    };

    // 开始姿态估计循环
    const startPoseEstimationLoop = () => {
      const estimatePose = async () => {
        if (!isCameraActive.value) return;
        
        try {
          // 估计姿态
          const result = await poseManager.estimatePoses(
            videoElement.value, 
            minPoseConfidence.value
          );
          
          if (result.poses.length > 0) {
            const pose = result.poses[0];
            
            // 更新关键点数据
            currentKeypoints.value = pose.keypoints.map(kp => ({
              name: kp.name,
              score: kp.score,
              position: { x: kp.x, y: kp.y }
            }));
            
            // 可视化姿态
            if (showSkeleton.value || showKeypoints.value) {
              poseVisualizer.drawPose(pose, 
                videoElement.value.videoWidth, 
                videoElement.value.videoHeight
              );
            }
            
            // 更新3D角色
            if (showCharacter.value) {
              characterManager.updatePose(pose.keypoints, smoothingFactor.value);
            }
            
            // 更新性能统计
            detectionTime.value = result.detectionTime.toFixed(1);
            keypointCount.value = result.keypointCount;
            detectionFrameCount++;
          }
          
          // 更新跟踪质量
          updateTrackingQuality(result);
          
        } catch (error) {
          console.error('姿态估计错误:', error);
        }
        
        // 继续下一帧
        animationFrameId = requestAnimationFrame(estimatePose);
      };
      
      estimatePose();
    };

    // 更新跟踪质量
    const updateTrackingQuality = (result) => {
      if (result.poses.length === 0) {
        trackingQuality.value = '无检测';
      } else {
        const pose = result.poses[0];
        const visibleKeypoints = pose.keypoints.filter(kp => kp.score > minKeypointConfidence.value).length;
        const totalKeypoints = pose.keypoints.length;
        const visibilityRatio = visibleKeypoints / totalKeypoints;
        
        if (visibilityRatio > 0.8) trackingQuality.value = '优秀';
        else if (visibilityRatio > 0.6) trackingQuality.value = '良好';
        else if (visibilityRatio > 0.4) trackingQuality.value = '一般';
        else trackingQuality.value = '较差';
      }
    };

    // 切换镜像模式
    const toggleMirror = () => {
      mirrorMode.value = mirrorMode.value === 'horizontal' ? 'none' : 'horizontal';
      // 实际实现需要更新可视化
    };

    // 截图
    const takeSnapshot = () => {
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');
      
      canvas.width = videoElement.value.videoWidth;
      canvas.height = videoElement.value.videoHeight;
      
      // 绘制视频帧
      ctx.drawImage(videoElement.value, 0, 0);
      
      // 绘制姿态覆盖
      if (poseCanvas.value) {
        ctx.drawImage(poseCanvas.value, 0, 0);
      }
      
      // 创建下载链接
      const link = document.createElement('a');
      link.download = `pose-snapshot-${Date.now()}.png`;
      link.href = canvas.toDataURL();
      link.click();
    };

    // 切换角色显示
    const toggleCharacter = () => {
      showCharacter.value = !showCharacter.value;
      if (showCharacter.value && characterManager.character) {
        characterManager.character.visible = true;
      } else if (characterManager.character) {
        characterManager.character.visible = false;
      }
    };

    // 重置姿势
    const resetPose = () => {
      characterManager.resetPose();
    };

    // 校准姿态
    const calibratePose = () => {
      // 实现校准逻辑
      alert('校准功能开发中...');
    };

    // 加载模型
    const loadModel = async () => {
      if (poseManager) {
        poseManager.dispose();
      }
      
      await initPoseEstimation();
    };

    // 加载角色
    const loadCharacter = () => {
      if (characterManager) {
        switch (characterType.value) {
          case 'stickman':
            characterManager.createStickFigure();
            break;
          case 'humanoid':
            // 加载人形角色
            break;
          case 'robot':
            // 加载机器人角色
            break;
        }
      }
    };

    // 开始录制
    const startRecording = () => {
      isRecording.value = true;
      // 实现录制逻辑
    };

    // 停止录制
    const stopRecording = () => {
      isRecording.value = false;
    };

    // 保存录制
    const saveRecording = () => {
      // 实现保存逻辑
      alert('保存功能开发中...');
    };

    // 加载录制
    const loadRecording = () => {
      // 实现加载逻辑
      alert('加载功能开发中...');
    };

    // 播放录制
    const playRecording = () => {
      isPlaying.value = true;
      // 实现播放逻辑
    };

    // 暂停录制
    const pauseRecording = () => {
      isPlaying.value = false;
    };

    // 性能监控循环
    const startPerformanceMonitor = () => {
      const updateStats = () => {
        const now = performance.now();
        
        if (now - lastFpsUpdate >= 1000) {
          detectionFPS.value = Math.round((detectionFrameCount * 1000) / (now - lastFpsUpdate));
          renderFPS.value = Math.round((renderFrameCount * 1000) / (now - lastFpsUpdate));
          
          detectionFrameCount = 0;
          renderFrameCount = 0;
          lastFpsUpdate = now;
        }
        
        requestAnimationFrame(updateStats);
      };
      
      updateStats();
    };

    // 3D渲染循环
    const startRenderLoop = () => {
      const render = () => {
        if (showCharacter.value) {
          renderer.render(scene, camera);
          renderFrameCount++;
        }
        requestAnimationFrame(render);
      };
      render();
    };

    onMounted(() => {
      init();
      startPerformanceMonitor();
      startRenderLoop();
      window.addEventListener('resize', handleResize);
    });

    onUnmounted(() => {
      stopCamera();
      if (poseManager) {
        poseManager.dispose();
      }
      if (animationFrameId) {
        cancelAnimationFrame(animationFrameId);
      }
      window.removeEventListener('resize', handleResize);
    });

    const handleResize = () => {
      if (renderer && camera && characterCanvas.value) {
        camera.aspect = characterCanvas.value.clientWidth / characterCanvas.value.clientHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(characterCanvas.value.clientWidth, characterCanvas.value.clientHeight);
      }
    };

    return {
      // 模板引用
      videoElement,
      poseCanvas,
      characterCanvas,
      
      // 状态数据
      isCameraActive,
      isLoading,
      loadingProgress,
      selectedModel,
      modelComplexity,
      minPoseConfidence,
      minKeypointConfidence,
      characterType,
      boneThickness,
      jointSize,
      smoothingFactor,
      showCharacter,
      enableRecording,
      isRecording,
      enablePlayback,
      isPlaying,
      enable3DReconstruction,
      enableSkeletonSmoothing,
      showKeypoints,
      showSkeleton,
      mirrorMode,
      showKeypointPanel,
      detectionFPS,
      renderFPS,
      detectionTime,
      keypointCount,
      trackingQuality,
      currentKeypoints,
      
      // 计算属性
      trackingQualityClass,
      
      // 方法
      toggleCamera,
      onVideoReady,
      toggleMirror,
      takeSnapshot,
      toggleCharacter,
      resetPose,
      calibratePose,
      loadModel,
      loadCharacter,
      startRecording,
      stopRecording,
      saveRecording,
      loadRecording,
      playRecording,
      pauseRecording
    };
  }
};
</script>

<style scoped>
.pose-estimation-container {
  width: 100%;
  height: 100vh;
  display: flex;
  background: #1a1a1a;
  color: white;
  overflow: hidden;
}

.main-view {
  flex: 1;
  display: flex;
  padding: 20px;
  gap: 20px;
}

.video-section, .character-section {
  flex: 1;
  display: flex;
  flex-direction: column;
  background: #2d2d2d;
  border-radius: 8px;
  padding: 15px;
  position: relative;
}

.video-feed, .character-canvas {
  width: 100%;
  height: 400px;
  background: #000;
  border-radius: 4px;
  object-fit: cover;
}

.pose-overlay {
  position: absolute;
  top: 15px;
  left: 15px;
  width: calc(100% - 30px);
  height: 400px;
  pointer-events: none;
}

.video-controls, .character-controls {
  display: flex;
  gap: 10px;
  margin-top: 15px;
  justify-content: center;
}

.control-button {
  padding: 8px 16px;
  background: #444;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
  transition: background 0.3s;
}

.control-button:hover:not(:disabled) {
  background: #555;
}

.control-button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.control-panel {
  width: 350px;
  background: #2d2d2d;
  padding: 20px;
  overflow-y: auto;
  border-left: 1px solid #444;
}

.panel-section {
  margin-bottom: 25px;
  padding-bottom: 20px;
  border-bottom: 1px solid #444;
}

.panel-section:last-child {
  margin-bottom: 0;
  border-bottom: none;
}

.panel-section h3 {
  color: #00ff88;
  margin-bottom: 15px;
  font-size: 16px;
}

.model-config, .character-config, .performance-monitor, .mocap-controls, .advanced-settings {
  display: flex;
  flex-direction: column;
  gap: 15px;
}

.config-group, .setting-group, .control-group {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.config-group label, .setting-group label {
  color: #ccc;
  font-size: 14px;
}

.config-group select, .config-group input[type="range"] {
  padding: 8px 12px;
  background: #444;
  border: 1px solid #666;
  border-radius: 4px;
  color: white;
  font-size: 14px;
}

.config-group span {
  color: #00ff88;
  font-size: 12px;
  text-align: right;
}

.performance-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 0;
  border-bottom: 1px solid #444;
}

.performance-item:last-child {
  border-bottom: none;
}

.performance-item span:first-child {
  color: #ccc;
}

.performance-item span:last-child {
  color: #00ff88;
  font-weight: bold;
}

.performance-item .excellent { color: #48bb78; }
.performance-item .good { color: #4299e1; }
.performance-item .fair { color: #ed8936; }
.performance-item .poor { color: #f56565; }

.recording-controls, .playback-controls {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 10px;
}

.setting-group label {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
}

.setting-group input[type="checkbox"] {
  margin: 0;
}

.keypoint-panel {
  position: absolute;
  top: 20px;
  right: 370px;
  background: rgba(45, 45, 45, 0.95);
  border-radius: 8px;
  border: 1px solid #444;
  backdrop-filter: blur(10px);
  max-width: 300px;
  max-height: 400px;
  overflow: hidden;
}

.panel-header {
  padding: 15px;
  background: #1a1a1a;
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 1px solid #444;
}

.panel-header h4 {
  margin: 0;
  color: #00ff88;
}

.close-button {
  background: none;
  border: none;
  color: #ccc;
  font-size: 20px;
  cursor: pointer;
  padding: 0;
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.keypoint-list {
  padding: 15px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-height: 350px;
  overflow-y: auto;
}

.keypoint-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 6px 8px;
  background: #444;
  border-radius: 4px;
  font-size: 11px;
  border-left: 3px solid #666;
}

.keypoint-item.active {
  border-left-color: #00ff88;
}

.keypoint-name {
  font-weight: bold;
  min-width: 80px;
}

.keypoint-score {
  color: #00ff88;
  min-width: 40px;
  text-align: right;
}

.keypoint-position {
  color: #ccc;
  font-family: monospace;
  min-width: 80px;
  text-align: right;
}

.loading-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.8);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}

.loading-content {
  text-align: center;
  color: white;
}

.spinner {
  width: 50px;
  height: 50px;
  border: 4px solid rgba(255, 255, 255, 0.3);
  border-top: 4px solid #00ff88;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin: 0 auto 20px;
}

.loading-content h3 {
  margin: 0 0 10px 0;
}

.loading-content p {
  margin: 0;
  color: #00ff88;
  font-size: 18px;
  font-weight: bold;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

/* 响应式设计 */
@media (max-width: 1024px) {
  .pose-estimation-container {
    flex-direction: column;
  }
  
  .control-panel {
    width: 100%;
    height: 400px;
  }
  
  .main-view {
    height: calc(100vh - 400px);
    flex-direction: column;
  }
  
  .keypoint-panel {
    right: 20px;
  }
}

@media (max-width: 768px) {
  .main-view {
    padding: 10px;
  }
  
  .video-controls, .character-controls {
    flex-wrap: wrap;
  }
  
  .keypoint-panel {
    display: none;
  }
}
</style>

高级特性

3D姿态重建算法

javascript 复制代码
// 3D姿态重建器
class Pose3DReconstructor {
    constructor() {
        this.cameraParams = {
            focalLength: 1000,
            principalPoint: { x: 0, y: 0 }
        };
        this.skeletonModel = this.createSkeletonModel();
    }

    // 从2D关键点重建3D姿态
    reconstruct3DPose(keypoints2D) {
        const keypoints3D = {};
        
        // 使用骨骼长度约束和运动学先验
        const rootPosition = this.estimateRootPosition(keypoints2D);
        
        for (const [name, keypoint2D] of Object.entries(keypoints2D)) {
            if (keypoint2D.score > 0.3) {
                // 反投影到3D空间
                const position3D = this.backprojectTo3D(keypoint2D, rootPosition);
                keypoints3D[name] = {
                    position: position3D,
                    score: keypoint2D.score
                };
            }
        }
        
        // 应用运动学约束
        this.applyKinematicConstraints(keypoints3D);
        
        return keypoints3D;
    }

    estimateRootPosition(keypoints2D) {
        // 使用臀部关键点作为根节点
        const leftHip = keypoints2D.left_hip;
        const rightHip = keypoints2D.right_hip;
        
        if (leftHip && rightHip) {
            const hipCenter2D = {
                x: (leftHip.x + rightHip.x) / 2,
                y: (leftHip.y + rightHip.y) / 2
            };
            
            // 假设初始深度为0
            return this.backprojectTo3D(hipCenter2D, { x: 0, y: 0, z: 0 });
        }
        
        return { x: 0, y: 0, z: 0 };
    }

    backprojectTo3D(keypoint2D, referencePoint) {
        // 简化反投影:使用针孔相机模型
        const x = (keypoint2D.x - this.cameraParams.principalPoint.x) / this.cameraParams.focalLength;
        const y = (keypoint2D.y - this.cameraParams.principalPoint.y) / this.cameraParams.focalLength;
        
        // 使用参考点深度估计
        const z = referencePoint.z || 0;
        
        return { x, y, z };
    }

    applyKinematicConstraints(keypoints3D) {
        // 应用骨骼长度约束
        const boneLengths = this.estimateBoneLengths(keypoints3D);
        this.enforceBoneLengths(keypoints3D, boneLengths);
        
        // 应用关节角度限制
        this.enforceJointLimits(keypoints3D);
    }

    estimateBoneLengths(keypoints3D) {
        // 基于人体比例估计骨骼长度
        const lengths = {};
        const connections = [
            ['left_shoulder', 'left_elbow', 'upper_arm'],
            ['left_elbow', 'left_wrist', 'lower_arm'],
            ['left_hip', 'left_knee', 'upper_leg'],
            ['left_knee', 'left_ankle', 'lower_leg']
        ];
        
        connections.forEach(([start, end, name]) => {
            const startPoint = keypoints3D[start];
            const endPoint = keypoints3D[end];
            
            if (startPoint && endPoint) {
                const length = this.calculateDistance(startPoint.position, endPoint.position);
                lengths[name] = length;
            }
        });
        
        return lengths;
    }

    calculateDistance(p1, p2) {
        return Math.sqrt(
            Math.pow(p2.x - p1.x, 2) +
            Math.pow(p2.y - p1.y, 2) +
            Math.pow(p2.z - p1.z, 2)
        );
    }

    enforceBoneLengths(keypoints3D, targetLengths) {
        // 迭代调整骨骼长度
        for (const [boneName, targetLength] of Object.entries(targetLengths)) {
            const [start, end] = this.getBoneJoints(boneName);
            const startPoint = keypoints3D[start];
            const endPoint = keypoints3D[end];
            
            if (startPoint && endPoint) {
                const currentLength = this.calculateDistance(startPoint.position, endPoint.position);
                const scale = targetLength / currentLength;
                
                if (Math.abs(scale - 1) > 0.1) {
                    // 调整骨骼长度
                    const direction = {
                        x: endPoint.position.x - startPoint.position.x,
                        y: endPoint.position.y - startPoint.position.y,
                        z: endPoint.position.z - startPoint.position.z
                    };
                    
                    endPoint.position.x = startPoint.position.x + direction.x * scale;
                    endPoint.position.y = startPoint.position.y + direction.y * scale;
                    endPoint.position.z = startPoint.position.z + direction.z * scale;
                }
            }
        }
    }
}

运动重定向系统

javascript 复制代码
// 运动重定向器
class MotionRetargeter {
    constructor(sourceSkeleton, targetSkeleton) {
        this.sourceSkeleton = sourceSkeleton;
        this.targetSkeleton = targetSkeleton;
        this.boneMapping = this.createBoneMapping();
    }

    createBoneMapping() {
        // 定义源骨架和目标骨架之间的骨骼映射
        return {
            'spine': 'spine',
            'left_upper_arm': 'left_upper_arm',
            'left_lower_arm': 'left_lower_arm',
            'right_upper_arm': 'right_upper_arm',
            'right_lower_arm': 'right_lower_arm',
            'left_upper_leg': 'left_upper_leg',
            'left_lower_leg': 'left_lower_leg',
            'right_upper_leg': 'right_upper_leg',
            'right_lower_leg': 'right_lower_leg'
        };
    }

    retargetMotion(sourcePose) {
        const targetPose = {};
        
        for (const [sourceBone, targetBone] of Object.entries(this.boneMapping)) {
            const sourceRotation = sourcePose[sourceBone];
            if (sourceRotation) {
                // 应用旋转到目标骨骼
                targetPose[targetBone] = this.adjustRotationForSkeleton(
                    sourceRotation, 
                    sourceBone, 
                    targetBone
                );
            }
        }
        
        return targetPose;
    }

    adjustRotationForSkeleton(rotation, sourceBone, targetBone) {
        // 根据骨架差异调整旋转
        const adjustment = this.calculateBoneAdjustment(sourceBone, targetBone);
        
        // 应用调整
        const adjustedRotation = new THREE.Quaternion();
        adjustedRotation.multiplyQuaternions(rotation, adjustment);
        
        return adjustedRotation;
    }

    calculateBoneAdjustment(sourceBone, targetBone) {
        // 计算源骨骼和目标骨骼之间的方向差异
        // 简化实现
        return new THREE.Quaternion();
    }
}

本节展示了如何将TensorFlow.js的机器学习能力与Three.js的3D渲染能力结合,实现实时的姿态估计和角色动画驱动。这种技术为虚拟试衣、运动分析、游戏交互等应用提供了强大的基础。

相关推荐
亚马逊云开发者2 小时前
云原生游戏网关架构:EKS + APISIX + Graviton 构建高性能游戏服务网关
人工智能
翔云 OCR API2 小时前
NFC护照鉴伪查验流程解析-ICAO9303护照真伪查验接口技术方案
开发语言·人工智能·python·计算机视觉·ocr
艾莉丝努力练剑2 小时前
【自动化测试实战篇】Web自动化测试实战:从用例编写到报告生成
前端·人工智能·爬虫·python·pycharm·自动化·测试
Mintopia2 小时前
💥 Trae Solo 编程 vs. Cursor:新机遇与新挑战
前端·人工智能·trae
Mintopia2 小时前
🌌 长上下文 AIGC 的性能瓶颈:Web 端技术的突破与妥协
前端·人工智能·trae
xuehaikj2 小时前
【甲状腺病理AI】基于YOLO11-SOEP的甲状腺乳头状癌病理特征识别与分类系统研究
人工智能·分类·数据挖掘
愿没error的x2 小时前
深度学习基础知识总结(二):激活函数(Activation Function)详解
人工智能·深度学习
阿巴~阿巴~3 小时前
NumPy数值分析:从基础到高效运算
人工智能·python·numpy
aneasystone本尊3 小时前
LiteLLM 快速入门
人工智能