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

雙眼一深度:立體視覺

了解兩台並排的相機如何把影像間的微小位移,轉換成真實可量測的距離。

雙眼如何看出深度

我們先做一個你現在馬上就能試的實驗。把一根手指舉到手臂伸直的距離,並挑一個它後面的遠處目標——一面牆、一個門口、一棵樹。先閉上左眼只用右眼看,再換成只用左眼看。你會看到手指相對於背景了一下。背景幾乎不動,但手指卻大幅移位。這一「跳」就是立體視覺的全部祕密;讀完這篇指南,你就能把它換算成以公尺為單位的距離。

兩個視點從略為不同的位置看同一個場景;近處的點在兩張影像之間移動得比遠處的點多。

一張示意圖,顯示左右兩台相機同時看著一個近物與一個遠物;近物在兩張影像中的投影位置差很多,遠物則幾乎不變。

你的兩隻眼睛相隔幾公分,所以各自捕捉到略為不同的世界視角。大腦把這兩張影像融合成一張,並把橫向移位的多寡解讀成深度。規則美得出奇地簡單:近的東西移很多,遠的東西幾乎不動。位於無窮遠的物體——月亮、遠山——在兩隻眼睛中看起來就停在完全相同的位置。這種有層次的移位量(近大、遠小)就是深度知覺所依賴的原始訊號。

電腦視覺給這個移位取了個名字:視差(disparity)。用白話說,視差就是同一個場景點在一張影像裡比另一張偏左多少——以像素為單位測量。鼻尖前的點視差很大;地平線上的點視差幾乎為零。整個立體幾何領域要做的,就是精確地量出這個像素移位量,並把它換算成真實距離。我們把每個像素的視差蒐集成一張視差圖,再從這張圖重建三維世界。

立體相機組與校正後影像

為了把這份直覺變成可測量的數值,我們架設一組立體相機組:兩台完全相同、已校正的相機並排安裝,朝同一方向,橫向相隔一段固定距離,稱為基線(baseline),記作 B。「完全相同且已校正」意味著我們已從第 1 篇的校正步驟得知每台相機的焦距與主點,且兩台相機共用相同的內部設定。「同一方向」意味著它們的光軸彼此平行——像兩隻都直視前方、而非鬥雞眼的眼睛。正是這種整齊的配置,讓數學變得乾淨。

現實中沒有兩台相機能裝得完美。鏡頭可能略為傾斜,兩邊的像素列不會剛好等高,左影像落在第 200 列的點,在右影像可能落在第 205 列。因此在進行匹配之前,我們先對兩張影像各做一次性的扭曲處理,稱為校正(rectification)。校正把兩張影像重新投影到同一個平面上,使它們的列完美對齊:校正之後,任何場景點在左、右影像中都會落在同一條水平列上。

校正之後,左影像中的某個點與它在右影像中真正的對應點落在同一條水平列上——因此尋找對應點就只是沿著一條線滑動。

已校正的左右影像,畫了一條共用的水平掃描線貫穿兩者;一個匹配點在兩張影像中出現在相同高度,僅水平位置不同。

為什麼這件事這麼重要?沒有校正時,要找一個像素的夥伴就得搜尋整張另一影像——對每個 (x, y) 進行二維搜尋。這既慢又容易出錯。校正之後,夥伴保證在同一列上,於是搜尋塌縮成沿單一水平線(稱為掃描線)的一維掃描。我們只需左右滑動,逐一問「在這裡嗎?這裡?這裡?」。把搜尋空間從二維平面縮成一維直線,正是讓立體匹配快到能實用、且大幅減少歧義的關鍵。

由視差到深度:關鍵方程式

現在來收割成果。我們想要一條公式,輸入以像素為單位的視差,就交給我們以公尺為單位的深度。我們用相似三角形,並沿用第 1 篇的同一個針孔相機模型來得到它。想像左右兩台針孔相機,相隔基線 B,光軸平行。一個場景點位於它們前方深度 Z 處。由於兩台相機從略為不同的位置觀看它,這個點在每張影像中投影到略為不同的水平位置——而這兩個位置之差正是視差 d。把從該點穿過各自針孔、落到感光元件的射線描出來,會形成兩個形狀相同的三角形,它們的比例彼此鎖死成一條乾淨的關係式。

Z = \dfrac{f \cdot B}{d}

立體視覺的基本方程式:由視差求深度。

我們為每個符號命名。Z 是我們要的深度——點離相機多遠,以公尺計。f 是以像素為單位的焦距,我們在第 1 篇校正時取得(它把鏡頭焦距與感光元件的像素大小綁成一個數值)。B 是基線,兩台相機之間的實體距離,以公尺計。d 是視差,匹配點在左右影像之間的水平像素移位量。把這條公式當作食譜來讀:把相機「拉近」的程度(f)乘上兩眼相隔多遠(B),再除以該點跳了多少(d)。跳得大表示近;跳得小表示遠。

這裡最重要、最該用直覺去體會的一點是反比關係:深度與視差成反比,Z ∝ 1/d。視差加倍,深度就減半。當一個點退向遠方,它的視差朝零縮小,而當 d → 0 時,Z → 無窮大。這就是「月亮在你兩眼之間不會移動」背後的數學。它也帶著一個警告:因為關係是 1/d,同樣的匹配在近處精確、在遠處愈來愈不確定。靠近相機時,每多一像素視差,深度只溫和地改變;在遠處,單單一像素的視差就橫跨極大的深度範圍。

我們代入真實數字。假設 f = 800 像素,B = 0.12 公尺(12 公分的基線,約等於人類兩眼間距),而我們匹配到一個視差 d = 40 像素的點。則 Z =(800 × 0.12)/ 40 = 96 / 40 = 2.4 公尺。再取一個 d = 4 像素的遠點:Z = 96 / 4 = 24 公尺,視差只剩十分之一,距離卻遠了十倍——正是反比律的現場示範。單位也吻合:f·B 的單位是「像素·公尺」,除以 d(像素)後剩下公尺。

尋找對應點:匹配問題

深度方程式假設我們已經知道視差 d。但 d 是哪來的?這正是立體視覺真正的工作所在。要取得左影像中某個像素的 d,我們必須在右影像裡找到那個相同的實體點,並量出它移了多遠。對每個像素都這麼做,稱為對應(correspondence)或匹配問題,而這也是讓立體視覺真正困難的部分。多虧了校正,我們不必搜尋整張影像——只沿那一條掃描線。但我們仍得在那條線上所有候選位置中,判斷哪一個才是真正的對應點。

主力方法是區塊匹配(block matching)。我們不去比較孤伶伶的一個像素(單一亮度值太模糊、太曖昧),而是在左像素周圍抓取一個小視窗——比方 5×5 或 9×9 像素——並讓這塊小貼片沿右側掃描線滑動。在每個候選移位 d 處,我們把左貼片和右貼片比一比,替它們的相似程度打分。讓兩塊貼片看起來最相似的那個移位勝出,這個獲勝的移位就是我們的視差。視窗比單一像素好用太多了,因為一小塊紋理遠比單一灰階值更具辨識度。

我們怎麼替「相似度」打分?用匹配成本(matching cost)。三個經典選擇:SSD(差平方和)把兩塊貼片之間亮度差的平方加總起來——平方會狠狠懲罰大的不一致。SAD(差絕對值和)把單純的絕對差加總——較便宜,也較能容忍單一壞像素。NCC(正規化互相關)在去掉貼片的平均亮度與尺度之後測量兩者的相關性,因此當其中一台相機只是單純比較亮時,它依然穩健。SSD/SAD 低代表匹配好;NCC 高(接近 1)代表匹配好。

\mathrm{SSD}(d) = \sum_{(x,y)\in W} \big(I_L(x,y) - I_R(x-d,\,y)\big)^2

差平方和,作為候選視差 d 的函數。

我們逐項來讀。I_L(x, y) 是左影像在像素 (x, y) 的亮度(強度);I_R(x − d, y) 是右影像在同一列 y、但向左移了候選視差 d 之後的亮度。我們把兩者相減,把差平方(讓正負不能互相抵消),再對 W——點周圍的那個小視窗——加總。結果 SSD(d) 是每個候選 d 對應的一個成本數值;最佳視差就是讓這個成本最小化的那個 d。注意全程都在同一列 y——這正是校正帶來的好處,讓我們只需在 x 方向移動。具體地說,若一個 9×9 視窗罩在一個銳利的角點上,在 d = 39 給出 SSD = 120000、在 d = 40 給出 SSD = 4000、在 d = 41 給出 SSD = 90000,那麼在 40 處那個深深的凹谷就告訴我們對應點在那裡。

def block_match_ssd(left, right, x, y, win=4, d_max=64):
    # Find the disparity d that best matches the left window
    # to a shifted right window, along the same scanline y.
    best_d, best_cost = 0, float('inf')
    for d in range(0, d_max):
        cost = 0.0
        # Sum squared brightness differences over a (2*win+1) square window
        for dy in range(-win, win + 1):
            for dx in range(-win, win + 1):
                il = left[y + dy][x + dx]
                ir = right[y + dy][x + dx - d]   # shift LEFT by d, same row
                diff = il - ir
                cost += diff * diff
        if cost < best_cost:      # keep the disparity with the lowest cost
            best_cost, best_d = cost, d
    return best_d                  # this is the disparity for pixel (x, y)
以 SSD 成本進行的逐像素樸素區塊匹配。實際系統會將其向量化,並加上第 6 節的各種精進手段。

從視差圖到三維點雲

到目前為止我們只找出了一個像素的視差。現在擴大到整張影像:在每個像素都跑一次區塊匹配,並把每個結果存進一張新影像,其中每個位置的值就是該像素的視差。這就是視差圖。我們通常把它視覺化成一張灰階圖,其中亮代表近(視差大)、暗代表遠(視差小)。瞄一眼一張好的視差圖,你幾乎就能讀出場景的三維形狀——一個人在暗背景前亮亮地凸出來,像一幅以深度上色的剪影。

不過,視差圖仍然是一張平的二維影像——每個格子只是一個數字。神奇之處在於,把這些數字逐一餵進深度方程式,就得到那個像素處的實體深度 Z。而一旦知道了 Z,我們就能透過第 1 篇那同一個針孔模型反投影(back-projection),還原該點完整的三維位置 (X, Y, Z)。對每個像素都這麼做,你就產生一個點雲:成千上萬個 (X, Y, Z) 點構成的雲,重建出相機前方真正的表面。這是本軌道中你第一次從影像建出真實、稠密的三維幾何。

X = \frac{(u - c_x)\,Z}{f}, \qquad Y = \frac{(v - c_y)\,Z}{f}, \qquad Z = \frac{f \cdot B}{d}

用由視差得到的深度,把一個像素反投影成一個有實體尺度的三維點。

逐項來讀。(u, v) 是像素在影像中的行(欄)與列。(c_x, c_y) 是主點——光軸刺穿感光元件的位置,我們在第 1 篇校正時量過;把它減掉,等於把座標重新置中,讓光軸成為原點。f 是同一個以像素為單位的焦距,而 Z 是我們剛從視差 d 經由 Z = f·B/d 算出的深度。結果 (X, Y, Z) 是該點相對於相機、以公尺為單位的真實位置。X 與 Y 的方程式並非新東西——它們不過是第 1 篇針孔投影的反解。正向時,針孔把 (X, Y, Z) 壓成一個像素並丟失了 Z;如今第二個視角把 Z 交還給我們,我們就能反向跑投影,把 X 與 Y 一併還原。

這些方程式背後有一幅美麗的幾何圖像:這整個步驟就是三角測量(triangulation)。左像素定義了一條從左相機射向空間的射線;匹配到的右像素定義了第二條來自右相機的射線。來自兩個已知視點、瞄準同一個實體點的兩條射線,恰好在一處相交——那個交點就是三維點。上面的代數不過是找出兩線交會處的記帳工作。把所有像素的交點疊起來,你就有了稠密的點雲。一句話的食譜:視差圖 + 相機校正 = 透過三角測量得到稠密三維。

def disparity_to_point_cloud(disparity, f, B, cx, cy):
    # Turn a whole disparity map into a list of metric 3D points.
    points = []
    H = len(disparity)
    W = len(disparity[0])
    for v in range(H):            # v = row
        for u in range(W):        # u = column
            d = disparity[v][u]
            if d <= 0:            # 0 / unknown disparity -> infinitely far, skip
                continue
            Z = f * B / d         # depth from the key equation
            X = (u - cx) * Z / f  # back-project, same pinhole as Guide 1
            Y = (v - cy) * Z / f
            points.append((X, Y, Z))
    return points                  # the dense 3D point cloud, in metres
每個有效像素都變成一個有實體尺度的三維點;它們合起來構成點雲。

立體視覺的限制與現代變體

我們以退一步綜觀作結。立體視覺的優點確實珍貴:它便宜(兩台普通相機加上一些運算,不需要奇特硬體),而且它直接從幾何給出有實體尺度的深度——真正的公尺,而非相對的猜測。這就是為什麼機器人、無人機、汽車與 AR 頭戴裝置上都裝著立體相機組。但我們也誠實面對了缺點:在無紋理牆面、重複圖案與遮擋上的匹配失敗;以及那條反比律帶來的不確定性,讓遠處深度即便只有一像素誤差也會飄忽。立體深度估計強大,但不是魔法;懂得它的失敗模式,正是把實務工作者與玩票者區分開來的關鍵。

實務工作者最先伸手去拿的升級是半全域匹配(Semi-Global Matching, SGM)。樸素區塊匹配孤立地決定每個像素,所以有雜訊或曖昧的像素會亂猜。SGM 加入了一個平滑性假設:相鄰像素多半屬於同一個表面,因此它們的視差不該無緣無故地跳動。它沿著貫穿影像的多個方向,對相鄰像素間的視差變化施加懲罰,並找出一張在「匹配良好」與「平滑」之間取得平衡的視差圖。效果非常顯著——雜訊般的斑點安定下來,無紋理區域向匹配良好的鄰居借來合理的數值,而真實的邊緣仍被允許跳變。

另外兩個家族主宰著現代系統。學習式/深度立體網路用一個在龐大資料集上訓練過的神經網路,取代手工設計的匹配成本;它們不比較原始像素貼片,而是比較學到的特徵、甚至學會平滑處理,這讓它們在無紋理與重複區域的表現遠比 SSD 優雅。主動式立體/結構光則直擊失敗的根本原因——缺乏紋理——做法是把紋理投射到場景上:投影機把已知的點陣或條紋圖案(常用紅外線)打到空白表面上,於是即使一面毫無特徵的白牆也突然有東西可匹配。初代微軟 Kinect 正是這樣運作的,這也是為什麼它能描繪出被動立體視覺會束手無策的素面客廳牆。

最後拉一條線索,引向下一篇指南。這裡的一切都倚靠著一個舒適的假設:我們的兩台相機整齊對齊且已校正,所以對應點都住在同一條水平列上。但世界很少給我們這麼整潔的相機組——如果兩個視角來自同一支移動中的手機,或來自完全不同角度拍的兩張照片呢?是什麼幾何把一個場景的任意兩個視角連在一起,即使它們未對齊?這正是下一篇指南要回答的問題,用的是我們在這裡只匆匆一瞥的那條隱藏規則:對極幾何。掌握了它,你就準備好從尋常、雜亂的照片中重建世界了。