社区计算机视觉课程文档
度量和相对单目深度估计:概述。微调 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 绝对深度估计微调
在本节中,我们将通过在 NYU-D 数据集上微调 Depth Anything V2 模型来预测绝对深度,从而重现 Depth Anything V2 论文中的结果,目标是实现与上一节最后一个表格中显示的指标相似的指标。
Depth Anything V2 的核心思想
Depth Anything V2 是一个强大的深度估计模型,由于几个创新概念而取得了显著成果:
- 异构数据上的通用训练方法:MiDaS 2020 论文中引入的这种方法使得在各种类型数据集上进行鲁棒训练成为可能。
- DPT 架构:《Vision Transformers for Dense Prediction》论文提出了这种架构,它本质上是一个 U-Net,带有一个 Vision 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 数据集包含 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)
我相信我们达到了预期的目标。性能上的细微差异可以归因于数据集大小的显著差异(40k 对 400k)。请记住,我们使用了 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 的作者克服了这一限制,生成了非常清晰的深度图,值得称赞。唯一的缺点是它们是相对的。
参考文献
- 迈向鲁棒单目深度估计:混合数据集实现零样本跨数据集迁移
- ZoeDepth:通过结合相对深度和度量深度实现零样本迁移
- 用于密集预测的视觉 Transformer
- Depth Anything:释放大规模未标注数据的力量
- Depth Anything V2