[AI] : Depth Anything V2


01. Depth Anything

1) Depth Anything

  • Depth Anything은 해당 논문에서 제안한 단안 깊이 추정(Monocular Depth Estimation)을 위한 솔루션이다. 기존 MiDaS에 비해서 좋은 성능을 보인다고 논문에서 설명하고 있다. Depth Anything은 성능 향상을 위해서 약 6200만 개의 대규모 레이블 없는 데이터를 수집하고, 자동 주석 시스템을 설계하여 데이터셋을 확장했다. 이로 인해 데이터 범위가 크게 확장되어, 모델의 일반화 오류를 줄일 수 있었다고 말하고 있다.

  • Depth Anything에서는 데이터 확장을 위해서 두가지 전략을 사용했다.

    • 데이터 증강 도구 활용: 더 어려운 최적화 목표를 설정하여, 모델이 시각적 지식을 더 적극적으로 학습하고 견고한 표현을 형성할 수 있게 했다.
    • 보조 감독 설계: 모델이 사전 훈련된 인코더에서 풍부한 의미적 지식을 상속받도록 강제하여, 더 나은 성능을 얻도록 했다.

image-center

[참고: https://arxiv.org/abs/2401.10891]


  • 단안 깊이 추정(Monocular Depth Estimation) : 단안 깊이 추정이란 단일 이미지에서 각 픽셀의 깊이(depth)를 추정하는 컴퓨터 비전 기술이다. 다른 고가의 장비(LiDAR, 스테레오 카메라 등)을 이용하지 않고 한 장의 2D 이미지로부터 3D 공간 정보를 유추하여, 이미지의 각 픽셀이 카메라로부터 얼마나 떨어져 있는지 계산한다. 이는 자율주행, AR 등 분야에서 사용된다. image-center
[참고: https://arxiv.org/abs/2401.10891]


2) Depth Anything V2

  • Depth Anything V2는 해당 논문에서 제안한 모델로써, V1보다 훨씬 더 세밀하고 강력한 깊이 예측을 제공한다. 이는 다음 세 가지 주요 변화 덕분이다.
    • 모든 라벨이 있는 실제 이미지를 대체하여 합성 이미지로 훈련을 진행.
    • 교사 모델(teacher model)의 용량을 확장.
    • 대규모의 의사 라벨이 붙은 실제 이미지를 사용하여 학생 모델을 교육.

02. Depth Anything Fine Tuning

1) Fine Tuning

  • Fine-Tuning(파인 튜닝)은 사전 학습된 모델을 기반으로 특정 작업에 맞춰 모델을 세부적으로 조정하는 기법이다. 일반적으로 대규모 데이터셋에서 미리 학습된 모델을 사용하고, 이를 특정 도메인에 적합하도록 소규모 데이터셋에서 재학습시키는 과정을 의미한다. Fine-Tuning은 특히 딥러닝에서 널리 사용되는 방법으로, 모델의 성능을 개선하면서 학습 시간을 단축하는 데 유용하다.

image-center

[참고: https://medium.com/@prasadmahamulkar/fine-tuning-phi-2-a-step-by-step-guide-e672e7f1d009 ]


2) Depth Anything Fine Tuning

  • Depth Anything은 파인 튜닝은 해당 링크를 참고하였다. 데이터셋이 크니, 처음 수행할때는 Data를 Subnet 하는 방식으로 테스트 하는게 좋다.

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 기준으로 예제가 짜여 있어서, 노트북 사용 시 아래 링크에서 데이터셋 가져와야함.
  !wget http://datasets.lids.mit.edu/fastdepth/data/nyudepthv2.tar.gz

2단계: 필요한 모듈 가져오기

  # 2단계: 필요한 모듈 가져오기
  import numpy as np  # Numpy는 다차원 배열 및 수학 함수 처리를 위한 라이브러리입니다.
  import matplotlib.pyplot as plt  # Matplotlib는 시각화 및 그래프 출력을 위해 사용됩니다.
  import os  # OS 모듈은 파일 시스템 경로를 처리하는 데 사용됩니다.
  from tqdm import tqdm  # Tqdm은 반복문 진행 상황을 시각화하는 진행 바를 제공합니다.
  import cv2  # OpenCV는 이미지 및 비디오 처리를 위한 라이브러리입니다.
  import random  # 랜덤화 및 무작위 요소 추가를 위해 사용됩니다.
  import h5py  # HDF5 파일 형식을 처리하는 데 사용됩니다.

  import sys
  sys.path.append('/home/K2023511/MDE/Depth-Anything-V2/metric_depth')  # 외부 모듈 및 경로를 추가하여 커스텀 함수 및 스크립트를 사용합니다.

  from accelerate import Accelerator  # Accelerate는 여러 장치에서 모델을 쉽게 학습할 수 있도록 도와줍니다.
  from accelerate.utils import set_seed  # 랜덤 시드 설정을 도와주는 유틸리티로, 실험 결과의 재현성을 높입니다.
  from accelerate import notebook_launcher  # 노트북 환경에서 Accelerate 학습을 실행할 수 있게 도와줍니다.
  from accelerate import DistributedDataParallelKwargs  # 분산 데이터 병렬 처리에서 추가 설정을 할 때 사용됩니다.

  import transformers  # Hugging Face transformers는 사전 훈련된 모델을 다루는 데 사용됩니다.

  import torch  # PyTorch는 딥러닝 모델 구현 및 학습에 사용되는 대표적인 라이브러리입니다.
  import torchvision  # Torchvision은 PyTorch에서 이미지 데이터를 처리하고 모델을 학습하는 데 도움을 줍니다.
  from torchvision.transforms import v2  # torchvision.transforms는 이미지 데이터의 변형을 돕는 모듈입니다.
  from torchvision.transforms import Compose  # 여러 데이터 변형을 결합하는 도구입니다.
  import torch.nn.functional as F  # PyTorch에서 기본적으로 제공하는 함수형 API, 주로 손실 함수 및 활성화 함수로 사용됩니다.
  import albumentations as A  # Albumentations는 이미지 증강을 위해 사용되는 고성능 라이브러리입니다.

  from depth_anything_v2.dpt import DepthAnythingV2  # Depth-Anything 모델의 버전을 가져옵니다.
  from util.loss import SiLogLoss  # SiLog 손실 함수는 깊이 추정의 로그 스케일 손실을 계산하는 데 사용됩니다.
  from dataset.transform import Resize, NormalizeImage, PrepareForNet, Crop  # 데이터셋의 이미지 전처리를 위한 다양한 변형 함수들입니다.

3단계: 교육 및 검증을 위한 모든 파일 경로 가져오기

  # 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

  # 훈련 데이터 경로 가져오기 (여기서는 NYU Depth Dataset을 사용하고 있습니다. 경로는 필요에 따라 변경해야 합니다.)
  train_paths = get_all_files('/home/K2023511/MDE/nyudepthv2/train')  # 훈련 데이터 경로 설정
  val_paths = get_all_files('/home/K2023511/MDE/nyudepthv2/val')  # 검증 데이터 경로 설정

4단계: PyTorch 데이터 세트 정의

  # 4단계: PyTorch 데이터 세트 정의
  # NYU Depth V2 40k 샘플 세트를 사용합니다. 원본 NYU 데이터는 400k 샘플입니다.
  class NYU(torch.utils.data.Dataset):
      def __init__(self, paths, mode, size=(518, 518)):
          
          self.mode = mode  # 'train' 또는 'val' 모드를 설정합니다.
          self.size = size  # 입력 이미지 크기를 설정합니다.
          self.paths = paths  # 데이터 경로를 저장합니다.
          
          net_w, net_h = size  # 네트워크 입력 크기 (너비, 높이)를 설정합니다.

          # 작가가 정의한 데이터 전처리 변환 리스트 (훈련 또는 검증 모드에 따라 다름)
          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,  # 너비와 높이를 14의 배수로 만듭니다.
                  resize_method='lower_bound',  # 가장 작은 크기에 맞게 조정합니다.
                  # 3차 방정식(세제곱 보간법, Cubic Interpolation)을 사용하여 새로운 픽셀 값을 계산. 이는 인접한 4x4 픽셀 영역을 참조하여 픽셀 값을 결정
                  image_interpolation_method=cv2.INTER_CUBIC,  # 이미지 크기 조정에 사용되는 방법(Cubic interpolation) 
              ),
              # 이미지 정규화 (mean(평균)과 std(표준편차)를 기반으로)
              NormalizeImage(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
              PrepareForNet(),  # 모델 입력에 맞게 준비
          ] + ([Crop(size[0])] if self.mode == 'train' else []))  # 훈련 모드일 때만 추가적으로 이미지를 자릅니다.

          # 데이터 증강 (훈련 시에만 사용)
          # 수평 반전, 색상 조정, 가우시안 노이즈 추가 등
          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)  # HDF5 파일에서 이미지를 로드합니다.
          
          if self.mode == 'train':  # 훈련 모드일 경우
              augmented = self.augs(image=image, mask=depth)  # 데이터 증강을 적용합니다.
              image = augmented["image"] / 255.0  # 이미지를 0-1 사이로 스케일링합니다.
              depth = augmented['mask']  # 깊이 맵을 동일하게 유지합니다.
          else:  # 검증 모드일 경우
              image = image / 255.0  # 이미지를 0-1 사이로 스케일링합니다.
          
          # 이미지와 깊이 맵에 변환을 적용합니다.
          sample = self.transform({'image': image, 'depth': depth})
          sample['image'] = torch.from_numpy(sample['image'])  # 이미지 데이터를 텐서로 변환
          sample['depth'] = torch.from_numpy(sample['depth'])  # 깊이 맵 데이터를 텐서로 변환
          
          # 깊이 맵의 유효 마스크를 반환할 수도 있습니다 (유효한 깊이 값만 처리할 때 사용)
          # sample['valid_mask'] = ... 
          
          return sample  # 전처리된 이미지와 깊이 맵 반환

      # 데이터셋의 크기 반환 (데이터 경로의 길이로 결정)
      def __len__(self):
          return len(self.paths)
      
      # HDF5 파일에서 이미지를 로드하는 함수
      def h5_loader(self, path):
          h5f = h5py.File(path, "r")  # HDF5 파일을 읽기 모드로 엽니다.
          rgb = np.array(h5f['rgb'])  # RGB 이미지를 불러옵니다.
          rgb = np.transpose(rgb, (1, 2, 0))  # 이미지를 (H, W, C) 형식으로 변환합니다.
          depth = np.array(h5f['depth'])  # 깊이 맵을 불러옵니다.
          return rgb, depth  # RGB 이미지와 깊이 맵을 반환합니다.

5단계: 데이터 시각화

  # 5단계: 데이터 시각화

  num_images = 5  # 시각화할 이미지와 깊이 맵의 개수를 5개로 설정합니다.

  fig, axes = plt.subplots(num_images, 2, figsize=(10, 5 * num_images))  # 5행 2열의 subplot을 생성하여 각 이미지를 시각화할 공간을 만듭니다.

  train_set = NYU(train_paths, mode='train')  # 'train' 모드로 NYU 데이터셋을 불러옵니다.

  for i in range(num_images):  # 설정한 5개의 이미지에 대해 반복합니다.
      sample = train_set[i*1000]  # 데이터셋에서 1000번째 간격으로 샘플을 선택합니다.
      img, depth = sample['image'].numpy(), sample['depth'].numpy()  # 이미지를 NumPy 배열로 변환하고, 깊이 맵도 NumPy 배열로 변환합니다.

      mean = np.array([0.485, 0.456, 0.406]).reshape((3, 1, 1))  # RGB 채널별로 정규화에 사용된 평균값을 설정합니다.
      std = np.array([0.229, 0.224, 0.225]).reshape((3, 1, 1))  # RGB 채널별로 정규화에 사용된 표준 편차 값을 설정합니다.
      img = img*std+mean  # 이미지를 역정규화하여 원본 색상으로 복원합니다.

      axes[i, 0].imshow(np.transpose(img, (1, 2, 0)))  # 이미지를 (Height, Width, Channel) 형식으로 변환하여 올바르게 표시합니다.
      axes[i, 0].set_title('Image')  # 첫 번째 열에 이미지를 나타내는 제목을 설정합니다.
      axes[i, 0].axis('off')  # 이미지를 깔끔하게 보기 위해 축을 숨깁니다.

      im1 = axes[i, 1].imshow(depth, cmap='viridis', vmin=0)  # 깊이 맵을 viridis 색상으로 표시하고, 최소 깊이를 0으로 설정합니다.
      axes[i, 1].set_title('True Depth')  # 두 번째 열에 깊이 맵을 나타내는 제목을 설정합니다.
      axes[i, 1].axis('off')  # 깊이 맵의 축을 숨깁니다.
      fig.colorbar(im1, ax=axes[i, 1])  # 깊이 맵 옆에 컬러바를 추가하여 색상 값과 실제 깊이 값의 관계를 표시합니다.

  plt.tight_layout()  # 그래프가 서로 겹치지 않도록 자동으로 레이아웃을 조정합니다.

6단계: Dataloader 준비

  # 6단계: Dataloader 준비
  from torch.utils.data import Subset

  def get_dataloaders(batch_size):
      
      # 사용할 데이터셋에서 첫 100개만 가져오기 (원하는 개수로 수정 가능)
      # train_subset_size = min(10000, len(train_paths))
      # val_subset_size = min(10000, len(val_paths))# 원하는 데이터 개수
      # train_indices = list(range(train_subset_size))  # 첫 100개의 인덱스 생성
      # val_indices = list(range(val_subset_size))    # 첫 100개의 인덱스 생성

      # train_paths와 val_paths에서 일부 데이터만 가져오도록 Subset 사용
      # train_dataset = Subset(NYU(train_paths, mode='train'), train_indices)
      # val_dataset = Subset(NYU(val_paths, mode='val'), val_indices)
      train_dataset = NYU(train_paths, mode='train')  # 훈련 데이터셋을 NYU 클래스에서 가져옵니다.
      val_dataset = NYU(val_paths, mode='val')  # 검증 데이터셋을 NYU 클래스에서 가져옵니다.
      
      # 훈련 데이터 로더 생성
      train_dataloader = torch.utils.data.DataLoader(train_dataset, 
                                                    batch_size=batch_size,  # 지정된 배치 크기로 데이터 로드
                                                    # shuffle=True,  # 훈련 데이터를 매 에포크마다 섞음
                                                    shuffle=False,  # 훈련 데이터를 매 에포크마다 섞음
                                                    num_workers=4,  # 데이터를 병렬로 로드하기 위한 워커 수
                                                    drop_last=True  # 배치 크기에 맞지 않는 마지막 배치는 버림
                                                    )

      # 검증 데이터 로더 생성
      val_dataloader = torch.utils.data.DataLoader(val_dataset, 
                                                batch_size=1,  # 동적 해상도 평가를 위해 배치 크기를 1로 설정 (패딩 없음)
                                                shuffle=False,  # 검증 데이터는 섞지 않음
                                                num_workers=4,  # 데이터를 병렬로 로드하기 위한 워커 수
                                                drop_last=True  # 배치 크기에 맞지 않는 마지막 배치는 버림
                                                  )
      
      return train_dataloader, val_dataloader  # 훈련 및 검증 데이터 로더를 반환

7단계: 지표 평가 함수

  # 7단계: 지표 평가 함수

  def eval_depth(pred, target):
      assert pred.shape == target.shape  # 예측값(pred)과 실제값(target)의 크기가 같아야 함을 보장하는 확인 코드

      thresh = torch.max((target / pred), (pred / target))  # 예측값과 실제값의 비율을 계산하고, 각 위치에서 최대값을 취함

      d1 = torch.sum(thresh < 1.25).float() / len(thresh)  # d1 지표: 비율이 1.25 미만인 요소들의 비율을 계산 (정확도를 나타냄)

      diff = pred - target  # 예측값과 실제값 간의 차이를 계산
      diff_log = torch.log(pred) - torch.log(target)  # 로그 공간에서의 차이를 계산 (상대적인 차이를 보기 위함)

      abs_rel = torch.mean(torch.abs(diff) / target)  # 절대 상대 오차(absolute relative difference)를 계산하여 오차의 비율을 측정

      rmse = torch.sqrt(torch.mean(torch.pow(diff, 2)))  # RMSE (Root Mean Squared Error) 계산: 제곱 오차의 평균을 구한 뒤 제곱근을 취함

      mae = torch.mean(torch.abs(diff))  # MAE (Mean Absolute Error) 계산: 절대 오차의 평균을 구함

      silog = torch.sqrt(torch.pow(diff_log, 2).mean() - 0.5 * torch.pow(diff_log.mean(), 2))  # SILog (Scale-Invariant Logarithmic Error) 계산: 로그 차이의 평균으로 스케일 불변 오차를 계산

      return {'d1': d1.detach(), 'abs_rel': abs_rel.detach(), 'rmse': rmse.detach(), 'mae': mae.detach(), 'silog': silog.detach()}  
      # 계산된 평가 지표들을 딕셔너리 형태로 반환

8단계: 하이퍼파라미터 정의

  # 8단계: 하이퍼파라미터 정의

  # 미리 학습된 모델 가중치 파일 경로 설정 (Depth Anything 모델을 사용할 예정)
  model_weights_path = '//home/K2023511/MDE/depth_anything_v2_vits.pth' 

  # 다양한 모델 설정을 저장한 딕셔너리
  # 각 모델에 따라 encoder와 features 및 출력 채널 크기가 다름
  model_configs = {
          'vits': {'encoder': 'vits', 'features': 64, 'out_channels': [48, 96, 192, 384]},  # 'vits' 모델에 대한 설정
          'vitb': {'encoder': 'vitb', 'features': 128, 'out_channels': [96, 192, 384, 768]},  # 'vitb' 모델에 대한 설정
          'vitl': {'encoder': 'vitl', 'features': 256, 'out_channels': [256, 512, 1024, 1024]},  # 'vitl' 모델에 대한 설정
          'vitg': {'encoder': 'vitg', 'features': 384, 'out_channels': [1536, 1536, 1536, 1536]}  # 'vitg' 모델에 대한 설정
      }

  # 사용할 모델의 인코더 선택 (여기서는 'vits'를 사용)
  model_encoder = 'vits'

  # 최대 깊이 값 (깊이 예측에서 사용할 최대값)
  max_depth = 10

  # 배치 크기 설정 (한 번에 처리할 데이터의 수)
  batch_size = 11

  # 학습률(Learning Rate) 설정 (모델이 학습할 때 가중치를 조정하는 속도)
  lr = 5e-6

  # 가중치 감쇠(Weight Decay) 설정 (과적합을 방지하기 위해 가중치가 너무 커지지 않도록 하는 정규화 기법)
  weight_decay = 0.01

  # 학습할 총 에포크(Epoch) 수 설정 (전체 데이터셋을 몇 번 반복할 것인지 결정)
  num_epochs = 10

  # 웜업 에포크 설정 (학습 초기에는 천천히 시작하여 모델의 안정성을 높임)
  warmup_epochs = 0.5

  # 스케줄러의 학습률 변동 속도 설정
  scheduler_rate = 1

  # 모델의 기존 상태를 불러올지 여부 (이전에 저장한 상태가 있을 경우)
  load_state = False

  # 모델 체크포인트 저장 경로 설정
  state_path = "/home/K2023511/MDE/cp"

  # 최종 학습된 모델이 저장될 경로 설정
  save_model_path = '/home/K2023511/MDE/model'

  # 시드 설정 (무작위성을 제어하여 실험의 재현성을 보장)
  seed = 42

  # 혼합 정밀도 설정 (fp16은 16-bit 부동 소수점을 사용하여 메모리 사용을 최적화)
  mixed_precision = 'fp16'

9단계: 모델 훈련 함수 정의

    # 9단계: 모델 훈련 함수 정의
    def train_fn():

        # 시드를 설정하여 결과의 재현성을 보장
        set_seed(seed)

        # DDP 관련 설정을 위한 DistributedDataParallelKwargs 설정
        ddp_kwargs = DistributedDataParallelKwargs(find_unused_parameters=True) 

        # 혼합 정밀도를 활성화하고 가속기 설정
        accelerator = Accelerator(mixed_precision=mixed_precision, 
                                  kwargs_handlers=[ddp_kwargs])

        # DepthAnythingV2 모델을 초기화, 인코더를 사전 학습된 가중치로 불러오고 디코더는 랜덤 초기화
        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)

        # SiLogLoss 사용 (논문에 나온 손실 함수)
        #  깊이 추정(deep estimation) 작업에서 사용되는 손실 함수로, 이미지로부터 깊이 맵을 예측할 때 예측 값과 실제 값 간의 차이를 계산하는 방식이다.
        #  이 손실 함수는 스케일 불변성(scale-invariance)을 보장하면서 깊이 값을 예측하는 과정에서 발생할 수 있는 오류를 최소화하는 특징을 가짐.
        criterion = SiLogLoss() 

        # 데이터 로더를 가져옴 (학습 및 검증 데이터 로더)
        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) 

                # 손실 계산 (유효한 깊이 값만 사용)
                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}
            accelerator.print(len(val_dataloader))
            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)

                    # 원본 해상도에서 평가
                    pred = F.interpolate(pred[:, None], depth.shape[-2:], mode='bilinear', align_corners=True)[0, 0]

                # 유효한 마스크 설정 (0.001보다 크고 max_depth보다 작은 깊이 값만 사용)
                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)

            # 검증 절대 오차(abs_rel)가 가장 낮은 경우 모델 저장
            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}")

에러 발생 시 수행

  # # RuntimeError: File /home/K2023511/MDE/model cannot be opened. 에러 발생시 수행
  import os

  save_dir = "/home/K2023511/MDE/"
  save_model_path = os.path.join(save_dir, "model.pth")

  # 디렉토리가 없으면 생성
  if not os.path.exists(save_dir):
      os.makedirs(save_dir)

10단계: Traning

  # 10단계: Traning
  #You can run this code with 1 gpu. Just set num_processes=1
  # num_processes 값을 통해 사용할 GPU의 개수를 지정하여 사용 가능.
  notebook_launcher(train_fn, num_processes=1)

image-center

11단계 학습된거 실제로 확인해보기

  # 모델 정의: 미리 정의된 DepthAnythingV2 모델을 max_depth 값과 함께 GPU로 로드
  model = DepthAnythingV2(**{**model_configs[model_encoder], 'max_depth': max_depth}).to('cuda')

  # 학습된 모델 파라미터를 로드하여 모델에 적용
  model.load_state_dict(torch.load(save_model_path))

  # 결과를 시각화할 이미지 수를 설정
  num_images = 10

  # 시각화를 위한 플롯 설정: 10개의 이미지에 대해 3개의 열을 가진 플롯(원본 이미지, 실제 깊이, 예측된 깊이)
  fig, axes = plt.subplots(num_images, 3, figsize=(15, 5 * num_images))

  # 검증 데이터셋 준비 (NYU Depth V2의 검증 데이터셋 사용)
  val_dataset = NYU(val_paths, mode='val')

  # 모델을 평가 모드로 전환 (이 때, 파라미터 업데이트는 발생하지 않음)
  model.eval()

  # 설정된 이미지 수(num_images)만큼 반복해서 결과 시각화
  for i in range(num_images):
      # 검증 데이터셋에서 i번째 샘플을 가져옴
      sample = val_dataset[i]
      img, depth = sample['image'], sample['depth']
      
      # 이미지 정규화: 평균(mean)과 표준 편차(std)를 재적용하여 이미지를 원래 상태로 변환
      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)
    
      # 모델 예측 수행: GPU에서 이미지를 입력으로 받아 깊이 맵을 예측
      with torch.inference_mode():
          pred = model(img.unsqueeze(0).to('cuda'))  # 이미지 차원 추가 및 GPU로 이동
          # 예측된 깊이 맵을 실제 깊이 맵의 해상도와 맞추기 위해 보간(interpolation) 수행
          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()

image-center

03. 끝마치며

  • 이번 포스팅에서는 Depth Anything에 대해서 간단하게 알아보고 Fine Tuning하는 예제를 가져와 수행보았다. 이는 자료가 잘 정리되어 있어, 한국어 주석 처리와 실제 실습을 위해서 코드 수정해 놓았다. 혹시 더 필요한 내용이나 질문은 메일이나 댓글로 질문 바랍니다.

댓글남기기