第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应用,将数字内容精准地锚定在现实世界中。

相关推荐
AI能见度5 天前
硬核:如何用大疆 SRT 数据实现高精度 AR 视频投射?
ar·无人机·webgl
程序员敲代码吗5 天前
A-Frame与WebXR:构建丰富的VR及AR体验
ar·vr
gis分享者5 天前
学习threejs,实现山谷奔跑效果
threejs·着色器·glsl·shadermaterial·unrealbloompass·山谷奔跑·simplex
ct9786 天前
ThreeJs材质、模型加载、核心API
webgl·材质·threejs
a1117767 天前
飞机躲避炸弹 网页游戏
前端·开源·html·threejs
a1117767 天前
3D赛车躲避游戏(html threeJS开源)
前端·游戏·3d·开源·html·threejs
a1117768 天前
水体渲染系统(html开源)
前端·开源·threejs·水体渲染
Once_day10 天前
GCC编译(6)静态库工具AR
c语言·ar·编译和链接
mtouch33310 天前
三维沙盘系统配置管理数字沙盘模块
人工智能·ai·ar·vr·虚拟现实·电子沙盘·数字沙盘
好家伙VCC14 天前
# 发散创新:基于ARCore的实时3D物体识别与交互开发实战 在增强现实(
java·python·3d·ar·交互