Vue前端展示Lingbot-Depth-Pretrain-ViTL-14深度估计效果深度估计技术能让计算机“看懂”图片的远近层次从一张普通的照片里提取出三维空间信息。Lingbot-Depth-Pretrain-ViTL-14是一个基于Vision Transformer架构的深度估计模型它在多个基准数据集上表现出色能够生成高质量的深度图。不过模型本身只是一个“大脑”我们需要一个直观的“窗口”来展示它的思考结果。这篇文章我就带你一起用Vue.js搭建一个交互式的前端应用让你不仅能上传图片、调用模型还能通过滑竿对比、3D点云等酷炫的方式亲眼看到深度估计的魅力。整个过程就像给这个聪明的“大脑”配上一双会说话的眼睛和一双灵巧的手。1. 项目概述与核心价值想象一下你拍了一张风景照上传到我们即将构建的应用里。几秒钟后你不仅能看到原图还能看到一个代表了物体远近的灰度图深度图甚至能拖动一个滑竿像揭开面纱一样在原图和深度图之间切换查看。更酷的是你还能把这个深度信息转换成3D点云从不同角度旋转观察仿佛走进了照片里的世界。这就是我们要做的事情。它的核心价值非常直接直观理解把抽象的深度数据变成可视化的图像和3D模型帮助开发者、研究者甚至普通用户理解模型到底“看”到了什么。交互体验不再是静态的图片输出用户可以通过滑竿、旋转等操作主动探索体验更沉浸。效果评估前端实时展示为模型效果的快速验证和对比提供了便利比如调整模型参数后立刻能看到生成深度图的变化。技术演示一个完整的、包含前端交互和后端推理的Demo本身就是展示技术能力的绝佳案例。整个应用的技术栈很清晰Vue 3作为前端框架Axios负责与后端API通信HTML Canvas用于绘制2D对比效果Three.js则用来构建那个迷人的3D点云世界。后端我们假设已经有一个提供深度估计服务的API它接收图片返回深度图数据。2. 环境搭建与项目初始化我们从头开始确保每一步都清晰。首先确保你的电脑上安装了Node.js建议版本16以上和npm或yarn、pnpm。打开终端我们用Vue官方工具创建一个新项目。这里我选择使用Vite作为构建工具因为它速度更快。npm create vuelatest lingbot-depth-demo创建过程中命令行会提示你进行一些选择。对于这个项目我的选择如下✔ Project name:lingbot-depth-demo✔ Add TypeScript?No(为了简化我们先不用TS)✔ Add JSX Support?No✔ Add Vue Router for Single Page Application?No(我们是一个单页应用但功能简单暂不需要路由)✔ Add Pinia for state management?No(状态管理暂不需要)✔ Add Vitest for Unit Testing?No✔ Add an End-to-End Testing Solution?No✔ Add ESLint for code quality?Yes(保持代码规范是个好习惯)按照提示完成创建后进入项目目录并安装基础依赖和我们将要用的几个核心库。cd lingbot-depth-demo npm install npm install axios threeaxios用来调用后端APIthree则是3D图形库。安装完成后你可以运行npm run dev启动开发服务器在浏览器打开http://localhost:5173应该能看到默认的Vue欢迎页面。接下来我们来清理一下默认生成的src/App.vue和src/components下的文件准备编写我们自己的组件。3. 构建图片上传与深度估计组件这是应用最基础的功能选择图片发送到后端获取深度图。我们在src/components下创建一个新文件DepthEstimator.vue。这个组件主要包含一个文件选择输入框 (input typefile)。一个按钮用于触发上传和估计过程。一个区域用于显示上传的原始图片。一个区域预留用来显示后端返回的深度图稍后填充。状态管理比如加载中、错误信息等。我们先搭建骨架和上传逻辑。!-- src/components/DepthEstimator.vue -- template div classdepth-estimator h2深度估计/h2 !-- 上传区域 -- div classupload-section input typefile idimageInput acceptimage/* changehandleFileSelect :disabledisLoading / label forimageInput classupload-btn {{ selectedFile ? 重新选择图片 : 选择图片 }} /label button clickestimateDepth :disabled!selectedFile || isLoading classestimate-btn {{ isLoading ? 处理中... : 开始深度估计 }} /button /div !-- 状态提示 -- div v-iferrorMessage classerror-message {{ errorMessage }} /div div v-ifisLoading classloading 正在调用模型生成深度图请稍候... /div !-- 图片展示区域 -- div v-iforiginalImageUrl || depthImageUrl classimage-display div classimage-container h3原始图片/h3 img v-iforiginalImageUrl :srcoriginalImageUrl alt原始图片 / p v-else未选择图片/p /div div classimage-container h3深度估计图/h3 img v-ifdepthImageUrl :srcdepthImageUrl alt深度图 / p v-else深度图将在此显示/p /div /div /div /template script setup import { ref } from vue; import axios from axios; // 定义后端API地址这里需要替换成你实际的后端服务地址 const API_BASE_URL http://your-backend-api.com; // 示例地址请修改 const selectedFile ref(null); const originalImageUrl ref(); const depthImageUrl ref(); const isLoading ref(false); const errorMessage ref(); // 处理文件选择 const handleFileSelect (event) { const file event.target.files[0]; if (!file) return; // 验证文件类型 if (!file.type.startsWith(image/)) { errorMessage.value 请选择图片文件; return; } selectedFile.value file; originalImageUrl.value URL.createObjectURL(file); // 创建本地预览URL errorMessage.value ; depthImageUrl.value ; // 清除之前的深度图 }; // 调用深度估计API const estimateDepth async () { if (!selectedFile.value) return; isLoading.value true; errorMessage.value ; const formData new FormData(); formData.append(image, selectedFile.value); try { // 假设后端有一个 /api/depth-estimate 的端点 const response await axios.post(${API_BASE_URL}/api/depth-estimate, formData, { headers: { Content-Type: multipart/form-data, }, responseType: blob, // 后端返回的是图片二进制流 }); // 将返回的Blob数据转换为可显示的URL const blob new Blob([response.data], { type: image/png }); // 假设返回PNG depthImageUrl.value URL.createObjectURL(blob); } catch (error) { console.error(深度估计请求失败:, error); errorMessage.value 请求失败: ${error.message || 未知错误}; depthImageUrl.value ; } finally { isLoading.value false; } }; /script style scoped .depth-estimator { padding: 20px; max-width: 1200px; margin: 0 auto; } .upload-section { margin-bottom: 20px; display: flex; gap: 15px; align-items: center; } #imageInput { display: none; } .upload-btn, .estimate-btn { padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; font-size: 16px; } .upload-btn { background-color: #4CAF50; color: white; } .estimate-btn { background-color: #008CBA; color: white; } .estimate-btn:disabled { background-color: #cccccc; cursor: not-allowed; } .error-message { color: #d32f2f; background-color: #ffebee; padding: 10px; border-radius: 5px; margin-bottom: 15px; } .loading { color: #ff9800; margin-bottom: 15px; } .image-display { display: flex; gap: 30px; margin-top: 30px; flex-wrap: wrap; } .image-container { flex: 1; min-width: 300px; border: 1px solid #ddd; padding: 15px; border-radius: 8px; text-align: center; } .image-container h3 { margin-top: 0; } .image-container img { max-width: 100%; max-height: 400px; display: block; margin: 10px auto; border-radius: 4px; } /style现在修改src/App.vue引入并使用这个组件。!-- src/App.vue -- template div idapp header h1Lingbot深度估计效果展示平台/h1 /header main DepthEstimator / !-- 后续我们会在这里添加对比滑竿和3D可视化组件 -- /main /div /template script setup import DepthEstimator from ./components/DepthEstimator.vue; /script style * { box-sizing: border-box; } body { font-family: sans-serif; margin: 0; background-color: #f5f5f5; } #app { min-height: 100vh; } header { background-color: #333; color: white; padding: 20px; text-align: center; } main { padding: 20px; } /style到这一步一个具备图片上传、调用API、展示结果的基础功能就完成了。记得把API_BASE_URL换成你实际的后端服务地址。如果后端API返回的是JSON数据包含深度图base64字符串或路径你需要相应调整estimateDepth方法中处理响应数据的逻辑。4. 实现深度图对比滑竿交互静态并排显示两张图不够直观。我们来实现一个经典的“Before/After”滑竿对比效果用户拖动滑竿可以平滑地揭示底层深度图或原图。我们将创建一个新的组件ComparisonSlider.vue。这个组件的原理是使用两个重叠的canvas元素分别绘制原始图片和深度图。然后通过一个滑块控制上层画布显示深度图的裁剪区域从而实现滑动揭示的效果。!-- src/components/ComparisonSlider.vue -- template div classcomparison-slider v-iforiginalImage depthImage h3深度图对比滑竿/h3 p拖动滑块查看原始图片与深度图的对比效果。/p div classslider-container refcontainerRef !-- 底层Canvas固定显示原始图片 -- canvas reforiginalCanvasRef classcanvas-layer/canvas !-- 上层Canvas显示深度图其绘制区域受滑块控制 -- canvas refdepthCanvasRef classcanvas-layer :style{ clipPath: inset(0 ${100 - sliderPosition}% 0 0) }/canvas !-- 滑块控件 -- div classslider-control :style{ left: ${sliderPosition}% } div classslider-handle mousedownstartDrag/div /div !-- 标签 -- div classlabel original-label原始图片/div div classlabel depth-label深度图/div /div input typerange min0 max100 v-model.numbersliderPosition classslider-input / /div /template script setup import { ref, watch, onMounted, onUnmounted } from vue; const props defineProps({ originalImage: HTMLImageElement, // 原始图片的Image对象 depthImage: HTMLImageElement, // 深度图的Image对象 }); const containerRef ref(null); const originalCanvasRef ref(null); const depthCanvasRef ref(null); const sliderPosition ref(50); // 滑块位置百分比 const isDragging ref(false); let containerWidth 0; let containerHeight 0; // 监听图片变化重新绘制Canvas watch(() [props.originalImage, props.depthImage], () { if (props.originalImage props.depthImage) { // 等待下一帧确保DOM已更新 setTimeout(() { updateCanvasSize(); drawImages(); }, 0); } }, { immediate: true }); // 初始化Canvas尺寸 const updateCanvasSize () { if (!containerRef.value) return; const containerStyle window.getComputedStyle(containerRef.value); containerWidth parseInt(containerStyle.width); containerHeight parseInt(containerStyle.height); const canvases [originalCanvasRef.value, depthCanvasRef.value]; canvases.forEach(canvas { if (canvas) { canvas.width containerWidth; canvas.height containerHeight; } }); }; // 在Canvas上绘制图片 const drawImages () { const originalCtx originalCanvasRef.value?.getContext(2d); const depthCtx depthCanvasRef.value?.getContext(2d); if (!originalCtx || !depthCtx || !props.originalImage || !props.depthImage) return; // 清空画布 originalCtx.clearRect(0, 0, containerWidth, containerHeight); depthCtx.clearRect(0, 0, containerWidth, containerHeight); // 计算图片绘制尺寸保持比例并居中 const drawImageOnCanvas (ctx, img) { const imgRatio img.width / img.height; const containerRatio containerWidth / containerHeight; let drawWidth, drawHeight, offsetX, offsetY; if (imgRatio containerRatio) { // 图片更宽宽度填满高度自适应 drawWidth containerWidth; drawHeight containerWidth / imgRatio; offsetX 0; offsetY (containerHeight - drawHeight) / 2; } else { // 图片更高高度填满宽度自适应 drawHeight containerHeight; drawWidth containerHeight * imgRatio; offsetX (containerWidth - drawWidth) / 2; offsetY 0; } ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight); }; drawImageOnCanvas(originalCtx, props.originalImage); drawImageOnCanvas(depthCtx, props.depthImage); }; // 滑块拖拽逻辑 const startDrag (e) { isDragging.value true; document.addEventListener(mousemove, handleDrag); document.addEventListener(mouseup, stopDrag); e.preventDefault(); }; const handleDrag (e) { if (!isDragging.value || !containerRef.value) return; const rect containerRef.value.getBoundingClientRect(); const x e.clientX - rect.left; const percent Math.max(0, Math.min(100, (x / rect.width) * 100)); sliderPosition.value percent; }; const stopDrag () { isDragging.value false; document.removeEventListener(mousemove, handleDrag); document.removeEventListener(mouseup, stopDrag); }; // 窗口大小变化时调整Canvas const handleResize () { updateCanvasSize(); drawImages(); }; onMounted(() { window.addEventListener(resize, handleResize); }); onUnmounted(() { window.removeEventListener(resize, handleResize); document.removeEventListener(mousemove, handleDrag); document.removeEventListener(mouseup, stopDrag); }); /script style scoped .comparison-slider { margin-top: 40px; } .slider-container { position: relative; width: 100%; max-width: 800px; height: 500px; margin: 20px auto; border: 2px solid #333; border-radius: 8px; overflow: hidden; cursor: ew-resize; } .canvas-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; /* 防止Canvas干扰鼠标事件 */ } .slider-control { position: absolute; top: 0; height: 100%; width: 4px; background-color: white; box-shadow: 0 0 10px rgba(0,0,0,0.5); transform: translateX(-50%); z-index: 10; pointer-events: none; } .slider-handle { position: absolute; top: 50%; left: 50%; width: 40px; height: 40px; background-color: white; border-radius: 50%; transform: translate(-50%, -50%); box-shadow: 0 2px 10px rgba(0,0,0,0.3); cursor: grab; pointer-events: all; } .slider-handle:active { cursor: grabbing; } .label { position: absolute; top: 20px; padding: 5px 15px; background-color: rgba(0, 0, 0, 0.7); color: white; border-radius: 4px; font-size: 14px; z-index: 5; } .original-label { left: 20px; } .depth-label { right: 20px; } .slider-input { display: block; width: 100%; max-width: 800px; margin: 15px auto; } /style现在我们需要修改DepthEstimator.vue组件在获取到深度图后将图片数据传递给ComparisonSlider组件。这涉及到将图片URL加载成HTMLImageElement对象。首先在DepthEstimator.vue的script setup中添加一个工具函数和响应式数据。// 在 DepthEstimator.vue 的 script 部分添加 import { ref } from vue; // ... 其他导入和变量 const originalImageElement ref(null); const depthImageElement ref(null); // 工具函数将图片URL加载为Image对象 const loadImage (url) { return new Promise((resolve, reject) { const img new Image(); img.crossOrigin anonymous; // 处理可能存在的跨域问题 img.onload () resolve(img); img.onerror reject; img.src url; }); }; // 修改 estimateDepth 方法的成功部分 // 在 depthImageUrl.value URL.createObjectURL(blob); 之后 try { // ... API调用 depthImageUrl.value URL.createObjectURL(blob); // 加载图片元素 const [originalImg, depthImg] await Promise.all([ loadImage(originalImageUrl.value), loadImage(depthImageUrl.value) ]); originalImageElement.value originalImg; depthImageElement.value depthImg; } catch (error) { // ... 错误处理 }然后在DepthEstimator.vue的模板部分引入并添加ComparisonSlider组件。!-- 在 DepthEstimator.vue 的 template 中image-display div 之后添加 -- ComparisonSlider v-iforiginalImageElement depthImageElement :original-imageoriginalImageElement :depth-imagedepthImageElement /别忘了导入组件。import ComparisonSlider from ./ComparisonSlider.vue;现在你的应用就拥有了一个交互式的对比滑竿。拖动中间的白色圆点或使用底部的range输入条就能平滑地查看深度图与原图的对比效果了。5. 集成Three.js实现3D点云可视化这是最令人惊叹的部分——将二维的深度图转换成三维的点云。点云中的每个点都对应原图的一个像素其Z轴坐标深度由深度图中该像素的亮度值决定。我们创建一个新组件PointCloudViewer.vue。由于Three.js的初始化、渲染循环和资源管理稍复杂我们将逻辑封装在一个Composition API函数usePointCloud中以保持组件清晰。这里我们假设后端API在返回深度图的同时也能返回归一化的深度值数组JSON格式。如果只有深度图我们需要在前端通过Canvas读取像素值来近似计算这会影响性能。为了演示我们假设有深度数据。首先确保安装了Three.js (npm install three)。然后创建组件。!-- src/components/PointCloudViewer.vue -- template div classpoint-cloud-viewer v-ifisVisible h33D点云可视化/h3 p基于深度图生成的三维点云。使用鼠标拖拽旋转滚轮缩放。/p div classviewer-container refcontainerRef/div div classcontrols label input typecheckbox v-modelautoRotate / 自动旋转 /label button clickresetView重置视角/button div classhint提示: 拖动鼠标旋转滚轮缩放/div /div /div /template script setup import { ref, onMounted, onUnmounted, watch } from vue; import * as THREE from three; import { OrbitControls } from three/examples/jsm/controls/OrbitControls.js; const props defineProps({ originalImage: HTMLImageElement, depthData: { // 假设是归一化的深度值数组 [width * height] type: Array, default: null, }, imageWidth: Number, imageHeight: Number, }); const containerRef ref(null); const isVisible ref(false); const autoRotate ref(false); let scene, camera, renderer, controls, points, pointCloud null; let animationId null; // 初始化Three.js场景 const initThreeScene () { if (!containerRef.value) return; const width containerRef.value.clientWidth; const height 500; // 固定高度 // 场景 scene new THREE.Scene(); scene.background new THREE.Color(0x111122); // 相机 camera new THREE.PerspectiveCamera(60, width / height, 0.1, 1000); camera.position.z 5; // 渲染器 renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(width, height); containerRef.value.innerHTML ; // 清空容器 containerRef.value.appendChild(renderer.domElement); // 轨道控制器实现鼠标交互 controls new OrbitControls(camera, renderer.domElement); controls.enableDamping true; // 平滑阻尼效果 controls.dampingFactor 0.05; // 添加一些环境光 const ambientLight new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight); const directionalLight new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(10, 20, 15); scene.add(directionalLight); // 坐标轴辅助可选调试用 // const axesHelper new THREE.AxesHelper(5); // scene.add(axesHelper); }; // 根据深度数据创建点云 const createPointCloud () { if (!props.depthData || !props.imageWidth || !props.imageHeight) { console.warn(深度数据或图片尺寸缺失无法创建点云); return; } // 清理旧的点云 if (pointCloud) { scene.remove(pointCloud); if (pointCloud.geometry) pointCloud.geometry.dispose(); if (pointCloud.material) pointCloud.material.dispose(); } const width props.imageWidth; const height props.imageHeight; const depthData props.depthData; // 创建几何体包含所有顶点 const geometry new THREE.BufferGeometry(); const vertices []; const colors []; // 采样步长避免点太多导致性能问题 const step Math.max(1, Math.floor(Math.sqrt(width * height) / 200)); // 假设深度数据是归一化的0-1数组1代表最近 // 我们将深度映射到Z坐标并创建XY平面网格 const scale 5; // 整体缩放因子 const depthScale 2; // Z轴深度缩放因子 for (let y 0; y height; y step) { for (let x 0; x width; x step) { const idx y * width x; if (idx depthData.length) continue; const depth depthData[idx]; // 计算归一化坐标 (-0.5 到 0.5) const nx (x / width) - 0.5; const ny 0.5 - (y / height); // 翻转Y轴 const nz (1.0 - depth) * depthScale; // 深度值反转使近处凸起 vertices.push(nx * scale, ny * scale, nz); // 从原始图片获取颜色如果提供了原图 if (props.originalImage) { // 注意这里需要将图片绘制到Canvas来取色简化演示我们用一个基于深度的颜色 const color new THREE.Color(); color.setHSL(0.6, 1.0, 0.5 depth * 0.3); // 用HSL生成一个与深度相关的颜色 colors.push(color.r, color.g, color.b); } else { // 默认颜色 colors.push(0.2, 0.6, 1.0); } } } geometry.setAttribute(position, new THREE.Float32BufferAttribute(vertices, 3)); geometry.setAttribute(color, new THREE.Float32BufferAttribute(colors, 3)); // 创建点云材质 const material new THREE.PointsMaterial({ size: 0.03, vertexColors: true, // 使用顶点颜色 transparent: false, }); pointCloud new THREE.Points(geometry, material); scene.add(pointCloud); // 调整相机视角让点云在视野内 const box new THREE.Box3().setFromObject(pointCloud); const center box.getCenter(new THREE.Vector3()); const size box.getSize(new THREE.Vector3()); camera.position.set(center.x, center.y, size.z * 1.5); controls.target.copy(center); controls.update(); }; // 渲染循环 const animate () { animationId requestAnimationFrame(animate); if (autoRotate.value pointCloud) { pointCloud.rotation.y 0.002; } controls.update(); // 只有启用阻尼时才需要 renderer.render(scene, camera); }; // 重置视角 const resetView () { if (!pointCloud) return; const box new THREE.Box3().setFromObject(pointCloud); const center box.getCenter(new THREE.Vector3()); const size box.getSize(new THREE.Vector3()); camera.position.set(center.x, center.y, size.z * 1.5); controls.target.copy(center); controls.update(); }; // 监听深度数据变化 watch(() [props.depthData, props.imageWidth, props.imageHeight], () { if (props.depthData props.imageWidth props.imageHeight) { isVisible.value true; createPointCloud(); } else { isVisible.value false; } }, { immediate: true }); // 监听容器大小变化 const handleResize () { if (!containerRef.value || !camera || !renderer) return; const width containerRef.value.clientWidth; const height 500; camera.aspect width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height); }; onMounted(() { initThreeScene(); animate(); window.addEventListener(resize, handleResize); }); onUnmounted(() { if (animationId) cancelAnimationFrame(animationId); window.removeEventListener(resize, handleResize); // 清理Three.js资源 if (pointCloud) { scene.remove(pointCloud); pointCloud.geometry.dispose(); pointCloud.material.dispose(); } if (controls) controls.dispose(); if (renderer) renderer.dispose(); }); /script style scoped .point-cloud-viewer { margin-top: 50px; padding-top: 30px; border-top: 1px solid #eee; } .viewer-container { width: 100%; max-width: 800px; height: 500px; margin: 20px auto; border: 1px solid #444; border-radius: 4px; overflow: hidden; } .controls { max-width: 800px; margin: 15px auto; display: flex; gap: 20px; align-items: center; flex-wrap: wrap; } .controls label { display: flex; align-items: center; gap: 8px; } .controls button { padding: 8px 16px; background-color: #555; color: white; border: none; border-radius: 4px; cursor: pointer; } .hint { color: #777; font-size: 0.9em; } /style最后我们需要在DepthEstimator.vue中集成这个组件。这要求后端API除了返回深度图最好还能返回结构化的深度数据例如JSON格式的数组。我们需要修改estimateDepth方法来同时请求深度数据并传递给PointCloudViewer。假设后端提供两个端点一个返回图片一个返回JSON数据。或者一个端点通过查询参数返回不同格式。这里为了演示我们假设调用/api/depth-estimate?formatjson返回包含深度数据和图片尺寸的JSON。你需要根据实际后端API调整请求逻辑。在DepthEstimator.vue中添加新的响应式数据并修改请求。// 在 DepthEstimator.vue 的 script 部分添加 const depthDataArray ref(null); const depthImageWidth ref(0); const depthImageHeight ref(0); // 修改 estimateDepth 方法假设我们同时请求两种数据 const estimateDepth async () { if (!selectedFile.value) return; isLoading.value true; errorMessage.value ; depthDataArray.value null; // 清空旧数据 const formData new FormData(); formData.append(image, selectedFile.value); try { // 并行请求深度图和深度数据 const [imageResponse, dataResponse] await Promise.all([ axios.post(${API_BASE_URL}/api/depth-estimate?formatimage, formData, { headers: { Content-Type: multipart/form-data }, responseType: blob, }), axios.post(${API_BASE_URL}/api/depth-estimate?formatjson, formData, { headers: { Content-Type: multipart/form-data }, }), ]); // 处理深度图 const imageBlob new Blob([imageResponse.data], { type: image/png }); depthImageUrl.value URL.createObjectURL(imageBlob); // 处理深度数据 const result dataResponse.data; depthDataArray.value result.depth_map; // 假设JSON结构 { depth_map: [...], width: 640, height: 480 } depthImageWidth.value result.width; depthImageHeight.value result.height; // 加载图片元素用于滑竿 const [originalImg, depthImg] await Promise.all([ loadImage(originalImageUrl.value), loadImage(depthImageUrl.value) ]); originalImageElement.value originalImg; depthImageElement.value depthImg; } catch (error) { // ... 错误处理 } finally { isLoading.value false; } };然后在DepthEstimator.vue的模板中添加点云组件。!-- 在 ComparisonSlider 之后添加 -- PointCloudViewer v-ifdepthDataArray depthImageWidth depthImageHeight :depth-datadepthDataArray :image-widthdepthImageWidth :image-heightdepthImageHeight :original-imageoriginalImageElement /同样别忘了导入组件。import PointCloudViewer from ./PointCloudViewer.vue;至此一个功能相对完整的Vue前端深度估计效果展示平台就搭建完成了。它包含了图片上传、模型调用、2D对比滑竿和3D点云可视化四大核心功能。6. 总结与后续优化建议走完这一趟我们从零搭建了一个能生动展示深度估计模型效果的Vue应用。整个过程下来感觉最深的就是前端可视化对于理解AI模型输出确实太有帮助了。那个滑竿对比一下子就让深度图的意义变得非常直观而3D点云更是把二维图片里的空间关系立体地呈现了出来效果很惊艳。在实际使用中有几点我觉得可以再优化一下。一个是性能特别是点云部分如果图片分辨率很高生成的点数量会暴增可能导致页面卡顿。可以考虑在传给Three.js之前先在后台对深度数据进行降采样或者设置一个可调节的点云密度参数。另一个是后端交互我们这里假设了比较理想的API格式真实情况可能需要处理不同的返回格式、错误码甚至考虑分块上传大图片。最后是样式和用户体验可以加入更多的交互提示、加载动画以及移动端的触摸适配让整个应用更精致、更友好。如果你也想动手做一个类似的展示平台我的建议是从最简单的图片上传和显示开始确保和后端的通信畅通。然后再一步步添加滑竿、3D效果这些“炫技”功能。每完成一步都能立刻看到反馈这个过程本身就很有成就感。希望这个项目能给你带来一些启发让你也能把那些“黑盒”AI模型的能力用更直观、更互动的方式展现出来。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。