社区计算机视觉课程文档
指标和相对单目深度估计:概述。微调 Depth Anything V2 👐 📚
并获得增强的文档体验
开始使用
指标和相对单目深度估计:概述。微调 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 绝对深度估计微调
在本节中,我们将重现 Depth Anything V2 论文中的结果,方法是在 NYU-D 数据集上微调模型以预测绝对深度,目标是获得与上一节最后一个表所示指标相似的指标。
Depth Anything V2 背后的关键思想
Depth Anything V2 是一个强大的深度估计模型,由于几个创新概念,它取得了显著的成果
- 异构数据上的通用训练方法:MiDaS 2020 论文中介绍了这种方法,它能够在各种类型的数据集上进行稳健的训练。
- DPT 架构:“用于密集预测的视觉 Transformer”论文介绍了这种架构,它本质上是一个 U-Net,带有一个视觉 Transformer (ViT) 编码器和一些修改。
- DINOv2 编码器:这种标准的 ViT 使用自监督方法在海量数据集上进行了预训练,可作为强大而通用的特征提取器。 近年来,CV 研究人员一直致力于创建类似于 NLP 中 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
您还可以在此处下载数据集
步骤 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 数据集包含 40.7 万个样本,但我们使用的是 4 万个样本的子集。 这将略微影响最终的模型质量。
- 该论文的作者仅使用了水平翻转进行数据增强。
- 有时,深度图中的某些点可能无法被正确处理,从而导致“坏像素”。除了图像和深度图之外,一些数据集还包含一个掩码,用于区分有效像素和无效像素。这个掩码对于从损失和指标计算中排除坏像素是必要的。
- 在训练期间,我们会调整图像大小,使较小边为 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 的作者克服了这一限制并生成了非常清晰的深度图。唯一的缺点是它们是相对的。
参考文献
- Towards Robust Monocular Depth Estimation: Mixing Datasets for Zero-shot Cross-dataset Transfer
- ZoeDepth: Zero-shot Transfer by Combining Relative and Metric Depth
- Vision Transformers for Dense Prediction
- Depth Anything: Unleashing the Power of Large-Scale Unlabeled Data
- Depth Anything V2