
拍一张照片能不能算出每个像素离镜头有多远这个问题听起来像科幻但这两年已经成了计算机视觉最繁荣的方向之一。Depth-Anything 模型把单目深度估计做到了接近开箱即用的水平而 Transformers.js 又把它直接从 Python 生态搬到了浏览器里。你不需要装深度学习环境打开一个网页就能把普通 RGB 图片转成深度图这在前端交互、视觉辅助、创意特效里都有很实际的用处。这篇文章我用跑过多次实战的角度把模型原理、浏览器端应用原理和可复现代码一次讲清楚新手也能跟着做。1. 单目深度估计这件事为什么这么难又这么有用1.1 从“看图猜距离”到“像素级测距”先回到基础概念。深度估计的目标是给图像里的每个像素一个距离值通常用深度图表示越近的地方越亮越远的地方越暗。工业上常见的方案有好几种比如结构光、双目立体视觉、激光雷达。它们都靠谱但都依赖特殊硬件。结构光要投射编码光斑双目要两个镜头做立体匹配激光雷达干脆直接发射激光脉冲。而单目深度估计只用一个普通摄像头、一张普通照片就要还原出场景的深度关系。这看起来像是在“猜”其实深度学习时代已经把它变成了一个可用的技术方向。Depth-Anything 就是这一类模型里非常典型的代表它不需要额外传感器也不需要多视角输入只要一张 RGB 图。为什么大家愿意研究单目深度估计因为成本太低了。手机、监控摄像头、行车记录仪、无人机图传全部都是单目设备。能在这些设备上直接得到深度信息就意味着可以给照片做背景虚化、给视障人士做障碍提醒、给机器人做初步避障、给电商商品图做三维展示。硬件的钱全省了软件模型来补。1.2 机器眼中的“深度”和人类有什么不同人类判断距离的方式其实很复杂有双眼视差、有运动视差、有焦点调节、有经验中的物体尺寸先验。看到一个站在门口的成年人你会下意识知道门框大概两米高这个人大概一米七。这种“先验知识”帮助你在单眼时也能估出远近比如你闭上一只眼照样能把手伸到杯子旁边。深度学习模型学到的也是类似的东西。它没法直接量出“这个墙角离我 3.2 米”但它会学习大量图像中的线索物体边缘、遮挡关系、透视消失点、阴影方向、相对大小、纹理梯度。Depth-Anything 这类模型输出的通常也是相对深度而不是绝对距离。相对深度描述的是“谁比谁近、近多少”绝对深度则是“距离镜头多少米”。想做增强现实遮挡、背景虚化、伪点云展示相对深度就已经够了想做自动驾驶精确测距还需要后续标定和尺度恢复。这里有个特别容易被新手误解的点深度图不是“一个数值”而是一张和原图同尺寸的灰度图。每个像素位置都存着一个浮点数代表该点的相对深度。拿到这张图之后你可以把它当成普通图像处理也可以把数值提取出来做三维坐标运算。1.3 Depth-Anything 解决的核心痛点在 Depth-Anything 出现之前单目深度估计领域有一个尴尬局面模型结构不缺缺的是数据。深度估计是稠密预测任务需要逐像素标注人工标注深度图的成本比目标检测的框标注高太多了。已有的公开数据集要么是室内场景要么是驾驶场景场景一旦切换模型精度立刻下滑。很多模型在某个数据集上指标很漂亮换到自己拍的照片上效果惨不忍睹。Depth-Anything 的思路是把“模型结构”和“训练策略”分开解决。它不追求发明一个全新的网络结构而是把注意力放在如何利用海量无标注真实图片上。先在一个标注相对充足的数据集上训练出一个基础教师模型再用这个教师模型去给千万级的无标注真实图片生成伪深度标签最后拿这些伪标签继续训练学生模型。这样模型见过的场景足够多泛化能力就上来了。这套策略带来的直接收益是你用手机随便拍一张环境杂乱的室内照片模型出来的深度图仍然能清晰区分桌面、椅子、墙壁和窗户。对应用开发者来说泛化能力比刷榜指标更重要。毕竟用户不会专门挑选跟数据集同分布的照片来测试你的功能。2. Depth-Anything 模型是怎么把“深度”学出来的2.1 编码器解码器结构先理解全局再恢复细节Depth-Anything 的整体结构用一句话概括ViT 风格的编码器负责提取全局特征DPT 风格的解码器负责把特征逐步还原成和原图同分辨率的深度图。先看编码器。它采用 Vision Transformer 架构把图像切成一连串的小块每块 14×14 或 16×16 像素然后通过自注意力机制建模各个小块之间的关系。自注意力的好处是感受野天然就是全局的图像左上角的小块可以直接和右下角的小块建立联系。这对深度估计特别重要因为判断一个像素的深度很多时候要看整张图的上下文。比如一个白色物体局部看无法判断它是墙还是天空但如果你看到它下面有一排树就能猜到它大概率是远处的建筑立面。解码器这边DPT 结构会把编码器不同层输出的特征重新组织起来。浅层特征分辨率高、包含边缘和纹理细节深层特征分辨率低、语义更强但包含更抽象的几何关系。DPT 的做法是把不同层级的特征通过拼接和上采样融合到一起让最终输出的深度图既有清晰的物体边界又有正确的远近层级。这个“编码器加解码器”的组合在视觉任务里很常见但 Depth-Anything 的特别之处在于编码器的预训练权重视觉能力非常强。一个在海量图片上做过自监督训练的 ViT 模型本身就懂很多纹理、轮廓和语义知识。深度估计只是把这些知识往“距离”这个维度上做了一次迁移。2.2 数据策略伪标签为什么能救命伪标签并不是新技术但在深度估计上用好它需要很小心。我拆解一下 Depth-Anything 的训练流程你就明白这件事为什么有效。第一步用带真实标注的混合数据集训练一个教师模型。所谓混合数据集就是把室内、室外、驾驶、街景等多种来源的标注数据拼在一起让模型先具备基础的深度感知能力。第二步把教师模型运行在海量无标注的真实图片上为每张图生成一份伪深度图。这些图片来自互联网、视频帧、街景采集等等分布远比人工标注数据集丰富。第三步用这些伪标签重新训练学生模型让学到的深度先验进一步泛化。关键技巧在于教师模型和训练数据的选择有讲究。如果教师模型本身精度不够伪标签里全是噪声学生模型学得越多反而越糊涂。Depth-Anything 的团队选择了泛化能力强的模型作为教师并且在伪标签生成时特意保留真实标注数据一起参与训练避免学生模型被伪标签带偏。用生活化的方式理解这就像一位老师傅先教实习生基本功然后实习生去大量真实项目里练习每做一个项目老师傅都会给反馈意见最后实习生的能力反而超过了老师傅在某些新场景下的表现。伪标签的“伪”不代表无效它只是没有人工标注那么精确但胜在数量巨大、场景多样。2.3 相对深度与绝对尺度输出到底能拿来做什么很多人在看深度图时会有个疑问这个值是米吗答案是默认不是。Depth-Anything 这类模型训练时大多采用相对深度表示输出值没有固定的度量单位只有大小关系是有意义的。理解这一点非常重要因为它直接影响你如何使用结果。做背景虚化时你需要的是主体和背景的深度差异相对深度就够。做 CSS 视差滚动时你只需要一个 0 到 1 的层深度相对深度完全胜任。但如果你想测量真实距离比如判断一个物体距离摄像头 3 米还是 5 米那就需要用额外的先验来做尺度恢复。常见做法是在场景里放置一个已知尺寸的参照物比如一个人的身高、一扇门的宽度用参照物的像素尺寸推算出整体尺度。另外要注意相对深度图的最小值和最大值不是固定的。不同图片输出的数值范围可能差异很大所以在可视化时一定要做归一化不然有的图片看起来一片白有的看起来一片黑。2.4 V1 和 V2 的差异该怎么选Depth-Anything 有 V1 和 V2 两个版本我在实际使用中感受最明显的是 V2 对边缘细节保留得更好。V1 的优势是参数配置成熟、社区里参考资料多、模型文件容易找到。它的整体深度结构已经不错但细长物体、栏杆、树枝这类高频细节容易糊成一片。V2 做了两个方向的改进一是教师模型更强伪标签质量更高二是在推理时对细节恢复做了优化输出的深度图边缘更锐利。如果你开发的是偏展示类的应用用 V2 会更讨喜。如果你需要把模型集成进已有的 Transformers.js 流程里那么 V1 的 small 版本因为文件小、加载快依然值得保留尤其适合移动端。我给一个简单的选择建议追求极致效果用 V2 large追求响应速度用 V1 small 或 V2 small追求均衡表现选 base。模型版本参数量级推理速度边缘细节适用场景V1 small约 25M快相对模糊移动端实时预览、低配设备V1 base约 100M中等中等桌面端互动效果V1 large约 335M慢较清晰离线批处理、高精度展示V2 small约 25M快较清晰移动端与 Web 端均衡选择V2 base约 100M中等清晰网页应用主力版本V2 large约 335M慢最清晰复杂场景高精度还原以上参数量级是大约值实际以你下载的模型文件内容为准。选型时不要只看参数还要结合浏览器运行时内存考虑因为大模型在低端手机上很容易被浏览器直接杀掉。3. Transformers.js 为浏览器带来的推理能力3.1 一套 API 把 Python 模型变成前端能力Transformers.js 是一个在前端生态里执行 Transformer 模型的库。它的核心思想是把 Python 生态里训练好的模型转换成浏览器可以运行的格式然后用 JavaScript 调用整个过程不需要后端服务也不需要 Python 运行时。它和深度估计的关系非常直接Depth-Anything 模型是 Python 生态训练出来的原始权重没法直接让浏览器识别。Transformers.js 提供了模型转换和加载的完整链路。你只需要引入库调用一个深度估计管线函数剩下的预处理、推理、后处理全都封装好了。对前端开发者来说这意味着一项能力从“需要运维 GPU 服务器”变成了“纯前端功能”。用户上传一张照片浏览器本地就能算出深度图照片不需要离开用户设备既保护隐私又省服务器带宽。对于原型验证和小型应用来说这种方式开发效率极高因为前端天然就能处理图像展示和用户交互不需要额外写一套前后端接口。3.2 运行时后端Wasm 和 WebGPU 是怎么工作的Transformer 模型在浏览器里不能直接跑需要一个可执行的运行时。Transformers.js 支持多种后端最常见的是 WebAssembly 和 WebGPU。WebAssembly 提供了一个接近原生性能的沙箱执行环境。它兼容性最好几乎所有现代浏览器都支持CPU 上就能运行但速度相对有限。WebGPU 则调用 GPU 进行并行计算矩阵乘法这类深度学习的核心操作可以在显卡上加速速度可能比 CPU 快好几倍。不过 WebGPU 对浏览器版本和设备有要求老设备不一定能用。我的建议是代码里不要把后端写死而是让 Transformers.js 自动检测。检测到 WebGPU 就用 GPU检测不到就回退到 Wasm。这样在主流环境里体验最好在老旧设备上也能保证功能可用只是慢一点。3.3 模型要怎么转换才能被前端使用从原始 PyTorch 权重到浏览器可加载的模型中间有一个转换步骤。Transformers.js 生态通常使用 ONNX 格式作为中间表示。ONNX 是一个开放的模型交换格式可以把 PyTorch 的模型结构连同权重一起导出然后再通过前端运行时加载。转换时最需要注意的是算子兼容性。模型里某些自定义算子可能 ONNX 不支持需要改写或者替换。Depth-Anything 本身的模块比较标准编码器、解码器结构都是常规算子所以转换比较顺利。如果你自己微调过模型或者加了自定义量化层就必须在转换前做一次完整的算子检查。转换之后还可以做量化。ONNX 支持把模型从 FP32 压缩成 FP16 甚至 INT8。FP16 精度损失很小推理速度提升明显INT8 文件更小但在某些层上精度损失可能让深度边缘变得粗糙。前端应用我建议优先试 FP16如果模型文件还是太大再考虑 INT8。量化的选择没有绝对答案最终要看你的应用对深度图细节的容忍度。3.4 为什么还要关注模型加载和缓存浏览器运行时有一个特殊问题模型文件需要从服务器下载到本地这个下载时间可能比推理时间还长。一个 V1 large 模型差不多 300 MB 左右就算放在 CDN 上用户首次打开页面也要等待很久。解决思路有几个。第一按需加载不要一进页面就把大模型拉下来等用户真正点击“开始处理”再加载。第二利用浏览器缓存让模型文件在首次加载后被缓存住第二次访问直接走本地缓存。第三用流式读取接口模型边下载边初始化配合进度条让用户感知到在加载而不是卡死。这些细节在 Demo 里无所谓但做成正式产品就必须考虑。我见过很多项目在本地开发环境跑得飞快部署上线后用户反馈“页面打不开”其实就是模型加载这块没做进度反馈用户以为网站挂了。4. 用 Transformers.js 跑通 Depth-Anything手把手实操4.1 最小环境准备我建议直接在 Node.js 项目里实验注意这里 Node.js 只是用来搭建本地调试服务最终模型依然在浏览器里跑。先建一个项目目录初始化 npm然后安装 Transformers.jsmkdir depth-demo cd depth-demo npm init -y npm install huggingface/transformers如果你不想用 Node 环境也可以直接通过 CDN 在 HTML 里引入script typemodule import { pipeline } from https://cdn.jsdelivr.net/npm/huggingface/transformers2.17.2; // 后续代码 /script这里我刻意锁定了一个常见版本号实际使用时请去官方页面确认最新版本。版本差异可能导致 API 名称变化尤其是早期版本的env.allowLocalModels等配置项在新版本里可能会有不同的默认值。4.2 第一次推理三行代码拿到深度图最小可用代码非常简单。核心就三步创建管线、输入图像、读取结果。import { pipeline } from huggingface/transformers; // 创建深度估计管线 const estimator await pipeline(depth-estimation, Xenova/depth-anything-small-hf); // 输入图片可以是 URL、本地文件或图片元素 const result await estimator(./input.jpg); // 预览深度图 await result.depth.save(./output.png);第一行pipeline是 Transformers.js 提供的高层封装。第一个参数depth-estimation表示任务类型第二个参数是模型标识。模型标识对应前端仓库里已经转换好的 ONNX 权重。small版本适合测试体积小、加载快。result对象里通常包含两类数据一类是深度图和普通图片一样有width、height、data属性另一类是原始张量数据适合做后续数学运算。直接保存result.depth就能得到一灰度图高亮区域表示近处暗色区域表示远处。如果运行环境是在浏览器里你可以直接把深度图画到 Canvas 上或者用drawImage塞进页面展示。注意深度图默认是单通道灰度值如果直接显示可能会因为颜色映射关系看起来灰蒙蒙的需要自己做伪彩色处理。4.3 把深度图变成伪彩色从数据到可视化默认的灰度深度图在视觉冲击力上不够强。为了让用户一眼看出远近关系我习惯把它转成伪彩色图。伪彩色就是把深度值映射到一条颜色渐变上比如近处用暖色调、远处用冷色调。在拿到result.depth.data之后遍历像素数据找到最小值和最大值再把每个像素归一化到 0 到 1 之间最后查询颜色映射表。下面这段代码可以在浏览器 Canvas 里渲染function renderDepthToCanvas(depth, canvas) { const ctx canvas.getContext(2d); const { width, height, data } depth; canvas.width width; canvas.height height; const imageData ctx.createImageData(width, height); let min Infinity; let max -Infinity; for (let i 0; i data.length; i) { if (data[i] min) min data[i]; if (data[i] max) max data[i]; } const range max - min || 1; for (let i 0; i data.length; i) { const t (data[i] - min) / range; // 一个简单的暖到冷映射近处红黄远处蓝青 imageData.data[i * 4] Math.floor(255 * (1 - t) 120 * t); imageData.data[i * 4 1] Math.floor(120 * (1 - t) 200 * t); imageData.data[i * 4 2] Math.floor(0 * (1 - t) 255 * t); imageData.data[i * 4 3] 255; } ctx.putImageData(imageData, 0, 0); }这个小工具函数我一直在用它不依赖任何第三方绘图库纯 JavaScript 就能跑。核心是先归一化再做颜色映射。你要是嫌麻烦也可以用现成的 ColorMap 库效果通常更专业但我觉得理解原理更重要。4.4 用深度图生成简单点云把 2D 照片变 3D深度图最有意思的应用之一是根据像素的灰度值推算三维坐标。假设相机是标准针孔模型那么图像上每个像素的深度值 d 可以换算成三维空间中的点const fx 500; // 假设焦距也可以用图片宽度估算 const fy 500; const cx width / 2; const cy height / 2; for (let y 0; y height; y) { for (let x 0; x width; x) { const d depthData[y * width x]; const z d; const X (x - cx) * z / fx; const Y (y - cy) * z / fy; // 保存 X, Y, z 到点云数组 } }这段代码把图片上的每个像素映射成一个三维点。因为是相对深度所以生成的模型在空间尺度上不是物理准确的但形状和相对位置是对的。你可以在 Three.js 里把这些点渲染成粒子系统做出“照片变立体”的效果。从一张普通照片直接能玩出这种效果对很多创作者来说已经够惊艳了。实际做的时候我建议先降采样比如把图缩到 256 像素宽不然几百万个点对浏览器渲染压力很大页面会卡到没法看。4.5 性能优化从加载到推理的完整链路跑通最小 Demo 之后接下来就要考虑性能了。我自己总结了一套优化顺序效果最明显的是这几点。第一输入图像要控制尺寸。Depth-Anything 对输入尺寸有推荐值常见是 518×518 或 384×384但你在浏览器里得到一张 4000×3000 的照片时直接送进模型会非常慢。应该先裁剪或缩放到模型合适的输入范围比如最长边缩到 518。缩得太小会损失细节但缩到推荐尺寸以上只会增加计算量对精度提升有限。第二复杂任务放 Worker 里执行。Transformers.js 的推理会占用主线程导致页面卡顿。用 Web Worker 把模型初始化和推理都放进去主线程只负责接收结果和渲染界面用户操作会流畅很多。值得注意Worker 里使用 Canvas 和图片解码有一些限制需要把图片转成 ImageData 或 Blob 再传进去。第三打开 WebGPU 后端。前面提到 WebGPU 能明显加速启用方法通常是在初始化阶段传入设备配置。你可以先写一个能力检测如果浏览器支持 WebGPU 就默认启用不支持就回退到 Wasm。用户设备多样性决定了后端选择不能一刀切。第四模型加载进度一定要给反馈。用 Transformers.js 的进度事件配合前端进度条避免用户面对空白页面。数据量越大的模型这个体验优化越重要。5. 我踩过的坑和避坑清单5.1 图片加载的 CORS 问题比想象中来得早第一次在浏览器里测试时我直接传了一个外链图片Control 台立刻报错。原因不是模型而是 Canvas 和像素读取的跨域限制。深度估计要把图片解码成像素数据如果图片来自不同域且没有正确的跨域头浏览器会果断拦截。解决办法也很简单要么把图片转成同源资源要么在你的服务器上增加 CORS 响应头要么用 FileReader 把用户上传的文件读成 Data URL 再进入模型。如果你只是做本地演示直接把图片放在和页面同一个目录下就能绕开这个问题。我在项目里更推荐让用户上传文件或者拖拽图片这样用户设备上的照片不需要经过网络隐私性更好也彻底回避了跨域问题。5.2 深度图“发灰”和“过曝”的困惑有段时间我很困惑为什么同一张图在不同模型版本里深度图的明暗风格完全不一样后来才意识到这就是相对深度的特点。模型输出的数值分布每次都不完全相同有些图片的最小值接近 0有些最小值是 -1 甚至更负。解决方法是保存和可视化阶段都要做归一化。不要直接用原始浮点数据里的极值去推颜色而是先做百分位裁剪。所谓百分位裁剪就是把最亮和最暗的 1% 像素砍掉避免单个极端像素毁掉整张图的对比度。这个技巧在图像处理里很常见拿来做深度图可视化同样有效。另外深度图在物体边缘容易出现模糊。尤其是小模型它在同色区域之间容易把前背景混在一起。如果你要拿深度图去做精细抠图建议尝试 V2 版本或者对结果再做一次边缘锐化处理。5.3 浏览器内存崩掉被 large 模型教训了一次我在一个演示项目里默认加载了 large 模型结果在性能普通的笔记本上页面每次加载都会卡死一段时间。后来排查发现模型文件先占了内存推理时再把整张图片和中间特征图放进内存峰值轻松超过 2 GB。浏览器标签页直接崩溃。从那以后我学乖了演示项目用 small正式产品用 baselarge 只留给离线批处理脚本。如果你确实需要 large 的精度可以在一台高配设备上把深度图预计算好存成图片或 JSON 再给前端使用而不是让每个用户都在浏览器里跑 large 推理。另外请记住用户的浏览器标签页同时打开很多网站你的页面占用过多内存会直接导致其他标签页崩溃这种体验很伤。及时释放不再需要的张量也可以帮上一点忙。5.4 常见问题速查表问题表现可能原因解决办法图片无法加载跨域限制使用同源图片、Data URL 或配置 CORS深度图整体灰暗没有归一化先做百分位裁剪再映射颜色推理速度极慢图片分辨率过大缩放至 518 或 384 边长页面卡死大模型加载换 small/base 模型或用 Worker边缘模糊模型太小换 V2 或 base/large 版本颜色映射发糊线性映射过于简单使用 Turbo 或 inferno 颜色表模型下载太慢文件太大启用进度提示或做量化压缩WebGPU 无法启用浏览器不支持自动回退到 Wasm 后端这张表基本覆盖了我遇到的九成问题。遇到问题时先对照一下能省不少时间。5.5 移动端的特殊注意事项移动端跑深度估计问题主要集中在内存和发热。同样是 small 模型桌面浏览器和手机浏览器的性能差距非常大。我在手机上测试过V1 small 虽然能跑但推理时手机会明显发烫连续处理多张图后页面可能被系统回收。如果你的目标用户是移动端我建议把图像最长边控制在 384 以内并且限制用户连续处理的数量。同时尽量在 Worker 里执行推理避免阻塞页面滚动。还有一个容易被忽略的点手机浏览器的标签页切换会导致后台标签页节流模型加载和推理都可能被暂停所以进度提示要写得更稳妥一些。5.6 把深度估计能力接到趣味交互里深度估计不只是一个离线的图像处理工具它非常适合做交互。我最喜欢的一个应用是把鼠标移动和深度图绑定鼠标悬停在某个区域时根据该区域的深度值调整元素位移、模糊程度或明暗关系。这会让人产生一种“画面真的变立体了”的错觉。实现起来不难把第 4.3 节的深度数据存成数组然后监听鼠标位置取出对应索引的深度值乘以一个缩放系数应用到 CSS 变换上。比如element.style.transform translateZ(${depthValue * 20}px);这里我要提醒一句translateZ 只有在三维渲染上下文里才会有立体效果普通文档流里不会生效。所以更好的做法是把视觉元素放进一个开启perspective的容器里让深度差真正表现在透视缩放上。交互应用的想象力很大电商商品页根据深度做视差展示相册 App 根据深度给照片做动态背景盲人辅助工具根据深度提示障碍物的方位。这些都是单目深度估计和前端结合的自然落点。我个人测试下来的体会是Depth-Anything 加上 Transformers.js 的组合是把一项曾经需要专属硬件的技术变成了普通的 Web 功能。模型的输出不一定完美但相对深度已经足够支撑大量创意应用。你在做的时候不用追求物理级精确先把相对关系用好看、用流畅效果往往就非常惊艳。最后再分享一个小技巧把深度数据用 JSON 格式缓存下来同一个用户切换展示模式时就不用重新推理这个简单的缓存策略在实际项目中非常实用。