简介:Depth-Anything-V2是一套开源的单目深度估计模型工程资源,面向计算机视觉研究者、算法工程师和深度学习初学者,解决从单张图像或视频中逐像素恢复场景深度的问题。压缩包共含99个文件,整体约89.99MB:38个Python脚本覆盖模型定义、训练、推理与深度图转点云等核心流程;20张jpg示例图和12个sample样例便于即时验证;6个txt配置说明与3个md文档辅助理解,还附带mp4演示视频和Git仓库元数据,方便复现与版本追踪。目前已有238人学习下载。借助这份工程包,可直接运行推理脚本处理静态图像或视频流,并用配套点云脚本生成三维点云;还可研读metric_depth下的训练脚本、分布式训练启动方式以及模型核心实现中的DINOv2特征层,深入理解大规模预训练模型在单目深度估计上的迁移思路,适合作为研究基线、课程设计或工程落地的参考。
1. Depth-Anything-V2 不是又一款深度估计模型:先看清它解决了什么
单目深度估计在计算机视觉里一直是那种“看得到但猜不准”的活:同一张图,远处墙壁和近处桌面可能深度差很多,但像素颜色却高度相似。Depth-Anything-V2 把这个问题往前推了一大步——它用大规模预训练加双教师蒸馏,让模型在弱纹理区域、透明玻璃和反光表面这些传统方法容易“翻车”的地方,也能给出结构上连贯的相对深度。我第一次跑通它的推理时,第一反应是把它换进自己搭的视觉避障管线里,结果确实是:边缘更锐利,远处地面不再糊成一团。它解决的不只是“输出一张深度图”,而是让深度结果能真正用于后续的避障、测量和三维重建。适合四类人:做自动驾驶感知的开发者、搞 AR 遮挡/交互的从业者、机器人导航方向的工程师,以及想在公开模型基础上做领域微调的研究者。
2. 原理与选型:双教师蒸馏、DPT骨架和权重版本怎么选
2.1 从单目深度到 V2:它到底改了什么
常见的单目深度估计分成两派:回归派直接预测深度值,结构派把深度看成图像分割问题。Depth-Anything-V2 走的是“预训练编码器 + 稠密预测头”路线,但又不像早期模型那样只靠多尺度特征拼接。V1 版本已经证明了“在海量无标注图像上做自监督预训练再迁移”的巨大优势;V2 的核心改动在于训练时的老师不再是单一模型,而是两个各有侧重的教师网络。
一个教师专注于整体布局,负责把“桌子在墙前面、天花板在上面”这种全局空间关系教给学生;另一个教师专注于高频细节,专门看边缘、纹理和遮挡边界。学生模型通过蒸馏同时吸收这两部分知识,所以最终效果是:大结构稳定,小边缘也清晰。这个思路和知识蒸馏的亲缘关系很明显,但有个关键差异——它不是在分类 Softmax 上做蒸馏,而是在深度特征图上做逐像素的特征对齐。
我自己在复现时体会最深的是特征对齐的写法。很多蒸馏 loss 只是把教师和学生的输出做 L1 或 L2 距离,但 V2 的做法里特征不是直接拉平,而是按通道做归一化后再算相似度。这样做的原因是:深度特征本身的数值范围在不同层差异很大,直接算 L2 会让某些大数值通道主导梯度,小数值但语义重要的通道被忽略。归一化之后,每个通道无论数值大小都被平等对待,学生才能真正学到“哪些地方该有边界”而不是“哪些地方数值碰巧相近”。
2.2 为什么骨架选 DPT:感受野和分辨率的平衡
模型架构上,V2 使用的头部结构延续了 DPT 的设计思路。DPT(Dense Prediction Transformer)把 ViT 输出的 token 重新组织成金字塔特征,再通过融合模块逐步上采样。相比纯粹的 CNN 编解码器,DPT 的编码器拥有全局感受野,能在一次前向里看到整张图。这对深度估计很重要,因为单目深度天然是“全局问题”:要判断一个物体是前方还是后方,往往要看它与画面中其他物体的遮挡关系,只盯局部像素是看不出来的。
但 DPT 的缺点是显存占用高、推理慢。所以在实际选择时,我一般会根据任务做取舍:如果做嵌入式实时避障,优先选小版本编码器,并把输入分辨率限制在 320 以下;如果做离线的三维重建或航拍处理,可以直接上大版本,让边界精度达到上限。V2 的训练方式让它对输入分辨率有一定鲁棒性,但注意:预训练时的输入尺寸和推理时的 input_size 不一致,会带来一定精度损失。
2.3 权重版本怎么选:性能、显存与场景匹配
V2 官方通常提供小、基础、大、巨大四个版本,对应不同规模的编码器和输出通道数。表里没有写死具体参数量,因为不同发布渠道的 checkpoint 可能微调过,但适用场景差异是很稳定的:
| 版本 | 编码器规模 | 推荐输入分辨率 | 显存参考 | 适用场景 |
|---|---|---|---|---|
| 小版 | ViT-S | 320x224 | 约 2-4 GB | 手机端、嵌入式、实时预览 |
| 基础版 | ViT-B | 518x384 | 约 6-10 GB | 单卡 RTX 3060 以上的通用推理 |
| 大版 | ViT-L | 518x518 | 约 12-20 GB | 离线精细深度图、三维重建 |
| 巨大版 | ViT-G | 518x518 或更高 | 约 24 GB 以上 | 学术评估、高精度标定 |
选择时不要只盯着版本大小,还要看任务对深度绝对尺度有没有要求。V2 预测的是相对深度,也就是只能告诉你“A 比 B 近”,不能直接告诉你“A 离相机 3 米”。如果你需要绝对度量,就必须在微调阶段引入真实深度标注的数据。换句话说,选版本前先想清楚:是只要相对关系做避障,还是需要绝对距离做测量。这个决定了你后面花在标定和微调上的时间。
注意:所谓“巨大版更强”不是免费的。我实测中,从大版换到巨大版,边界精度大概提升一两个点,但推理显存和耗时几乎翻倍。如果你的数据本身就有噪声,这多出来的性能很可能被数据噪声吃掉,得不偿失。
3. 安装与推理:把权重跑通并接到视频流里
3.1 环境准备与权重落位
先把这个项目拉下来,通常一个 Depth-Anything-V2 工程会包含模型定义、配置文件和推理脚本。我这里假设你已经有了一个目录,下面只讲从环境到出图的最小步骤。
git clone <项目地址> cd Depth-Anything-V2 pip install torch torchvision pip install -r requirements.txt这里有个容易踩的坑:requirements.txt里的 opencv-python 版本和 torch 版本如果有冲突,装完会报错。我一般先把 torch 装好,然后单独装 opencv 和 timm,反而更少出问题。v2 的推理代码会用到torchvision.transforms里的 resize 和 normalize,timm 主要用来加载预训练编码器。
权重文件不需要你自己训练,官方发布时会附带两个关键文件:模型权重和配置文件。我把权重放在checkpoints/目录下,这种命名习惯能避免后面写代码时路径混乱。
3.2 单张图片推理:核心脚本与参数说明
推理脚本本身不复杂,但模型配置必须和权重版本严格对应。下面这段代码可以直接保存成infer.py:
import torch import cv2 import numpy as np from depth_anything_v2.dpt import DepthAnythingV2 # 版本与编码器配置的映射,必须和 checkpoints 里实际加载的权重一致 model_configs = { 'v2-s': {'encoder': 'vits', 'features': 64, 'out_channels': [48, 96, 192, 384]}, 'v2-b': {'encoder': 'vitb', 'features': 128, 'out_channels': [96, 192, 384, 768]}, 'v2-l': {'encoder': 'vitl', 'features': 256, 'out_channels': [256, 512, 1024, 1024]}, 'v2-g': {'encoder': 'vitg', 'features': 384, 'out_channels': [1536, 1536, 1536, 1536]}, } version = 'v2-b' device = 'cuda' if torch.cuda.is_available() else 'cpu' model = DepthAnythingV2(**model_configs[version]) model.load_state_dict(torch.load(f'checkpoints/depth_anything_v2_{version}.pth')) model.eval().to(device) # 读取并预处理图像,注意输入图像要转成 RGB 并缩放到 [0, 1] image = cv2.imread('input.jpg') image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) / 255.0 # input_size 控制推理分辨率,越大边缘越细但显存越高 depth = model.infer_image(image, input_size=518) # 输出的深度是相对深度,做一次 min-max 拉伸方便可视化 depth = (depth - depth.min()) / (depth.max() - depth.min() + 1e-8) depth_uint8 = (depth * 255).astype(np.uint8) cv2.imwrite('output.png', depth_uint8)这段代码的逻辑很清楚:先根据版本选择模型配置,加载权重,然后经过一次前向得到原始深度图,最后拉伸成 0-255 保存。input_size=518是官方预训练时常用的尺寸,它决定图像会被 resize 到这个短边长度,再用 14 的倍数补齐。如果显存吃紧,可以降到 448 或 384,深度图的边缘会略变钝。
out_channels这个参数是给 DPT 头部用的,它表示从不同 stage 提取特征后需要对齐的通道数。很多人直接复制配置却改错顺序,导致加载权重时报size mismatch,所以这里一定要和权重发布说明对应,不要自己凭感觉填。
3.3 接摄像头或视频流:让模型跑起来
真实工程里很少用一张静态图,更常见的是视频流或摄像头。下面是一个把推理循环封装成帧处理函数的示例:
cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while True: ret, frame = cap.read() if not ret: break # 把帧缩放到 64 的倍数,减少 resize 抖动 h, w = frame.shape[:2] scale = 518 / max(h, w) new_h, new_w = int(h * scale) // 14 * 14, int(w * scale) // 14 * 14 resized = cv2.resize(frame, (new_w, new_h)) rgb = cv2.cvtColor(resized, cv2.COLOR_BGR2RGB) / 255.0 with torch.no_grad(): depth = model.infer_image(rgb, input_size=new_h) depth_norm = (depth - depth.min()) / (depth.max() - depth.min() + 1e-8) depth_vis = (depth_norm * 255).astype(np.uint8) depth_bgr = cv2.applyColorMap(depth_vis, cv2.COLORMAP_INFRARED) cv2.imshow('depth', depth_bgr) if cv2.waitKey(1) & 0xFF == ord('q'): break视频流推理的瓶颈通常不在显存而在 CPU 预处理。上面的代码用scale先算出新尺寸,再强制对齐到 14 的倍数,避免模型内部为了填补非对齐尺寸多做一次额外 resize。这个操作很多人会漏掉,直接传原始 640x480 进去,得到的结果要么边缘有锯齿,要么速度慢一截。
注意:摄像头场景下深度图的尺度漂移是正常的。V2 预测的是相对深度,帧与帧之间没有保持同一绝对尺度。如果你要做连续测距,需要额外做时间序列滤波或引入已知深度参考物。
4. 微调与迁移:用你自己的数据训练深度模型
4.1 数据准备:从标注深度到训练索引
微调前最痛苦的是数据整理。你手上的数据可能是一堆 PNG 深度图和对应的 RGB 图,但模型训练需要的是一个明确对应关系。我通常先把所有配对路径写进一个train.txt,每行格式如下:
/rgb/000001.png /depth/000001.png /rgb/000002.png /depth/000002.png深度图需要是真实深度值,而不是已经归一化到 0-255 的可视化图。如果你拿到的深度图是彩色可视化图,必须先反推成灰度深度,否则模型只会学到颜色映射关系。一个常见做法是把深度图转成浮点,单位统一成米,并且生成一个有效掩码:
depth = cv2.imread(depth_path, cv2.IMREAD_UNCHANGED) if depth.dtype == np.uint16: depth = depth.astype(np.float32) / 1000.0 # 假设以毫米存储 elif depth.dtype == np.uint8: depth = depth.astype(np.float32) / 255.0 valid = (depth > 0) & np.isfinite(depth)这个valid掩码在计算 loss 时非常关键,因为很多深度相机在黑色物体和玻璃上会产生无效点,如果不管三七二十一全算 loss,模型会拼命去拟合那些“黑洞”,最终把整张图都预测成灰的。
4.2 微调脚本:冻结编码器还是整体微调
微调策略我一般分两步走:第一步冻结编码器,只训练解码器;第二步解冻编码器,用小学习率整体微调。这一步能避免灾难性遗忘,又能让模型适应你的数据。下面是一个精简训练循环:
model = DepthAnythingV2(**model_configs['v2-b']) model.load_state_dict(torch.load('checkpoints/depth_anything_v2_v2-b.pth')) # 第一步:冻结编码器,只训练解码器和融合模块 for name, param in model.encoder.named_parameters(): param.requires_grad = False model.decode_head.requires_grad_(True) optimizer = torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr=1e-4, weight_decay=0.01 ) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5)这里有个参数细节:model.encoder在不同实现里可能叫pretrained或backbone,但逻辑一样。冻结编码器后,优化器只接收那些requires_grad=True的参数,避免把大量 ViT 参数也加进去导致显存爆掉。学习率 1e-4 对 starter 阶段比较安全,如果你发现 loss 完全不动,可以改成 1e-3 试一两个 epoch。
Loss 部分我用的是组合损失。单目深度的 log 空间损失比线性空间更稳定,因为远处深度的绝对误差天然大于近处,直接算 L1 会让模型更关注远处。下面是一个可用的损失函数:
def depth_loss(pred, gt, valid): # 只保留有效区域 pred = pred[valid] gt = gt[valid] # 转换到 log 空间再算 L1,避免远处大数值主导 pred_log = torch.log(torch.clamp(pred, min=1e-6)) gt_log = torch.log(torch.clamp(gt, min=1e-6)) l1_loss = torch.abs(pred_log - gt_log).mean() # 加上梯度一致性损失,让边缘更清晰 pred_grad = torch.abs(pred[:, 1:, :] - pred[:, :-1, :]).mean() gt_grad = torch.abs(gt[:, 1:, :] - gt[:, :-1, :]).mean() grad_loss = torch.abs(pred_grad - gt_grad) return l1_loss + 0.2 * grad_loss这段代码里有几个坑。valid必须是布尔掩码,直接用来索引后,维度会变成一维,这时梯度一致性计算需要拿到pred的原始形状,所以一般会先算 l1 再单独算梯度。我的习惯是pred在经过 sigmoid 后还原成米制尺度时加一个最小 clamp,避免 log(0) 出现 NaN。0.2 这个权重是经验值,如果你的数据边缘噪声很大,可以降到 0.1,否则梯度损失会放大噪声。
4.3 评估指标:怎么判断模型真的变好了
微调后不能只看 loss 下降,要用几个固定指标评估。常见的深度估计指标有四个:
| 指标 | 含义 | 计算方式 |
|---|---|---|
| RMSE | 均方根误差,对异常值敏感 | sqrt(mean((pred - gt)^2)) |
| ARD | 平均相对误差,误差与真实深度比值 | mean(abs(pred - gt) / gt) |
| delta_1 | 预测值与真实值比值在 1.25 以内的比例 | mean(max(pred/gt, gt/pred) < 1.25) |
| SILog | 对数空间平方误差 | mean(d^2) - mean(d)^2,d 为 log 深度差 |
评估时需要先做尺度对齐,因为 V2 输出相对深度,你的 GT 是绝对深度。常见做法是计算中位数比例并乘上,也就是所谓median scaling。在代码里简单实现就是:
ratio = torch.median(gt) / torch.median(pred) pred_aligned = pred * ratio如果用了这个对齐,你会看到 delta_1 指标比不做好看很多,但这其实只能说明模型在相对结构上是对的。如果你要的是绝对深度,就不要做这个对齐,直接看 RMSE,那会残酷得多。
5. 避坑与排查:从灰蒙蒙预测到训练不下降的实战手册
5.1 现象:预测深度图整体偏灰,对比度极低
第一次跑通时最容易遇到这个问题:保存下来的深度图像蒙了一层白雾,物体轮廓能看出来,但层次感很差。
原因在于 V2 推理输出的深度值虽然已经做了对数空间预测,但可视化前如果只做全局 min-max 拉伸,个别极端像素会把大部分有效深度压缩到 0.4-0.6 区间。
解决方法是改用百分位数裁剪,而不是简单 min-max:
p_low, p_high = np.percentile(depth, [2, 98]) depth = np.clip((depth - p_low) / (p_high - p_low), 0, 1)这样相当于把 2% 和 98% 之间的深度拉开,白雾就消失了。用 2% 而不是 0% 的另一个好处是能容忍深度图里少量飞点。
5.2 现象:黑色物体或玻璃区域出现大面积纯黑
我拿手机对着黑色书包测过一次,V2 的输出在书包表面整块黑掉,几乎看不出和背景的边界。这其实不是模型坏了,而是单目深度本身对无纹理区域就是“猜”。黑色物体在 RGB 图像里纹理信息极少,预训练模型只能依赖周围环境推断深度,一旦物体占据画面大部分,就会失去参考。
解决思路有两个。第一个是数据端:在微调时给这种图像增加随机亮度扰动和局部模糊,强迫模型不要只依赖纯纹理。第二个是后处理端:对深度图做边缘感知的滤波,用 RGB 边缘引导深度填充。我在代码里常用联合双边滤波,它能把相近颜色的像素深度拉近,从而把黑色区域从周围背景慢慢填起来。
5.3 现象:训练时 loss 不降,或者直接变成 NaN
微调新手最容易撞上这个问题。loss 不降往往是因为深度 GT 数值范围差异太大,比如有的图是厘米单位,有的是米单位,模型被搅晕了。
解决的第一步是统一单位,我直接把所有深度图除以需要缩放的常数,确保训练集中位数深度在 1.0 左右。第二步是检查是否有无效深度参与计算,NaN 通常是 log(0) 或除以 0 造成的。我自己的血泪经验是:在损失函数里加了torch.clamp(gt, min=0.01)之后,大部分 NaN 场景都消失了。如果还有,就要检查valid掩码是不是把整个图像的边界裁掉了。
5.4 现象:显存总是爆掉,无法复现官方指标
复制官方配置文件直接跑,在 8G 显存卡上很容易 OOM。原因通常是官方评测时用了超大输入尺寸,而且可能开了多尺度融合。而本地推理只需要一个尺度。
解决方式是先把input_size降到 384,其次把模型切换为fp16。V2 的推理在 fp16 下视觉上几乎无损:
model = model.half().to(device)注意infer_image内部如果涉及归一化操作,需要把输入图像也转成torch.float16。另外,如果只是为了看效果,没必要一次加载巨大版,基础版在大多数场景下已经够用。
5.5 现象:微调后指标反而不如预训练模型
我做过一次迁移实验:在只有几百张工业零件深度图上微调,RMSE 下降但边界精度下降。原因是数据集太小,编码器被破坏得太多。
这里我的建议是:小数据集永远不要解冻编码器。把冻结阶段拉长到 10 个 epoch,只训练解码器,让解码器先适应你的深度分布。解冻编码器时学习率必须低于 1e-5,并且加上早停。否则模型会用很短的时间把预训练学到的通用结构知识覆盖掉,变成只认得自己见过的那几百张图。
6. 进阶:用多尺度一致性检验模型可信度
深度估计模型最大的问题是“自信地给出错误深度”。V2 没有像贝叶斯模型那样直接输出不确定性,但我们可以利用它的一个性质:把同一张图分别用不同input_size推理,正常区域的深度结果应该基本一致,而纹理混乱或遮挡复杂的区域会剧烈变化。这个“多尺度一致性”可以当作一个零成本的置信度指标。
具体操作是跑两次前向,一次 518,一次 320,然后把两张深度图都 resize 到相同尺寸,计算逐像素的相对差异:
with torch.no_grad(): depth_518 = model.infer_image(rgb, input_size=518) depth_320 = model.infer_image(rgb, input_size=320) # 各自归一化后比较,消除尺度差异 d1 = (depth_518 - depth_518.min()) / (depth_518.max() - depth_518.min() + 1e-8) d2 = (depth_320 - depth_320.min()) / (depth_320.max() - depth_320.min() + 1e-8) consistency = 1.0 - torch.abs(d1 - d2)consistency越高,说明该区域在不同感受野下得到的相对深度越稳定;越低,说明模型其实也拿不准。我在避障系统里就是靠这个掩码决定是否相信某一帧的深度结果:如果图像中心区域一致性低于 0.7,就切换到备用策略,不再继续往前走。
这个技巧的物理含义很直接——模型在小分辨率下主要依赖全局语义,大分辨率下能看到更多细节。正常物体无论从哪个尺度看,它的前后关系都成立;但栏杆、树叶这类高频细节,尺度不同得到的结构就不同。把这些区域找出来,既可以用作可信度评估,也可以用来生成注意力样本,引导后续微调重点处理那些难例。
从那以后,我每次跑完一个深度模型,都会强制做一遍多尺度一致性统计,而不是只看可视化出的深度图“顺眼不顺眼”。一两行代码带来的可靠性提升,比换更大版本模型更明显。希望这个习惯也能帮到你。这份资源里我整理了各版本配置、推理脚本和一个可扩展的微调样例,下载后照着第 3 章先跑通,再决定要不要上自己的数据集。
本文还有配套的精品资源,点击获取