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

相关推荐
虹科数字化与AR12 小时前
安宝特方案丨协作提效 + 盈利升级:安宝特 AR 眼镜赋能工业 4.0
ar
虹科数字化与AR12 小时前
安宝特新闻丨Vuzix展示LX1 AR智能眼镜与仓储自动化系统
运维·自动化·ar
世岩清上12 小时前
世岩清上:科技向善,让乡村“被看见”更“被理解”
人工智能·ar·乡村振兴·和美乡村
虹科数字化与AR12 小时前
安宝特案例丨又一落地!AR远程医疗示教实现肩关节专家经验分享
ar
虹科数字化与AR14 小时前
安宝特方案丨AR 智能眼镜医疗套装:打造可落地的远程医疗解决方案
ar
ar012314 小时前
AR远程协助如何提升能源行业运维效率
人工智能·ar
北京阿法龙科技有限公司14 小时前
AR眼镜仓储物流分拣技术应用与落地方案
运维·人工智能·ar·xr
虹科数字化与AR14 小时前
安宝特案例丨契合 WHO 最佳实践:M400 AR 眼镜完善老挝小儿癌症跨国诊疗体系
ar
虹科数字化与AR14 小时前
安宝特案例丨AR眼镜实现远程颅骨修复手术突破,安宝特以双重医疗认证树立行业标杆
ar
秋邱16 小时前
AR 应用流量增长与品牌 IP 打造:从被动接单到主动获客
开发语言·人工智能·后端·python·ar·restful