社群計算機視覺課程文件
度量和相對單目深度估計:概述。微調 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