SLAM의 프론트엔드와 백엔드 — 실시간이라는 제약이 구조를 만든다
서론
지난 글에서 카메라와 점을 전부 미지수로 놓고 한꺼번에 최적화하는 번들 조정까지 왔습니다. 이론적으로는 이걸로 끝입니다. 관측을 전부 모아 번들 조정을 돌리면 최적해가 나옵니다.
문제는 로봇이 지금 어디 있는지 33밀리초 안에 답해야 한다는 것입니다.
이 제약 하나가 시스템의 구조를 결정합니다. 모든 관측을 다 모을 때까지 기다릴 수 없고, 지도가 커진다고 계산량이 무한히 늘어도 안 되며, 답은 즉시 제어 루프로 넘어가야 합니다. 그 결과 SLAM 시스템은 거의 예외 없이 프론트엔드와 백엔드로 쪼개집니다. 이 글은 그 분업이 왜 생겼고 각자 무엇을 하는지에 대한 이야기입니다.
마지막에는 2차원 포즈 그래프 최적화를 직접 구현해서, 루프 클로저 하나가 궤적을 어떻게 되돌려 놓는지 숫자로 확인합니다.
1. SfM과 SLAM은 무엇이 다른가
둘 다 “이미지에서 카메라 자세와 3차원 구조를 복원한다”는 같은 수학을 씁니다. 다른 것은 운영 조건입니다.
| SfM | SLAM | |
|---|---|---|
| 데이터 | 전부 미리 주어짐 (배치) | 순차적으로 도착 (온라인) |
| 시간 | 몇 시간 써도 됨 | 프레임당 수십 밀리초 |
| 계산량 | 전체 크기에 비례해도 됨 | 유계여야 함 |
| 결과 용도 | 나중에 본다 | 즉시 제어에 들어간다 |
| 순서 | 이미지 순서 무관 | 시간 순서가 정보 |
“계산량이 유계여야 한다”가 특히 무겁습니다. 로봇이 한 시간을 돌아다니면 프레임이 10만 장 쌓이는데, 프레임 수에 비례해 느려지는 알고리즘은 몇 분 만에 실시간을 놓칩니다. 버릴 것을 버리고, 요약할 것을 요약하는 설계가 필수입니다.
2. 그래서 둘로 나눈다
해법은 성격이 다른 두 일을 분리하는 것입니다.
프론트엔드는 센서 데이터를 제약으로 바꾸고, 백엔드는 그 제약을 모아 최적 상태를 푼다. 루프 클로저는 프론트엔드가 찾아내 백엔드에 새 제약으로 던져 준다.
- 프론트엔드 — 센서 원시 데이터를 제약(constraint) 으로 바꿉니다. “이 프레임과 저 키프레임 사이의 상대 자세는 대략 이것”이라는 형태입니다. 매 프레임 돌아야 하므로 빠르고 근사적입니다.
- 백엔드 — 쌓인 제약을 모아 최적 상태를 풉니다. 앞 글의 비선형 최소제곱이 여기서 돌아갑니다. 매 프레임 돌 필요는 없으니 별도 스레드에서 비동기로 처리합니다.
이 분리가 주는 것은 단순한 모듈화 이상입니다. 프론트엔드가 틀려도 백엔드가 고칠 수 있고, 백엔드가 느려도 프론트엔드는 계속 돌 수 있습니다. ORB-SLAM 계열이 트래킹·로컬 매핑·루프 클로징을 세 스레드로 나눠 돌리는 것이 이 구조의 전형입니다.1
3. 프론트엔드 — 본질은 데이터 연관
프론트엔드에서 자세 추정 자체는 사실 쉬운 축입니다. 3차원 점과 그 투영을 알면 PnP로 자세가 바로 나옵니다. 진짜 어려운 건 그 앞 단계, “이 픽셀과 저 픽셀이 같은 점인가” 를 정하는 일 — 데이터 연관(data association) 입니다.
접근은 크게 둘로 갈립니다.
- 특징 기반(indirect). ORB 같은 특징점을 뽑아 기술자로 매칭하고, 재투영 오차를 최소화합니다. 조명 변화에 강하고 루프 클로저에 그대로 재활용할 수 있습니다. 대신 특징점 밖의 정보는 버립니다.
- 직접법(direct). 특징점을 뽑지 않고 픽셀 밝기 차이(광도 오차) 를 직접 최소화합니다. 텍스처가 약한 곳에서도 정보를 쓸 수 있지만, 밝기 불변 가정이 필요해 자동 노출이나 조명 변화에 약합니다. LSD-SLAM과 DSO가 대표적입니다.2
그리고 프론트엔드가 반드시 하는 또 하나의 일이 키프레임 선정입니다. 왜 모든 프레임을 저장하지 않을까요?
- 가만히 서 있는 로봇의 100프레임은 정보가 사실상 1프레임입니다. 중복입니다.
- 앞 글에서 본 것처럼 삼각측량은 베이스라인이 있어야 의미가 있습니다. 붙어 있는 두 프레임으로는 깊이를 못 잡습니다.
- 백엔드의 비용은 노드 수에 달려 있습니다. 노드를 줄이는 것이 곧 실시간성입니다.
그래서 “시야가 충분히 바뀌었을 때만” 키프레임으로 승격시키고, 백엔드에는 키프레임만 넘깁니다. 버릴 것을 버리는 설계가 여기서 처음 나타납니다.
한 가지 더 — 잘못된 데이터 연관 하나는 지도 전체를 망가뜨릴 수 있습니다. 그래서 프론트엔드는 항상 RANSAC 같은 기하 검증을 끼고 돕니다. 에피폴라 기하 글에서 “실제로 에피폴라 제약은 매칭 검증 도구로 가장 많이 쓰인다”고 한 것이 바로 이 자리입니다.
4. 백엔드 — 풀 BA에서 포즈 그래프까지
백엔드가 이상적으로 풀고 싶은 것은 모든 키프레임과 모든 점에 대한 풀 번들 조정입니다. 하지만 지도가 커지면 매번 돌릴 수 없습니다. 그래서 두 단계로 타협합니다.
① 로컬 BA (슬라이딩 윈도우). 최근 키프레임 몇 개와 거기서 보이는 점만 최적화합니다. 창 밖으로 밀려나는 변수는 그냥 버리는 게 아니라 마지널라이즈해서 남은 변수들 사이의 사전 정보로 압축합니다. 이때 쓰는 도구가 앞 글의 Schur 보수와 같습니다. 다만 마지널라이제이션은 원래 없던 연결(fill-in)을 만들어 희소성을 갉아먹기 때문에, 무엇을 언제 버릴지가 설계 포인트가 됩니다.
② 포즈 그래프 최적화. 더 과감하게, 3차원 점을 전부 소거하고 포즈끼리의 상대 제약만 남깁니다. 노드는 키프레임 자세, 엣지는 “i에서 본 j의 상대 자세는 이것이고 그 신뢰도는 이만큼”이라는 측정입니다.
\[\min_{\{X_i\}} \;\sum_{(i,j) \in \mathcal{E}} e_{ij}^\top\, \Omega_{ij}\, e_{ij}, \qquad e_{ij} = \log\bigl(Z_{ij}^{-1} X_i^{-1} X_j\bigr)\]여기서 $\log$ 는 리 군의 원소를 접공간 벡터로 내리는 연산이고, 정보 행렬 $\Omega$ 는 그 측정을 얼마나 믿을지를 나타냅니다. 점이 사라졌으니 변수는 포즈뿐이고, 그래프가 희소한 만큼 정규방정식도 희소합니다. 수만 개 노드도 초 단위에 풀립니다.3
대가는 분명합니다. 점을 지웠으므로 구조는 더 이상 개선되지 않고, 상대 제약을 만들 때 쓴 선형화가 고정됩니다. 그래서 실제 시스템은 둘을 함께 씁니다 — 평소에는 로컬 BA로 정밀도를 유지하고, 루프가 닫히는 큰 사건에서는 포즈 그래프로 전체를 재배치합니다.
5. 루프 클로저 — SLAM을 SLAM으로 만드는 것
오도메트리는 상대 변위를 적분합니다. 적분은 오차도 함께 쌓습니다. 아무리 좋은 프론트엔드를 써도 드리프트는 원리적으로 피할 수 없습니다.
이걸 되돌릴 방법은 하나뿐입니다. 예전에 왔던 곳을 알아보는 것. 그 순간 “지금 자세와 100프레임 전 자세가 사실상 같다”는 제약이 하나 생기고, 이 제약 하나가 그 사이 모든 포즈를 끌어당겨 재배치합니다. Localization과 Mapping 사이의 M이 제대로 작동하기 시작하는 지점입니다.
절차는 보통 셋입니다.
- 장소 인식 — 이미지 기술자를 시각 단어 가방(BoW)이나 학습 기반 전역 기술자로 요약해 후보를 빠르게 찾습니다.4
- 기하 검증 — 후보에 대해 특징 매칭 + RANSAC으로 실제 상대 자세가 나오는지 확인합니다.
- 제약 추가 + 재최적화 — 포즈 그래프에 엣지를 추가하고 백엔드를 돌립니다.
2번이 형식적인 절차처럼 보이지만 그렇지 않습니다. 거짓 루프 클로저 하나가 지도 전체를 접어 버립니다. 복도처럼 비슷한 장면이 반복되는 환경에서 특히 위험합니다. 그래서 기하 검증을 빡빡하게 걸고, 백엔드에도 로버스트 커널이나 switchable constraint 같은 안전장치를 둡니다 — 나중에 “이 엣지는 아무래도 틀린 것 같다”고 판단되면 최적화가 스스로 그 엣지의 영향력을 꺼 버릴 수 있게 하는 장치입니다.5
6. 직접 돌려보기 — 2D 포즈 그래프 최적화
말로만 하면 와닿지 않으니 직접 구현해서 확인합니다. 로봇이 한 변 8m인 정사각형을 두 바퀴 돌고, 오도메트리에는 스텝당 위치 3cm·각도 0.86°의 노이즈가 섞입니다.
엣지 하나의 오차와 야코비안이 전부입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def edge_error_jac(xi, xj, z):
"""e = z^-1 ⊖ (xi^-1 ⊕ xj) 와 그 야코비안 A(∂e/∂xi), B(∂e/∂xj)."""
Ri, Rz = rot(xi[2]), rot(z[2])
dt = xj[:2] - xi[:2]
e = np.array([*(Rz.T @ (Ri.T @ dt - z[:2])), wrap(xj[2] - xi[2] - z[2])])
dRiT = np.array([[-np.sin(xi[2]), np.cos(xi[2])],
[-np.cos(xi[2]), -np.sin(xi[2])]]) # d(Ri^T)/dθi
A = np.zeros((3, 3))
A[:2, :2] = -Rz.T @ Ri.T
A[:2, 2] = Rz.T @ dRiT @ dt
A[2, 2] = -1.0
B = np.zeros((3, 3))
B[:2, :2] = Rz.T @ Ri.T
B[2, 2] = 1.0
return e, A, B
나머지는 앞 글과 같은 Gauss-Newton입니다. 엣지마다 계산한 블록을 희소 행렬에 누적하고 풀면 됩니다.
1
2
3
4
5
6
7
for (i, j, z, Om) in edges:
e, A, B = edge_error_jac(X[i], X[j], z)
H[si, si] += A.T @ Om @ A; H[si, sj] += A.T @ Om @ B
H[sj, si] += B.T @ Om @ A; H[sj, sj] += B.T @ Om @ B
b[si] += A.T @ Om @ e; b[sj] += B.T @ Om @ e
H[0:3, 0:3] += np.eye(3) * 1e9 # 첫 포즈 고정 (게이지)
X = X + spsolve(csr_matrix(H), -b).reshape(-1, 3)
H[0:3, 0:3]에 큰 값을 더하는 한 줄이 앞 글에서 말한 게이지 자유도 처리입니다. 지도 전체를 회전·평행이동해도 상대 제약은 그대로이므로, 어딘가 한 점을 못 박아야 유일해가 나옵니다.
왼쪽: 오도메트리를 적분한 궤적. 두 바퀴가 어긋나고 출발점으로 돌아오지 못한다. 오른쪽: 루프 클로저 제약(빨간 점선)을 넣고 최적화한 결과. 궤적이 참값 위로 되돌아오고 시작점과 끝점이 겹친다.
숫자로 보면 이렇습니다. 포즈 137개, 엣지 143개(오도메트리 136 + 루프 클로저 7).
| 최적화 전 | 최적화 후 | |
|---|---|---|
| ATE (전체 궤적 RMS 오차) | 1.005 m | 0.202 m |
| 최종 위치 오차 | 1.305 m | 0.021 m |
| $\chi^2$ | 87,672 | 23.2 |
9번의 반복으로 수렴했습니다. 여기서도 검산을 해 볼 수 있습니다. 잔차는 143 × 3 = 429개, 자유 변수는 137 × 3 − 3(고정) = 408개이므로 자유도는 21이고, 잘 수렴한 $\chi^2$ 의 기대값은 21입니다. 실측 23.2 — 측정 노이즈가 허용하는 만큼 정확히 맞춘 셈입니다.
주목할 점은 루프 클로저 7개가 궤적 137개를 통째로 되돌려 놓았다는 것입니다. 제약 하나하나는 국소적이지만, 그래프로 연결되어 있으므로 정보가 전체로 퍼집니다. 이것이 백엔드가 존재하는 이유입니다.
7. 시스템 지형도
필터에서 최적화로. 초기 SLAM은 EKF로 상태를 순차 갱신했습니다. 지금은 대부분 최적화 기반입니다. 결정적인 근거는 “같은 계산량이라면 필터보다 키프레임 + 번들 조정이 더 정확하다”는 비교 연구였습니다.6 필터는 과거를 즉시 마지널라이즈해 선형화를 고정해 버리는데, 최적화는 과거를 다시 펴서 재선형화할 수 있기 때문입니다.
대표 시스템. 특징 기반 시각 SLAM은 ORB-SLAM 계열, 직접법은 DSO 계열, 시각-관성 융합은 VINS-Mono 계열, LiDAR는 Cartographer 계열이 기준점입니다. 백엔드 라이브러리로는 g2o, GTSAM(iSAM2), Ceres가 널리 쓰입니다.
센서 융합. 단안 카메라만으로는 스케일을 알 수 없다는 근본 한계가 있습니다. IMU를 붙이면 스케일과 중력 방향이 관측 가능해집니다. 이때 IMU 데이터를 키프레임 사이에서 미리 적분해 하나의 제약으로 압축하는 프리인테그레이션이 표준 기법입니다.
그리고 다시 3DGS로. 이 시리즈의 첫 글에서 언급한 SplaTAM이나 MonoGS는, 여기서 말한 백엔드의 지도 표현을 점군 대신 가우시안으로 바꾼 것입니다. 렌더링이 미분 가능하므로 “렌더한 이미지와 관측이 일치하도록” 자세를 최적화하는 식으로 프론트엔드까지 같은 표현 위에서 돌립니다.
8. 여전히 어려운 것
- 동적 환경. 대부분의 SLAM은 세상이 정지해 있다고 가정합니다. 사람이 지나가고 물건이 옮겨지면 가정이 깨집니다.
- 텍스처 없는 공간. 흰 벽과 긴 복도는 데이터 연관이 성립하지 않습니다.
- 장기 운영. 며칠·몇 달 단위로 돌리면 지도가 무한정 커지고, 계절과 조명이 바뀌어 예전 지도와 지금 관측이 안 맞습니다. 지도 요약과 평생 지도 관리는 아직 열린 문제입니다.
- 잘못된 루프 클로저. 앞서 말한 대로, 여전히 가장 무서운 실패 모드입니다.
마치며
세 가지로 줄입니다.
- 분업의 이유는 실시간이다. 프론트엔드는 센서를 제약으로 바꾸고(빠르고 근사적), 백엔드는 제약으로 최적 상태를 푼다(느리고 정확). 계산량을 유계로 만드는 장치가 키프레임과 슬라이딩 윈도우다.
- 포즈 그래프는 점을 지운 번들 조정이다. 구조 개선을 포기하는 대신 큰 지도를 빠르게 재배치할 수 있고, 루프 클로저의 무대가 된다.
- 루프 클로저가 드리프트를 되돌린다. 제약 7개가 궤적 137개를 고쳤다. 단, 거짓 양성 하나가 지도를 접는다.
이것으로 3DGS → 에피폴라 기하 → 삼각측량·번들 조정 → SLAM으로 이어진 네 편이 한 바퀴를 돌았습니다. 사진 여러 장에서 기하를 뽑아내고, 그 오차를 함께 다듬고, 실시간 제약 아래 다시 배치하고, 그 결과를 렌더링 가능한 장면 표현으로 되돌리는 흐름입니다.
다음에는 이 지도 위에서 로봇이 실제로 움직이는 쪽 — 경로 계획과 제어로 넘어가 볼까 합니다.
참고문헌
Mur-Artal, R., & Tardós, J. D. (2017). ORB-SLAM2: An Open-Source SLAM System for Monocular, Stereo and RGB-D Cameras. IEEE T-RO, 33(5). / Campos, C. et al. (2021). ORB-SLAM3. IEEE T-RO, 37(6). ↩︎
Engel, J., Koltun, V., & Cremers, D. (2018). Direct Sparse Odometry. IEEE TPAMI, 40(3), 611–625. ↩︎
Kümmerle, R. et al. (2011). g2o: A General Framework for Graph Optimization. ICRA. — 포즈 그래프 정식화의 원류는 Lu, F., & Milios, E. (1997). Globally Consistent Range Scan Alignment for Environment Mapping. Autonomous Robots, 4(4). ↩︎
Gálvez-López, D., & Tardós, J. D. (2012). Bags of Binary Words for Fast Place Recognition in Image Sequences. IEEE T-RO, 28(5). ↩︎
Sünderhauf, N., & Protzel, P. (2012). Switchable Constraints for Robust Pose Graph SLAM. IROS. ↩︎
Strasdat, H., Montiel, J. M. M., & Davison, A. J. (2010). Real-time Monocular SLAM: Why Filter?. ICRA. ↩︎