단안 깊이와 기하 추정(Monocular Depth & Geometry Estimation)

깊이 맵(Depth Map)은 각 픽셀(pixel)이 카메라(camera)로부터의 거리를 담는 단일 채널 이미지(single-channel image)입니다. 단일 RGB 프레임(frame)에서 이를 예측하는 일은 스테레오(stereo)나 LiDAR 없이는 사실상 불가능하다고 여겨졌습니다. 2026년에는 동결된(frozen) ViT 인코더(encoder)와 가벼운 헤드(head)만으로 정답값(ground truth)에 몇 퍼센트 이내까지 접근합니다.

유형: Build + Use 언어: Python 선수 지식: Phase 4 Lesson 14 (ViT), Phase 4 Lesson 17 (Self-Supervised Vision), Phase 4 Lesson 07 (U-Net) 예상 시간: 약 60분

학습 목표(Learning Objectives)

  • 상대 깊이(relative depth)와 미터 단위 깊이(metric depth)를 구분하고, MiDaS, Marigold, Depth Anything V3, ZoeDepth 같은 실서비스 모델이 각각 어떤 문제를 푸는지 설명할 수 있습니다.
  • Depth Anything V3(DINOv2 backbone)를 사용해 별도의 보정(calibration) 없이 임의의 단일 이미지에 대한 깊이를 예측할 수 있습니다.
  • 단일 이미지에서 단안 깊이(monocular depth)가 어떻게든 작동하는 이유(원근 단서, 텍스처 그래디언트, 학습된 사전지식)와, 단안 깊이로 복원할 수 없는 것(절대 스케일, 가려진 기하)을 설명할 수 있습니다.
  • 깊이 맵과 핀홀 카메라(pinhole camera) 내부 파라미터(intrinsics)를 사용해 2D 검출 결과를 3D 점으로 들어 올릴(lift) 수 있습니다.

문제

깊이는 2D 컴퓨터 비전(computer vision)에서 빠져 있는 축입니다. RGB 영상만 있으면 이미지 평면(image plane) 위에서 사물이 어디에 보이는지는 알 수 있지만, 그 사물이 얼마나 멀리 있는지는 알 수 없습니다. 스테레오 리그(stereo rig), LiDAR, ToF(Time-of-Flight) 같은 깊이 센서(depth sensor)는 이 문제를 직접 풀어 주지만 가격이 비싸고, 충격에 약하며, 측정 가능한 거리 범위에도 한계가 있습니다.

단안 깊이 추정(monocular depth estimation), 즉 단일 RGB 프레임만으로 깊이를 예측하는 작업은 예전에는 흐릿하고 신뢰하기 어려운 결과만 내놓았습니다. 2026년에는 대규모로 사전학습된 인코더(large pretrained encoder)가 그 흐름을 바꿔 놓았습니다. Depth Anything V3는 동결된 DINOv2 백본(backbone)을 그대로 사용해 실내, 실외, 의료(medical), 위성(satellite) 등 서로 다른 도메인(domain) 사이에서도 일관된 깊이 맵을 만들어 냅니다. Marigold는 깊이 추정을 조건부 확산(conditional diffusion) 문제로 다시 정의합니다. ZoeDepth는 실제 미터 단위 거리를 회귀(regression)로 예측합니다.

깊이는 또한 2D 검출(detection)과 3D 이해(understanding) 사이를 잇는 다리이기도 합니다. 검출된 박스의 픽셀에 깊이를 곱하면 2D 객체를 3D 점 구름(point cloud)으로 들어 올릴 수 있습니다. 이 한 단계가 AR 오클루전(occlusion) 시스템, 장애물 회피(obstacle avoidance) 파이프라인, "컵을 집어 줘"라고 말하는 로봇의 핵심입니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.상대 깊이(relative depth)와 미터 단위 깊이(metric depth)의 핵심 차이는 무엇인가요?

2.Depth Anything V3는 동결된(frozen) DINOv2 인코더와 DPT 스타일 디코더를 사용합니다. 인코더를 동결하는 이유는 무엇인가요?

0/2 답변 완료

개념

상대 깊이(relative depth)와 미터 단위 깊이(metric depth)

  • 상대 깊이(relative depth) — 실세계 단위가 없는 순서가 정해진 z 값입니다. "픽셀 A가 픽셀 B보다 가깝다"라는 사실은 알 수 있지만, 두 거리의 비율이 미터 같은 실제 단위에 묶여 있지는 않습니다.
  • 미터 단위 깊이(metric depth) — 카메라로부터의 절대 거리를 미터로 표현한 값입니다. 모델이 이미지의 단서와 실제 거리 사이의 통계적 관계를 학습해 두어야 합니다.

MiDaS와 Depth Anything V3는 상대 깊이를 만들어 냅니다. Marigold도 상대 깊이를 출력합니다. ZoeDepth, UniDepth, Metric3D는 미터 단위 깊이를 출력합니다. 미터 단위 모델은 카메라 내부 파라미터(intrinsics)에 민감하고, 상대 깊이 모델은 그렇지 않습니다.

인코더-디코더 패턴(encoder-decoder pattern)

flowchart LR
    IMG["Image (H x W x 3)"] --> ENC["Frozen ViT encoder<br/>(DINOv2 / DINOv3)"]
    ENC --> FEATS["Dense features<br/>(H/14, W/14, d)"]
    FEATS --> DEC["Depth decoder<br/>(conv upsampler,<br/>DPT-style)"]
    DEC --> DEPTH["Depth map<br/>(H, W, 1)"]

    style ENC fill:#dbeafe,stroke:#2563eb
    style DEC fill:#fef3c7,stroke:#d97706
    style DEPTH fill:#dcfce7,stroke:#16a34a

Depth Anything V3는 인코더를 동결한 채 DPT 스타일의 디코더만 학습합니다. 인코더는 풍부한 특징(feature)을 제공하고, 디코더는 이 특징을 다시 이미지 해상도로 보간(interpolation)해 깊이를 회귀합니다.

단일 이미지만으로도 깊이가 나오는 이유

2D 이미지에는 깊이와 상관관계를 갖는 단안 단서(monocular cue)가 여러 가지 들어 있습니다.

  • 원근(perspective) — 3D에서 평행한 선이 2D에서는 한 점으로 수렴합니다.
  • 텍스처 그래디언트(texture gradient) — 멀리 있는 표면일수록 텍스처가 더 작고 조밀하게 보입니다.
  • 가림 순서(occlusion order) — 가까운 객체가 먼 객체를 가립니다.
  • 크기 항상성(size constancy) — 자동차나 사람처럼 익숙한 객체는 대략적인 스케일을 제공합니다.
  • 대기 원근(atmospheric perspective) — 야외 장면에서 먼 객체는 더 흐릿하고 푸르게 보입니다.

수십억 장의 이미지로 학습된 ViT는 이런 단서를 내재화합니다. 충분한 데이터와 강한 백본이 있으면, 명시적인 3D 감독(3D supervision) 없이도 단안 깊이는 꽤 쓸 만한 정확도에 도달합니다.

단안 깊이가 할 수 없는 일

  • 내부 파라미터(intrinsics)나 장면 안의 알려진 객체 없이는 절대 미터 스케일(absolute metric scale)을 알아낼 수 없습니다. 네트워크는 "컵이 숟가락보다 두 배 멀다"는 식의 비율은 예측할 수 있지만, 그 컵이 1m 떨어져 있는지 10m 떨어져 있는지는 알지 못합니다.
  • 가려진 기하(occluded geometry) — 의자의 뒷면처럼 카메라가 보지 못한 부분은 신뢰할 수 있게 추론할 수 없습니다.
  • 텍스처가 거의 없거나 반사가 심한 표면 — 거울, 유리, 균일한 색의 벽 같은 경우입니다. 네트워크는 그럴듯해 보이지만 사실은 틀린 깊이를 자신 있게 내놓습니다.

2026년의 Depth Anything V3

  • 표준 DINOv2 ViT-L/14를 인코더로 쓰며, 학습 중에는 동결합니다.
  • DPT 디코더를 사용합니다.
  • 다양한 출처에서 모은, 자세(pose)가 주어진 이미지 쌍(image pair)으로 학습합니다. 측광 일관성(photometric consistency) 외에는 별도의 명시적 깊이 감독이 필요하지 않습니다.
  • 임의 개수의 시각 입력으로부터 공간적으로 일관된 기하를 예측하며, 카메라 자세(camera pose)가 알려져 있든 없든 작동합니다.
  • 단안 깊이, 임의 시점 기하(any-view geometry), 시각 렌더링(visual rendering), 카메라 자세 추정(camera pose estimation) 전반에서 SOTA를 기록합니다.

2026년에 깊이가 필요할 때 그냥 갖다 쓰면 되는 기본 모델입니다.

Marigold — 깊이를 위한 확산 모델(diffusion)

Marigold(Ke et al., CVPR 2024)는 깊이 추정을 조건부 이미지-투-이미지(image-to-image) 확산 문제로 재정의합니다. 조건(conditioning)은 RGB 이미지이고, 목표(target)는 깊이 맵입니다. 백본으로는 사전학습된 Stable Diffusion 2의 U-Net을 사용합니다. 출력되는 깊이 맵은 객체 경계가 유난히 또렷합니다. 단점은 순방향(feed-forward) 모델보다 추론이 느리다는 점입니다. 10~50 단계의 디노이징(denoising)이 필요합니다.

내부 파라미터(intrinsics)와 핀홀 카메라

깊이 d를 가진 픽셀 (u, v)를 카메라 좌표계의 3D 점 (X, Y, Z)로 들어 올리려면 다음과 같이 계산합니다.

fx, fy, cx, cy = camera intrinsics
X = (u - cx) * d / fx
Y = (v - cy) * d / fy
Z = d

내부 파라미터는 EXIF 메타데이터, 보정 패턴(calibration pattern), 혹은 단안 내부 파라미터 추정기(Perspective Fields, UniDepth)에서 얻을 수 있습니다. 내부 파라미터가 없더라도 시야각(FOV)을 60~70도 정도로 가정하고 주점(principal point)을 중간 해상도 수준으로 잡으면 점 구름을 만들 수는 있습니다. 다만 이렇게 만든 결과는 시각화(visualisation)에는 쓸 수 있어도 측정(measurement) 용도로는 적합하지 않습니다.

평가(Evaluation)

표준 지표는 두 가지입니다.

  • AbsRel (absolute relative error)mean(|d_pred - d_gt| / d_gt). 낮을수록 좋습니다. 실서비스 모델은 0.05~0.1 수준입니다.
  • delta < 1.25 (threshold accuracy)max(d_pred/d_gt, d_gt/d_pred) < 1.25를 만족하는 픽셀 비율입니다. 높을수록 좋습니다. SOTA는 0.9 이상입니다.

상대 깊이 모델(Depth Anything V3, MiDaS)을 평가할 때는 두 지표 모두 스케일과 시프트에 불변한(scale-and-shift invariant) 버전을 사용합니다.

직접 만들기

Step 1: 깊이 평가 지표(Depth metrics)

import torch

def abs_rel_error(pred, target, mask=None):
    if mask is not None:
        pred = pred[mask]
        target = target[mask]
    return (torch.abs(pred - target) / target.clamp(min=1e-6)).mean().item()


def delta_accuracy(pred, target, threshold=1.25, mask=None):
    if mask is not None:
        pred = pred[mask]
        target = target[mask]
    ratio = torch.maximum(pred / target.clamp(min=1e-6), target / pred.clamp(min=1e-6))
    return (ratio < threshold).float().mean().item()

평가하기 전에는 항상 유효하지 않은 깊이 픽셀(0, NaN, 포화된 값)을 마스킹(masking)합니다.

Step 2: 스케일-시프트 정렬(Scale-and-shift alignment)

상대 깊이 모델을 평가할 때는 지표를 계산하기 전에 예측값을 정답값에 맞춰 정렬해야 합니다. a * pred + b = target 형태의 최소제곱 적합(least-squares fit)을 사용합니다.

def align_scale_shift(pred, target, mask=None):
    if mask is not None:
        p = pred[mask]
        t = target[mask]
    else:
        p = pred.flatten()
        t = target.flatten()
    A = torch.stack([p, torch.ones_like(p)], dim=1)
    coeffs, *_ = torch.linalg.lstsq(A, t.unsqueeze(-1))
    a, b = coeffs[:2, 0]
    return a * pred + b

MiDaS나 Depth Anything 같은 모델을 평가할 때는 abs_rel_error를 호출하기 전에 align_scale_shift를 먼저 실행합니다.

Step 3: 깊이를 점 구름으로 들어 올리기

import numpy as np

def depth_to_point_cloud(depth, intrinsics):
    H, W = depth.shape
    fx, fy, cx, cy = intrinsics
    v, u = np.meshgrid(np.arange(H), np.arange(W), indexing="ij")
    z = depth
    x = (u - cx) * z / fx
    y = (v - cy) * z / fy
    return np.stack([x, y, z], axis=-1)


depth = np.random.uniform(0.5, 4.0, (240, 320))
intr = (320.0, 320.0, 160.0, 120.0)
pc = depth_to_point_cloud(depth, intr)
print(f"포인트 클라우드 shape: {pc.shape}  (H, W, 3)")

함수 하나만 있으면 3D로 들어 올리는 모든 응용이 가능합니다. 점 구름을 .ply 파일로 내보내고 MeshLab이나 CloudCompare에서 열어 볼 수 있습니다.

Step 4: 합성 깊이 장면으로 스모크 테스트(smoke test)

def synthetic_depth(size=96):
    yy, xx = np.meshgrid(np.arange(size), np.arange(size), indexing="ij")
    # 바닥: 위(가까움)에서 아래(멈)로 가는 선형 그래디언트
    depth = 1.0 + (yy / size) * 4.0
    # 가운데 박스: 더 가까움
    mask = (np.abs(xx - size / 2) < size / 6) & (np.abs(yy - size * 0.6) < size / 6)
    depth[mask] = 2.0
    return depth.astype(np.float32)


gt = torch.from_numpy(synthetic_depth(96))
pred = gt + 0.3 * torch.randn_like(gt)  # 모사한 예측값
aligned = align_scale_shift(pred, gt)
print(f"정렬 전 absRel = {abs_rel_error(pred, gt):.3f}")
print(f"정렬 후 absRel = {abs_rel_error(aligned, gt):.3f}")

Step 5: Depth Anything V3 사용 예시 (참고)

import torch
from transformers import pipeline
from PIL import Image

pipe = pipeline(task="depth-estimation", model="LiheYoung/depth-anything-v2-large")

image = Image.open("street.jpg").convert("RGB")
out = pipe(image)
depth_np = np.array(out["depth"])

코드 세 줄이면 끝납니다. out["depth"]는 PIL의 그레이스케일(grayscale) 이미지이므로, 수치 계산을 하려면 numpy 배열로 변환합니다. Depth Anything V3가 정식 공개되면 model 인자만 새 모델 ID로 바꾸면 됩니다. API는 그대로입니다.

사용해보기(Use It)

  • Depth Anything V3 (Meta AI / ByteDance, 2024~2026) — 상대 깊이의 기본값입니다. 실서비스에서 가장 빠른 ViT-large 백본 모델입니다.
  • Marigold (ETH, 2024) — 시각적 품질이 가장 높지만 추론이 느립니다.
  • UniDepth (ETH, 2024) — 카메라 내부 파라미터 추정까지 함께 수행하는 미터 단위 깊이 모델입니다.
  • ZoeDepth (Intel, 2023) — 미터 단위 깊이 모델입니다. 오래되긴 했지만 여전히 안정적으로 동작합니다.
  • MiDaS v3.1 — 레거시(legacy)지만 안정적입니다. 비교용 베이스라인(baseline)으로 적합합니다.

일반적인 통합(integration) 패턴은 다음과 같습니다.

  1. RGB 프레임이 들어옵니다.
  2. 깊이 모델이 깊이 맵을 만들어 냅니다.
  3. 검출기(detector)가 박스를 만들어 냅니다.
  4. 박스의 중심점을 깊이 값을 사용해 3D로 들어 올리고, 점 구름이 있다면 이 점들과 병합합니다.
  5. 그 결과를 AR 오클루전, 경로 계획(path planning), 객체 크기 추정(object-size estimation), 스테레오 대체 등 다양한 다운스트림(downstream)에 활용합니다.

실시간 사용 시 Depth Anything V2 Small(INT8 양자화 버전)은 일반 소비자용 GPU에서 518x518 해상도 기준으로 약 30fps를 냅니다.

산출물 만들기(Ship It)

이 강의에서는 다음을 만듭니다.

  • outputs/prompt-depth-model-picker.md — 지연 시간(latency), 미터/상대 깊이 요구사항, 장면 유형에 따라 Depth Anything V3, Marigold, UniDepth, MiDaS 중에서 적합한 모델을 골라 주는 프롬프트입니다.
  • outputs/skill-depth-to-pointcloud.md — 깊이 맵에서 내부 파라미터를 올바르게 처리해 점 구름을 만들고 .ply 형식으로 내보내는 스킬(skill)입니다.

연습문제(Exercises)

  1. (쉬움) 책상 사진 10장에 Depth Anything V2를 돌려 봅니다. 깊이를 그레이스케일 PNG로 저장하고 한 장씩 살펴봅니다. 예측 깊이가 잘못된 객체를 하나 찾아내고, 어떤 단안 단서가 무너졌는지 설명해 봅니다.
  2. (중간) Depth Anything V2가 만들어 낸 RGB와 깊이를 점 구름으로 들어 올린 뒤 open3d로 렌더링합니다. 실내 장면과 실외 장면을 각각 만들어 비교하고, 어느 쪽이 더 그럴듯해 보이는지 기록합니다.
  3. (어려움) 알려진 객체의 위치만 다른 이미지 쌍을 다섯 쌍 준비합니다(예: 병을 30cm 앞으로 옮긴 전/후 사진). UniDepth로 두 이미지에서 미터 단위 깊이를 예측한 뒤, 예측된 거리 차이와 실제 30cm를 비교해 봅니다.

핵심 용어(Key Terms)

용어흔한 설명실제 의미
단안 깊이(Monocular depth)"단일 이미지 깊이"스테레오나 LiDAR 없이 RGB 한 장으로 깊이를 추정하는 것
상대 깊이(Relative depth)"순서가 있는 깊이"실세계 단위 없이 순서만 정해진 z 값
미터 단위 깊이(Metric depth)"절대 거리"미터 단위 깊이. 보정이나 미터 단위 감독으로 학습된 모델이 필요
AbsRel"절대 상대 오차"`mean(
Delta accuracy"delta < 1.25"예측값이 정답의 25% 이내에 들어오는 픽셀 비율
핀홀 카메라(Pinhole camera)"fx, fy, cx, cy"(u, v, d)(X, Y, Z)로 들어 올릴 때 사용하는 카메라 모델
DPT"Dense Prediction Transformer"동결된 ViT 인코더 위에 얹어 깊이를 예측하는 컨볼루션 기반 디코더
DINOv2 백본(backbone)"이게 잘 되는 이유"깊이 라벨 없이도 도메인 사이를 잘 일반화하는 자기지도학습(self-supervised) 특징

더 읽을거리(Further Reading)

실습 코드

이 강의의 실습 코드 1개

main
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 2개

skill-depth-to-pointcloud

Build point clouds from depth maps with correct intrinsics handling and export to .ply

Skill
prompt-depth-model-picker

Pick Depth Anything V3 / Marigold / UniDepth / MiDaS given latency, metric-vs-relative need, and scene type

Prompt

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.예측 깊이 `d`를 가진 픽셀 `(u, v)`를 3D로 들어 올릴 때 `X = (u - cx) * d / fx`, `Y = (v - cy) * d / fy`, `Z = d`를 사용합니다. fx, fy, cx, cy는 무엇인가요?

2.MiDaS, Depth Anything 같은 상대 깊이 모델을 평가할 때 AbsRel을 계산하기 전에 스케일-시프트 정렬(scale-and-shift alignment)을 적용하는 이유는 무엇인가요?

3.유리벽으로 된 안내 데스크에 대해 Depth Anything이 그럴듯해 보이지만 유리 영역의 깊이는 명백히 잘못 보고했습니다. 이유는 무엇일까요?

0/3 답변 완료