目录
前言
最近想看看单目深度估计的一些工作,选择了 Depth Anything 这项工作来学习,这篇文章主要分享 Depth Anything 的 paper 讲解以及 Depth Anything 模型的 ONNX 导出。若有问题欢迎各位看官批评指正😄
V1 paper:Depth Anything: Unleashing the Power of Large-Scale Unlabeled Data
V1 repo:https://github.com/LiheYoung/Depth-Anything
V2 paper:Depth Anything V2
V2 repo:https://github.com/DepthAnything/Depth-Anything-V2
reference:https://github.com/spacewalk01/depth-anything-tensorrt
1. Depth Anything V1
以下内容均来自于视频的讲解
视频链接:论文精读: Depth Anything
1.1 摘要
我们知道从 2D 图像到 3D 空间的映射其实是一对多的关系,图像上的像素点映射到 3D 空间中是一条射线,如果深度可知那么这个关系是唯一确定的,而深度信息我们一般会借助像 LiDAR 这样的传感器获取
那单目深度估计任务要解决的就是从一张 RGB 的图像中直接预测得到每个像素的深度,如下图所示:
上图展示了 Depth Anything 的效果图,输入 RGB 图像,输出对应的深度图,在深度图中距离我们越近的部分则越亮,深度图黑色的部分表示说距离我们非常远
值得注意的是图片展示的是一个相对深度,那大家可能会想模型只能预测一个相对的深度,那对于我们实际的任务例如自动驾驶、机器人巡检等任务有什么帮助呢?
那 Depth Anything 的作者也有提到只要我相对深度估计做得好,模型足够的强壮,泛化性能足够好,那它在绝对深度估计任务上其实可以很快的通过微调的方式去实现,也就是说我们不用担心它在实际使用中的一个场景
那 Depth Anything 在训练和推理的时候都是在相对深度上去做的,只有在 paper 中的消融实验体现模型性能部分才会去数据集上微调,将它变成一个绝对深度估计的模型,这个是我们需要注意的
Depth Anything 的工作简单总结就是三个点:
- Unlabled data(scale up dataset)
- 无标注数据,作者通过大量的无标注数据扩充数据集规模
- Optimization target(data augmentation)
- 设置一些更具挑战性的优化目标,通过数据增强去做的
- Semantic prior(auxiliary supervision)
- 加入语义先验,通过辅助监督去实现的
下面我们按照论文的顺序简单分析下 Depth Anything 这篇工作的内容
1.2 引言
深度数据集大部分来自于 LiDAR 等传感器,采集的设备非常昂贵,相比而言未标注数据非常的廉价,因此作者主要将目光放在了大规模的未标注数据上,它们更加容易获取,数据分布更加广泛且容易标注。容易标注的意思是说我们可以在已有的标注数据集上训练一个教师模型(teacher model),然后在未标注数据上进行一个推理得到对应的深度标签,那这个标签并不是来自于真实传感器采集的,因此大部分情况下我们把它称为 伪标签
作者收集了 62M 分布广泛的未标注数据,它们来自于 SA-1B、Open Images、BDD100K 等大规模数据集,另外收集了 1.5M 已标注的数据,它们来自于 6 个公开数据集。作者在已标注的数据上训练了一个 MDE(Monocular Depth Estimation) 模型,并利用训练好的模型在 62M 未标注数据打上了相应的伪标签
作者将真实标签和伪标签的图像直接全部放一起训练一个学生模型(student model),结果失败了,最后发现训练出的学生模型和 baseline(teacher model) 相比实际上性能并没有什么提升,作者推测这可能是因为在这种 naive 自我教学方式中模型获得的额外知识是相当有限的。那为了解决这个问题,作者提出在 student model 训练的时候是不是可以加上一些具有挑战性的目标,让它学习一些更高级的特征呢
此外,作者希望加入一些语义先验,很直观的感觉这应该会带来一些帮助,基于上述分析作者总结了下他们这篇文章的工作,主要包括:
- 引入大量廉价、多样化的未标注数据来做单目深度估计
- 加入一些更具挑战性的学习目标期望模型学习到一些额外的知识,而不是直接联合学习将标注数据和未标注数据一起训练
- 从预训练的 encoder(语义分割的 encoder)中继承一些丰富的语义先验,而不是直接的去使用辅助的语义分割任务
- 模型泛化性能非常强大
1.3 方法
作者提到他们的工作利用了有标注和未标注的图像,以促进更好的单目深度估计(MDE),形式上有标注数据集表示为 D l = { ( x i , d i ) } i = 1 M \mathcal{D}^l = \{(x_i,d_i)\}_{i=1}^M Dl={(xi,di)}i=1M,未标注数据集表示为 D u = { u i } i = 1 N \mathcal{D}^u = \{u_i\}_{i=1}^N Du={ui}i=1N。我们的目标是从 D l \mathcal{D}^l Dl 中学习一个教师模型 T T T,然后利用 T T T 为 D u \mathcal{D}^u Du 分配伪深度标签,最后利用有标注数据集和未标注数据集的组合来训练一个学生模型 S S S
1.3.1 Learning Labeled Images
大部分的单目深度估计工作在训练的时候都是在绝对深度数据集上做的,但像 ZoeDepth 以及这里的 Depth Anything 在训练的时候是使用的相对深度做的,那具体是怎么把数据集中的绝对深度转换为相对深度呢,它其实有点像减均值除标准差这种预处理操作,我们一起来看下:
L l = 1 H W ∑ i = 1 H W ρ ( d i ∗ , d i ) \mathcal{L}_l=\frac{1}{HW}\sum_{i=1}^{HW}\rho(d_i^*,d_i) Ll=HW1i=1∑HWρ(di∗,di)
对于有标注的数据的 Loss 如上所示,其中 d i ∗ d_i^* di∗ 是模型推理的深度结果, d i d_i di 是 ground truth, ρ ( d i ∗ , d i ) = ∣ d ^ i ∗ − d ^ i ∣ \rho(d_{i}^{*},d_{i}) = |\hat{d}_{i}^{*}-\hat{d}_{i}| ρ(di∗,di)=∣d^i∗−d^i∣,也就是做差取绝对值,最后求和平均一下,那这是一个很自然的想法
Note:这里的深度值首先会通过 d = 1 / t d = 1/t d=1/t 变换到视锥空间,然后在每个深度图上归一化为 0 ∼ 1 0 \sim 1 0∼1,也就是拿深度的倒数做,因此像天空这种距离我们非常远的背景我们会假设是无穷远,取个倒数后就是 0
其中 d ^ i ∗ \hat{d}_{i}^{*} d^i∗ 和 d ^ i \hat{d}_{i} d^i 都做了一些变换,如下所示:
d ^ i = d i − t ( d ) s ( d ) \hat{d}_i=\frac{d_i-t(d)}{s(d)} d^i=s(d)di−t(d)
这个有点像减均值除标准差,也就是归一化的操作,那其实是差不多的,只不过作者用了一些其它的表达方式,原理都是一致的,其中:
t ( d ) = median ( d ) , s ( d ) = 1 H W ∑ i = 1 H W ∣ d i − t ( d ) ∣ . t(d)=\text{median}(d), \ \ s(d)=\frac{1}{HW}\sum_{i=1}^{HW}|d_i-t(d)|. t(d)=median(d), s(d)=HW1i=1∑HW∣di−t(d)∣.
这就是对有标注数据的处理,将绝对深度转换为相对深度,这个 Loss 也比较好理解。此外对于在已标注数据上训练的教师模型 T T T,作者提到使用 DINOv2 来作为语义先验的 encoder
Note:中位数作为均值主要是考虑平均数容易受到极端值的影响,比如我一张图大部分是天空,那用平均数可能会受到很大的影响,会比较极端
1.3.2 Learning Unleashing the Power of Unlabeled Images
接着我们来看未标注数据的处理,作者提到这部分是他们工作的主要内容,他们选择了 8 个大规模的公开数据集作为不同场景的未标注数据,总共包含 6200 万张图像,详细信息如下表所示:
作者提到先利用教师模型 T T T 在未标注数据集 D u \mathcal{D}^u Du 上预测可以得到一个伪标签数据集 D ^ u \hat{\mathcal{D}}^u D^u:
D ^ u = { ( u i , T ( u i ) ) ∣ u i ∈ D u } i = 1 N \hat{\mathcal{D}}^u=\{(u_i,T(u_i))|u_i\in\mathcal{D}^u\}_{i=1}^N D^u={(ui,T(ui))∣ui∈Du}i=1N
将标签数据集 D l \mathcal{D}^l Dl 和伪标签数据集 D ^ u \hat{\mathcal{D}}^u D^u 组合在一起来训练一个学生模型 S S S,遵循之前的一些工作,为了获得更好的性能,作者这里提到重新初始化了学生模型 S S S 而不是在 T T T 上微调获得 S S S
那在最开始的一些实验中,作者提到他们失败了,没有取得预想的结果,那是什么原因呢,作者推测在当前的数据中已经有足够多的标注图像,从额外的未标注图像中获得的额外知识是相当有限的。特别是考虑到教师模型和学生模型共享相同的预训练和网络架构(DINOv2),它们在未标注数据集上很可能会有相似的判断,所以导致性能上没办法得到一个提升
基于这个问题,作者提出要设置一些更困难的优化目标,这样才有可能让学生模型在未标注数据集上学到一些额外的视觉知识,unlabeled image 是我们的重点,作者认为如果有提升一定是在这个上面有提升,那因此我们就要在 unlabeled image 上去加一些更困难的优化目标。具体怎么做的呢?作者提到我们加入了一些很强的扰动在 unlabeled image 上,也就是大家熟知的数据增强
作者提到主要引入了两种扰动:
- 强烈的色彩扰动,包括颜色抖动和高斯模糊
- 空间扰动即 CutMix
色彩增强是一种普遍的增强手段,关于 CutMix 可以用下图来说明:
例如原始图像是一张小狗,Mixup 的意思是说我把一张猫和一张狗放在一起,Cutout 就是把图像某部分直接裁剪,CutMix 是将 Cutout 切掉的部分把小猫的图片放过来
在论文中作者通过一个矩形区域的二值掩码 M M M 来实现的 CutMix:
u a b = u a ⊙ M + u b ⊙ ( 1 − M ) u_{ab}=u_a\odot M+u_b\odot(1-M) uab=ua⊙M+ub⊙(1−M)
其中 u a u_a ua 和 u b u_b ub 是两张未标注图像
对于未标注数据的损失 L u \mathcal{L}_u Lu 来自于 M M M 和 1 − M 1-M 1−M 两部分有效区域:
L u M = ρ ( S ( u a b ) ⊙ M , T ( u a ) ⊙ M ) , L u 1 − M = ρ ( S ( u a b ) ⊙ ( 1 − M ) , T ( u b ) ⊙ ( 1 − M ) ) \begin{aligned}&\mathcal{L}_{u}^{M}=\rho\big(S(u_{ab})\odot M, T(u_{a})\odot M\big),\\&\mathcal{L}_{u}^{1-M}=\rho\big(S(u_{ab})\odot(1-M),T(u_{b})\odot(1-M)\big)\end{aligned} LuM=ρ(S(uab)⊙M,T(ua)⊙M),Lu1−M=ρ(S(uab)⊙(1−M),T(ub)⊙(1−M))
我们先看 L u M \mathcal{L}_{u}^{M} LuM,其中 ρ \rho ρ 的定义和前面一样,模型预测的深度值和 ground truth 的深度值做差然后取绝对值, S ( u a b ) S(u_{ab}) S(uab) 是 CutMix 图像经过学生模型预测的深度结果, ⊙ M \odot M ⊙M 代表在 M M M 这个 Mask 上的深度预测结果, T ( u a ) T(u_{a}) T(ua) 是 u a u_a ua 图像在教师模型下推理得到的伪标签,接着 ⊙ M \odot M ⊙M 得到在 M M M 这个 Mask 上的伪深度标签值,关于 L u 1 − M \mathcal{L}_{u}^{1-M} Lu1−M 的计算和 L u M \mathcal{L}_{u}^{M} LuM 类似,这边不再赘述
那最后做一个加权求和就得到整个未标注数据的 Loss:
L u = ∑ M H W L u M + ∑ ( 1 − M ) H W L u 1 − M \mathcal{L}_u=\frac{\sum M}{HW}\mathcal{L}_u^M+\frac{\sum(1-M)}{HW}\mathcal{L}_u^{1-M} Lu=HW∑MLuM+HW∑(1−M)Lu1−M
1.3.3 Semantic-Assisted Perception
接着我们看语义辅助感知这部分,作者认为通过高层次的语义相关的一些信息对我们的这个深度感知是有好处的,例如语义分割出的一个物体它们整体的深度估计大体上应该差别不大,同时这些来自其它任务的辅助监督信号也可以在一定程度上对抗伪深度标签中的潜在噪声。因此,作者提到在训练阶段就通过一个共享的 encoder 和两个独立的 decoder 迫使模型同时产生深度和语义的一个预测结果,具体如下图所示:
输入图像经过 shared encoder 一个共享的编码器得到 features 图像特征,一条支路通过 depth decoder 深度的解码器得到深度估计,由于我们在标注数据集上有真实的 label,在未标注数据集上有伪标签 pseudo label,因此我们就可以得到一个深度意义上的损失即 depth loss
另外一条支路通过 semantic decoder 语义的解码器得到语义的预测,我们再拿它与真实语义分割模型得到的预测结果对比(也可以认为是一种伪标签),就能得到一个语义上的损失即 semantic loss
那我们把两条支路的损失组合就得到了我们最后的损失,这是一个很符合直觉的想法,但是作者说在最初的尝试中他们失败了,在原有的 MDE 模型上并没有得到一个性能上的提升。那作者认为虽然语义信息是一个比较高层次的信息,但是它是离散的,离散的语义损失对于连续的深度估计损失可能没办法起到一个很好的指导作用,所以我们要找语义分割中连续性的东西
我们想计算语义损失的这部分能不能提前,不要在最后离散的时候去做,而是在连续的时候去做,这样它可能会带来更多的信息从而去指导深度估计的任务。出于此目的,作者将语义的损失提前,提前到特征上来,相对于离散的 mask 而言特征空间是高维且连续的,因此我们考虑做一个特征对齐的损失,而不再是一个离散的类的损失,修改后如下图所示:
相比于前面那张图而言,这里变化的点在于我们在特征部分通过 DINOv2 encoder 去做对比,得到特征对齐的损失,即语义相关的损失,其损失表达式如下:
L f e a t = 1 − 1 H W ∑ i = 1 H W cos ( f i , f i ′ ) , \mathcal{L}_{feat}=1-\frac{1}{HW}\sum_{i=1}^{HW}\cos(f_{i},f_{i}'), Lfeat=1−HW1i=1∑HWcos(fi,fi′),
其中 cos ( . , . ) \cos(.,.) cos(.,.) 表示两个特征向量间的余弦相似度, f i f_i fi 是学生模型得到的特征向量, f i ′ f_{i}' fi′ 是冻结的 DINOv2 encoder 得到的特征向量
通过这种方式可以更好的去监督深度估计任务,因为在特征空间的语义信息是连续的,但 DINOv2 在提取特征时会有一个小问题,我们以下图为例来讲解:
在上图中的红色车辆的车头似乎离我们更近,车尾似乎离我们更远,但是在语义分割任务看来它们都是一样的,不论是车头还是车尾都是车,但是从深度估计的角度出发我们更希望车头和车尾的特征能有些许差别,因为毕竟我们最终的任务还是深度估计,这样我们单纯的用语义特征损失来指导深度估计任务可能反而会起到一个副作用
因此作者在文中提到为了解决这个问题,设置了一个特征对齐的容忍度 α \alpha α,如果计算的特征向量的余弦相似度大于 α \alpha α,则该像素不会考虑到最后的特征损失中去,这是一个小 trick
最后的损失由前面提到的 L l \mathcal{L}_{l} Ll、 L u \mathcal{L}_{u} Lu 以及 L f e a t \mathcal{L}_{feat} Lfeat 三部分组成
那我们现在可以回过头来看 depth anything 整个 pipeline:
上图中实线部分是指有标注图像的工作流,虚线部分是未标注图像的工作流,先看实线部分,有标注图像数据集经过 encoder 然后经过 decoder 得到学生模型的 prediction,通过人工标签进行一个监督学习
未标注图像数据集有两条线,我们前面说过它有语义特征对齐的损失,也有深度的损失,因此一条线是经过色彩和空间干扰(即数据增强)的图像,通过 encoder 通过 decoder 得到未标注数据的预测结果,之后拿 teacher model 生成的伪标签进行一个监督学习
另一条线是 unlabeled image 直接送入 DINOv2 encoder 得到的特征和上面经过色彩和空间干扰后的 unlabeled image 送入 encoder 得到的特征需要尽可能一致,做一个语义信息的保留,这部分就是前面提到的 L f e a t \mathcal{L}_{feat} Lfeat,特征的损失
OK,以上是 Depth Anything 的原理,下面我们简单看下实验细节
1.4 实验细节
先看 Depth Anything 在 Zero-shot 相对深度估计的性能,如下表所示:
作者提到相对深度估计对比实验采用的数据集都是未使用的,也就是 Zero-shot 的,从表 2 中可以看出Depth Anything 相比于 MiDaS 性能还是有很大的提升,它较小的 encoder 性能都比 MiDas 要好,而且 MiDaS 在训练时可能是见过上述这些数据集的,不再是 Zero-shot,但是它的性能仍然无法比过 Zero-shot 的 Depth Anything,这是一个强有力的证明,说明 Depth Anything 有一个非常强的泛化能力
那在绝对深度估计 Depth Anything 表现怎么样呢?作者在 In-Domain 和 Zero-shot 两种情况下都进行了相关实验,In-Domain 是在 NYUv2 和 KITTI 数据集分别做了,结果如下表所示:
从表 3 和表 4 中可以看到 depth anything 在这两个数据集上都达到了 SOTA 的性能
下面再看下绝对深度估计 Zero-shot 情况下 depth anything 的性能,如下表所示:
实验对比时为了体现一致性,作者仅仅把 MiDas encoder 替换成了 Depth Anything encoder,其他组件不变,在这样一个实验条件下 depth anything 的性能也是非常强大的
比较有意思的是作者还在语义分割任务上做了相关实验,来验证 MDE encoder 的性能也是非常强大的,结果如下表所示:
这个其实也在情理之中,既然你语义分割的 loss 可以帮助深度估计任务学习得更好,反过来你深度估计的 loss 同样可以帮助语义分割任务学习得更好,例如假设在深度图上两个像素点比较接近,它们的深度估计差不多,那么它们很有可能在语义分割上也是接近的,很有可能属于同一个类
接着看消融实验部分,在表 6 中作者单独在某个数据集上训练,然后在 6 个 zero-shot 的数据集上进行测试,那作者的意思就是说想要知道哪个数据集能带来一个更好的性能
作者对比了 6 个不同的数据集,发现拿 HRWSI 数据集单独训练时性能是最好的,这个数据集虽然只有 2w 张图像,但是图像多样性非常好,而 MegaDepth 数据集的表现就比较差,那这与 MegaDepth 的图像数据相关,那这个数据集里面的图像都是比较远拍摄的,如下面这张非常远的城市图片:
那由于数据集都是远景拍摄,导致你整个绝对深度就比较单一,泛化性能也没有那么强,在一些更广泛的数据集上当然表现就没那么好
表 9 中作者想说明数据增强和语义约束的重要性,第一行是只有有标注的数据,第二行是仅仅加入一些未标注的数据,可以发现二者差异不大,说明单纯的增加未标注数据并没有对模型有一个明显的性能提升,前面也提到过这主要是因为有标注的数据中多样性已经非常丰富了,单纯增加数量不能让模型学习未标注数据中的一些知识
那第三行是对未标注数据加入了扰动(数据增强),可以看到此时模型的性能有了一个提升,最后一行是加入了语义约束,此时性能得到进一步的提升
此外作者还与一些性能比较强大的模型在下游任务中进行了对比,如下表所示:
前面提到既然我们在未标注数据集上应用语义特征对比能够提升 MDE 模型的性能,那我们能不能在已标注数据集上也应用语义特征对齐,这样会有什么样的效果呢?会不会进一步提升模型性能呢?
结果如上表所示,有意思的是单独在有标注数据集上进行语义特征对齐对 baseline 模型而言并没有什么提升,那是什么原因呢?作者认为在已标注数据集上它已经有很高质量的深度标签,那我的语义损失可能用处就不那么大。而伪标签可能会更加的 noise,有更多的噪声,那这个时候使用语义先验可以更好的帮助到它,但是对于高质量的标签数据它并没有 unlabeled data 的一些劣势,所以它可能帮助就不是那么大
我们回过头来看一下,这篇文章作者说他们主要的三个贡献是:
- Unabled data(scale up dataset)
- Optimization target(data augmentation)
- Semantic prior(auxiliary supervision)
第一个贡献是引入大量的未标注数据,第二个是通过数据增强优化目标,那优化目标其实也是在未标注数据上做的,最后的语义先验根据前面的消融实验我们知道如果做在已标注数据集上,它的提升是很有限的,这个语义先验其实也是要用到未标注数据上的,它才能体现出一个更强的作用
归根到底,这三个贡献其实都可以归结于引入了 unlabeled data,所以这也可能是作者把论文名字说成 Unleashing the Power of Large-Scale Unlabeled Data 的原因,因为确实整篇文章作者都是在尽可能的去挖掘这个更大规模的未标注数据的能力,主要是通过 unlabeled data 才实现了性能的提升,他没有去引入一些最新的模块,他只是通过充分挖掘大规模数据就能实现一个性能的提升,而且这个提升不仅仅是在深度估计任务上,在下游的语义分割任务上也有提升
最后我们来看一些定性结果的分析:
在定性结果图中 depth anything 的表现也非常好,那比如说第三行图的红绿灯在 depth anything 就有一个很好的效果,再比如第五行图中的树并没有因为光影而变得杂乱无章,那这些都可能是语义先验或者说更大规模的数据带来的一个优势
OK,以上就是 depth anything 这篇论文的讲解啦
更多的细节大家可以阅读原始 paper:Depth Anything: Unleashing the Power of Large-Scale Unlabeled Data
Note:超级喜欢这种详细剖析整篇论文的视频,通俗易懂,非常感谢讲解者🤗
2. Depth Anything V2
以下内容均来自于视频的讲解
Depth Anything V2 主要是对 depth anything v1 在细节和鲁棒性方面的改进,包括以下三个部分的改进:
- 用合成图像替换所有带真实标签的图像数据
- 扩大教师模型的大小
- 通过大规模伪标签真实图像来训练学生模型
开始前我们先简单回顾下 Depth Anything V1 的工作,那以往的单目深度估计工作主要在 labeled image 上去做的,而实际上大部分的数据都是 unlabeled,那 V1 的作者主要是充分挖掘并利用未标注数据集的能力来提升 MDE 模型的性能
在 V1 中作者使用了一个比较简单的 framework 去利用这些未标注的数据,如上图所示,首先利用有标签的数据即 labeled image 去训练一个教师模型 teacher model,接着利用教师模型对大规模未标注数据打上一个伪标签
随后将 labeled image 和 unlabeled image 放一起来训练一个学生模型 student model,在训练这个学生模型的过程中,作者提出了两项主要的工作:
- 在未标注数据上增加扰动(色彩/空间)迫使模型学习更多额外的知识
- 加入语义先验,通过 feature loss 去强化模型的语义感知能力
那以上是 Depth Anything V1 的一些回顾
在 V2 中作者提到在对比 Depth Anything V1 和 CVPR 的另一项工作 Marigold 时发现它们各自都有一些问题
在一面斑马纹的图像中,Marigold 的预测结果显然不太正确,对于一整面墙它的深度应该是差不多的,但是 Marigold 预测出了不同的深度,而 Depth Anything V1 就给出了一个还不错的预测结果。Marigold 相较于 Depth Anything V1 而言在复杂场景或者令人 confusing 的场景它的鲁棒性是不够的
那对于另一张篮筐的图像来说,Marigold 给出了一个非常精细的深度预测结果,而 Depth Anything V1 预测结果比较粗糙,特别是篮网中镂空部分,相比于 Marigold 而言 Depth Anything V1 在细节上的鲁棒性是不够的
作者通过分析总结了 Marigold 和 Depth Anything V1 的一些优缺点,如表 1 所示,Marigold 在精细度、透明物体(如塑料瓶)以及反射面(如镜子)方面的鲁棒性是非常好的,相比而言,Depth Anything V1 在复杂场景中鲁棒性较好,并且它的执行效率相比 Marigold 也是更快的,在一些下游任务中移植也是非常方便的
那 Marigold 和 Depth Anything V1 它们有各自的优缺点,在 Depth Anything V2 中作者希望能够同时达到上述提到的六个优点
因此作者重新回顾了 V1 中的一些训练数据,发现这些真实的训练数据的 GT labels 中有很多的 noise,如下图所示:
例如图 (a) 中由传感器采集的深度数据在这种玻璃门场景中,传感器会直接穿透玻璃将其深度认为是后面的物体,这显然是不对的,图 (b) 中的深度是由一些 stereo matching 的算法获得的,对于近处的人和远处的建筑效果是非常差的,还有一些深度(例如图 (c))是由 SfM(Structure from Motion)得到的,对于动态的物体这种方式获取到的深度标签也是不准确的,那在这些有着 noise 的 label 上训练出来的模型预测结果也是不好的,如图 (d) 所示
那像 Marigold 这种则是在合成图像数据上去做的训练,那合成图像也就是基于 stable diffusion 这种生成的图像它们的 depth map 是非常非常精细的,具体对比如下图所示:
那对比真实图像的 depth map 和合成图像的 depth map 我们可以很清晰的看到它们之间的差异,在图 (c) 的预测结果中,中间的预测结果是在真实的有着 noise label 的图像上训练出的模型的推理效果,可以看到深度预测还是非常粗糙的,相比而言最右边的是在合成图像上训练出的模型的推理效果,可以看到预测是非常精细的,对于远处的鸟和树的枝叶都可以区分开来
所以作者就提出了一个想法我们只需要在合成图像上训练出一个模型就行了,那在训练的过程中作者遇到了一些问题,第一个问题就是在合成图像上训练后迁移到真实图像数据时,一些 vision encoder 的表现非常的糟糕,如下图所示:
作者尝试了不同的 vision encoder 发现 DINOv2-Giant 这个 encoder 在合成图像到真实图像迁移的时候能得到一个还不错的预测结果。那 DINOv2-Giant 也有它的问题,例如在部署阶段我们是很难去 forward 这样一个庞大的模型的(3B),除了部署和 inference 的代价大,DINOv2-Giant 也有它自己的 failure case 的情况,如下图所示:
例如合成图像中天空的 diversity 是不够的,所以迁移到真实图像泛化时哪怕是像 DINOv2-Giant 这样的模型它也会出现一些很奇怪很离谱的错误,例如天空某些部分是预测有深度的,还有右边黑暗场景下,对人头和人身的深度预测不一致也是有问题的
所以作者提出了一个三阶段的训练 pipeline,希望这个三阶段的 pipeline 同时兼顾到合成数据的优点和真实数据的优点,具体流程如下图所示:
首先,在第一阶段我们会在纯合成图像上去训练一个教师模型(DINOv2-Giant),合成图像的优点是它非常的准确,对于透明和细小的物体都有标注值,缺点是它和真实图像的分布有一定的差异,另外合成图像不可能完全模拟整个真实世界,所以它的 diversity 也是比较局限的
所以有了我们的第二阶段,利用第一阶段训练的教师模型在我们大规模的未标注的真实图像打上伪标签,从而获得一个比较准确且比较精细的深度值
最后第三阶段在大量的伪标签真实图像上去训练一个最终的学生模型,这个学习模型就享有合成数据和真实数据的优点,包括更准确、更精细的深度预测结果且符合真实世界的分布
此外作者还分析了当前深度估计评估 benchmarks 存在的问题,包括:
- Rich noise
- Limited diversity
- Low resolution
第一个是现在的 evaluation benchmarks 中有非常多的 noise 在它们的 Ground Truth 中,例如上图中 NYU-D 的两张测试图像,模型的预测结果是相对准确的,而真实的标签反而是不正确的,这就会导致模型在这些 benchmark 上反而会得到一个更低的评测分数
第二个是它的 diversity 非常的受限,现在的 benchmarks 一般是室内的几个场景和室外的像 KITTI 数据集这种自动驾驶的一些场景,那这些场景并不能完全覆盖我们真实世界
第三个是当前的 benchmarks 中图像的分辨率都比较低,大部分都只有 500x500 的分辨率,但是我们在真实场景中大部分都是一些高分辨率的图像,那在这些低分辨率下的评测分数是否能真实的反映模型在高分辨率下的性能呢
因此受前面的启发,作者提出了一个新的评测的 benchmark,具有以下优点:
- Exceptional Preciseness
- Rich diversity
- High resolution
那关于评测图像的标签作者有提到他们是先用 SAM 模型来预测目标的 mask,但是他们并不是使用 mask,而是利用 proposal 这些 mask 的关键点,接着随机抽取两个关键点像素,并交给四个专家模型去评测并投票,如果这些模型预测的深度结果是一致的,例如第一个点比较近第二个点比较远,那么就会重新去 sampling 一对点,如果有分歧,这对关键点数据将会给人工去 annotation,通过这种方式可以将一个稠密的任务转换为一个 sparse 的任务从而获得更精细的标签
那在 diversity 中作者提到他们有收集八个场景下的图像,如上右图所示,关于分辨率作者提到他们的图像一般都是 1k~2k 的这种高分辨率
上表展示了 Depth Anything V2 在之前的 benckmarks 上的一些指标,可以看到它和 V1 其实都差不多,那主要原因是现有的 benchmarks 已经不太能够直接反映模型的性能
那在作者提出的 DA-2K 的 benchmark 上 Depth Anything V2 的性能是远远超过前面的一些工作的
在透明物体和反射面这样特殊的深度估计下,Depth Anything V2 在 NTIRE 的一个比赛中不进行 fine-tuning 直接 Zero-shot 得到的结果也是非常好的 0.836 比 V1 高出了 30 个点,那简单微调下就可以达到 0.912 的效果,非常接近第一名的水平
上表是 unlabeled real images 的消融实验,我们可以看到对于不同的 Encoder 加入未标注数据后性能都是有提升的
下表还对比了 manual label 和 pseudo label 的质量差异,可以看到 pseudo label 可以产生一个更高的迁移的性能
下面是一些定性的分析结果,虽然模型是在 518x518 分辨率上训练的,但是在测试时可以使用更大的分辨率,并且会产生更精细的预测结果
那作者还对比了利用纯合成图像训练的模型和在合成图像加上一些真实数据集发现加上真实数据集训练出来的模型的预测结果反而没有那么精细,会受到一些影响,如下图所示:
下面是 V1 和 V2 的一些比较效果图,可以看到对于一些细节还有玻璃状的物体时 V2 的效果要比 V1 好很多
下面是和 Marigold 的对比,可以看到 Depth Anything V2 的鲁棒性还是要好很多的
下面展示了伪标签真实数据的意义,如果我们单纯用标注的合成数据去训练一个 DINOv2-small 这样一个模型的话,可以看到在真实图像上的泛化性是非常差的,但是如果我们用伪标签的真实图像去训练同样一个 DINOv2-small 模型的话就会看到它在真实图像上有一个非常好的泛化能力
OK,以上就是 depth anything v2 这篇论文的讲解啦
更多的细节大家可以阅读原始 paper:Depth Anything V2
3. 环境配置
前面我们简单过了一遍 depth anything v1 & v2 的算法原理,下面我们就来看下如何部署(以 V2 版本为例讲解)
在开始之前我们有必要配置下环境,Depth Anything V2 的环境可以通过 Depth-Anything-V2/README.md 文档中安装,由于没有什么特殊的依赖,大家可以参考官方的指令安装即可,如下所示:
git clone https://github.com/DepthAnything/Depth-Anything-V2
cd Depth-Anything-V2
pip install -r requirements.txt
Note:大家在自己现有的 conda 虚拟环境中应该都能运行,因为 requirements.txt 中的依赖非常少,大家缺什么补什么就行
4. Demo测试
OK,环境准备好后我们就可以执行 demo,具体可以参考:Depth-Anything-V2/readme/running-script-on-images
我们一个个来,首先是推理验证测试,官方给的图像推理脚本如下所示:
python run.py --encoder vitl --img-path assets/examples --outdir depth_vis
在这之前我们需要把 Depth-Anything-V2 这个项目给 clone 下来,执行如下指令:
git clone https://github.com/DepthAnything/Depth-Anything-V2.git
也可手动点击下载,点击右上角的 Code
按键,将代码下载下来。至此整个项目就已经准备好了。
此外还要下载相关的预训练权重用于 Demo 测试和 ONNX 导出
预训练权重的下载可以通过 README 提供的链接获取:
值得注意的是官方提供了三个不同大小的权重,我们这里拿最小的 Depth-Anything-V2-Small 做 Demo 测试和后续的部署即可。博主这里也准备了下载好的权重和代码,大家可以点击 here 下载(注意代码和权重下载于 2024-11-26 日,若有改动请参考最新)
接着需要在 Depth-Anything-V2 这个项目下新建一个 checkpoints 文件夹,将下载好的权重文件放在该文件夹下,如下图所示:
源码和模型都准备好后,执行如下指令即可进行推理:
python run.py --encoder vits --img-path assets/examples --outdir depth_vis
输出如下图所示:
同时在当前目录下还会生成 depth_vis 文件夹,里面保存着推理后的图片,如下所示:
5. ONNX导出
Demo 测试成功后我们下面来导出 Depth Anything V2 的 ONNX 模型
值得注意的是在 GitHub 已经上有一些开源的与 Depth Anything 部署相关的工作,因此我们可以拿过来直接用
Note:博主部署过程中主要参考的是:https://github.com/spacewalk01/depth-anything-tensorrt
在 depth-anything-tensorrt 中提到 Depth Anything V2 的 ONNX 模型导出需要先修改文件
具体修改的是 depth_anything_v2/dpt.py 文件,将 184 行的 forward 返回值进行修改,如下所示:
def forward(self, x):
patch_h, patch_w = x.shape[-2] // 14, x.shape[-1] // 14
features = self.pretrained.get_intermediate_layers(x, self.intermediate_layer_idx[self.encoder], return_class_token=True)
depth = self.depth_head(features, patch_h, patch_w)
depth = F.relu(depth)
#return depth.squeeze(1)
return depth
修改完成之后我们在 Depth Anything V2 项目下新建一个 export.py 文件,用于 ONNX 的导出,其内容如下:
import argparse
import cv2
import glob
import numpy as np
import os
import torch
import torch.onnx
from depth_anything_v2.dpt import DepthAnythingV2
def main():
parser = argparse.ArgumentParser(description='Depth Anything V2')
parser.add_argument('--input-size', type=int, default=518)
parser.add_argument('--encoder', type=str, default='vitl', choices=['vits', 'vitb', 'vitl', 'vitg'])
args = parser.parse_args()
# we are undergoing company review procedures to release Depth-Anything-Giant checkpoint
model_configs = {
'vits': {'encoder': 'vits', 'features': 64, 'out_channels': [48, 96, 192, 384]},
'vitb': {'encoder': 'vitb', 'features': 128, 'out_channels': [96, 192, 384, 768]},
'vitl': {'encoder': 'vitl', 'features': 256, 'out_channels': [256, 512, 1024, 1024]},
'vitg': {'encoder': 'vitg', 'features': 384, 'out_channels': [1536, 1536, 1536, 1536]}
}
depth_anything = DepthAnythingV2(**model_configs[args.encoder])
depth_anything.load_state_dict(torch.load(f'checkpoints/depth_anything_v2_{args.encoder}.pth', map_location='cpu'))
depth_anything = depth_anything.to('cpu').eval()
# Define dummy input data
dummy_input = torch.ones((3, args.input_size, args.input_size)).unsqueeze(0)
# Provide an example input to the model, this is necessary for exporting to ONNX
example_output = depth_anything.forward(dummy_input)
onnx_path = f'depth_anything_v2_{args.encoder}.onnx'
# Export the PyTorch model to ONNX format
torch.onnx.export(depth_anything, dummy_input, onnx_path, opset_version=11, input_names=["input"], output_names=["output"], verbose=True)
print(f"Model exported to {onnx_path}")
if __name__ == "__main__":
main()
接着执行如下指令即可完成 ONNX 的导出:
cd Depth-Anything-V2
python export.py --encoder vits --input-size 518
部分输出如下所示:
执行成功后会在当前目录下生成 depth_anything_v2_vits.onnx 模型文件
我们一起来看下刚导出的模型文件
可以看到这个 ONNX 模型前面还是比较“脏”的,constant 节点被 trace 导致一些不必要的节点,因此我们可以考虑拿 onnx-simplifier 先优化一下
在 export.py 新增如下代码:
import onnx
model_onnx = onnx.load(onnx_path)
# Simplify
try:
import onnxsim
print(f"simplifying with onnxsim {onnxsim.__version__}...")
model_onnx, check = onnxsim.simplify(model_onnx)
assert check, "Simplified ONNX model could not be validated"
except Exception as e:
print(f"simplifier failure: {e}")
onnx.save(model_onnx, f"depth_anything_v2_{args.encoder}.sim.onnx")
print(f"simplify done. onnx model save in depth_anything_v2_{args.encoder}.sim.onnx")
Note:完整的 export.py 代码在后面会提供,这里只是做演示说明
接着再次执行上述导出指令,执行完成后会在当前目录下生成 depth_anything_v2_vits.sim.onnx 即优化后的 ONNX 模型,我们一起来看下:
可以看到优化后的 ONNX 模型简洁了不少,那还有可以优化的地方吗?有!
我们在 ONNX 模型中可以找到如下的结构:
我们在韩君老师的课程中有讲过这个就是一个典型的 LayerNormalization 算子,大家感兴趣的可以看下:三. TensorRT基础入门-快速分析开源代码并导出onnx
那我们知道 ONNX 在 opset17 版本之后就开始支持 LayerNormalization 整个算子的导出了,具体可以参考:https://github.com/onnx/onnx/blob/main/docs/Operators.md
因此我们可以将 opset_version
修改为 17,如下所示:
torch.onnx.export(depth_anything, dummy_input, onnx_path, opset_version=17, input_names=["input"], output_names=["output"], verbose=True)
接着再次执行下导出指令,导出完成后可以看到新导出的 ONNX 模型的变化
Note:opset_version=17 需要 pytorch 版本大于 2.0.1,否则会出现如下的错误:
新导出的 ONNX 模型如下所示:
可以看到 LayerNormalization 作为一个完整的算子导出了,符合我们的预期
这里还有一个点需要大家注意,那就是 TensorRT 只有在 8.6 版本之后才开始支持 LayerNormalization 算子,因此如果你导出的 ONNX 中包含该算子,则需要你保证 TensorRT 在 8.6 版本以上,不然会出现算子节点无法解析的错误,具体可以参考:https://github.com/onnx/onnx-tensorrt/blob/release/8.6-EA/docs/Changelog.md
Note:如果你希望在低版本的 tensorRT 中也能解析 LayerNormalization 算子,那么你可以写插件来支持,大家感兴趣的可以看看:LayerNorm Plugin的使用与说明
我们再来看下动态 batch 模型的导出,简单增加下动态维度:
dynamic_batch = {"input": {0: "batch"}, "output": {0: "batch"}}
torch.onnx.export(depth_anything, dummy_input, onnx_path, opset_version=17, input_names=["input"], output_names=["output"], verbose=True, dynamic_axes=dynamic_batch)
再次执行后生成的 ONNX 模型就是 batch 维度动态,如下所示:
可以看到输入输出都保证了 batch 维度动态,似乎没有什么问题,但是大家往后看会发现动态 batch 模型的复杂度相比静态 batch 模型更高,新出现了诸如 Shape、Gather、Unsqueeze 等节点,这个主要是因为一些 shape 节点的 trace 导致的,之前杜老师的课程中有讲过,大家感兴趣的可以看看:6.3.tensorRT高级(1)-yolov5模型导出、编译到推理(无封装)
经过我们的调试分析(省略…😄)可知我们需要修改部分代码来断开 shape 等节点的 trace,如下所示:
# ========== patch_embed.py ==========
# depth_anything_v2/dinov2_layers/patch_embed.py第69行,forward函数
# def forward(self, x: Tensor) -> Tensor:
# _, _, H, W = x.shape
# patch_H, patch_W = self.patch_size
# assert H % patch_H == 0, f"Input image height {H} is not a multiple of patch height {patch_H}"
# assert W % patch_W == 0, f"Input image width {W} is not a multiple of patch width: {patch_W}"
# x = self.proj(x) # B C H W
# H, W = x.size(2), x.size(3)
# x = x.flatten(2).transpose(1, 2) # B HW C
# x = self.norm(x)
# if not self.flatten_embedding:
# x = x.reshape(-1, H, W, self.embed_dim) # B H W C
# return x
# 修改为:
def forward(self, x: Tensor) -> Tensor:
_, _, H, W = x.shape
patch_H, patch_W = self.patch_size
assert H % patch_H == 0, f"Input image height {H} is not a multiple of patch height {patch_H}"
assert W % patch_W == 0, f"Input image width {W} is not a multiple of patch width: {patch_W}"
x = self.proj(x) # B C H W
_, C, H, W = map(int, x.shape)
x = x.reshape(-1, C, H * W)
x = x.transpose(1, 2)
x = self.norm(x)
if not self.flatten_embedding:
x = x.reshape(-1, H, W, self.embed_dim) # B H W C
return x
# ========== dinov2.py ==========
# depth_anything_v2/dinov2.py第212行,prepare_tokens_with_masks函数
# def prepare_tokens_with_masks(self, x, masks=None):
# B, nc, w, h = x.shape
# x = self.patch_embed(x)
# if masks is not None:
# x = torch.where(masks.unsqueeze(-1), self.mask_token.to(x.dtype).unsqueeze(0), x)
# x = torch.cat((self.cls_token.expand(x.shape[0], -1, -1), x), dim=1)
# x = x + self.interpolate_pos_encoding(x, w, h)
# if self.register_tokens is not None:
# x = torch.cat(
# (
# x[:, :1],
# self.register_tokens.expand(x.shape[0], -1, -1),
# x[:, 1:],
# ),
# dim=1,
# )
# return x
# 修改为:
def prepare_tokens_with_masks(self, x, masks=None):
B, nc, w, h = x.shape
x = self.patch_embed(x)
if masks is not None:
x = torch.where(masks.unsqueeze(-1), self.mask_token.to(x.dtype).unsqueeze(0), x)
_, hw, embed_dim = map(int, self.cls_token.shape)
cls_tokens = self.cls_token.expand(x.shape[0], hw, embed_dim)
x = torch.concat((cls_tokens, x), dim=1)
x = x + self.interpolate_pos_encoding(x, w, h)
if self.register_tokens is not None:
x = torch.cat(
(
x[:, :1],
self.register_tokens.expand(x.shape[0], -1, -1),
x[:, 1:],
),
dim=1,
)
return x
执行下导出脚本,再看下导出的模型结构的变化:
可以看到后半部分非常简洁和静态 batch 模型基本上没区别,前面部分 shape 等节点也去除了,但是相比静态 batch 模型而言还是没那么干净,在模型前半部分有一些节点的增加,如下图所示:
那这部分节点的增加主要是下面这行代码导致的:
cls_tokens = self.cls_token.expand(x.shape[0], hw, embed_dim)
由于 x.shape[0]
即 batch size 的 trace 导致诸如 Shape、Gather、Unsqueeze 等多余节点的产生,但常量 self.cls_tokens
本身的维度是 1x1xemb_dim 又必须要 trace 到 batch size,因此这里博主暂时没有什么更好的办法去断开它们之间的 trace,索性让 self.cls_tokens
直接去 trace
OK,以上就是 Depth Anything V2 整个 ONNX 模型的导出流程了
6. ONNX导出总结
经过上面的分析,我们来总结下 Depth Anything V2 模型的 ONNX 到底该如何导出,该修改哪些文件呢?下面我们一一说明:
Step 1. 修改 dpt.py 文件
# ========== dpt.py ==========
# depth_anything_v2/dpt.py第184行,forward函数
# return depth.squeeze(1)
# 修改为:
return depth
Step 2. 修改 patch_embed.py 文件(动态 batch 导出需要)
# ========== patch_embed.py ==========
# depth_anything_v2/dinov2_layers/patch_embed.py第69行,forward函数
# def forward(self, x: Tensor) -> Tensor:
# _, _, H, W = x.shape
# patch_H, patch_W = self.patch_size
# assert H % patch_H == 0, f"Input image height {H} is not a multiple of patch height {patch_H}"
# assert W % patch_W == 0, f"Input image width {W} is not a multiple of patch width: {patch_W}"
# x = self.proj(x) # B C H W
# H, W = x.size(2), x.size(3)
# x = x.flatten(2).transpose(1, 2) # B HW C
# x = self.norm(x)
# if not self.flatten_embedding:
# x = x.reshape(-1, H, W, self.embed_dim) # B H W C
# return x
# 修改为:
def forward(self, x: Tensor) -> Tensor:
_, _, H, W = x.shape
patch_H, patch_W = self.patch_size
assert H % patch_H == 0, f"Input image height {H} is not a multiple of patch height {patch_H}"
assert W % patch_W == 0, f"Input image width {W} is not a multiple of patch width: {patch_W}"
x = self.proj(x) # B C H W
_, C, H, W = map(int, x.shape)
x = x.reshape(-1, C, H * W)
x = x.transpose(1, 2)
x = self.norm(x)
if not self.flatten_embedding:
x = x.reshape(-1, H, W, self.embed_dim) # B H W C
return x
Step3. 修改 dinov2.py 文件(动态 batch 导出需要)
# ========== dinov2.py ==========
# depth_anything_v2/dinov2.py第212行,prepare_tokens_with_masks函数
# def prepare_tokens_with_masks(self, x, masks=None):
# B, nc, w, h = x.shape
# x = self.patch_embed(x)
# if masks is not None:
# x = torch.where(masks.unsqueeze(-1), self.mask_token.to(x.dtype).unsqueeze(0), x)
# x = torch.cat((self.cls_token.expand(x.shape[0], -1, -1), x), dim=1)
# x = x + self.interpolate_pos_encoding(x, w, h)
# if self.register_tokens is not None:
# x = torch.cat(
# (
# x[:, :1],
# self.register_tokens.expand(x.shape[0], -1, -1),
# x[:, 1:],
# ),
# dim=1,
# )
# return x
# 修改为:
def prepare_tokens_with_masks(self, x, masks=None):
B, nc, w, h = x.shape
x = self.patch_embed(x)
if masks is not None:
x = torch.where(masks.unsqueeze(-1), self.mask_token.to(x.dtype).unsqueeze(0), x)
_, hw, embed_dim = map(int, self.cls_token.shape)
cls_tokens = self.cls_token.expand(x.shape[0], hw, embed_dim)
x = torch.concat((cls_tokens, x), dim=1)
x = x + self.interpolate_pos_encoding(x, w, h)
if self.register_tokens is not None:
x = torch.cat(
(
x[:, :1],
self.register_tokens.expand(x.shape[0], -1, -1),
x[:, 1:],
),
dim=1,
)
return x
Step4. 在 Depth-Anything-V2 项目下新建导出文件 export.py
import torch
import argparse
from depth_anything_v2.dpt import DepthAnythingV2
def main():
parser = argparse.ArgumentParser(description='Depth Anything V2')
parser.add_argument('--input-size', type=int, default=518)
parser.add_argument('--encoder', type=str, default='vits', choices=['vits', 'vitb', 'vitl', 'vitg'])
args = parser.parse_args()
# we are undergoing company review procedures to release Depth-Anything-Giant checkpoint
model_configs = {
'vits': {'encoder': 'vits', 'features': 64, 'out_channels': [48, 96, 192, 384]},
'vitb': {'encoder': 'vitb', 'features': 128, 'out_channels': [96, 192, 384, 768]},
'vitl': {'encoder': 'vitl', 'features': 256, 'out_channels': [256, 512, 1024, 1024]},
'vitg': {'encoder': 'vitg', 'features': 384, 'out_channels': [1536, 1536, 1536, 1536]}
}
depth_anything = DepthAnythingV2(**model_configs[args.encoder])
depth_anything.load_state_dict(torch.load(f'checkpoints/depth_anything_v2_{args.encoder}.pth', map_location='cpu'))
depth_anything = depth_anything.to('cpu').eval()
# Define dummy input data
dummy_input = torch.ones((3, args.input_size, args.input_size)).unsqueeze(0)
onnx_path = f'depth_anything_v2_{args.encoder}.onnx'
dynamic_batch = {"images": {0: "batch"}, "output": {0: "batch"}}
# Export the PyTorch model to ONNX format
torch.onnx.export(
depth_anything,
dummy_input,
onnx_path,
opset_version=17,
input_names=["images"],
output_names=["output"],
dynamic_axes=dynamic_batch
)
import onnx
model_onnx = onnx.load(onnx_path)
# Simplify
try:
import onnxsim
print(f"simplifying with onnxsim {onnxsim.__version__}...")
model_onnx, check = onnxsim.simplify(model_onnx)
assert check, "Simplified ONNX model could not be validated"
except Exception as e:
print(f"simplifier failure: {e}")
onnx.save(model_onnx, f"depth_anything_v2_{args.encoder}.sim.onnx")
print(f"simplify done. onnx model save in depth_anything_v2_{args.encoder}.sim.onnx")
if __name__ == "__main__":
main()
Step5. 终端执行导出指令
python export.py --encoder vits --input-size 518
这里有几点需要额外补充说明:
- 1. 预训练权重需要放在新建的 checkpoints 文件夹下
- 2. 如果想要导出的 encoder 不是 vits 或者输入尺寸不是 518 可以通过
--encoder
和--input-size
参数修改 - 3. 默认导出的是动态 batch 模型,如果只需要导出静态 batch 的 ONNX 模型将 dynamic_axes 设置为 None 即可,导出的 ONNX 模型会更加简洁
- 4.
opset_version
默认设置的 17 是为了将 LayerNormalization 作为一个完整算子导出,如果在部署时的 tensorRT 版本较低,可以将 opset_version 设置得小点
7. 拓展-Depth Anything V1的ONNX导出
Depth Anyting V1 模型的 ONNX 导出步骤如下:
Step 1. 修改 dpt.py 文件
# ========== dpt.py ==========
# depth_anything/dpt.py第5行,注释
# from huggingface_hub import PyTorchModelHubMixin, hf_hub_download
# depth_anything/dpt.py第166行,forward函数
# return depth.squeeze(1)
# 修改为:
return depth
Step2. 在 Depth-Anything 项目下新建导出文件 export.py
import torch
import argparse
import torch.onnx
from depth_anything.dpt import DPT_DINOv2
def export_model(encoder: str, load_from: str, image_shape: tuple):
# Initializing model
assert encoder in ['vits', 'vitb', 'vitl']
if encoder == 'vits':
depth_anything = DPT_DINOv2(encoder='vits', features=64, out_channels=[48, 96, 192, 384], localhub='localhub')
elif encoder == 'vitb':
depth_anything = DPT_DINOv2(encoder='vitb', features=128, out_channels=[96, 192, 384, 768], localhub='localhub')
else:
depth_anything = DPT_DINOv2(encoder='vitl', features=256, out_channels=[256, 512, 1024, 1024], localhub='localhub')
total_params = sum(param.numel() for param in depth_anything.parameters())
print('Total parameters: {:.2f}M'.format(total_params / 1e6))
# Loading model weight
depth_anything.load_state_dict(torch.load(load_from, map_location='cpu'), strict=True)
depth_anything.eval()
# Define dummy input data
dummy_input = torch.ones(image_shape).unsqueeze(0)
onnx_path = load_from.split('/')[-1].split('.pth')[0] + '.onnx'
dynamic_batch = {"images": {0: "batch"}, "output": {0: "batch"}}
# Export the PyTorch model to ONNX format
torch.onnx.export(
depth_anything,
dummy_input,
onnx_path,
opset_version=17,
input_names=["images"],
output_names=["output"],
dynamic_axes=None
)
import onnx
model_onnx = onnx.load(onnx_path)
# Simplify
try:
import onnxsim
print(f"simplifying with onnxsim {onnxsim.__version__}...")
model_onnx, check = onnxsim.simplify(model_onnx)
assert check, "Simplified ONNX model could not be validated"
except Exception as e:
print(f"simplifier failure: {e}")
onnx.save(model_onnx, f"depth_anything_{encoder}.sim.onnx")
print(f"simplify done. onnx model save in depth_anything_{encoder}.sim.onnx")
print(f"Model exported to {onnx_path}")
def main():
parser = argparse.ArgumentParser(description="Export Depth DPT model to ONNX format")
parser.add_argument("--encoder", type=str, choices=['vits', 'vitb', 'vitl'], help="Type of encoder to use ('vits', 'vitb', 'vitl')")
parser.add_argument("--load_from", type=str, help="Path to the pre-trained model checkpoint")
parser.add_argument("--image_shape", type=int, nargs=3, metavar=("channels", "height", "width"), help="Shape of the input image")
args = parser.parse_args()
export_model(args.encoder, args.load_from, tuple(args.image_shape))
if __name__ == "__main__":
main()
Step3. 终端执行导出指令
python export.py --encoder vits --load_from depth_anything_vits14.pth --image_shape 3 518 518
这里有几点需要额外补充说明:
- 1. 如果想要导出的 encoder 不是 vits 或者输入尺寸不是 518 可以通过
--encoder
、--load_from
和--image_shape
参数修改 - 2. 默认导出的是静态 batch 模型,如果需要导出动态 batch 的 ONNX 模型将 dynamic_axes 设置为 dynamic_batch 即可,但导出的 ONNX 模型会更加复杂
- 3. 由于 Depth Anything V1 中 DINOV2 模型的实现只提供了接口,因此无法通过修改代码来使得导出的动态 batch 模型更加的简洁
- 4.
opset_version
默认设置的 17 是为了将 LayerNormalization 作为一个完整算子导出,如果在部署时的 tensorRT 版本较低,可以将 opset_version 设置得小点
结语
博主在这里对 Depth Anything 这篇工作进行了相关的学习,主要目的是想要更多的去了解跟单目深度估计有关的任务,并参考 depth-anything-tensorrt 这个 repo 对 Depth Anything 模型进行了 ONNX 导出
OK,以上就是 Depth Anything 算法原理和模型导出的全部内容了,下节我们来学习如何利用 tensorRT 推理 Depth Anything,敬请期待😄
下载链接
参考
- Depth Anything: Unleashing the Power of Large-Scale Unlabeled Data
- https://github.com/LiheYoung/Depth-Anything
- Depth Anything V2
- https://github.com/DepthAnything/Depth-Anything-V2
- https://github.com/spacewalk01/depth-anything-tensorrt
- 论文精读: Depth Anything
- Talk|香港大学杨丽鹤:Depth Anything V2 - 更精细更鲁棒的单目深度估计基础模型
- Marigold
- 三. TensorRT基础入门-快速分析开源代码并导出onnx
- https://github.com/onnx/onnx/blob/main/docs/Operators.md
- https://github.com/onnx/onnx-tensorrt/blob/release/8.6-EA/docs/Changelog.md
- LayerNorm Plugin的使用与说明
- 6.3.tensorRT高级(1)-yolov5模型导出、编译到推理(无封装)