公制和相对单目深度估计:概述。微调 Depth Anything V2 👐 📚
模型演变
在过去的十年中,单目深度估计模型取得了显著进步。让我们通过视觉之旅回顾这一演变。
我们从这样的基本模型开始
逐步发展到更复杂的模型
现在,我们有了最先进的模型,Depth Anything V2
令人印象深刻,不是吗?
今天,我们将揭开这些模型的工作原理,并简化复杂概念。此外,我们将使用自定义数据集微调我们自己的模型。“等等,”你可能会问,“当最新模型在任何环境中表现都如此出色时,为什么还需要在自己的数据集上微调模型呢?”
这正是本文的重点,其中涉及细微之处和具体细节。如果你渴望探索单目深度估计的复杂性,请继续阅读。
基础知识
“好的,深度到底是什么?”通常,它是一个单通道图像,其中每个像素代表从相机或传感器到与该像素对应的空间点的距离。然而,事实证明这些距离可以是绝对的或相对的——真是意想不到!
- 绝对深度:每个像素值直接对应于物理距离(例如,以米或厘米为单位)。
- 相对深度:像素值表示哪些点更近或更远,而不参考真实世界的测量单位。通常相对深度是反向的,即数字越小,点越远。
我们稍后将更详细地探讨这些概念。
“那么,单目是什么意思呢?”它仅仅意味着我们只需要使用一张照片来估计深度。这有什么挑战的呢?看看这个
正如你所看到的,将 3D 空间投影到 2D 平面可能会由于透视而产生歧义。为了解决这个问题,有精确的数学方法可以使用多幅图像进行深度估计,例如立体视觉、运动结构和更广泛的摄影测量领域。此外,还可以使用激光扫描仪(例如 LiDAR)进行深度测量。
相对和绝对(又称公制)深度估计:意义何在?
让我们探讨一些突出相对深度估计必要性的挑战。为了更科学,让我们参考一些论文。
预测公制深度的优势在于其对计算机视觉和机器人技术中许多下游应用的实用性,例如地图绘制、规划、导航、物体识别、3D 重建和图像编辑。然而,在多个数据集上训练单个公制深度估计模型通常会降低性能,特别是当集合中包含深度尺度差异很大的图像时,例如室内和室外图像。因此,当前的 MDE 模型通常过度拟合特定数据集,并且不能很好地泛化到其他数据集。
通常,这种图像到图像任务的架构是编码器-解码器模型,例如 U-Net,并进行了各种修改。形式上,这是一个像素级回归问题。想象一下,对于神经网络来说,精确预测每个像素的距离(从几米到几百米)是多么具有挑战性。
这引出了一个想法,即不再使用能够预测所有场景中精确距离的通用模型。相反,让我们开发一个能够近似(相对)预测深度的模型,通过指示哪些物体相对于彼此和我们更远或更近来捕捉场景的形状和结构。如果需要精确的距离,我们可以在特定数据集上微调这个相对模型,利用其对任务的现有理解。
问题不止于此
该模型不仅要处理使用不同相机和相机设置拍摄的图像,还要学习调整场景整体尺度的大幅变化。
除了不同的尺度外,正如我们前面提到的,一个重要的问题在于相机本身,它们可能对世界有截然不同的视角。
请注意焦距的变化如何显著改变对背景距离的感知!
最后,许多数据集根本缺少绝对深度图,只有相对深度图(例如,由于缺少相机校准)。此外,每种获取深度的方法都有其自身的优点、缺点、偏差和问题。
我们确定了三个主要挑战。1) 深度固有的不同表示:直接与逆深度表示。2) 尺度模糊性:对于某些数据源,深度仅在未知尺度下给出。3) 偏移模糊性:某些数据集仅提供视差,且该视差在未知尺度和全局视差偏移下给出,而全局视差偏移是未知基线和后处理导致的中心点水平偏移的函数。
简而言之,我希望我已经说服你,你不能只是从互联网上随意获取零散的深度图,然后用像素级 MSE 来训练模型。
但是,我们如何消除所有这些差异呢?我们如何最大限度地抽象出差异,并从所有这些数据集中提取共同点——即场景的形状和结构,以及物体之间的比例关系,指示哪个更近,哪个更远?
尺度和偏移不变损失 😎
简单来说,我们需要对所有我们想要训练和评估指标的深度图进行某种形式的归一化。我们有一个想法:我们想要创建一个不考虑环境尺度或各种偏移的损失函数。剩下的任务是将这个想法转化为数学术语。
具体来说,深度值首先通过 转换到视差空间,然后对每个深度图归一化到 。为了实现多数据集联合训练,我们采用仿射不变损失来忽略每个样本的未知尺度和偏移: 其中 和 分别是预测值和真实值。并且 是仿射不变平均绝对误差损失: ,其中 和 是预测值 和真实值 的经过缩放和偏移的版本: 其中 和 用于将预测值和真实值对齐,使其具有零平移和单位尺度:
事实上,还有许多其他方法和函数可以帮助消除尺度和偏移。损失函数也有不同的附加项,例如梯度损失,它关注的不是像素值本身,而是它们变化的速度(因此得名——梯度)。你可以在 MiDaS 论文中阅读更多相关内容,我会在最后列出一些有用的文献。在进入最激动人心的部分——使用自定义数据集进行绝对深度微调之前,让我们简要讨论一下指标。
指标
在深度估计中,通常使用几种标准指标来评估性能,包括 MAE(平均绝对误差)、RMSE(均方根误差)及其对数变体,以平滑距离中的大差异。此外,请考虑以下内容
- 绝对相对误差 (AbsRel):此度量类似于 MAE,但以百分比表示,衡量预测距离与真实距离平均相差的百分比。
- 阈值精度 ( ):这衡量了预测像素与真实像素差异不超过 25% 的百分比。
重要考量
对于我们所有的模型和基线,在测量误差之前,我们都会对每张图像的预测值和真实值进行尺度和偏移对齐。
的确,如果我们正在训练预测相对深度,但想在一个具有绝对值的数据集上测量质量,并且我们不感兴趣在此数据集上进行微调或获取绝对值,我们可以像损失函数一样,将尺度和偏移从计算中排除,并将所有内容标准化为统一的度量。
四种度量计算方法
理解这些方法有助于避免在分析论文中的度量时产生混淆
- 零样本相对深度估计
- 训练模型在一组数据集上预测相对深度,并在其他数据集上测量质量。由于深度是相对的,因此显著不同的尺度不是问题,并且在其他数据集上的指标通常保持较高,类似于训练数据集的测试集。
- 零样本绝对深度估计
- 训练一个通用的相对模型,然后在好的数据集上微调它以预测绝对深度,并测量在不同数据集上的绝对深度预测质量。在这种情况下,指标通常比前一种方法更差,这凸显了在不同环境中准确预测绝对深度的挑战。
- 微调(域内)绝对深度估计
- 与前一种方法类似,但现在在用于微调绝对深度预测的数据集的测试集上测量质量。这是最实用的方法之一。
- 微调(域内)相对深度估计
- 训练模型预测相对深度,并在训练数据集的测试集上测量质量。这可能不是最精确的名称,但其思想是直接的。
Depth Anything V2 绝对深度估计微调
在本节中,我们将通过在 NYU-D 数据集上微调模型以预测绝对深度来重现 Depth Anything V2 论文中的结果,旨在实现与上一节最后一个表格中显示的指标相似的指标。
Depth Anything V2 背后的核心思想
Depth Anything V2 是一个强大的深度估计模型,由于其几个创新概念而取得了显著成果
- 异构数据上的通用训练方法:MiDaS 2020 论文中引入的这种方法使得在各种类型数据集上进行鲁棒训练成为可能。
- DPT 架构:“用于密集预测的视觉 Transformer”论文介绍了这种架构,它本质上是一个带有 Vision Transformer (ViT) 编码器和一些修改的 U-Net。
- DINOv2 编码器:这个标准的 ViT,使用自监督方法在海量数据集上进行了预训练,作为一个强大而通用的特征提取器。近年来,计算机视觉研究人员致力于创建类似于自然语言处理中的 GPT 和 BERT 的基础模型,DINOv2 是朝着这个方向迈出的重要一步。
- 合成数据的使用:下面的图片很好地描述了训练流程。这种方法使作者能够获得如此清晰和准确的深度图。毕竟,如果你仔细思考,从合成数据中获得的标签才是真正的“地面真相”。
微调入门
现在,让我们深入代码。如果您没有强大的 GPU,我强烈建议使用 Kaggle 而不是 Colab。Kaggle 具有以下几个优点
- 每周最多 30 小时的 GPU 使用时间
- 无连接中断
- 快速便捷地访问数据集
- 在其中一种配置中可以同时使用两个 GPU,这将帮助您练习分布式训练
您可以直接使用 Kaggle 上的这个 notebook 来查看代码。
我们将在此处详细讲解所有内容。首先,让我们从作者的存储库中下载所有必需的模块,以及带有 ViT-S 编码器的最小模型的检查点。
步骤 1:克隆存储库并下载预训练权重
!git clone https://github.com/DepthAnything/Depth-Anything-V2
!wget -O depth_anything_v2_vits.pth https://huggingface.co/depth-anything/Depth-Anything-V2-Small/resolve/main/depth_anything_v2_vits.pth?download=true
如果您不在 Kaggle 工作,您可以在这里下载数据集
步骤 2:导入所需模块
import numpy as np
import matplotlib.pyplot as plt
import os
from tqdm import tqdm
import cv2
import random
import h5py
import sys
sys.path.append('/kaggle/working/Depth-Anything-V2/metric_depth')
from accelerate import Accelerator
from accelerate.utils import set_seed
from accelerate import notebook_launcher
from accelerate import DistributedDataParallelKwargs
import transformers
import torch
import torchvision
from torchvision.transforms import v2
from torchvision.transforms import Compose
import torch.nn.functional as F
import albumentations as A
from depth_anything_v2.dpt import DepthAnythingV2
from util.loss import SiLogLoss
from dataset.transform import Resize, NormalizeImage, PrepareForNet, Crop
步骤 3:获取训练和验证的所有文件路径
def get_all_files(directory):
all_files = []
for root, dirs, files in os.walk(directory):
for file in files:
all_files.append(os.path.join(root, file))
return all_files
train_paths = get_all_files('/kaggle/input/nyu-depth-dataset-v2/nyudepthv2/train')
val_paths = get_all_files('/kaggle/input/nyu-depth-dataset-v2/nyudepthv2/val')
步骤 4:定义 PyTorch 数据集
#NYU Depth V2 40k. Original NYU is 400k
class NYU(torch.utils.data.Dataset):
def __init__(self, paths, mode, size=(518, 518)):
self.mode = mode #train or val
self.size = size
self.paths = paths
net_w, net_h = size
#author's transforms
self.transform = Compose([
Resize(
width=net_w,
height=net_h,
resize_target=True if mode == 'train' else False,
keep_aspect_ratio=True,
ensure_multiple_of=14,
resize_method='lower_bound',
image_interpolation_method=cv2.INTER_CUBIC,
),
NormalizeImage(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
PrepareForNet(),
] + ([Crop(size[0])] if self.mode == 'train' else []))
# only horizontal flip in the paper
self.augs = A.Compose([
A.HorizontalFlip(),
A.ColorJitter(hue = 0.1, contrast=0.1, brightness=0.1, saturation=0.1),
A.GaussNoise(var_limit=25),
])
def __getitem__(self, item):
path = self.paths[item]
image, depth = self.h5_loader(path)
if self.mode == 'train':
augmented = self.augs(image=image, mask = depth)
image = augmented["image"] / 255.0
depth = augmented['mask']
else:
image = image / 255.0
sample = self.transform({'image': image, 'depth': depth})
sample['image'] = torch.from_numpy(sample['image'])
sample['depth'] = torch.from_numpy(sample['depth'])
# sometimes there are masks for valid depths in datasets because of noise e.t.c
# sample['valid_mask'] = ...
return sample
def __len__(self):
return len(self.paths)
def h5_loader(self, path):
h5f = h5py.File(path, "r")
rgb = np.array(h5f['rgb'])
rgb = np.transpose(rgb, (1, 2, 0))
depth = np.array(h5f['depth'])
return rgb, depth
以下是需要注意的几点
- 原始 NYU-D 数据集包含 407k 个样本,但我们使用的是其中的 40k 个子集。这会稍微影响最终模型的质量。
- 论文作者仅对数据增强使用了水平翻转。
- 偶尔,深度图中的某些点可能未正确处理,导致出现“坏像素”。一些数据集除了图像和深度图之外,还包含一个用于区分有效像素和无效像素的掩码。此掩码对于从损失和度量计算中排除坏像素是必需的。
- 在训练期间,我们将图像大小调整为较短边为 518 像素,然后进行裁剪。对于验证,我们不裁剪或调整深度图大小。相反,我们对预测的深度图进行上采样,并以原始分辨率计算指标。
步骤 5:数据可视化
num_images = 5
fig, axes = plt.subplots(num_images, 2, figsize=(10, 5 * num_images))
train_set = NYU(train_paths, mode='train')
for i in range(num_images):
sample = train_set[i*1000]
img, depth = sample['image'].numpy(), sample['depth'].numpy()
mean = np.array([0.485, 0.456, 0.406]).reshape((3, 1, 1))
std = np.array([0.229, 0.224, 0.225]).reshape((3, 1, 1))
img = img*std+mean
axes[i, 0].imshow(np.transpose(img, (1,2,0)))
axes[i, 0].set_title('Image')
axes[i, 0].axis('off')
im1 = axes[i, 1].imshow(depth, cmap='viridis', vmin=0)
axes[i, 1].set_title('True Depth')
axes[i, 1].axis('off')
fig.colorbar(im1, ax=axes[i, 1])
plt.tight_layout()
如您所见,图像非常模糊和嘈杂。因此,我们无法获得 Depth Anything V2 预览中看到的细粒度深度图。在黑洞伪影中,深度为 0,我们稍后将利用此事实来遮蔽这些空洞。此外,数据集包含许多同一位置几乎相同的照片。
第6步:准备数据加载器
def get_dataloaders(batch_size):
train_dataset = NYU(train_paths, mode='train')
val_dataset = NYU(val_paths, mode='val')
train_dataloader = torch.utils.data.DataLoader(train_dataset,
batch_size = batch_size,
shuffle=True,
num_workers=4,
drop_last=True
)
val_dataloader = torch.utils.data.DataLoader(val_dataset,
batch_size = 1, #for dynamic resolution evaluations without padding
shuffle=False,
num_workers=4,
drop_last=True
)
return train_dataloader, val_dataloader
第7步:指标评估
def eval_depth(pred, target):
assert pred.shape == target.shape
thresh = torch.max((target / pred), (pred / target))
d1 = torch.sum(thresh < 1.25).float() / len(thresh)
diff = pred - target
diff_log = torch.log(pred) - torch.log(target)
abs_rel = torch.mean(torch.abs(diff) / target)
rmse = torch.sqrt(torch.mean(torch.pow(diff, 2)))
mae = torch.mean(torch.abs(diff))
silog = torch.sqrt(torch.pow(diff_log, 2).mean() - 0.5 * torch.pow(diff_log.mean(), 2))
return {'d1': d1.detach(), 'abs_rel': abs_rel.detach(),'rmse': rmse.detach(), 'mae': mae.detach(), 'silog':silog.detach()}
我们的损失函数是SiLog。训练绝对深度时,我们似乎应该忘记对尺度不变性和其他用于相对深度训练的技术。然而,事实证明并非完全如此,我们通常仍然希望使用一种“尺度正则化”,但程度要小一些。参数λ=0.5有助于平衡全局一致性和局部准确性。
第8步:定义超参数
model_weights_path = '/kaggle/working/depth_anything_v2_vits.pth'
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]}
}
model_encoder = 'vits'
max_depth = 10
batch_size = 11
lr = 5e-6
weight_decay = 0.01
num_epochs = 10
warmup_epochs = 0.5
scheduler_rate = 1
load_state = False
state_path = "/kaggle/working/cp"
save_model_path = '/kaggle/working/model'
seed = 42
mixed_precision = 'fp16'
请注意参数“max_depth”。我们模型中的最后一层是每个像素的sigmoid函数,输出范围从0到1。我们只需将每个像素乘以“max_depth”以表示从0到“max_depth”的距离。
第9步:训练函数
def train_fn():
set_seed(seed)
ddp_kwargs = DistributedDataParallelKwargs(find_unused_parameters=True)
accelerator = Accelerator(mixed_precision=mixed_precision,
kwargs_handlers=[ddp_kwargs],
)
# in the paper they initialize decoder randomly and use only encoder pretrained weights. Then full fine-tune
# ViT-S encoder here
model = DepthAnythingV2(**{**model_configs[model_encoder], 'max_depth': max_depth})
model.load_state_dict({k: v for k, v in torch.load(model_weights_path).items() if 'pretrained' in k}, strict=False)
optim = torch.optim.AdamW([{'params': [param for name, param in model.named_parameters() if 'pretrained' in name], 'lr': lr},
{'params': [param for name, param in model.named_parameters() if 'pretrained' not in name], 'lr': lr*10}],
lr=lr, weight_decay=weight_decay)
criterion = SiLogLoss() # author's loss
train_dataloader, val_dataloader = get_dataloaders(batch_size)
scheduler = transformers.get_cosine_schedule_with_warmup(optim, len(train_dataloader)*warmup_epochs, num_epochs*scheduler_rate*len(train_dataloader))
model, optim, train_dataloader, val_dataloader, scheduler = accelerator.prepare(model, optim, train_dataloader, val_dataloader, scheduler)
if load_state:
accelerator.wait_for_everyone()
accelerator.load_state(state_path)
best_val_absrel = 1000
for epoch in range(1, num_epochs):
model.train()
train_loss = 0
for sample in tqdm(train_dataloader, disable = not accelerator.is_local_main_process):
optim.zero_grad()
img, depth = sample['image'], sample['depth']
pred = model(img)
# mask
loss = criterion(pred, depth, (depth <= max_depth) & (depth >= 0.001))
accelerator.backward(loss)
optim.step()
scheduler.step()
train_loss += loss.detach()
train_loss /= len(train_dataloader)
train_loss = accelerator.reduce(train_loss, reduction='mean').item()
model.eval()
results = {'d1': 0, 'abs_rel': 0,'rmse': 0, 'mae': 0, 'silog': 0}
for sample in tqdm(val_dataloader, disable = not accelerator.is_local_main_process):
img, depth = sample['image'].float(), sample['depth'][0]
with torch.no_grad():
pred = model(img)
# evaluate on the original resolution
pred = F.interpolate(pred[:, None], depth.shape[-2:], mode='bilinear', align_corners=True)[0, 0]
valid_mask = (depth <= max_depth) & (depth >= 0.001)
cur_results = eval_depth(pred[valid_mask], depth[valid_mask])
for k in results.keys():
results[k] += cur_results[k]
for k in results.keys():
results[k] = results[k] / len(val_dataloader)
results[k] = round(accelerator.reduce(results[k], reduction='mean').item(),3)
accelerator.wait_for_everyone()
accelerator.save_state(state_path, safe_serialization=False)
if results['abs_rel'] < best_val_absrel:
best_val_absrel = results['abs_rel']
unwrapped_model = accelerator.unwrap_model(model)
if accelerator.is_local_main_process:
torch.save(unwrapped_model.state_dict(), save_model_path)
accelerator.print(f"epoch_{epoch}, train_loss = {train_loss:.5f}, val_metrics = {results}")
#P.S. While testing one configuration, I encountered an error in which the loss turned into nan.
# This is fixed by adding a small epsilon to the predictions to prevent division by 0
在论文中,作者随机初始化解码器并仅使用编码器权重。然后他们对整个模型进行微调。其他值得注意的几点包括:
- 对解码器和编码器使用不同的学习率。编码器的学习率较低,因为我们不想显著改变已经很优秀的权重,这与随机初始化的解码器不同。
- 作者在论文中使用了多项式调度器,而我使用了带预热的余弦调度器,因为我喜欢它。
- 在掩码中,如前所述,我们通过使用条件“depth >= 0.001”来避免深度图中的黑洞。
- 在训练周期中,我们计算调整大小后的深度图的损失。在验证期间,我们对预测进行上采样,并以原始分辨率计算指标。
- 看看我们如何使用HF Accelerate轻松地将自定义PyTorch代码封装用于分布式计算。
第10步:启动训练
#You can run this code with 1 gpu. Just set num_processes=1
notebook_launcher(train_fn, num_processes=2)
我相信我们已经达到了预期的目标。性能上的轻微差异可能归因于数据集大小的显著差异(4万 vs 40万)。请记住,我们使用了ViT-S编码器。
让我们展示一些结果
model = DepthAnythingV2(**{**model_configs[model_encoder], 'max_depth': max_depth}).to('cuda')
model.load_state_dict(torch.load(save_model_path))
num_images = 10
fig, axes = plt.subplots(num_images, 3, figsize=(15, 5 * num_images))
val_dataset = NYU(val_paths, mode='val')
model.eval()
for i in range(num_images):
sample = val_dataset[i]
img, depth = sample['image'], sample['depth']
mean = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1)
std = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1)
with torch.inference_mode():
pred = model(img.unsqueeze(0).to('cuda'))
pred = F.interpolate(pred[:, None], depth.shape[-2:], mode='bilinear', align_corners=True)[0, 0]
img = img*std + mean
axes[i, 0].imshow(img.permute(1,2,0))
axes[i, 0].set_title('Image')
axes[i, 0].axis('off')
max_depth = max(depth.max(), pred.cpu().max())
im1 = axes[i, 1].imshow(depth, cmap='viridis', vmin=0, vmax=max_depth)
axes[i, 1].set_title('True Depth')
axes[i, 1].axis('off')
fig.colorbar(im1, ax=axes[i, 1])
im2 = axes[i, 2].imshow(pred.cpu(), cmap='viridis', vmin=0, vmax=max_depth)
axes[i, 2].set_title('Predicted Depth')
axes[i, 2].axis('off')
fig.colorbar(im2, ax=axes[i, 2])
plt.tight_layout()
验证集中的图像比训练集中的图像更清晰、更准确,这就是为什么我们的预测相对显得有些模糊。请再看看上面的训练样本。
总的来说,关键的启示是模型的质量在很大程度上取决于所提供的深度图的质量。向Depth Anything V2的作者致敬,他们克服了这一限制,生成了非常清晰的深度图。唯一的缺点是它们是相对的。