恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Depth-Anything-V2深度解析:从单目深度估计到尺度标定的完整实践
首页
资讯中心
/
Depth-Anything-V2深度解析:从单目深度估计到尺度标定的完整实践
Depth-Anything-V2深度解析:从单目深度估计到尺度标定的完整实践
发布时间:2026/10/13 1:55:40
简介面向计算机视觉研究与工程开发者的单目深度估计项目 Depth-Anything-V2利用深度学习从单张图像恢复场景深度可应用于机器人导航、增强现实、三维重建等场景。压缩包内共 99 个文件大小约 89.99MB主体为 38 个 Python 脚本和 20 张示例图片脚本覆盖模型定义、训练与推理、视频/点云处理等环节另有样例数据、演示视频、Markdown 说明、配置文件与 License便于复现与二次开发。压缩包结构清晰资源提供 metric_depth 模块及多个入口脚本examples 目录可直观对比不同场景下的深度输出效果README 也标明了使用方式。目前已有 238 人浏览学习适合想系统上手最新深度估计模型、理解单目深度估计原理并快速开展实验的中高级开发者拿到包后可直接从 run.py、app.py 等入口体验也可参考训练脚本调整模型。1. Depth-Anything-V2 是什么一张普通照片凭什么能估出连续深度某开发者第一次跑 Depth-Anything-V2 的时候输入是一张随手拍的雨天街道照片没有深度传感器没有点云输出却是一张逐像素连续变化的深度图近处的车、路面的反光、远处的楼全都分开了层级。这就是单目深度估计——用一张 RGB 图直接推断每个像素的远近。Depth-Anything-V2 是这个方向上目前非常值得试跑的模型版本它用视觉 Transformer 做主干以零样本泛化和边缘细节锐利著称。简单说V2 在上一代的基础上调整了训练目标和深度头的设计让输出的相对深度更稳定、细节更清楚尤其在室内场景里不会把墙和家具糊成同一层。这篇内容适合三类人做三维重建想快速获取单张图深度先验的做避障或分割不想依赖深度相机的以及做过 MiDaS、嫌弃旧模型细节太肉想换方案的人。后面每一章都围绕一个具体问题展开——指标怎么读、命令怎么写、微调怎么配、上线前会踩哪些坑。读完你至少能独立跑通一条从单张图到深度图再到尺度标定的完整链路。2. 先搞清楚 Depth-Anything-V2 的底细单目深度估计为什么选它不选 MiDaS 或前作2.1 模型结构ViT 编码器加 DPT 解码头它到底在算什么单目深度估计的本质是一个回归任务输入一张三通道图片输出一个和原图同分辨率的单通道图每个像素值表示该点到相机的远近。传统做法是设计一个编码器提取多尺度特征再接一个上采样头逐像素回归深度。Depth-Anything-V2 沿用了这个思路但把编码器换成了视觉 Transformer解码器用的是 DPT 风格的密集预测头。ViT 的好处是能建模全局依赖判断一片区域是墙面还是走廊往往需要看整张图的上下文而不是只看局部纹理。V2 和前作最明显的变化公开资料里通常集中在两点一是编码器的预训练策略调整让模型在不同数据域之间迁移时更稳二是深度解码头对局部细节的保留更好边缘不再是一条条模糊的渐变带。实际跑起来你会发现家具轮廓、人体边缘、树叶缝隙这些地方V2 的深度边界比老方案干净得多这一点对后续做点云后处理非常关键因为深度边缘糊了三维重建的物体边界就会跟着糊。2.2 精度与速度的取舍vit-s、vit-b、vit-l 三档怎么选Depth-Anything-V2 的公开权重通常按编码器尺寸分成几档vit-s 最小vit-l 最大。选型时先回答三个问题你的输入分辨率是多少显存预算有多少延迟容忍度是多少。我这里给一个常见的经验对应关系具体数字会随推理框架和显卡变化但趋势是稳定的。编码器建议输入分辨率显存占用趋势典型使用场景vit-s512 以内低2GB 级可跑移动端、实时预览、批量离线任务vit-b512~1024中等6GB 级可跑桌面工具、科研验证、边缘设备vit-l1024 以上高建议 10GB高质量三维重建、离线精细深度我一般的做法是先用 vit-b 把完整流程走通确认效果满足需求后再决定要不要升 vit-l。很多人一上来直接上 vit-l 加 2048 分辨率结果显存爆了又回去降分辨率折腾半天。记住一个原则单目深度估计对分辨率的敏感度很高低分辨率下边缘信息会被直接抹掉与其用最大的模型跑小图不如用中等模型跑大图。2.3 和常见替代方案对比读指标也要读适用边界很多人会拿 Depth-Anything-V2 和 MiDaS 对比。MiDaS 是单目深度估计里一个绕不开的经典方案它的优势是生态成熟、小模型多、部署资料好找。V2 的优势主要体现在两个方面零样本泛化能力更强换个没见过的数据域不用微调也能出相对合理的深度细节保持更好薄物体和复杂边缘不容易糊成一片。如果你的任务是室内外通用深度估计V2 通常起步体验更好。另一类对比对象是真正输出绝对深度的方案比如基于双目或结构光的深度相机。这个要分清楚Depth-Anything-V2 输出的是相对深度它知道 A 比 B 近但不知道 A 距离相机是 1 米还是 2 米。没有标定或额外先验你不能直接拿它去算物理尺寸。这一点会在后面专门讲怎么救。选型时先想清楚你要的是分得清远近还是量得出距离前者 V2 直接能用后者需要加一层尺度标定。3. 用官方推理脚本跑通第一帧最小命令与逐行参数3.1 准备环境与权重依赖、模型文件、编码器对应关系建议准备一台带 NVIDIA 显卡的 Linux 机器显存不用很大6GB 跑 vit-b 就够。环境上需要 Python 3.8 以上的解释器、PyTorch 的稳定版、TorchVision、OpenCV 的 Python 包以及 NumPy。这些依赖用 pip 装就行不需要额外编译什么扩展。如果只有 CPU也能跑只是 vit-l 会慢到让人失去耐心vit-b 勉强可以接受。权重文件按编码器分档存放去模型仓库页找到对应 vit-s、vit-b、vit-l 的 checkpoint 下下来放到一个 models 目录下。下载后先别急着跑看一眼文件大小和 SHA 校验值这一步能省掉后面大半的玄学报错。常见翻车现场是有人把 vit-b 的权重配给 vit-l 的模型结构加载时直接报 key 不匹配或者更隐蔽的权重能加载进去但输出的深度图一团糟。# 示例目录结构 Depth-Anything-V2/ ├── run.py ├── models/ │ ├── depth_anything_v2_vits.pth │ ├── depth_anything_v2_vitb.pth │ └── depth_anything_v2_vitl.pth └── demo.jpg3.2 推理命令与输出产物图片进深度图出仓库里通常带一个推理入口脚本参数一般包括编码器类型、输入图片路径、输出目录和可选的深度上限。以 vit-b 为例命令大致长这样python run.py --encoder vitb --img-path ./demo.jpg --outdir ./output --max-depth 20逻辑说明这个命令加载 models 目录下对应的 vitb 权重把 demo.jpg 预处理成模型要求的尺寸和通道顺序前向推理得到深度图再保存到 output 目录。--max-depth 参数控制的是可视化时的深度映射范围它不影响模型内部的推理只影响导出图的明暗分布。调小这个值会让近处物体的亮度拉开差距方便直接肉眼检查。参数说明--encoder 必须和权重文件对应vitb 就配 vitb 权重--img-path 可以指向单张图片或一个存放多张图片的目录--outdir 不存在时会自动创建。如果跑完发现输出目录里只有图没有数值文件说明脚本默认只做了可视化需要自己写保存逻辑时往下看 3.3 节。# 验证输出的快速方式 python -c from PIL import Image; im Image.open(output/demo.png); print(im.size, im.mode, im.getextrema())这段命令用来确认输出图片的尺寸、通道模式和像素值范围。合理的结果通常是单通道灰度图像素范围有明显的明暗分布。如果 getextrema 返回 (0, 0) 或 (255, 255)说明深度图没有正常写出来先检查输入图片是否成功加载。3.3 自己写加载脚本时的通用写法状态字典 key 与输入预处理跑通官方脚本只是第一步产品化的时候你大概率要自己写加载逻辑。核心就三件事加载权重、前向推理、后处理保存。下面是一个通用骨架模型定义部分以你自己下载到的代码为准注意对齐状态字典的 key 名。import torch import torchvision.transforms as T import numpy as np from PIL import Image # 1. 加载模型 model build_depth_model() # 用仓库里的模型定义函数 state_dict torch.load(models/depth_anything_v2_vitb.pth, map_locationcuda) model.load_state_dict(state_dict, strictTrue) model model.cuda().eval() # 2. 预处理图像缩放 归一化 transform T.Compose([ T.Resize((518, 518)), # 常见输入大小可调 T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) img Image.open(demo.jpg).convert(RGB) x transform(img).unsqueeze(0).cuda() # 3. 推理与后处理 with torch.no_grad(): depth model(x) # 形状: [B, 1, H, W] depth depth.squeeze().cpu().numpy() depth (depth - depth.min()) / (depth.max() - depth.min()) # 归一化到0-1 depth_16bit (depth * 65535).astype(np.uint16) Image.fromarray(depth_16bit).save(output/depth_16bit.png)逻辑说明模型定义函数从仓库代码里拿不要自己重写结构加载权重时 strictTrue 能立刻暴露 key 不匹配的问题。预处理里的均值标准差用的是 ImageNet 常见统计值如果你的输入是灰度图或特殊传感器图要重新算。后处理里做了最小最大归一化这一步让深度图变成 0 到 1 的相对值方便统一保存格式。参数说明Resize 到 518 是一个兼顾速度和质量的经验值不要小于 384否则边缘细节会大量丢失保存成 16 位 PNG 是为了不损失深度精度8 位图在暗部会有明显断层。如果你后续要做尺度标定或三维重建建议直接保存成 npy 格式保留原始浮点深度不要走 PNG 的整型量化。4. 微调 Depth-Anything-V2冻结主干还是全量微调损失函数怎么配4.1 什么时候该微调什么时候不该微调零样本泛化强不等于万能。Depth-Anything-V2 在自然场景、室内场景上表现很好但换到了一些它没见过的高反差、非朗伯体或严重遮挡场景就会翻车。最典型的是水下摄影、内窥镜画面、工业零件特写这类域差距大的数据模型可能把近处的透明玻璃当成没有深度的区域或者把雾气当成统一的前景。遇到这种情况第一反应不应该是改后处理而是收集一批目标域数据做微调。那什么时候不该微调如果你的目标域和自然图像差别不大比如室内监控、车载前视、机器人巡视V2 的表现通常已经足够微调反而可能引入过拟合。另外一个常见判断标准是先跑一批测试图看深度图是否合理如果 80% 的图肉眼看着都对就别动模型去解决尺度标定问题更划算。微调是会消耗标注成本和时间成本的不是默认动作。4.2 数据组织与标注格式深度图怎么进训练循环微调需要成对数据一张 RGB 图配一张深度图。深度图可以是真实传感器采集的也可以是现有单目模型生成的伪标签但伪标签的精度上限受限于生成模型本身。数据组织上常见做法是一个目录放 RGB另一个目录放深度图文件名一一对应。深度图的格式要统一如果用的是 16 位 PNG要明确它的缩放系数比如某个数据集里像素值除以 256 才是真实米数这个系数一旦在训练和验证时不一致损失函数会直接乱掉。import numpy as np from PIL import Image import torch from torch.utils.data import Dataset class DepthDataset(Dataset): def __init__(self, rgb_dir, depth_dir, scale1.0): self.rgb_paths sorted(rgb_dir.iterdir()) self.depth_paths sorted(depth_dir.iterdir()) self.scale scale def __getitem__(self, idx): rgb Image.open(self.rgb_paths[idx]).convert(RGB) depth Image.open(self.depth_paths[idx]) depth np.array(depth).astype(np.float32) / self.scale # 深度图中的无效区域常见做法是置为0并记录mask mask (depth 0).astype(np.float32) rgb T.Compose([T.Resize((518, 518)), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225])])(rgb) depth torch.from_numpy(depth).unsqueeze(0) mask torch.from_numpy(mask).unsqueeze(0) return rgb, depth, mask逻辑说明这个 Dataset 的核心是把深度图读成浮点数并除以缩放系数转成和 RGB 相同空间尺寸的张量。mask 的构造很关键深度传感器在反光面、远距离、黑物体上经常给不出有效值这些位置如果参与损失计算会把模型往错误方向拉。RGB 的预处理要和推理时保持一致否则训练和测试的输入分布不匹配。参数说明scale 参数完全取决于你的深度图来源可以在数据准备阶段先用一个已知深度的场景验证。Resize 尺寸训练时用 518 或稍低都可以但如果推理时用的是其他尺寸两者差距不要太大否则深度头的感受野会不匹配。mask 里被置零的区域在损失函数里也要显式排除。4.3 最小微调脚本骨架与三个必调参数微调策略上我倾向于先冻结主干只训练深度解码头。原因是 ViT 主干在预训练阶段已经学到了足够通用的视觉特征目标域和自然图像的差距主要体现在深度头如何把这些特征映射成深度。全量微调在数据量不足时很容易过拟合还会让模型忘记通用的深度先验。等冻结训练收敛后如果你还有余量数据再解冻最后两层 ViT 做低学习率的精调。model build_depth_model() state_dict torch.load(models/depth_anything_v2_vitb.pth) model.load_state_dict(state_dict) # 冻结主干 for name, param in model.named_parameters(): if head not in name: # 按实际模块名调整 param.requires_grad False optimizer torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr1e-4, weight_decay0.01 ) criterion silog_loss # 尺度不变对数损失 for epoch in range(30): for rgb, depth, mask in dataloader: pred model(rgb) loss criterion(pred, depth, mask) optimizer.zero_grad() loss.backward() optimizer.step()逻辑说明冻结主干的关键是按模块名过滤参数不同仓库的命名不一样一定要先打印出参数名看清哪些属于 depth head。SILog 损失函数的核心思想是用对数空间里的误差和方差来评估深度对相对深度特别友好它不要求预测值和真实值在绝对尺度上对齐只要求相对远近关系正确这和 Depth-Anything-V2 的输出特性是吻合的。参数说明学习率 1e-4 是个保守起点如果 loss 降得太慢可以提到 5e-4但微调阶段超过 1e-3 很容易让 ViT 的权重产生灾难性遗忘。weight_decay 默认 0.01 不用动。批量大小取决于显存vit-b 下 4 到 8 比较常见批量太小的时候建议用梯度累积模拟更大的 batch。训练过程中每个 epoch 结束要在验证集上算一次深度指标而不是只看训练 loss。5. 避坑Depth-Anything-V2 上线前的 6 个常见拦路点5.1 现象、原因、解决六条血泪经验第一条坑输出深度图全黑或者全白。现象是跑完推理后保存的图片要么一片漆黑要么一片惨白肉眼根本看不出结构。原因几乎都出在值域处理上模型输出是浮点张量范围可能是负值也可能集中在很窄的区间直接转成 8 位整数后全被截断到 0 或 255。解决方法是先做最小最大归一化再量化或者保存成 16 位 PNG 和 npy 保留原始范围不要在 8 位量化上省事。第二条坑加载权重报错提示 key name 不匹配。现象是 load_state_dict 时出现 missing keys 和 unexpected keys 一大片。原因多半是编码器型号选错或者把不同版本的权重混用。解决方法是先确认 checkpoint 里的 key 前缀是什么再对比模型定义的对应模块用 strictTrue 加载能让这类错误立刻暴露不要改成 strictFalse 假装无事发生。第三条坑换成 vit-l 后显存直接溢出。现象是 8GB 显卡上跑 1024 以上分辨率报 CUDA out of memory。原因很明显vit-l 的参数量和中间激活值都非常大加上 DPT 头在整个分辨率上做上采样显存消耗会随着输入尺寸超线性增长。解决方法是先降到 518 分辨率确认流程再考虑换个中等尺寸的编码器如果必须用大模型大分辨率可以把输入切块做滑窗推理再把深度图拼接回去。第四条坑同一场景连续两帧的深度值忽大忽小。现象是视频里深度图看着没什么问题但某个物体的相对深度在不同帧之间跳动。原因不能怪模型单目深度估计输出的是相对深度没有全局尺度约束任何输入上的微小抖动都会被放大成输出上的尺度漂移。解决方法是接一个时间维度的平滑或者引入一个已知深度的参考点做每帧尺度校正这一条在第 6 章详细展开。第五条坑透明玻璃和白色高光墙面出现深度断层。现象是玻璃区域深度突然变成背景的深度或者白色墙面不同位置深度差异很大。原因在于单目深度估计只能靠颜色和纹理线索推断深度透明材质没有可靠视觉特征高光区域又会被当成不同平面。解决方法是不要指望模型在物理上正确后处理里检测这类区域并做插值或者干脆在业务逻辑里把这类区域标记为不可信。第六条坑微调后模型在通用场景上的表现明显变差。现象是目标域效果上去了但拿回原来的自然图片测试深度图质量断崖式下跌。原因是灾难性遗忘微调时学习率太高或者训练太久ViT 主干产生了大幅偏移。解决方法是一开始就冻结主干只训解码头如果必须要全量微调学习率降到 1e-5 级别并且每个 epoch 混入一部分原始通用数据一起训练。5.2 一帧图自检模型好坏的三个快速步骤任何新场景上线前我都建议先拿一帧有代表性的图做三件事。第一步看深度图的直方图分布正常的深度直方图应该呈连续分布有多个峰如果整个直方图挤在一个极窄区间说明模型没有区分出场景层次。第二步做边缘对齐检查把深度图的梯度叠加在 RGB 图上看人物、车辆、家具的边缘是否和深度突变位置重合如果深度边缘偏离物体边缘两三个像素说明深度头的细节能力不够。第三步检查远处区域的平滑性天空或者远处墙面在真实世界里深度变化很小模型输出也应该平滑渐变如果出现一堆细碎噪点说明场景对模型来说太陌生需要微调而不是修后处理。6. 把 Depth-Anything-V2 接到产品里深度尺度的最后一公里标定6.1 用已知距离做线性映射把相对深度换算成真实距离模型输出是相对深度你要的是这面墙离我 2.3 米那就得做一次尺度标定。最实用的方法是线性映射假设相对深度 d 和真实深度 z 满足 z a * d b。标定的思路是找两个已知距离的参考点联立方程解出 a 和 b。你可以在地上放一个标定物量出它到相机的真实距离再读出模型输出的相对深度两次测量解一个二元一次方程组就够了。import numpy as np # 已知的真实距离和模型输出深度 mul 1.0 z1, z2 1.5, 4.0 # 两个参考点的真实距离单位米 d1, d2 0.2, 0.8 # 模型输出对应的相对深度需在同一张图上取值 a (z1 - z2) / (d1 - d2) b z1 - a * d1 # 应用映射 depth_relative np.load(output/depth.npy) # 模型原始输出 depth_real a * depth_relative b逻辑说明取参考点时要注意 d1 和 d2 不能选得太近否则线性拟合的数值稳定性很差。两个参考点覆盖的范围越宽映射的可靠性越高。如果你能取到三个点就用最小二乘拟合它能平均掉单点取值时的噪声。这个步骤做完后深度图就有了物理意义后续做三维重建或避障才能在真实尺度下运行。参数说明a 和 b 只在相机的内参和外参保持不变时有效。一旦换了相机、换了对焦距离、甚至裁了画面尺寸都需要重新标定。标定的另一个陷阱是相对深度和真实深度在近距离可能不是严格线性关系如果你的业务场景主要覆盖 0.5 米以内的近距离建议用多项式拟合或者查找表。6.2 多帧一致性验证别让深度在时间轴上跳舞标定完成后AOI 场景里最常做的验证是让相机固定不动拍一段视频检查静态物体的深度输出是否恒定。单帧深度图看着都合理但放到视频里深度会抖动这种问题在标定之后依然存在原因是输入图像在时间轴上的噪声被端到端模型放大成了深度变化。验证方法是在画面里选三个静态区域分别记录每帧输出的深度值画一条随帧数变化的曲线正常情况下曲线应该平稳如果波动超过真实距离的 5% 到 10%就要引入时间域滤波。我自己的习惯是把这条验证流程固化下来每换一个场景先标定尺度再做多帧稳定性测试最后才真正接入业务逻辑。这套流程看着简单但能省掉后面大量莫名其妙的深度不准返工。深度估计这类任务模型出了问题可以换尺度出了问题才是真正难排查的因为它藏在两套坐标系的转换里。希望帮到你。本文还有配套的精品资源点击获取