JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

重建世界:SfM、SLAM 與光束法平差

透過還原每台相機的姿態、再以光束法平差統一精修,將數百張照片縫合成 3D 模型。

從兩視圖到多視圖

在第 3 篇我們從恰好兩張照片中榨出了幾何資訊:給定左右兩張影像之間配對好的點,對極約束讓我們得以還原兩台相機彼此的相對位姿,並三角化出少數幾個三維點。這是個漂亮的結果,但也很「迷你」。真實世界的地標——一座大教堂、一尊雕像、一面山壁——是由不同的人、從不同角度、在一天中不同時刻、以毫無順序拍下的數百張照片所記錄。本篇要問的是:我們要如何把這堆雜亂的影像,變成一個單一、連貫的三維模型?

答案就是 運動恢復結構(Structure from Motion,SfM)。這個名字很直白:當相機在場景周圍「移動」時,點在影像之間移動的方式同時編碼了結構(三維點在哪裡)與運動(每台相機在哪裡)。它的輸出一次給你兩樣東西:每一台相機的位姿,以及一個 點雲——也就是場景表面點的三維座標集合。從原始照片中還原出這份幾何,正是我們所說的 三維重建,而 SfM 就是完成這件事的經典引擎。

目前主流的策略是增量式(incremental)SfM:我們不是一次解出所有相機,而是一次加入一台相機、讓重建逐步長大,就像從一塊有把握的拼圖出發、向外擴展整幅拼圖。下面用敘事的方式呈現整條流程。本篇後面的章節再放大檢視真正困難的部分——讓一切保持誠實的最佳化(光束法平差),以及把它填滿、變稠密的步驟(多視圖立體)。

  1. 偵測與配對特徵。在每張影像中找出可重複偵測的關鍵點(角點、斑點),並用一個向量描述每個關鍵點。再把這些描述子在影像對之間配對,猜測不同照片裡哪些像素其實是同一個實體點。
  2. 從一對好影像初始化。挑選一對擁有大量可靠配對、且基線夠寬的影像,套用第 3 篇的兩視圖幾何,還原它們的相對位姿並三角化出第一批三維點。
  3. 註冊下一台相機。找出一張尚未使用、卻看得到許多既有三維點的影像。由於我們同時知道這些三維點、以及它們在這張新影像中對應的像素位置,便能直接解出新相機的位姿(這就是 PnP/後方交會步驟)。
  4. 三角化更多點。新相機就定位後,它與先前相機共享、但尚未進入模型的配對,就成為新的三維點,讓點雲持續長大。
  5. 精煉並重複。週期性地用光束法平差(第 3 節)清除誤差,然後回到註冊下一台相機的步驟,直到每一張可用的影像都被納入為止。

跨影像的特徵軌跡

SfM 的好壞,完全取決於餵給它的對應關係,所以讓我們看看它所依賴的原始資料。先前的電腦視覺指南介紹過特徵偵測子與描述子,例如 SIFT:這類演算法能在影像中找出獨特、可重複偵測的關鍵點,並把每個關鍵點周圍的鄰域,總結成一個向量(描述子),且這個向量被刻意設計成在視角旋轉、相機靠近、或光照改變時仍大致不變。這份不變性正是重點所在——它讓同一個真實窗角,在相隔數分鐘、數公尺拍下的兩張照片中,產生幾乎一模一樣的描述子。

現在來定義關鍵物件:特徵軌跡(feature track)。一條軌跡,就是同一個實體三維點在多張影像中被看見的紀錄,透過一對對的配對串接起來。如果影像 1 的關鍵點 A 配對到影像 2 的關鍵點 B,而 B 又配對到影像 3 的關鍵點 C,那麼 A–B–C 就構成一條軌跡——一條跟隨那個窗角穿過整個照片集的線索。想想監視器:要在一棟大樓裡追蹤某個人,你不需要一台神奇的全能相機;你是把不同相機一格又一格拍到的同一個人串接起來。特徵軌跡正是如此,只不過這裡的「人」是表面上一個固定的點,而「相機」就是你的那些照片。

把同一個關鍵點跨多個畫面串接起來,就形成一條軌跡——點在視圖之間的位移,正是 SfM 用來讀取結構與運動的訊號。

一張示意圖,顯示同一批場景點在一連串相機畫面中被追蹤,箭頭把每個點從一個畫面到下一個畫面的位置連起來。

但有個陷阱:描述子配對是啟發式的,會產生大量錯誤配對——重複立面上的兩扇不同窗戶可以長得幾乎一模一樣。一個錯誤配對會毒害一整條軌跡,把整個重建拉歪。解法是幾何驗證。對每一對候選影像,我們在穩健的 RANSAC 迴圈中,估計第 3 篇的 基礎矩陣:凡是遵守對極約束的配對(每個點都落在其夥伴對極線附近)被保留為內點;違反約束的則被當成外點丟棄,無論它們的描述子看起來多麼相似。只有通過這道幾何檢驗的配對,才值得信任、才能拿來建立軌跡。

光束法平差:最佳化的核心

增量式 SfM 有個無聲的病:它會累積誤差。每一台新相機,都是對著一些本身就由更早、略帶瑕疵的相機所估出的點來註冊。微小的錯誤層層疊加,加到第一百台相機之後,模型可能明顯地彎曲或漂移——筆直的牆面拱起來、繞著雕像一圈的相機接不回起點。解藥是 光束法平差(bundle adjustment,BA):我們不再信任那條增量估計的鎖鏈,而是退一步,用單一的全域準則,同時精煉每一台相機的參數與每一個三維點的位置

那個單一準則,就是重投影誤差(reprojection error)。名字背後的畫面是這樣的:從每個三維點,畫出一條光線,射向某台相機、落在它的像平面上成為一個像素——這就是預測的投影。我們同時也知道那個點實際上被觀測在哪裡(特徵軌跡告訴了我們)。預測像素與觀測像素之間的差距,就是誤差。「光束」(bundle)指的是從每個三維點射向所有看得到它的相機的那一束光線;我們「平差/調整」(adjust)相機與點,使每一束光線都盡可能緊密地收斂到真實的觀測上。

\min_{\{K_j,R_j,t_j\}_j,\;\{X_i\}_i}\;\sum_{i}\sum_{j}\bigl\lVert\, x_{ij}-\pi\!\left(K_j,R_j,t_j,X_i\right)\bigr\rVert^{2}

光束法平差,一次對所有點與所有相機,最小化重投影誤差的平方總和。

讓我們拆解每個符號。X_i 是世界中第 i 個三維點(三個數字,即它的 xyz 座標)。相機 j 由它的內參 K_j(第 1 篇的焦距與主點——它如何把方向轉成像素)以及位姿 (R_j, t_j)(一個旋轉 R_j 與一個平移 t_j,說明相機坐落在哪、朝向何方)所描述。函數 \pi(\cdot) 就是第 1 篇的投影函數:它把一個世界點搬進相機 j 的座標系,再透過鏡頭壓平成一個預測像素。x_{ij} 則是點 i 在影像 j 中實際被觀測到的位置(來自特徵軌跡的一個二維像素)。範數 \lVert\cdot\rVert 衡量預測與觀測之間的像素距離,我們再取平方。雙重總和 \sum_i\sum_j 跑遍整個資料集中每一筆(點、相機)觀測——也就是每條軌跡的每一階。關鍵在於,這個 \min 是同時對兩組未知數取的:所有三維點 \{X_i\} 以及所有相機參數 \{K_j,R_j,t_j\} 一起被微調,把總失配量壓低。

對數字有點具體感受:假設某個三維點目前在某影像中投影到像素 (102, 247),但特徵軌跡說它實際被觀測在 (100, 250)。殘差是 (2, −3),為總和貢獻 2^2+3^2 = 13 平方像素。若有一百萬筆這樣的觀測,總和起初可能高達數百萬;一次成功的 BA 也許能把平均殘差壓到一個像素以下,使每個點都幾乎準確地落在每台相機所見之處。由於 \pi 是非線性的(它要除以深度),這是一個龐大的非線性最小平方問題,以Levenberg–Marquardt(LM)演算法迭代求解——這個方法融合了梯度下降(離得遠時走安全的小步)與 Gauss–Newton(靠得近時走快速的大步),因此能穩健收斂。

當相機可能多達數萬台、點多達數百萬個——也就是數百萬個未知數時,這為何還解得動?救命的關鍵是稀疏性(sparsity)。每一步 LM 都要解一個由 Jacobian(描述每個殘差如何隨每個未知數變化的矩陣)建構的線性系統。但每個三維點只出現在少數幾台相機中,而每台相機也只看得到一部分的點——所以幾乎每一組「點—相機」配對都毫無貢獻,使 Jacobian(以及由它導出的正規方程矩陣)絕大多數元素都是零。求解器以Schur 補(Schur complement)技巧善用這一點:由於矩陣的點區塊是區塊對角的(點之間不直接耦合,只透過共享的相機間接相連),你可以先廉價地消去那數百萬個點變數,留下一個只含相機參數、小得多的稠密系統來解,再回代求出各點。正是這一個結構性事實,把一個不可能的問題變成了例行公事。

# Bundle adjustment as nonlinear least squares (conceptual pseudocode).
# Unknowns: poses (R, t) and intrinsics K for each camera, plus every 3D point X.

def residuals(cameras, points, observations):
    r = []
    for (i, j, x_obs) in observations:          # point i seen in camera j at pixel x_obs
        x_pred = project(cameras[j], points[i]) # pi(K_j, R_j, t_j, X_i) -> predicted pixel
        r.append(x_obs - x_pred)                # 2D reprojection residual
    return stack(r)

# Solve with Levenberg-Marquardt; the optimizer needs the Jacobian J = d(residuals)/d(unknowns).
# J is SPARSE: residual (i, j) depends ONLY on point i and camera j, nothing else.
solution = levenberg_marquardt(
    residual_fn = residuals,
    init        = (cameras, points),
    jacobian    = sparse_jacobian,   # exploit block structure
    linear_solve = schur_complement, # eliminate the many point vars first, solve for cameras
)
# Objective being minimized:  sum over observations of || x_obs - project(cam, X) ||^2
BA 的骨架:定義重投影殘差,再讓 Levenberg–Marquardt 利用稀疏 Jacobian 與 Schur 補,最小化它們的平方和。

邁向稠密:多視圖立體

經過 SfM 與光束法平差之後,我們手上到底有什麼?極為準確的相機位姿與內參——但只有一個稀疏的點雲,因為我們是每條特徵軌跡放一個三維點,而軌跡只存在於那些獨特的關鍵點上(角點、有紋理的位置)。兩扇窗戶之間那面平坦的牆並未產生關鍵點,於是它在我們的點雲中根本是空的。要知道相機在哪,這沒問題;但作為一個表面模型,它毫無用處。要得到一個能算繪或三維列印的模型,我們需要在(幾乎)每一個像素上都有一個點。

多視圖立體(multi-view stereo,MVS)就是把它填滿的步驟。它的關鍵動作是凍結SfM 辛苦算出的相機位姿,把它們當成真值。相機固定後,MVS 便為幾乎每一個像素計算一個稠密深度,融合來自多張重疊視圖的證據。最直接的想像方式,是把第 2 篇的立體視覺加以推廣:第 2 篇我們有一組整齊、幾何已知的左右相機;MVS 做的是同樣的三角化,只是改用你那許多任意角度的照片——因為現在 SfM 已經精確告訴我們每一張是從哪裡拍的。

在沒有配對好的關鍵點下,它究竟如何找到深度?靠的是測試假設並檢查照片一致性(photo-consistency)。對參考影像中的某個像素,MVS 先猜一個深度,這個深度把該像素擺在某個特定的三維位置;接著它把這個三維位置(利用各鄰近視圖的已知位姿)投影到鄰近視圖中,並問:圍繞所有這些投影位置的小影像塊,看起來真的彼此相似嗎?若是,這個深度猜測八成是對的;若這些影像塊互相矛盾,那就是錯的。PatchMatch 等現代方法不會盲目測試每一個深度,因而效率很高:它們從隨機猜測出發,然後把好的深度傳播給鄰近像素(表面大多平滑,所以一個像素的正確深度通常跟鄰居很接近),再隨機微調,迅速收斂出每張影像的一張深度圖。

於是每張影像都產出自己的一張深度圖——也就是它自己逐像素的 深度估計。最後一個動作是深度圖融合(depth-map fusion):把所有逐影像的深度反投影回三維空間(它們在那裡應當彼此吻合),合併成單一、一致的稠密 點雲,丟掉只有一張視圖相信的點,並在多張視圖一致之處對雜訊取平均。其成果就是稠密的 三維重建——以數百萬個點鋪滿整個表面,而非只在角落散落數千個點。

SLAM:即時重建

到目前為止的一切都是離線的:先蒐集所有照片,再從容地一一跑過 SfM、BA 與 MVS。但機器人、無人機或 AR 頭戴裝置可等不了。它必須在移動的當下、就在此刻理解世界。這正是 視覺 SLAM(Simultaneous Localization And Mapping,即時定位與地圖建構)的任務。這個名字精確道出了那個雞生蛋的困局:要知道你在哪(定位)需要一張地圖,但要建一張地圖又需要先知道你在哪。SLAM 同時做這兩件事,線上、逐格地,從一台在未知環境中移動的相機完成。

把 SLAM 與 SfM 正面對比會很有幫助,因為它們在幾何上是兄弟——兩者都透過最小化重投影誤差,從影像估計相機位姿與三維結構。差別在於它們所處的限制。SfM 是批次且離線的:它一開始就看得到每一張影像、可以任意順序處理、也負擔得起昂貴的全域光束法平差。SLAM 則是循序且受延遲約束的:影像按時間順序到來,它無法偷看未來,而且必須在數毫秒內回傳當前畫面的位姿,否則機器人就撞上去了。SLAM 用 SfM 的一部分準確度與全域最佳性,換取 SfM 給不了的那一樣東西——當下的答案。

為了趕上那個死線,SLAM 把工作拆成兩條平行執行的執行緒。追蹤(tracking)在每一張進來的畫面上執行,做的是快活:把當前畫面對著目前的地圖做匹配,即時估出相機的位姿。建圖(mapping)在背景以較慢的步調執行,做的是重活:擴展並精煉三維地圖。兩者之間的橋樑是關鍵畫面(keyframe)——SLAM 不會用每秒 30 張(多半重複)的所有畫面建圖,而是只把偶爾帶來真正新視角的畫面晉升為關鍵畫面,並用它們來建構地圖。

還有兩個部件讓 SLAM 在長途旅程中保持穩健。第一,一個局部(視窗式)光束法平差 持續地只對最近的少數幾張關鍵畫面、以及它們看到的點執行——就是第 3 節那套重投影誤差最佳化,但規模小到能即時完成,讓近期估計保持緊實,而不必重解整段歷史。第二,也是關鍵的——迴環閉合(loop closure):當你四處遊走時,小誤差會累積成漂移,所以當你回到先前看過的地方,你估計的位置已悄悄偏離了真值。迴環閉合偵測器會認出重訪的場景(「我來過這裡!」),加入一條把「現在」連到「當時」的約束,再由一次全域最佳化把整條軌跡與地圖一舉拉回對齊——以單次修正把漂移摺疊消去。

從點到曲面

MVS 交給我們一個稠密的 點雲——數百萬個三維點鋪滿表面——但點雲終究只是一堆彼此不相連的點。點與點之間有縫隙、沒有內外之分、也無法承接光線。最後一個經典步驟是網格化(meshing):把那些點轉換成一個無破洞、密合(watertight)的曲面,一張由三角形連結而成、能真正把實體與空無分開的網。

標準工具是 Poisson 曲面重建(Poisson surface reconstruction),其直覺相當優雅。在每個點旁,我們都有(或能估出)一個表面法向量(normal)——那支小箭頭,指出表面朝向哪一邊,也就是哪一側是「外面」。Poisson 重建尋找一個單一、平滑的曲面,使它自身的法向量處處與那一片量測到的法向量場吻合。把所有那些小箭頭想像成從真實表面向外吹的風;Poisson 找出那一層密合的外皮,使它處處都順著風的方向。因為它求的是單一的全域平滑曲面,所以能自然地跨越小縫隙、把雜訊抹平,給出乾淨的網格而非毛糙的一團。

不過光禿禿的灰色網格看起來仍不真實——它有形狀卻沒有皮膚。收尾步驟是貼圖(texturing):對每一個三角形,我們回頭看當初拍到它的那些原始照片(SfM 仍保有每台相機的位姿!),挑出對那一塊看得最清楚、變形最小的視圖,再把實際的影像色彩貼到表面上。跨越數百張原始照片拼接並做色彩平衡之後,成果就是一個擬真的三維模型,你可以隨意旋轉、從任意角度打光。

  1. 標定(第 1 篇):建立每台相機如何把三維方向轉成像素的模型——它的內參與投影函數 π。
  2. 配對(第 2 篇與本篇):偵測特徵、在影像間配對,並把可靠的配對串接成通過幾何驗證的特徵軌跡。
  3. 兩視圖幾何(第 3 篇):從一對好的初始影像,透過對極約束還原相對位姿,並三角化出最初的三維點。
  4. SfM + 光束法平差(本篇):一台台加入相機(PnP)、三角化更多點,並透過最小化重投影誤差,聯合精煉一切。
  5. 多視圖立體(本篇):凍結位姿、計算稠密的逐像素深度,再把深度圖融合成稠密點雲。
  6. 網格 + 貼圖(本篇):重建一個密合的曲面(Poisson),再用原始照片的色彩為它上色,得到擬真模型。

退一步看看我們在第 1 至 4 篇打造了什麼:一條完整、明確、幾何化的路徑,把一個資料夾的尋常照片變成一個帶貼圖的三維模型——每一階段都由相機幾何所支撐,每一個數字都是你能指著解釋清楚的東西。這就是成熟的經典多視圖幾何之最佳樣貌。本軌跡的最後一篇則押下不同的賭注:它不再把場景表示為明確的點、網格與三角化幾何,而是去學習一個表徵——用於 三維重建 的神經網路(預測深度),以及像 NeRF 與高斯潑濺(Gaussian Splatting)這類把整個世界存成最佳化參數的場景編碼。它們保留了你在這裡學到的相機幾何,卻把點與曲面的機制換成學習而來的東西。同樣的目標,全新的工具——這正是我們接下來要去的地方。