第40节:AR基础:Marker识别与跟踪

第40节:AR基础:Marker识别与跟踪

概述

增强现实(AR)技术将虚拟内容叠加到真实世界中,创造沉浸式体验。本节重点介绍基于标记(Marker)的AR技术,涵盖标记识别、姿态估计、虚拟物体跟踪等核心概念。

AR系统架构:
摄像头输入 标记识别 姿态估计 边缘检测 轮廓分析 ID解码 角点检测 单应性矩阵 相机姿态 标记跟踪 虚拟内容渲染 实时交互

核心原理

标记识别流程

步骤 技术方法 输出结果
图像预处理 灰度化、高斯模糊、二值化 优化后的二值图像
轮廓检测 Canny边缘检测、轮廓查找 潜在标记轮廓
形状分析 多边形近似、矩形验证 候选标记区域
编码解码 透视校正、比特矩阵读取 标记ID和姿态

姿态估计算法

javascript 复制代码
// 相机姿态估计
class PoseEstimator {
    // 解决PnP问题:从2D-3D点对应关系估计相机姿态
    solvePnP(imagePoints, objectPoints, cameraMatrix) {
        // imagePoints: 图像中的2D点
        // objectPoints: 对应的3D物体点
        // cameraMatrix: 相机内参矩阵
        
        // 使用EPnP或迭代法求解
        return {
            rotation: this.estimateRotation(imagePoints, objectPoints),
            translation: this.estimateTranslation(imagePoints, objectPoints),
            reprojectionError: this.calculateError(imagePoints, objectPoints)
        };
    }
    
    // 计算重投影误差
    calculateError(imagePoints, objectPoints, rotation, translation) {
        let totalError = 0;
        for (let i = 0; i < imagePoints.length; i++) {
            const projected = this.projectPoint(objectPoints[i], rotation, translation);
            const error = Math.sqrt(
                Math.pow(projected.x - imagePoints[i].x, 2) +
                Math.pow(projected.y - imagePoints[i].y, 2)
            );
            totalError += error;
        }
        return totalError / imagePoints.length;
    }
}

完整代码实现

AR标记跟踪系统

vue 复制代码
<template>
  <div class="ar-marker-container">
    <!-- 视频流显示 -->
    <div class="video-section">
      <video ref="videoElement" class="video-feed" autoplay playsinline></video>
      <canvas ref="processingCanvas" class="processing-canvas"></canvas>
      
      <!-- 状态指示器 -->
      <div class="status-indicator" :class="trackingStatus">
        {{ statusMessage }}
      </div>
    </div>

    <!-- 控制面板 -->
    <div class="control-panel">
      <div class="panel-section">
        <h3>🎯 标记设置</h3>
        
        <div class="marker-controls">
          <div class="control-group">
            <label>标记类型</label>
            <select v-model="markerType">
              <option value="aruco">ArUco标记</option>
              <option value="qr">QR码</option>
              <option value="custom">自定义</option>
            </select>
          </div>
          
          <div class="control-group">
            <label>标记ID</label>
            <input type="number" v-model="targetMarkerId" min="0" max="1023">
          </div>
          
          <div class="control-group">
            <label>标记尺寸 (cm)</label>
            <input type="number" v-model="markerSize" min="1" max="50">
          </div>
        </div>
      </div>

      <div class="panel-section">
        <h3>📱 相机控制</h3>
        
        <div class="camera-controls">
          <button @click="toggleCamera" class="control-button">
            {{ isCameraActive ? '🛑 停止相机' : '📷 启动相机' }}
          </button>
          
          <div class="control-group">
            <label>相机分辨率</label>
            <select v-model="cameraResolution">
              <option value="low">低 (640x480)</option>
              <option value="medium">中 (1280x720)</option>
              <option value="high">高 (1920x1080)</option>
            </select>
          </div>
        </div>
      </div>

      <div class="panel-section">
        <h3>🎮 虚拟内容</h3>
        
        <div class="content-controls">
          <div class="control-group">
            <label>显示模型</label>
            <select v-model="selectedModel">
              <option value="cube">立方体</option>
              <option value="sphere">球体</option>
              <option value="teapot">茶壶</option>
              <option value="custom">自定义模型</option>
            </select>
          </div>
          
          <div class="control-group">
            <label>模型缩放</label>
            <input type="range" v-model="modelScale" min="0.1" max="3" step="0.1">
            <span>{{ modelScale }}x</span>
          </div>
          
          <button @click="addVirtualObject" class="control-button">
            ➕ 添加物体
          </button>
        </div>
      </div>

      <div class="panel-section">
        <h3>📊 跟踪信息</h3>
        
        <div class="tracking-info">
          <div class="info-item">
            <span>标记状态:</span>
            <span :class="trackingStatus">{{ trackingStatusText }}</span>
          </div>
          
          <div class="info-item">
            <span>检测到的ID:</span>
            <span>{{ detectedMarkerId !== null ? detectedMarkerId : '无' }}</span>
          </div>
          
          <div class="info-item">
            <span>置信度:</span>
            <span>{{ detectionConfidence }}%</span>
          </div>
          
          <div class="info-item">
            <span>跟踪帧率:</span>
            <span>{{ trackingFPS }} FPS</span>
          </div>
          
          <div class="info-item">
            <span>位置误差:</span>
            <span>{{ positionError.toFixed(2) }} px</span>
          </div>
        </div>
      </div>
    </div>

    <!-- 3D预览面板 -->
    <div class="preview-panel" v-if="show3DPreview">
      <div class="preview-header">
        <h4>3D场景预览</h4>
        <button @click="show3DPreview = false" class="close-button">×</button>
      </div>
      <canvas ref="previewCanvas" class="preview-canvas"></canvas>
    </div>

    <!-- 加载状态 -->
    <div v-if="isLoading" class="loading-overlay">
      <div class="loading-spinner"></div>
      <p>初始化AR系统...</p>
    </div>
  </div>
</template>

<script>
import { onMounted, onUnmounted, ref, reactive } from 'vue';
import * as THREE from 'three';

// 简化版标记检测器
class SimpleMarkerDetector {
  constructor() {
    this.canvas = document.createElement('canvas');
    this.ctx = this.canvas.getContext('2d');
    this.detectionParams = {
      minContourArea: 1000,
      aspectRatioRange: { min: 0.8, max: 1.2 }
    };
  }

  // 检测图像中的标记
  async detectMarkers(videoElement) {
    const { width, height } = this.getOptimalCanvasSize(videoElement);
    this.canvas.width = width;
    this.canvas.height = height;
    
    // 绘制视频帧到画布
    this.ctx.drawImage(videoElement, 0, 0, width, height);
    
    // 获取图像数据
    const imageData = this.ctx.getImageData(0, 0, width, height);
    
    // 简化检测逻辑
    const markers = this.findCandidateMarkers(imageData);
    return this.validateMarkers(markers);
  }

  // 寻找候选标记
  findCandidateMarkers(imageData) {
    const candidates = [];
    const grayData = this.grayscale(imageData);
    const binaryData = this.adaptiveThreshold(grayData);
    const contours = this.findContours(binaryData);
    
    for (const contour of contours) {
      if (this.isPotentialMarker(contour)) {
        const corners = this.approxPolygon(contour);
        if (corners.length === 4) {
          candidates.push({
            corners,
            area: this.contourArea(contour)
          });
        }
      }
    }
    
    return candidates;
  }

  // 验证标记
  validateMarkers(candidates) {
    const validMarkers = [];
    
    for (const candidate of candidates) {
      // 透视校正
      const warped = this.perspectiveWarp(candidate.corners);
      
      // 解码标记ID
      const markerInfo = this.decodeMarker(warped);
      if (markerInfo) {
        validMarkers.push({
          id: markerInfo.id,
          corners: candidate.corners,
          confidence: markerInfo.confidence
        });
      }
    }
    
    return validMarkers;
  }

  // 图像处理辅助方法
  grayscale(imageData) {
    const data = new Uint8ClampedArray(imageData.data.length / 4);
    for (let i = 0, j = 0; i < imageData.data.length; i += 4, j++) {
      data[j] = Math.round(
        0.299 * imageData.data[i] +
        0.587 * imageData.data[i + 1] +
        0.114 * imageData.data[i + 2]
      );
    }
    return data;
  }

  adaptiveThreshold(data) {
    const binary = new Uint8ClampedArray(data.length);
    const blockSize = 15;
    const c = 5;
    
    // 简化实现 - 实际应使用积分图像
    for (let i = 0; i < data.length; i++) {
      binary[i] = data[i] > 128 ? 255 : 0;
    }
    
    return binary;
  }

  // 其他图像处理方法的简化实现...
  findContours() { return []; }
  isPotentialMarker() { return true; }
  approxPolygon() { return []; }
  contourArea() { return 0; }
  perspectiveWarp() { return null; }
  decodeMarker() { return { id: 1, confidence: 0.9 }; }
  getOptimalCanvasSize() { return { width: 640, height: 480 }; }
}

// 姿态估计器
class PoseEstimator {
  estimatePose(markerCorners, markerSize, cameraMatrix) {
    // 3D物体点(假设标记在XY平面上,Z=0)
    const objectPoints = [
      [-markerSize/2, -markerSize/2, 0],
      [markerSize/2, -markerSize/2, 0],
      [markerSize/2, markerSize/2, 0],
      [-markerSize/2, markerSize/2, 0]
    ];
    
    // 使用EPnP算法求解姿态
    return this.solveEPnP(markerCorners, objectPoints, cameraMatrix);
  }

  solveEPnP(imagePoints, objectPoints, cameraMatrix) {
    // 简化实现 - 实际应使用数值优化
    const rotation = new THREE.Matrix3();
    const translation = new THREE.Vector3(0, 0, 50); // 默认距离
    
    return {
      rotation: new THREE.Matrix4().setFromMatrix3(rotation),
      translation,
      reprojectionError: 2.5
    };
  }
}

// AR场景管理器
class ARSceneManager {
  constructor(renderer, camera) {
    this.renderer = renderer;
    this.camera = camera;
    this.scene = new THREE.Scene();
    this.virtualObjects = 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(1, 1, 1);
    this.scene.add(directionalLight);
  }

  // 添加虚拟物体到标记
  addVirtualObject(markerId, objectType = 'cube', scale = 1) {
    let geometry, material;
    
    switch (objectType) {
      case 'cube':
        geometry = new THREE.BoxGeometry(1, 1, 1);
        material = new THREE.MeshStandardMaterial({ 
          color: 0x00ff88,
          transparent: true,
          opacity: 0.8
        });
        break;
        
      case 'sphere':
        geometry = new THREE.SphereGeometry(0.5, 32, 32);
        material = new THREE.MeshStandardMaterial({ 
          color: 0xff4444,
          transparent: true,
          opacity: 0.8
        });
        break;
        
      case 'teapot':
        // 简化茶壶几何体
        geometry = new THREE.CylinderGeometry(0.5, 0.3, 1, 8);
        material = new THREE.MeshStandardMaterial({ 
          color: 0x8844ff,
          transparent: true,
          opacity: 0.8
        });
        break;
    }
    
    const mesh = new THREE.Mesh(geometry, material);
    mesh.scale.setScalar(scale);
    mesh.visible = false; // 初始隐藏
    
    this.scene.add(mesh);
    this.virtualObjects.set(markerId, mesh);
    
    return mesh;
  }

  // 更新虚拟物体位置
  updateObjectPose(markerId, pose) {
    const object = this.virtualObjects.get(markerId);
    if (!object) return;
    
    object.visible = true;
    object.position.copy(pose.translation);
    object.rotation.setFromRotationMatrix(pose.rotation);
  }

  // 隐藏物体
  hideObject(markerId) {
    const object = this.virtualObjects.get(markerId);
    if (object) {
      object.visible = false;
    }
  }

  // 渲染场景
  render() {
    this.renderer.render(this.scene, this.camera);
  }
}

export default {
  name: 'ARMarkerTracking',
  setup() {
    // 响应式数据
    const videoElement = ref(null);
    const processingCanvas = ref(null);
    const previewCanvas = ref(null);
    
    const isCameraActive = ref(false);
    const isLoading = ref(false);
    const trackingStatus = ref('searching');
    const statusMessage = ref('寻找标记中...');
    
    const markerType = ref('aruco');
    const targetMarkerId = ref(1);
    const markerSize = ref(10);
    const cameraResolution = ref('medium');
    
    const selectedModel = ref('cube');
    const modelScale = ref(1);
    const show3DPreview = ref(false);
    
    const detectedMarkerId = ref(null);
    const detectionConfidence = ref(0);
    const trackingFPS = ref(0);
    const positionError = ref(0);
    
    // 计算属性
    const trackingStatusText = {
      searching: '寻找标记',
      tracking: '跟踪中',
      lost: '跟踪丢失'
    }[trackingStatus.value];
    
    // AR系统组件
    let markerDetector, poseEstimator, sceneManager;
    let camera, renderer, arRenderer;
    let animationFrameId;
    let frameCount = 0;
    let lastFpsUpdate = 0;
    
    // 初始化AR系统
    const initARSystem = async () => {
      isLoading.value = true;
      
      // 初始化组件
      markerDetector = new SimpleMarkerDetector();
      poseEstimator = new PoseEstimator();
      
      // 初始化3D渲染
      await init3DRenderer();
      
      isLoading.value = false;
    };
    
    // 初始化3D渲染器
    const init3DRenderer = async () => {
      if (!previewCanvas.value) return;
      
      // 创建Three.js场景
      renderer = new THREE.WebGLRenderer({ 
        canvas: previewCanvas.value,
        antialias: true,
        alpha: true
      });
      
      camera = new THREE.PerspectiveCamera(60, 1, 0.1, 1000);
      sceneManager = new ARSceneManager(renderer, camera);
      
      // 添加示例物体
      sceneManager.addVirtualObject(1, 'cube', 1);
      
      // 启动渲染循环
      animate3DScene();
    };
    
    // 3D场景动画
    const animate3DScene = () => {
      animationFrameId = requestAnimationFrame(animate3DScene);
      sceneManager.render();
    };
    
    // 相机控制
    const toggleCamera = async () => {
      if (isCameraActive.value) {
        stopCamera();
      } else {
        await startCamera();
      }
    };
    
    // 启动相机
    const startCamera = async () => {
      try {
        const stream = await navigator.mediaDevices.getUserMedia({
          video: {
            width: { ideal: 1280 },
            height: { ideal: 720 },
            facingMode: 'environment'
          }
        });
        
        videoElement.value.srcObject = stream;
        isCameraActive.value = true;
        
        // 开始AR跟踪循环
        startARTracking();
        
      } catch (error) {
        console.error('无法访问相机:', error);
        statusMessage.value = '相机访问失败';
      }
    };
    
    // 停止相机
    const stopCamera = () => {
      if (videoElement.value.srcObject) {
        videoElement.value.srcObject.getTracks().forEach(track => track.stop());
        videoElement.value.srcObject = null;
      }
      
      isCameraActive.value = false;
      trackingStatus.value = 'searching';
      
      if (animationFrameId) {
        cancelAnimationFrame(animationFrameId);
      }
    };
    
    // AR跟踪循环
    const startARTracking = () => {
      const processFrame = async () => {
        if (!isCameraActive.value) return;
        
        // 检测标记
        const markers = await markerDetector.detectMarkers(videoElement.value);
        
        // 处理检测结果
        await processDetectionResults(markers);
        
        // 更新性能统计
        updatePerformanceStats();
        
        // 继续下一帧
        animationFrameId = requestAnimationFrame(processFrame);
      };
      
      processFrame();
    };
    
    // 处理检测结果
    const processDetectionResults = async (markers) => {
      const targetMarker = markers.find(m => m.id === targetMarkerId.value);
      
      if (targetMarker) {
        // 找到目标标记
        trackingStatus.value = 'tracking';
        statusMessage.value = `跟踪标记 #${targetMarker.id}`;
        detectedMarkerId.value = targetMarker.id;
        detectionConfidence.value = Math.round(targetMarker.confidence * 100);
        
        // 估计姿态
        const cameraMatrix = this.getCameraMatrix(); // 需要实现
        const pose = poseEstimator.estimatePose(
          targetMarker.corners,
          markerSize.value,
          cameraMatrix
        );
        
        positionError.value = pose.reprojectionError;
        
        // 更新虚拟物体
        sceneManager.updateObjectPose(targetMarker.id, pose);
        
      } else {
        // 未找到标记
        trackingStatus.value = 'searching';
        statusMessage.value = '寻找标记中...';
        detectedMarkerId.value = null;
        detectionConfidence.value = 0;
        
        if (detectedMarkerId.value !== null) {
          sceneManager.hideObject(detectedMarkerId.value);
        }
      }
    };
    
    // 更新性能统计
    const updatePerformanceStats = () => {
      frameCount++;
      const now = performance.now();
      
      if (now - lastFpsUpdate >= 1000) {
        trackingFPS.value = Math.round((frameCount * 1000) / (now - lastFpsUpdate));
        frameCount = 0;
        lastFpsUpdate = now;
      }
    };
    
    // 添加虚拟物体
    const addVirtualObject = () => {
      sceneManager.addVirtualObject(targetMarkerId.value, selectedModel.value, modelScale.value);
    };
    
    // 获取相机矩阵(简化实现)
    const getCameraMatrix = () => {
      return new THREE.Matrix4().makePerspective(
        60 * Math.PI / 180,
        videoElement.value.videoWidth / videoElement.value.videoHeight,
        0.1,
        1000
      );
    };
    
    onMounted(() => {
      initARSystem();
    });
    
    onUnmounted(() => {
      stopCamera();
      if (animationFrameId) {
        cancelAnimationFrame(animationFrameId);
      }
    });
    
    return {
      // 模板引用
      videoElement,
      processingCanvas,
      previewCanvas,
      
      // 状态数据
      isCameraActive,
      isLoading,
      trackingStatus,
      statusMessage,
      markerType,
      targetMarkerId,
      markerSize,
      cameraResolution,
      selectedModel,
      modelScale,
      show3DPreview,
      detectedMarkerId,
      detectionConfidence,
      trackingFPS,
      positionError,
      trackingStatusText,
      
      // 方法
      toggleCamera,
      addVirtualObject
    };
  }
};
</script>

<style scoped>
.ar-marker-container {
  width: 100%;
  height: 100vh;
  display: flex;
  background: #000;
  overflow: hidden;
}

.video-section {
  flex: 1;
  position: relative;
  background: #000;
}

.video-feed {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.processing-canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.status-indicator {
  position: absolute;
  top: 20px;
  left: 20px;
  padding: 10px 20px;
  background: rgba(0, 0, 0, 0.7);
  color: white;
  border-radius: 20px;
  font-size: 14px;
  backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.2);
}

.status-indicator.searching {
  background: rgba(255, 165, 0, 0.7);
}

.status-indicator.tracking {
  background: rgba(0, 255, 0, 0.7);
}

.status-indicator.lost {
  background: rgba(255, 0, 0, 0.7);
}

.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: #00ffff;
  margin-bottom: 15px;
  font-size: 16px;
}

.control-group {
  margin-bottom: 15px;
}

.control-group label {
  display: block;
  margin-bottom: 8px;
  color: #ccc;
  font-size: 14px;
}

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

.control-group input[type="range"] {
  width: 100%;
  margin: 8px 0;
}

.control-button {
  width: 100%;
  padding: 12px;
  background: linear-gradient(45deg, #667eea, #764ba2);
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.3s ease;
}

.control-button:hover {
  transform: translateY(-2px);
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}

.tracking-info {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

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

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

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

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

.preview-panel {
  position: absolute;
  bottom: 20px;
  right: 370px;
  width: 300px;
  height: 200px;
  background: #2d2d2d;
  border-radius: 8px;
  border: 1px solid #444;
  overflow: hidden;
}

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

.preview-header h4 {
  margin: 0;
  color: #00ffff;
  font-size: 14px;
}

.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;
}

.preview-canvas {
  width: 100%;
  height: calc(100% - 45px);
  display: block;
}

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

.loading-spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #333;
  border-top: 4px solid #00ffff;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin-bottom: 15px;
}

.loading-overlay p {
  color: white;
  margin: 0;
}

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

/* 响应式设计 */
@media (max-width: 1024px) {
  .ar-marker-container {
    flex-direction: column;
  }
  
  .control-panel {
    width: 100%;
    height: 300px;
  }
  
  .preview-panel {
    right: 20px;
    bottom: 320px;
  }
}

@media (max-width: 768px) {
  .preview-panel {
    display: none;
  }
  
  .control-panel {
    height: 400px;
  }
}
</style>

高级特性实现

多标记跟踪系统

javascript 复制代码
// 多标记跟踪管理器
class MultiMarkerTracker {
  constructor() {
    this.activeMarkers = new Map();
    this.trackingHistory = new Map();
    this.maxHistorySize = 60; // 保留60帧历史
  }

  // 更新标记状态
  updateMarkers(detectedMarkers) {
    const currentTime = performance.now();
    
    // 更新现有标记
    for (const [markerId, marker] of this.activeMarkers) {
      const detected = detectedMarkers.find(m => m.id === markerId);
      
      if (detected) {
        // 更新位置和置信度
        marker.lastSeen = currentTime;
        marker.corners = detected.corners;
        marker.confidence = detected.confidence;
        marker.isVisible = true;
        
        this.addToHistory(markerId, detected);
      } else {
        // 标记暂时丢失
        marker.isVisible = false;
        marker.confidence *= 0.9; // 置信度衰减
      }
    }
    
    // 添加新检测到的标记
    for (const detected of detectedMarkers) {
      if (!this.activeMarkers.has(detected.id)) {
        this.activeMarkers.set(detected.id, {
          id: detected.id,
          corners: detected.corners,
          confidence: detected.confidence,
          firstSeen: currentTime,
          lastSeen: currentTime,
          isVisible: true
        });
      }
    }
    
    // 清理长时间未见的标记
    this.cleanupOldMarkers(currentTime);
  }

  // 添加位置历史
  addToHistory(markerId, markerData) {
    if (!this.trackingHistory.has(markerId)) {
      this.trackingHistory.set(markerId, []);
    }
    
    const history = this.trackingHistory.get(markerId);
    history.push({
      timestamp: performance.now(),
      corners: markerData.corners,
      confidence: markerData.confidence
    });
    
    // 限制历史记录大小
    if (history.length > this.maxHistorySize) {
      history.shift();
    }
  }

  // 清理旧标记
  cleanupOldMarkers(currentTime) {
    const timeout = 5000; // 5秒超时
    for (const [markerId, marker] of this.activeMarkers) {
      if (currentTime - marker.lastSeen > timeout) {
        this.activeMarkers.delete(markerId);
        this.trackingHistory.delete(markerId);
      }
    }
  }

  // 获取稳定位置(使用历史数据平滑)
  getStablePosition(markerId, smoothingFactor = 0.3) {
    const history = this.trackingHistory.get(markerId);
    if (!history || history.length < 2) {
      return this.activeMarkers.get(markerId)?.corners;
    }
    
    // 使用加权平均平滑位置
    const recentFrames = history.slice(-5); // 最近5帧
    let weightedCorners = null;
    
    for (const frame of recentFrames) {
      if (!weightedCorners) {
        weightedCorners = frame.corners.map(corner => ({
          x: corner.x * frame.confidence,
          y: corner.y * frame.confidence
        }));
      } else {
        frame.corners.forEach((corner, index) => {
          weightedCorners[index].x += corner.x * frame.confidence;
          weightedCorners[index].y += corner.y * frame.confidence;
        });
      }
    }
    
    // 归一化
    const totalConfidence = recentFrames.reduce((sum, frame) => sum + frame.confidence, 0);
    return weightedCorners.map(corner => ({
      x: corner.x / totalConfidence,
      y: corner.y / totalConfidence
    }));
  }
}

姿态平滑过滤器

javascript 复制代码
// 卡尔曼滤波器用于姿态平滑
class PoseKalmanFilter {
  constructor() {
    this.state = {
      x: 0, y: 0, z: 0,
      vx: 0, vy: 0, vz: 0,
      qx: 0, qy: 0, qz: 0, qw: 1
    };
    
    this.covariance = this.initializeCovariance();
    this.processNoise = 0.1;
    this.measurementNoise = 1.0;
  }

  // 预测步骤
  predict(deltaTime) {
    // 状态转移:x = x + v * dt
    this.state.x += this.state.vx * deltaTime;
    this.state.y += this.state.vy * deltaTime;
    this.state.z += this.state.vz * deltaTime;
    
    // 简化协方差更新
    this.covariance = this.covariance.map(row => 
      row.map(value => value + this.processNoise)
    );
    
    return this.state;
  }

  // 更新步骤
  update(measurement) {
    // 计算卡尔曼增益
    const K = this.calculateKalmanGain();
    
    // 状态更新
    this.state.x += K[0] * (measurement.x - this.state.x);
    this.state.y += K[1] * (measurement.y - this.state.y);
    this.state.z += K[2] * (measurement.z - this.state.z);
    
    // 四元数球面线性插值
    this.state = this.slerpQuaternion(this.state, measurement, K[3]);
    
    // 协方差更新
    this.updateCovariance(K);
    
    return this.state;
  }

  // 四元数球面线性插值
  slerpQuaternion(from, to, t) {
    const result = { ...from };
    
    // 简化SLERP实现
    const dot = from.qx * to.qx + from.qy * to.qy + 
                from.qz * to.qz + from.qw * to.qw;
    
    if (dot > 0.9995) {
      // 线性插值
      result.qx = from.qx + t * (to.qx - from.qx);
      result.qy = from.qy + t * (to.qy - from.qy);
      result.qz = from.qz + t * (to.qz - from.qz);
      result.qw = from.qw + t * (to.qw - from.qw);
    } else {
      // 球面插值
      const theta = Math.acos(dot);
      const sinTheta = Math.sin(theta);
      const ratio1 = Math.sin((1 - t) * theta) / sinTheta;
      const ratio2 = Math.sin(t * theta) / sinTheta;
      
      result.qx = ratio1 * from.qx + ratio2 * to.qx;
      result.qy = ratio1 * from.qy + ratio2 * to.qy;
      result.qz = ratio1 * from.qz + ratio2 * to.qz;
      result.qw = ratio1 * from.qw + ratio2 * to.qw;
    }
    
    // 归一化
    const length = Math.sqrt(
      result.qx * result.qx + result.qy * result.qy +
      result.qz * result.qz + result.qw * result.qw
    );
    
    result.qx /= length;
    result.qy /= length;
    result.qz /= length;
    result.qw /= length;
    
    return result;
  }

  calculateKalmanGain() {
    // 简化卡尔曼增益计算
    return [0.8, 0.8, 0.8, 0.5];
  }

  initializeCovariance() {
    return [
      [1, 0, 0, 0],
      [0, 1, 0, 0],
      [0, 0, 1, 0],
      [0, 0, 0, 1]
    ];
  }

  updateCovariance(K) {
    // 简化协方差更新
    for (let i = 0; i < this.covariance.length; i++) {
      for (let j = 0; j < this.covariance[i].length; j++) {
        this.covariance[i][j] *= (1 - K[i]);
      }
    }
  }
}

本节介绍了基于标记的AR系统核心实现,包括标记识别、姿态估计和虚拟内容跟踪。通过这套系统,开发者可以构建稳定的AR应用,将数字内容精准地锚定在现实世界中。

相关推荐
二川bro2 天前
第33节:程序化生成与无限地形算法
前端·算法·3d·threejs
ar01232 天前
AR眼镜在工业制造业的质量检测应用探讨
人工智能·ar
佩京科技VR3 天前
AR党建互动台-VR智慧党建沙盘-AR党建识别桌
ar·vr党建沙盘·ar党建互动桌
私人珍藏库3 天前
[Android] AR绘画素描1.0版(AR Draw - Sketch Anime Cartoon 1.0)
macos·ar·sketch
二川bro3 天前
第30节:大规模地形渲染与LOD技术
前端·threejs
北京阿法龙科技有限公司4 天前
AR眼镜基于上下文智能识别:电力运维高效规范操作应用方案|阿法龙XR云平台
运维·ar·xr
罗不丢7 天前
自回归模型例题(AR)与ACF/PACF图绘制
数据挖掘·回归·ar·acf·pacf
美摄科技8 天前
什么是AR人脸特效sdk?
ar
美摄科技8 天前
AR短视频SDK,打造差异化竞争壁垒
ar·音视频