Depth Anything 3 튜토리얼: 사진 한 장을 움직이는 3D 영상으로
on this page
TL;DR
- Depth Anything 3(DA3)로 사진 한 장의 깊이를 추정하고, 이를 이용해 카메라가 움직이는 듯한 패럴랙스 영상을 만들었습니다.
- gcube RTX 5060 8GB에서 사진 1장 깊이 추정에 0.07~0.23초, PyTorch 최대 메모리 0.3~3.0GB가 들었습니다.
- 품질은
DA3MONO-LARGE가 가장 좋았습니다. 건물 경계가 선명하고 하늘을 가장 먼 곳으로 정확히 잡습니다. - 카메라가 움직이면 화면의 약 3%가 빈 곳으로 드러납니다. OpenCV inpaint로 메우면 가장자리의 검은 틈이 사라집니다.
- 사용한 모델은 모두 Apache 2.0이라 상업 이용이 가능하고, 별도 동의 절차 없이 받을 수 있습니다.
들어가며
사진은 평면이지만, 사진 속 각 픽셀이 카메라에서 얼마나 멀리 있는지 알면 입체감을 만들 수 있습니다. 가까운 곳은 많이, 먼 곳은 조금 움직이면 카메라가 옆으로 흘러가는 듯한 패럴랙스 효과가 생깁니다.
이번 글에서는 ByteDance Seed가 2025년 11월 공개한 Depth Anything 3(DA3)로 사진의 깊이를 추정하고, OpenCV로 패럴랙스 영상을 만듭니다. 모두 gcube RTX 5060 8GB 한 장에서 실행했습니다.
모델 선택: 라이선스부터 확인
DA3는 모델마다 라이선스가 다릅니다. 이름에 LARGE·GIANT가 붙은 범용 모델은 비상업(CC BY-NC 4.0)이라, 이 글에서는 Apache 2.0 모델만 썼습니다. 세 모델 모두 Hugging Face에서 별도 동의 절차 없이 받을 수 있습니다.
| 모델 | 파라미터 | 라이선스 | 용도 |
|---|---|---|---|
| DA3-SMALL | 0.08B | Apache 2.0 | 가장 가벼움. 여러 장 입력·카메라 추정도 지원 |
| DA3-BASE | 0.12B | Apache 2.0 | 속도와 품질의 중간 |
| DA3MONO-LARGE | 0.35B | Apache 2.0 | 사진 한 장 깊이 추정 전용 + 하늘 영역 구분 |
테스트 사진은 Unsplash의 서울 사진 3장입니다. 청계천(Zequn Gui), 북촌 한옥 골목(Elliot Gouy), 간판 거리(Markus Winkler)처럼 앞뒤 거리 차이가 뚜렷한 장면을 골랐습니다.
1. 환경 준비: gcube 워크로드 만들기
gcube 콘솔에서 RTX 5060 8GB를 선택하고 아래와 같이 입력합니다.
| 항목 | 입력값 |
|---|---|
| 컨테이너 이미지 | pytorch/pytorch:2.9.1-cuda12.8-cudnn9-runtime |
| 컨테이너 포트 | 8888 |
| 환경변수 | HF_HOME=/workspace/hf_cache |
컨테이너 명령
bash -c "apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y git wget ffmpeg libgl1 libglib2.0-0 && pip install jupyterlab && jupyter lab --ip=0.0.0.0 --port=8888 --no-browser --allow-root --ServerApp.token='' --ServerApp.password=''"
- git: DA3는 PyPI 패키지가 아니라 GitHub 저장소를 받아 설치합니다.
- libgl1, libglib2.0-0: DA3가 쓰는 OpenCV가 이미지를 읽을 때 필요합니다.
- ffmpeg: 패럴랙스 영상을 MP4(H.264)로 저장할 때 씁니다.
- HF_HOME: 모델 가중치(3종 합계 약 2GB)를
/workspace에 둡니다. 개인 저장소를 연결하면 재시작 후 다시 받지 않습니다.
워크로드가 실행되면 서비스 주소 뒤에 /lab을 붙여 JupyterLab에 접속하고, photos/ 폴더에 테스트 사진을 cheonggyecheon.jpg, bukchon.jpg, street.jpg라는 이름으로 올립니다.
2. 설치
새 노트북(Python 3)에서 공식 저장소를 받아 설치합니다. 설치는 약 20초 걸렸습니다.
!cd /workspace && git clone -q https://github.com/ByteDance-Seed/Depth-Anything-3.git
!cd /workspace/Depth-Anything-3 && pip install -q -e . addict
- addict: 저장소의 설치 목록에 빠져 있어 없으면
ModuleNotFoundError: No module named 'addict'가 납니다. 함께 설치해 주세요. - torch는 그대로: 함께 설치되는 xformers 0.0.33은 torch 2.9용이라, 이미지의 torch 2.9.1+cu128이 바뀌지 않습니다. RTX 5060(sm_120)에서도 정상 동작했습니다. numpy는 1.26.4로 내려갑니다.
설치 후에는 커널을 다시 시작한 다음 확인합니다.
import torch, numpy
from depth_anything_3.api import DepthAnything3
print(torch.__version__, numpy.__version__, torch.cuda.get_device_name(0))
2.9.1+cu128 1.26.4 NVIDIA GeForce RTX 5060이 보이면 준비 완료입니다. gsplat이 필요하다는 경고는 3D 가우시안 렌더링용이라 이 글에서는 무시해도 됩니다.
3. 깊이 지도 만들기와 모델 비교
모델을 불러오고 inference()에 사진 경로를 넣으면 깊이 지도가 나옵니다. 아래 코드는 깊이 값을 .npy로 저장하고, 가까울수록 밝게 칠한 미리보기 이미지도 만듭니다.
import os, time, torch, numpy as np, cv2
from depth_anything_3.api import DepthAnything3
os.makedirs("depth", exist_ok=True)
model = DepthAnything3.from_pretrained("depth-anything/DA3MONO-LARGE").to("cuda").eval()
for name in ["cheonggyecheon", "bukchon", "street"]:
t = time.time()
pred = model.inference([f"photos/{name}.jpg"], process_res=1008)
d = pred.depth[0] # (H, W) float32, 값이 클수록 멀다
np.save(f"depth/{name}.npy", d)
inv = 1.0 / np.clip(d, 1e-6, None) # 가까울수록 큰 값으로 변환
v = (inv - inv.min()) / (inv.max() - inv.min())
cv2.imwrite(f"depth/{name}.png", cv2.applyColorMap((v * 255).astype("uint8"), cv2.COLORMAP_INFERNO))
print(name, d.shape, f"{time.time() - t:.2f}s")
- process_res: 깊이를 계산할 때의 해상도(긴 변 기준)입니다. 기본값은 504이고, 패럴랙스 영상에는 경계가 더 선명한 1008을 썼습니다.
- 처음 실행할 때는 Hugging Face에서 가중치를 내려받습니다(DA3MONO-LARGE 약 1.3GB).
모델별 속도와 메모리 (RTX 5060 8GB, 1920px 사진 3장 평균)
| 모델 | process_res | 사진 1장 처리 시간 | PyTorch 최대 메모리 |
|---|---|---|---|
| DA3-SMALL | 504 | 0.065초 | 0.32GB |
| DA3-BASE | 504 | 0.071초 | 0.91GB |
| DA3MONO-LARGE | 504 | 0.103초 | 2.11GB |
| DA3MONO-LARGE | 1008 | 0.229초 | 2.98GB |
모델을 한 번 예열한 뒤 잰 값입니다. nvidia-smi로 본 GPU 전체 사용량은 최대 약 6.3GB였고, 실험 전부터 쓰이던 약 1.4GB가 포함된 값입니다. 8GB 안에서 충분히 돌아갑니다.
품질 비교
- DA3-SMALL: 빠르지만 건물과 나무의 경계가 뭉개집니다.
- DA3-BASE: 경계가 조금 더 또렷해지지만, 하늘을 구름 모양대로 얼룩하게 처리합니다.
- DA3MONO-LARGE: 처마 끝, 간판 같은 작은 물체까지 경계가 선명하고, 하늘을 가장 먼 곳(검은색)으로 깔끔하게 잡습니다. 패럴랙스에서 하늘이 앞으로 튀어나오는 왜곡을 막아 주는 부분입니다.
4. 패럴랙스 영상 만들기
원리
- 깊이 지도를 "가까운 정도"(0~1)로 바꾸고, 사진을 24개 층으로 나눕니다.
- 프레임마다 카메라 이동량을 정하고, 가까운 층일수록 많이 옮기고 확대합니다.
- 먼 층부터 그려서 가까운 층이 위를 덮게 합니다.
- 앞의 물체가 움직이며 드러나는 빈 곳은 OpenCV inpaint로 주변 픽셀을 이용해 채웁니다.
- 프레임을 ffmpeg로 바로 넘겨 MP4(H.264)로 저장합니다.
코드
아래 내용을 parallax.py로 저장합니다.
"""사진 + 깊이 지도 -> 패럴랙스(2.5D) 영상.
usage: python parallax.py photo.jpg depth.npy out.mp4 [--no-inpaint]"""
import sys, time, subprocess, numpy as np, cv2
photo, depth_path, out = sys.argv[1:4]
inpaint = "--no-inpaint" not in sys.argv
W = 1280; FPS = 30; SEC = 4; LAYERS = 24
AMP_X, AMP_Y, ZOOM = 0.035, 0.012, 0.06 # 이동 폭(화면 비율), 확대 비율
img = cv2.imread(photo)
H = round(img.shape[0] * W / img.shape[1] / 2) * 2
img = cv2.resize(img, (W, H), interpolation=cv2.INTER_AREA)
d = np.load(depth_path).astype(np.float32)
disp = 1.0 / np.clip(d, 1e-6, None) # 가까울수록 큰 값
disp = cv2.resize(disp, (W, H), interpolation=cv2.INTER_LINEAR)
lo, hi = np.percentile(disp, 2), np.percentile(disp, 98)
disp = np.clip((disp - lo) / (hi - lo + 1e-9), 0, 1)
disp = cv2.medianBlur((disp * 255).astype(np.uint8), 5).astype(np.float32) / 255
edges = np.linspace(0, 1, LAYERS + 1)
layer_id = np.clip(np.digitize(disp, edges) - 1, 0, LAYERS - 1)
cx, cy = W / 2, H / 2
ff = subprocess.Popen(["ffmpeg","-y","-loglevel","error","-f","rawvideo","-pix_fmt","bgr24","-s",f"{W}x{H}","-r",str(FPS),"-i","-",
"-c:v","libx264","-preset","veryfast","-crf","20","-pix_fmt","yuv420p","-movflags","+faststart",out], stdin=subprocess.PIPE)
t0 = time.time(); hole_ratio = []
for f in range(FPS * SEC):
ph = 2 * np.pi * f / (FPS * SEC)
dx, dy, z = AMP_X * W * np.sin(ph), AMP_Y * H * np.sin(2 * ph), ZOOM * (0.5 - 0.5 * np.cos(ph))
canvas = np.zeros_like(img); filled = np.zeros((H, W), np.uint8)
for k in range(LAYERS): # 먼 층부터 그려서 가까운 층이 덮도록
m = (layer_id == k).astype(np.uint8)
if not m.any(): continue
dk = (edges[k] + edges[k + 1]) / 2
s = 1 + z * dk
M = np.float32([[s, 0, (1 - s) * cx + dx * dk], [0, s, (1 - s) * cy + dy * dk]])
wi = cv2.warpAffine(img, M, (W, H), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT)
wm = cv2.warpAffine(m, M, (W, H), flags=cv2.INTER_NEAREST)
canvas[wm > 0] = wi[wm > 0]; filled |= wm
holes = (filled == 0).astype(np.uint8)
hole_ratio.append(holes.mean())
if inpaint and holes.any():
canvas = cv2.inpaint(canvas, cv2.dilate(holes, np.ones((3, 3), np.uint8)), 3, cv2.INPAINT_TELEA)
ff.stdin.write(canvas.tobytes())
ff.stdin.close(); ff.wait()
print(f"frames={FPS*SEC} size={W}x{H} render={time.time()-t0:.1f}s holes_avg={100*np.mean(hole_ratio):.2f}% holes_max={100*np.max(hole_ratio):.2f}%")
실행
!python parallax.py photos/bukchon.jpg depth/bukchon.npy bukchon_parallax.mp4
!python parallax.py photos/bukchon.jpg depth/bukchon.npy bukchon_noinpaint.mp4 --no-inpaint
- 움직임 조절:
AMP_X(좌우),AMP_Y(상하),ZOOM(확대)을 키우면 입체감이 커지지만, 드러나는 빈 곳도 늘어납니다. - 층 수:
LAYERS를 늘리면 층 사이 계단 현상이 줄고, 렌더링은 느려집니다.
5. 결과
완성 영상
북촌 한옥 골목
청계천
da3_cheonggyecheon_parallax.mp4
간판 거리
빈 곳 메우기 전후
카메라가 움직이면 앞의 물체 뒤에 가려져 있던 곳이 드러납니다. 사진에는 그 부분의 정보가 없어서 빈 곳으로 남습니다. 이번 설정에서는 화면의 평균 3.1%(최대 4.2%)가 빈 곳이었습니다.
메우지 않으면 집 가장자리와 화분 주변에 검은 틈이 생깁니다. OpenCV inpaint로 채우면 눈에 띄는 틈이 사라집니다.
처리 시간과 처리량
| 단계 | 처리 장치 | 시간 | 시간당 처리량 (환산) |
|---|---|---|---|
| 깊이 추정 (MONO-LARGE, 504) | GPU | 0.103초/장 | 약 35,000장 |
| 깊이 추정 (MONO-LARGE, 1008) | GPU | 0.229초/장 | 약 15,700장 |
| 패럴랙스 렌더링 (1280px, 4초, 120프레임) | CPU | 16.2초/편 | 약 220편 (프로세스 1개) |
- 병목은 렌더링: 깊이 추정은 GPU에서 0.2초면 끝나고, 시간의 대부분은 CPU 렌더링(층별 이동·inpaint)에 쓰입니다.
- 더 빠르게: 워크로드의 CPU 12코어를 활용해 여러 프로세스로 동시에 렌더링하면 처리량을 늘릴 수 있습니다.
6. FAQ
Q. 상업적으로 써도 되나요?
이 글에서 쓴 DA3-SMALL, DA3-BASE, DA3MONO-LARGE는 Apache 2.0이라 상업 이용이 가능합니다. 다만 DA3-LARGE, DA3-GIANT, DA3NESTED 계열은 비상업(CC BY-NC 4.0)이니 모델 이름을 꼭 확인하세요.
Q. 8GB보다 작은 GPU에서도 되나요?
DA3-SMALL은 PyTorch 메모리를 0.32GB만 썼습니다. 메모리가 부족하면 모델을 작은 것으로 바꾸거나 process_res를 낮추세요.
Q. No module named 'addict' 오류가 나요.
저장소의 설치 목록에 빠진 패키지입니다. pip install addict로 추가 설치하면 됩니다.
Q. 영상의 입체감을 더 키우고 싶어요.
AMP_X·ZOOM 값을 키우면 됩니다. 다만 빈 곳도 늘어나서 inpaint 자국이 보일 수 있으니 조금씩 조절하세요. 앞뒤 거리 차이가 뚜렷한 사진(골목, 도로, 산)일수록 효과가 좋습니다.
마무리
사진 한 장에서 깊이를 추정하고, 그 깊이로 움직이는 입체 영상까지 만들어 보았습니다. 깊이 추정 자체는 RTX 5060 8GB에서 사진 1장당 0.1~0.2초로 끝납니다. 상품 사진의 여백 연출, SNS용 숏폼, 여행·부동산 사진의 입체 미리보기처럼 사진을 대량으로 처리하는 작업에 활용할 수 있습니다.
gcube에서는 RTX 5060부터 RTX 5090까지 필요한 만큼 GPU를 빌려 이런 실험을 바로 해볼 수 있습니다.
참고 자료
- Depth Anything 3 공식 저장소 (ByteDance-Seed)
- DA3MONO-LARGE (Hugging Face)
- DA3-BASE (Hugging Face)
- 사진: Unsplash — Zequn Gui(청계천), Elliot Gouy(북촌), Markus Winkler(간판 거리)