当前位置: 首页 > news >正文

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

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

概述

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

在这里插入图片描述

AR系统架构:

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

核心原理

标记识别流程

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

姿态估计算法

// 相机姿态估计
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标记跟踪系统

<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);// 解码标记IDconst 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>

高级特性实现

多标记跟踪系统

// 多标记跟踪管理器
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}));}
}

姿态平滑过滤器

// 卡尔曼滤波器用于姿态平滑
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 * dtthis.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应用,将数字内容精准地锚定在现实世界中。

http://www.dtcms.com/a/613241.html

相关文章:

  • 新能源汽车动力系统在环(HIL)半实物仿真测试台架深度解析
  • 企业BI建议--数据治理平台
  • 锒川市住房和城乡建设局网站公告湖北省建设工程质量安全监督网站
  • 从裂变能力竞争到技术水平竞争:开源AI智能名片链动2+1模式S2B2C商城小程序对微商企业竞争格局的重塑
  • 09-mcp-server案例分享-即梦MCP-Server实战教程-让Claude直接调用AI生图视频能力
  • SpringBoot18-redis的配置
  • PHP 表单 - 必需字段
  • python爬虫入门案例day05:Pexels
  • android studio Gradle 打包任务配置
  • 【AI学习-comfyUI学习-1批量抠图换背景工作流+2视频抠图工作流-各个部分学习-第十节】
  • Redis(124)Redis在电商系统中的应用有哪些?
  • [Dify 实战案例] 用 Dify 做一个多语种文档翻译工具:支持 TXT / DOCX / XLSX / PPTX 全格式
  • 自然语言编程:从一段Perl程序说起
  • OpenAI Whisper:技术、实战、生态
  • 【ZeroRange WebRTC】DTLS(Datagram Transport Layer Security)技术深度分析
  • 南京本地网站合肥建网站要多少钱
  • 从丝印判断ESP32-WROOM-32E模组Flash容量
  • react 学习
  • 语言模型(Language Model)介绍
  • 基于协同过滤推荐算法的求职招聘推荐系统u1ydn3f4(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
  • 在 Ubuntu 中把系统字符集从英文改成支持中文
  • PyTorch实战:从零搭建CV模型
  • 网站权重不够高 导致友情链接批量查询
  • 如何在校园网让虚拟机联网并固定IP
  • 5. Qt深入 线程例子
  • 虚拟服务器和如何创建网站网站 注册模块怎么做
  • 【Linux日新月异(二)】CentOS 7用户与用户组管理深度指南:保障系统安全的基石
  • 大模型-提示工程
  • ARM编译器深度解析:从Keil到VSCode的STM32开发之
  • 支持CAS身份认证,支持接入Oracle11数据源,SQLBot开源智能问数系统v1.3.0版本发布