포스트

MVS — 희소한 점군을 표면으로 만드는 고전적인 방법

MVS — 희소한 점군을 표면으로 만드는 고전적인 방법

서론

앞 글에서 SfM이 카메라 포즈와 희소 점군을 뱉는 데까지 왔습니다. 그런데 그 점군은 특징점이 잡힌 자리에만 있습니다. 이미지 한 장에 수천 개, 장면 전체로는 수만 개 정도입니다. 이걸로는 물체의 표면이라고 부를 만한 게 안 나옵니다.

MVS(Multi-View Stereo) 는 그 다음 단계를 맡습니다.

카메라 포즈를 이미 안다고 가정하고, 이번엔 픽셀마다 깊이를 구한다.

포즈가 주어졌다는 건 엄청난 단순화입니다. 삼각측량에서 봤듯 두 카메라의 포즈를 알면 한 픽셀에 대응하는 3차원 점은 그 픽셀의 시선 위 어딘가로 이미 좁혀져 있습니다. 미지수가 3개(x, y, z)에서 1개(깊이) 로 줄어든 것입니다.

이 글은 그 1차원 탐색을 어떻게 하는지, 어디서 잘 되고 어디서 무너지는지, 그리고 그 무너지는 지점이 왜 결국 3DGS와 NeRF를 불러왔는지를 다룹니다.


1. 유일한 단서 — 광도 일관성

깊이를 알아낼 근거는 하나뿐입니다.

같은 3차원 점을 여러 카메라가 봤다면, 그 점 주변은 각 이미지에서 비슷하게 보여야 한다.

이걸 광도 일관성(photometric consistency) 이라고 합니다. 거꾸로 쓰면 알고리즘이 됩니다. 깊이를 하나 가정하고, 그 가정에 따라 이웃 이미지의 대응 위치를 찾아가서, 두 패치가 얼마나 닮았는지 재고, 가장 닮은 깊이를 고릅니다.

닮음을 재는 방법은 몇 가지가 있습니다.

\[\text{SSD} = \sum_i (a_i - b_i)^2, \qquad \text{NCC} = \frac{\sum_i (a_i - \bar a)(b_i - \bar b)}{\sqrt{\sum_i (a_i - \bar a)^2}\sqrt{\sum_i (b_i - \bar b)^2}}\]

MVS가 거의 언제나 NCC를 쓰는 이유가 있습니다. 카메라가 다르면 노출과 화이트밸런스가 다르고, 같은 표면도 보는 각도에 따라 밝기가 달라집니다. NCC는 평균을 빼고 표준편차로 나누므로 $b = \alpha a + \beta$ 형태의 선형 밝기 변화에 완전히 불변입니다. SSD는 노출 차이 하나에 그대로 무너집니다1


2. 평면 스윕 — 깊이를 하나씩 대입해 보기

가장 직관적인 구현이 평면 스윕(plane sweep) 입니다. 기준 카메라 앞에 깊이가 다른 평면을 여러 장 세워 두고, 각각에 대해 이웃 이미지를 워핑해 와서 비교합니다.

평면 스윕 개념과 표면별 NCC 곡선 왼쪽: 기준 픽셀의 시선 위에 깊이 가설 $d_1 \dots d_5$ 를 늘어놓으면, 각 가설은 이웃 영상의 서로 다른 자리에 대응한다. 실제 표면이 있는 깊이에서만 두 패치가 일치한다. 오른쪽: 세 가지 표면에 대해 NCC를 실제로 계산한 것. 무늬 있는 표면(파랑)은 정답에서 뾰족한 봉우리 하나. 반복 패턴(주황)은 똑같이 높은 봉우리가 여러 개 — 어느 쪽이 진짜인지 알 방법이 없다. 무텍스처 면(회색)은 봉우리 자체가 없다.

오른쪽 그림 하나에 MVS의 성패가 전부 들어 있습니다. 비용 곡선의 봉우리가 뾰족하고 유일할 때만 깊이가 정해집니다.

평면을 세우고 워핑한다는 건 구체적으로 호모그래피 하나를 만드는 일입니다. 법선 $n$, 깊이 $d$ 인 평면을 통해 기준 카메라에서 이웃 카메라로 가는 변환은

\[H = K'\left(R - \frac{t\,n^\top}{d}\right)K^{-1}\]

입니다. SfM 글의 기하 검증에서 “장면이 평면이면 $H$ 가 잘 맞는다”고 했던 그 $H$ 와 같은 것입니다. 거기서는 걸러 내야 할 퇴화였는데, 여기서는 일부러 평면을 가정해서 써먹습니다. 호모그래피가 어떤 변환인지는 다크프로그래머의 2D 변환(Transformations)에 강체 변환부터 차근차근 나옵니다.

정면 평행 스테레오(두 카메라가 나란히 있고 평면이 이미지면과 평행)로 좁히면 이 호모그래피는 그냥 가로 방향 이동 한 개로 줄어듭니다. 그 이동량이 시차 $d$ 이고, 깊이와는 $Z = fB/d$ 로 반비례합니다. 코드로 쓰면 이렇게 짧아집니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import numpy as np
from scipy.ndimage import uniform_filter

def ncc_volume(L, R, dmax, win=9):
    """cost[d, y, x] = 시차 d 를 가정했을 때의 윈도 NCC."""
    uf   = lambda a: uniform_filter(a, win, mode="nearest")
    muL  = uf(L)
    varL = np.maximum(uf(L * L) - muL ** 2, 1e-12)

    vol = np.empty((dmax + 1,) + L.shape)
    for d in range(dmax + 1):
        Rd = np.roll(R, d, axis=1)          # 깊이 가설 = 가로 이동량
        Rd[:, :d] = R[:, :1]
        muR  = uf(Rd)
        varR = np.maximum(uf(Rd * Rd) - muR ** 2, 1e-12)
        vol[d] = (uf(L * Rd) - muL * muR) / np.sqrt(varL * varR)
    return vol

disparity = np.argmax(ncc_volume(L, R, dmax=40), axis=0)   # 가장 닮은 가설

uniform_filter가 윈도 합을 상수 시간에 계산해 주므로, 전체 비용은 이미지 크기 × 깊이 가설 수에 비례합니다. 이게 비용 볼륨(cost volume) 이라는 이름으로 불리는 3차원 배열입니다.


3. 실제로 돌리면 이렇게 됩니다

위 코드를 합성 장면에 그대로 적용한 결과입니다. 정답 깊이를 알고 있으니 어디서 틀리는지도 셀 수 있습니다.

블록 매칭 깊이 추정 결과 같은 알고리즘, 같은 파라미터. 무늬가 있는 곳은 깨끗하게 복원되지만 무텍스처 면과 하늘은 완전히 무작위다. 1픽셀 이내로 맞은 비율이 전체로는 63%인데, NCC 봉우리가 뾰족한 픽셀만 남기면(오른쪽 아래 초록) 전체의 44%만 살아남는 대신 그중 87%가 정확하다.

여기서 읽어야 할 것은 두 가지입니다.

첫째, MVS는 “모르는 곳을 모른다고 말할 수 있습니다.” 봉우리의 높이와 뾰족함이 곧 신뢰도입니다. 이건 대단히 중요한 성질입니다. 틀린 깊이를 자신 있게 내놓는 것보다 “여기는 모르겠다”고 비워 두는 편이 훨씬 낫습니다. 뒤에서 여러 깊이 맵을 합칠 때, 이 신뢰도가 걸러 내는 기준이 됩니다.

둘째, 복원된 상자 면에 계단이 보입니다. 상자 앞면은 살짝 기울어져 있는데 결과는 층층이 끊겨 있습니다. 원인은 알고리즘이 암묵적으로 “창 안의 모든 픽셀이 같은 깊이” 라고 가정하기 때문입니다. 기울어진 표면에서는 이 가정이 틀리고, 창이 클수록 더 크게 틀립니다.


4. 창 크기의 딜레마, 그리고 PatchMatch

창 크기는 양쪽으로 손해입니다.

창이 작으면창이 크면
무늬 정보가 적어 잡음에 취약통계가 안정적
물체 경계가 살아남음경계에서 앞뒤 깊이가 섞여 뭉개짐
기울어진 면에서 편향이 작음기울어진 면에서 편향이 큼

PatchMatch Stereo2가 이 딜레마를 정면으로 다룹니다. 깊이 하나만 추정하는 대신 픽셀마다 기울어진 평면(깊이 + 법선, 3자유도)을 추정합니다. 창이 커도 기울기를 함께 맞추므로 편향이 사라집니다.

문제는 탐색 공간이 1차원에서 3차원으로 늘어난다는 것인데, PatchMatch는 여기를 영리하게 뚫습니다.3

  1. 무작위 초기화 — 픽셀마다 평면을 아무렇게나 하나씩 준다.
  2. 전파 — 이웃 픽셀의 평면을 가져와 써 보고, 더 좋으면 바꾼다.
  3. 정제 — 현재 평면 주변을 점점 좁은 범위로 흔들어 본다.

핵심 통찰은 “무작위로 뿌리면 그중 몇 개는 맞는다, 그리고 표면은 연속적이므로 맞은 답은 이웃으로 번져 나간다“는 것입니다. 실제 장면에서 놀랄 만큼 잘 동작하고, COLMAP의 MVS 모듈이 쓰는 방식이기도 합니다.4


5. 깊이 맵에서 점군으로 — 융합

여기까지 하면 이미지마다 깊이 맵 한 장이 나옵니다. 이걸 하나의 3차원 모델로 합치는 단계가 융합(fusion) 입니다.

가장 중요한 장치는 다중 뷰 일관성 검사입니다.

이미지 $i$ 의 픽셀을 자기 깊이로 3차원에 띄운 다음, 이웃 이미지 $j$ 에 투영한다. 그 자리의 깊이 맵이 말하는 값과 내가 띄운 점의 깊이가 일치하면 채택하고, 어긋나면 버린다.

두 대 이상이 독립적으로 같은 말을 할 때만 믿는 것입니다. 앞의 반복 패턴처럼 두 시점만으로는 구별이 안 되던 모호함이, 세 번째 시점을 넣으면 대개 깨집니다. MVS가 스테레오(2장)가 아니라 멀티뷰(N장)인 이유가 여기 있습니다.

살아남은 점들을 모으면 밀집 점군이 되고, 여기에 법선을 추정해 푸아송 표면 복원(Poisson surface reconstruction)5 같은 방법을 쓰면 메시가 됩니다. 텍스처를 입히면 우리가 아는 그 3D 모델입니다.

전체 흐름을 다시 정리하면 이렇습니다.

\[\text{사진} \xrightarrow{\ \text{SfM}\ } \text{포즈 + 희소 점군} \xrightarrow{\ \text{MVS}\ } \text{깊이 맵} \xrightarrow{\ \text{융합}\ } \text{밀집 점군} \xrightarrow{\ \text{Poisson}\ } \text{메시}\]

6. 그런데 이 파이프라인이 못 하는 것

MVS는 오래되고 잘 다듬어진 기술입니다. 그런데도 3DGS와 NeRF가 등장한 데는 이유가 있습니다. MVS의 실패는 구현이 부족해서가 아니라 가정에서 나옵니다.

MVS는 세 가지를 암묵적으로 가정합니다.

  1. 장면은 불투명한 표면으로 되어 있다. 픽셀 하나에 깊이 하나가 대응한다는 말입니다.
  2. 표면은 램버시안이다. 즉 어느 방향에서 봐도 같은 색이다.
  3. 표면은 국소적으로 매끄럽다. 창 안에서 하나의 평면으로 근사된다.

현실은 셋 다 자주 어깁니다.

상황어떤 가정이 깨지나결과
유리, 물, 연기불투명깊이가 하나로 정해지지 않는다
젖은 바닥, 금속, 광택램버시안반사광이 시점마다 움직여 NCC가 엉뚱한 곳에서 최대가 된다
머리카락, 나뭇잎, 철망매끄러움창 안에 여러 깊이가 섞여 전부 뭉개진다
무텍스처 벽(정보 자체가 없음)구멍으로 남는다

특히 두 번째가 뼈아픕니다. 뷰에 따라 색이 달라지는 현상은 “오차”가 아니라 실제 물리인데, MVS는 그것을 오차로 취급해 버립니다. 그리고 복원된 메시는 표면마다 색을 하나씩만 들고 있으므로, 그런 현상을 담을 그릇 자체가 없습니다.

이 지점이 갈림길입니다. MVS는 “장면 = 표면 + 표면당 색 하나”라는 표현을 골랐고, 그 표현이 담을 수 없는 것들이 전부 실패로 나타났습니다. NeRF와 3DGS는 표현을 바꿉니다. 장면 = 공간에 퍼진 밀도 + 방향에 따라 달라지는 색으로요. 그러면 반투명도, 반사도, 얇은 구조도 “표현할 수는 있는” 것이 됩니다.


7. 자주 나오는 질문들

Q1. 그럼 MVS는 이제 필요 없나?

아닙니다. 측정이 목적일 때는 여전히 MVS가 답입니다. 문화재 실측, 건설 현장 진척 관리, 검사·계측처럼 “이 벽은 몇 미터인가”를 물어야 하는 일에서는 명시적인 표면 메시가 필요하고, 오차를 mm 단위로 따질 수 있어야 합니다. 3DGS는 보기에 그럴듯한 그림을 만드는 데 최적화돼 있지, 표면의 정확한 위치를 보장하지 않습니다. 실제로 3DGS에서 깨끗한 메시를 뽑는 건 별도의 연구 주제입니다.

Q2. 3DGS도 SfM 점군만 쓰던데, MVS 점군을 넣으면 더 좋지 않나?

더 좋아지는 경우가 있고, 실제로 그렇게 초기화하는 변형들도 있습니다. 다만 트레이드오프가 있습니다. MVS 점군은 수천만 개라 그대로 가우시안으로 바꾸면 메모리가 감당이 안 되고, MVS가 틀린 곳(반사면, 무텍스처)의 오류를 그대로 물려받습니다. 3DGS의 밀도 조정은 “부족한 곳을 스스로 늘리는” 장치라, 희소하지만 믿을 만한 SfM 점군에서 출발하는 편이 대체로 안전합니다.

Q3. 딥러닝 MVS(MVSNet 계열)는 뭐가 다른가?

비용 볼륨을 만드는 데까지는 같습니다. 다른 점은 그 볼륨에서 깊이를 뽑는 규칙을 손으로 만든 NCC + argmax 대신 학습된 3D 합성곱망이 맡는다는 것입니다. 학습을 통해 “무늬가 없어도 주변 문맥으로 미루어 짐작하는” 능력이 생겨서, 고전 MVS가 구멍을 남기는 자리를 메꿉니다. 대신 학습 데이터의 분포를 벗어나면 그 짐작이 그럴듯한 거짓말이 됩니다.

Q4. 깊이 맵과 시차 맵은 같은 것인가?

같은 정보의 다른 표현입니다. 정면 평행 스테레오에서 $Z = fB/d$ 이므로 시차는 깊이의 역수에 비례합니다. 탐색은 시차 공간에서 균등하게 하는 것이 보통인데, 그래야 가까운 곳은 촘촘하게, 먼 곳은 성기게 보게 되기 때문입니다. 이미지의 정보량이 실제로 그렇게 분포합니다. NeRF가 먼 배경을 다룰 때 역깊이(disparity) 공간에서 샘플링하는 것도 같은 이유입니다.


마치며

  • MVS는 포즈를 안다고 가정하고 픽셀마다 깊이 1차원만 탐색한다. 근거는 광도 일관성 하나뿐이다.
  • 비용 곡선의 봉우리가 뾰족하고 유일할 때만 깊이가 정해진다. 무텍스처는 봉우리가 없고, 반복 패턴은 봉우리가 여럿이다.
  • 창 크기는 잡음과 경계 보존 사이의 딜레마이고, PatchMatch는 기울어진 평면을 함께 추정해 이를 우회한다.
  • 여러 깊이 맵을 다중 뷰 일관성으로 걸러 합치면 밀집 점군이, 거기에 표면 복원을 붙이면 메시가 나온다.
  • MVS의 한계는 구현이 아니라 “불투명한 램버시안 표면”이라는 가정에서 온다.

여기까지가 “사진에서 기하를 뽑는” 쪽 이야기입니다. 다음 세 편은 반대 방향, 그러니까 가지고 있는 3차원을 화면에 그리는 쪽으로 넘어갑니다. 먼저 래스터화입니다.


이 시리즈 — 3DGS를 읽기 위한 기초지식

  1. MLP와 역전파
  2. SGD에서 Adam까지
  3. SfM 파이프라인
  4. MVS — 밀집 복원 ← 지금 읽는 글
  5. 래스터화
  6. 알파 블렌딩과 볼륨 렌더링
  7. 구면 조화 함수
  8. 미분 가능 렌더링
  9. NeRF
  10. 3D Gaussian Splatting ← 도착지

참고문헌

  1. Szeliski, R. (2022). Computer Vision: Algorithms and Applications (2nd ed.). Springer. — 12장 스테레오, 13장 3D 복원. ↩︎

  2. Bleyer, M., Rhemann, C., & Rother, C. (2011). PatchMatch Stereo — Stereo matching with slanted support windows. BMVC. ↩︎

  3. Barnes, C., Shechtman, E., Finkelstein, A., & Goldman, D. B. (2009). PatchMatch: A randomized correspondence algorithm for structural image editing. ACM TOG, 28(3). — 무작위 초기화 + 전파라는 발상의 원형. ↩︎

  4. Schönberger, J. L., Zheng, E., Frahm, J.-M., & Pollefeys, M. (2016). Pixelwise view selection for unstructured multi-view stereo. ECCV. — COLMAP MVS. ↩︎

  5. Kazhdan, M., Bolitho, M., & Hoppe, H. (2006). Poisson surface reconstruction. Eurographics Symposium on Geometry Processing. ↩︎

이 기사는 저작권자의 CC BY-NC-ND 4.0 라이센스를 따릅니다.

License

CC BY-NC-ND 4.0

이 포스트는 CC BY-NC-ND 4.0 라이선스로 제공됩니다. 출처 표시는 필요하며, 상업적 이용과 변경은 허용되지 않습니다.