지난 글 끝에서 「카메라 한 대의 그림에는 깊이가 없다」고 적었다. 그런데 사람은 사진 한 장만 보고도 무엇이 앞이고 무엇이 뒤인지 안다. 원근으로 좁아지는 선, 앞의 것이 뒤의 것을 가리는 모양, 익숙한 물건의 크기, 멀수록 흐려지는 대기 — 이런 단서들을 평생 봐 왔기 때문이다.
단안 깊이 추정(monocular depth estimation)은 그 단서 읽기를 모델에게 시키는 일이다. 그림 한 장을 넣으면 픽셀마다 거리 값이 들어찬 지도가 나온다. 최근 몇 해 사이 이 분야의 모델들이 크게 좋아져서, 이제는 특별한 준비 없이 그냥 갖다 써도 쓸 만한 지도가 나온다.
문제는 그 지도의 숫자가 무엇인지다. 여기를 오해한 채로 갖다 쓰면 조용히 틀린다.
크기와 거리는 함께 정해지지 않는다
먼저 원리적인 한계를 짚고 가야 한다. 카메라는 3차원을 2차원 평면에 눌러 담는데, 이 과정에서 잃는 것은 「거리」 하나가 아니라 거리와 크기의 조합이다.
작은 물건이 가까이 있는 것과 큰 물건이 멀리 있는 것은 카메라에 정확히 같은 그림을 만든다. 그림 안에는 둘을 가릴 단서가 없다. 이것을 스케일 모호성이라고 부른다.
사람이 이 모호함을 넘어가는 방법은 아는 것을 끌어오는 것이다. 사진에 사람이 서 있으면 「사람 키는 대략 이 정도」를 알고 있으므로 거리가 정해진다. 모델도 같은 일을 한다 — 그래서 익숙한 물건이 많은 실내 사진에서는 곧잘 맞히고, 처음 보는 크기의 물체만 있는 장면에서는 크게 틀린다.
세 종류의 깊이
이 모호함 때문에 모델이 내는 값에 세 등급이 생긴다. 무엇을 받았는지 모르면 쓸 수 없다.
| 종류 | 담긴 정보 | 쓸 수 있는 일 | 못 하는 일 |
|---|---|---|---|
| 순서 | A가 B보다 앞이라는 것만 | 앞뒤 가리기, 층 나누기 | 얼마나 앞인지 |
| 배율·치우침 미상 | 값의 모양은 맞지만 곱할 수와 더할 수가 미정 | 배경 흐림, 상대 비교, 3D 사진 | 거리 재기 |
| 미터 단위 | 실제 거리 | 로봇 회피, 측정, 실물 크기 합성 | — |
범용 단안 모델이 기본으로 내는 것은 가운데다. 값을 나란히 놓고 보면 순서와 굴곡은 맞는데 「2.4가 2.4미터인가」에는 답할 수 없다. 위쪽은 가운데에서 그냥 얻어진다 — 크기를 견주기만 하면 되니까. 아래쪽이 공짜가 아니다.
시차라는 함정
한 가지 더 있다. 많은 모델이 내는 것은 깊이가 아니라 시차(disparity)에 해당하는 값이다. 시차는 깊이의 역수에 비례하는 양으로, 가까울수록 크고 멀수록 0에 가까워진다.
여기서 가 모델이 낸 값이고 와 가 우리가 모르는 곱할 수와 더할 수, 가 실제 깊이다. 모델이 시차 쪽을 내는 데는 이유가 있다. 가까운 곳의 분해능을 촘촘히 가져가면서 아주 먼 곳도 유한한 값으로 담을 수 있기 때문이다 — 하늘은 그냥 0에 가까운 값이 된다.
대가는 뒤집을 때 나온다. 역수를 취하는 순간 먼 곳의 오차가 폭발한다. 시차가 0.01에서 0.005로 절반만 어긋나도 깊이는 두 배로 벌어진다. 그래서 이 계열의 값으로 먼 거리를 재는 것은 원리적으로 무리다. 가까운 곳은 믿을 만하고 먼 곳은 순서 정도만 믿는 것이 맞는 태도다.
미터로 올리기
가운데 등급의 값을 실제 거리로 바꾸려면 위 식의 와 를 정해야 한다. 미지수가 둘이므로 실제 거리를 아는 점이 최소 둘 필요하다. 실무에서 쓰는 기준은 대개 이런 것들이다.
- 바닥면에 놓인 것들 — 카메라 높이와 각도를 알면 바닥 픽셀의 거리가 계산된다
- 크기를 아는 물체 — A4 용지, 표준 규격의 표지판, 사람 키
- 다른 센서가 재 준 몇 점 — 거리계 한 대만 있어도 충분하다
점이 여럿이면 최소제곱으로 맞추는 것이 안정적이다.
import numpy as np
def fit_scale_shift(pred, known):
"""모델 출력 pred(시차 계열)와 실측 깊이 known으로 s, t를 맞춘다.
known은 일부 픽셀에만 있어도 된다 — NaN은 빼고 푼다."""
m = ~np.isnan(known)
x = pred[m].ravel()
y = 1.0 / known[m].ravel() # 깊이를 시차 쪽으로 옮긴다
A = np.stack([x, np.ones_like(x)], axis=1)
s, t = np.linalg.lstsq(A, y, rcond=None)[0]
return s, t
def to_metric(pred, s, t, eps=1e-6):
return 1.0 / np.maximum(s * pred + t, eps)
eps 로 막아 둔 자리가 실제로 문제가 되는 지점이다. 하늘이나 아주 먼 배경에서 분모가 0에 가까워지면 깊이가 수천 미터로 튀어 나온다. 상한을 정해 잘라 두지 않으면 이 몇 픽셀이 이후 계산을 통째로 망가뜨린다.
카메라 정보를 함께 넣어 처음부터 미터 단위를 내도록 학습된 모델도 있다. 초점거리를 조건으로 받는 방식인데, 그 값이 틀리면 결과도 그만큼 틀린다. 휴대폰 사진의 메타데이터에 적힌 초점거리는 대체로 믿을 만하지만, 잘라 낸 사진이나 화면 캡처에는 그 값이 없거나 실제와 맞지 않는다.
다른 수단들과 견주면
| 방식 | 얻는 것 | 강한 곳 | 약한 곳 |
|---|---|---|---|
| 단안 | 배율 미상 지도 | 아무 사진에나 된다 | 절대 거리, 처음 보는 크기의 물체 |
| 스테레오 | 미터 단위 | 카메라 둘 사이 간격이 기준이 된다 | 무늬 없는 벽, 두 눈 간격보다 먼 거리 |
| 구조광·비행시간 | 미터 단위 | 어두워도 되고 정확하다 | 실외 햇빛, 원거리, 검은 물체 |
| 라이다 | 미터 단위, 희소 | 원거리까지 정확하다 | 값이 비싸고 점이 성기다 |
| 다시점 복원 | 미터 미상 지도 | 정밀하다 | 카메라가 움직여야 하고 느리다 |
실제 시스템은 자주 이 둘을 겹친다. 성긴 라이다 점 몇으로 단안 모델의 배율을 맞추면, 촘촘하면서 미터 단위인 지도가 나온다. 서로의 약점이 정확히 반대라서 잘 맞는 조합이다.
자주 무너지는 자리
하늘·거울·유리. 하늘은 거리라는 개념이 없고, 거울은 비친 상의 거리를 내고, 투명한 유리는 뒤의 것을 낸다. 셋 다 「틀렸다」기보다 정의되지 않은 것에 가깝다. 마스크로 빼고 다루는 편이 낫다.
경계가 번진다. 깊이 지도는 대개 원본보다 작은 격자에서 나와 확대된다. 그래서 물체 가장자리에서 앞과 뒤의 값이 섞인다. 이 지도를 점구름으로 바꾸면 물체 윤곽을 따라 아무 데도 속하지 않는 점들이 떠 있는 모양이 되는데, 이걸 날아다니는 픽셀이라 부른다. 경계에서 깊이 변화가 큰 픽셀을 지우는 후처리가 흔한 대응이다.
타일 이음매. 고해상도 사진을 조각으로 나눠 각각 추정한 뒤 붙이면 조각마다 배율이 달라 경계에 계단이 생긴다. 겹치게 자르고 겹친 영역에서 배율을 맞춘 뒤 이어야 한다.
영상에서 배율이 흔들린다. 프레임마다 따로 추정하면 같은 벽의 깊이가 매 프레임 조금씩 달라진다. 카메라가 조금 움직인 것뿐인데 장면 전체가 앞뒤로 출렁이는 것처럼 보인다. 기준이 되는 영역을 정해 프레임 간 배율을 맞추거나, 영상 전용 모델을 쓴다.
평가할 때 정렬을 잊지 않는다. 배율 미상 모델을 실측값과 그냥 비교하면 당연히 나쁘게 나온다. 이 계열은 평가 전에 배율과 치우침을 맞추고 재는 것이 규칙이고, 그 사실을 안 밝힌 비교 수치는 신뢰할 수 없다.
정리
- 카메라 한 대의 그림은 크기와 거리를 함께 잃는다. 작은 것이 가까운 것과 큰 것이 먼 것이 같은 그림을 만든다
- 출력은 세 등급이다 — 순서만, 배율·치우침 미상, 미터 단위. 범용 모델의 기본은 가운데다
- 많은 모델이 내는 값은 깊이가 아니라 깊이의 역수 쪽이다. 뒤집는 순간 먼 곳의 오차가 폭발한다
- 미터로 올리려면 실제 거리를 아는 점이 최소 둘 필요하다. 바닥면, 크기를 아는 물체, 다른 센서의 몇 점 중 하나면 된다
- 성긴 거리 센서로 촘촘한 단안 지도의 배율을 맞추는 조합이 실무에서 가장 값이 좋다
- 하늘·거울·유리는 마스크로 빼고, 경계의 날아다니는 픽셀은 지우고, 영상은 프레임 간 배율을 맞춘다
- 배율 미상 모델은 정렬한 뒤에 평가한다. 정렬 없이 잰 수치는 의미가 없다
읽어주셔서 감사합니다. 😊

