
簡介計算機視覺中從二維圖像恢復三維深度信息是諸多應用的基礎而立體匹配正是連接雙目圖像與深度數據的關鍵橋梁。通過計算左右相機圖像中對應像素的水平偏移量即視差可以反推出場景深度進而支撐三維重建、障礙物檢測與智能測距等任務。本文從最基礎的窗口匹配代價函數出發逐層剖析SAD、SSD與ZNCC三類經典算法的數學原理與代碼實現并對比其在不同光照和噪聲條件下的魯棒性。隨后工程上廣泛應用的OpenCV BM與SGBM算法被引入重點解釋半全局能量優化和參數調優策略。無論你是入門學生還是從事雙目視覺開發的工程師掌握這些基礎算法都能為后續深度估計和點云生成提供扎實的認知底座幫助在實際場景中快速選型與排障。 如果你接觸過雙目視覺一定繞不開立體匹配。它的任務說簡單也簡單左右兩個相機拍攝同一塊場景找出左圖里每個像素在右圖中的對應點這個對應關系產生的橫向偏移就是視差。有了視差才談得上深度計算、三維重建、障礙物檢測和測距。最近我用 Python 把幾個最基礎的立體匹配算法從頭到尾實現并對比了一遍包括 SAD、SSD、ZNCC 這三個基于窗口代價函數的自由實現以及 OpenCV 直接可用的 BM、SGBM。這篇文章就把實現過程、關鍵原理、調參經驗和踩坑記錄完整整理出來適合剛入門雙目視覺的學生、準備做雙目測距的工程師以及想搞清楚視差圖背后原理的開發者。先說結論如果你想快速拿到一張可用的視差圖直接用 OpenCV 的 SGBM 是最省力的方案但如果你想真正理解立體匹配在做什么手寫一遍 SAD 或 ZNCC 是繞不開的路。我建議按這個順序學SAD 理解窗口匹配的直覺ZNCC 理解光照魯棒性再看 BM 和 SGBM 如何把這些思想工程化。下面按這個思路展開。1. 立體匹配到底在解決什么問題在動手寫代碼之前先把問題的邊界框清楚。立體匹配不是單純在兩張圖上找相似區域它背后是一個完整的成像幾何模型。只有知道我們假設了什么、化簡了什么后面看算法和調參才不會一頭霧水。1.1 視差和深度一個反比關系雙目相機的核心是兩臺相距一定距離的相機這個距離叫基線baseline。由于兩臺相機位置不同同一個三維空間點在左右圖像里的成像位置不會一致這個位置差就是視差disparity通常記作 d x_left - x_right。在理想情況下匹配點只存在水平方向的偏移垂直方向的偏移為 0也就是極線校正后的標準雙目形態。深度 Z 和視差 d 的關系是反比Z f * B / d其中 f 是焦距B 是基線長度。這個公式告訴我們一個直觀結論物體離相機越近視差越大越遠視差越小逼近無窮遠時視差趨近 0。所以立體匹配輸出的視差圖本質上是一張蘊含深度信息的中間產物。拿到它之后配合相機內參就能生成三維點云這也是很多三維重建項目的基礎。理解了這一層你就明白為什么匹配必須做對——某一個像素的視差錯了對應的深度就會錯得離譜后續的點云和距離估計全部白費。1.2 經典四步框架所有算法都能裝進去2002 年 Scharstein 和 Szeliski 在立體匹配綜述里提出了一個經典框架到現在依然適用。幾乎所有立體匹配算法都可以拆進這四個步驟匹配代價計算對每個像素、在每一個候選視差下計算一個代價代價越小表示越相似。SAD、SSD、ZNCC 就是這一層最典型的代價函數。代價聚合單像素的代價對噪聲和弱紋理區域太敏感通常會在一個窗口或區域內累加、加權讓匹配結果更穩定。視差計算/優化常見的做法是 WTAWinner Takes All也就是直接取最小代價對應的視差作為結果SGBM 這類算法則會在代價基礎上加平滑約束進行能量函數優化。視差細化左右一致性檢查、亞像素插值、中值濾波、斑點濾波等用于消除誤匹配和孤立噪點。這套框架的好處是你可以把五種方法放在同一個骨架上對比SAD、SSD、ZNCC 停在第一二步BM 是工程化的局部匹配SGBM 則在第三步引入了全局能量優化。理解了框架后面講每個算法時你就知道它卡在哪個環節、優勢和短板在哪里。1.3 環境準備、圖像校正與實驗數據代碼層面只需要三個東西Python 3.8 或更高版本、OpenCV、NumPy。OpenCV 提供了 BM 和 SGBM 的完整實現手寫部分只用 NumPy 就夠。import cv2 import numpy as np left_gray cv2.imread(left.png, cv2.IMREAD_GRAYSCALE) right_gray cv2.imread(right.png, cv2.IMREAD_GRAYSCALE) print(left_gray.shape, right_gray.shape)這里有一個容易被新手忽略的前提輸入圖像必須是極線校正后的雙目圖像對。極線校正rectification讓同名點只出現在同一水平線上這樣匹配搜索才能從二維平面搜索變成一維水平掃描。如果圖像沒有校正你在代碼里看到的正確匹配會全部錯位這不是算法的鍋是數據預處理沒做好。工程上可以用cv2.stereoRectify和cv2.initUndistortRectifyMap配合標定參數做校正本文的討論默認輸入已經是校正好的圖像。測試數據建議用 Middlebury 數據集里的標準圖像對比如 Tsukuba、Cones這些圖都是公開的而且帶有 ground truth方便你評估算法好壞。自己拍的雙目圖也可以但務必確認已經校正。2. SSD、SAD、ZNCC三個基礎代價函數從數學到代碼這一節是整個主題的基石。SAD、SSD、ZNCC 都是局部立體匹配方法核心思路非常一致取左圖某個像素周圍的窗口在右圖同一行上、候選視差范圍內滑動同樣大小的窗口計算兩個窗口之間的相似度最相似的位置對應的偏移量就是這個像素的視差。區別只在于“相似度怎么定義”。2.1 SAD窗口內絕對差求和SADSum of Absolute Differences是最直觀的代價函數。對左右兩個窗口內的所有像素做差取絕對值再累加cost(d) Σ | L(xi, yj) - R(xi-d, yj) |這里的窗口大小是奇數比如 5x5i 和 j 遍歷窗口內坐標。代價越小說明兩個窗口內容越接近。這個操作的本質是假設窗口內的像素在左右圖中顏色一致偏差主要來自噪聲和輕微視差變化絕對值和可以穩定量化這種偏差。手寫實現如下這個版本刻意保留了三層循環方便對照原理def sad(left, right, max_disp, win_size5): h, w left.shape half win_size // 2 disp np.zeros((h, w), dtypenp.float32) for y in range(half, h - half): for x in range(half, w - half): best_d, best_cost 0, float(inf) for d in range(max_disp): if x - d half: break l_win left[y-half:yhalf1, x-half:xhalf1].astype(np.int16) r_win right[y-half:yhalf1, x-half-d:xhalf1-d].astype(np.int16) cost np.sum(np.abs(l_win - r_win)) if cost best_cost: best_cost cost best_d d disp[y, x] best_d return disp注意幾個細節左右圖要轉成 int16 再做減法避免 uint8 溢出搜索時如果x - d小于窗口半寬說明窗口已經超出圖像邊界直接結束當前候選視差循環最終每個像素取代價最小的視差。這個版本不是性能最優的但它是理解算法的最佳版本。實測時你會發現當左右圖像亮度一致、噪聲不大時SAD 的結果已經相當可用尤其是在紋理豐富的區域邊界的輪廓能比較清晰地還原出來。2.2 SSD平方差求和一個容易踩的取舍SSDSum of Squared Differences和 SAD 幾乎一樣只是把絕對差換成了平方差cost(d) Σ ( L(xi, yj) - R(xi-d, yj) )^2在代碼里只改一行把np.abs(l_win - r_win)換成(l_win - r_win) ** 2??雌饋聿顒e很小但數學上行為很不一樣平方會放大較大的像素差異因此在高紋理區域SSD 的匹配區分度會比 SAD 更高正確匹配和錯誤匹配之間的代價差距更明顯。但也正因為平方放大了異常值SSD 對噪聲、高光反射、左右圖曝光差異非常敏感。只要窗口里有幾個像素因為反光或者傳感器噪聲出現較大差異代價就會被這幾個點主導導致錯誤匹配。我在實驗里發現亮度和噪聲水平接近的圖像對SSD 和 SAD 差別不大一旦圖像里有高光區域SSD 的視差圖會出現比 SAD 更多的毛刺。所以如果你的數據源不太干凈SAD 的魯棒性反而比 SSD 好。這個對比值得記在心里更強的差異放大不代表更好的匹配它同時放大了噪聲。2.3 ZNCC歸一化之后光照變化不再是災難ZNCCZero-mean Normalized Cross Correlation和前面兩個思路完全不同。它不是算差的累積而是算兩個窗口的相關系數ncc(d) Σ (L - mean_L) * (R - mean_R) / sqrt( Σ(L - mean_L)^2 * Σ(R - mean_R)^2 )實現時先把窗口內像素減去窗口均值再做歸一化相關。這樣做的意義是什么減去均值等價于消除了窗口整體的亮度偏移除以標準差則消除了對比度縮放。也就是說左右圖即使整體亮度不一致、曝光有明顯差異ZNCC 依然能測出兩個窗口的“形狀”是否相似。在代碼里ZNCC 要尋找的是最大的相關系數而不是最小代價。這一點最容易搞反具體實現如下def zncc(left, right, max_disp, win_size5): h, w left.shape half win_size // 2 disp np.zeros((h, w), dtypenp.float32) for y in range(half, h - half): for x in range(half, w - half): best_d, best_score 0, -1.0 l_win left[y-half:yhalf1, x-half:xhalf1].astype(np.float32) l_mean l_win.mean() l_norm l_win - l_mean for d in range(max_disp): if x - d half: break r_win right[y-half:yhalf1, x-half-d:xhalf1-d].astype(np.float32) r_mean r_win.mean() r_norm r_win - r_mean num np.sum(l_norm * r_norm) den np.sqrt(np.sum(l_norm**2) * np.sum(r_norm**2)) score num / (den 1e-6) if score best_score: best_score score best_d d disp[y, x] best_d return disp注意den 1e-6是為了防止窗口內像素完全相同時除零。ZNCC 的代價函數是三個里最抗光照變化的代價是計算量大很多。每個候選視差都要重復計算窗口均值、方差和乘積和純 Python 實現下速度幾乎是 SAD 的三到五倍。所以 ZNCC 適合在左右圖亮度差異明顯、或者多相機曝光不一致的場景使用比如室外自然光變化的環境。2.4 三個算法的實際效果對比我用標準測試圖對三個算法做了對比。先用 5x5 窗口、max_disp 設為 64在紋理豐富的區域三者都能得到基本合理的視差圖但在左右圖亮度差異明顯的模擬光照下SAD 和 SSD 的錯誤區域明顯增多ZNCC 基本不受影響。下面是幾個關鍵維度的總結指標SADSSDZNCC匹配標準最小化代價最小化代價最大化相關系數抗光照變化較弱較弱強抗噪聲中等較差較好計算量較小較小大實現復雜度低低中典型適用場景亮度一致的圖像對高紋理區域曝光不一致或光照變化一個我常說的類比SAD 像拿直尺量差距SSD 像把差距平方后再量ZNCC 則是把兩段波形先對齊到同一水平線再比較形狀。量差距的方式對絕對亮度敏感比較形狀的方式對亮度不敏感這就是 ZNCC 在復雜光照下更穩的根本原因。3. BM和SGBM工程級匹配算法的原理與調用手寫代價函數的過程能幫你建立直覺但真實項目里幾乎不會用純 Python 三層循環去跑匹配性能完全扛不住。OpenCV 提供的 BM 和 SGBM 才是工程主角。不過它們也不是黑盒理解了參數背后的意義你才能把它調好。3.1 BM塊匹配的工程化加速BMBlock Matching的思路和前面說的窗口匹配一脈相承。它把圖像劃分成塊在極線方向上滑動用 SAD 之類的代價函數計算匹配代價再用 WTA 選出視差。OpenCV 的StereoBM實現做了大量加速比如積分圖、紋理過濾所以它能在 CPU 上達到很高的幀率適合實時場景做初步深度估計。調用方式非常簡潔bm cv2.StereoBM_create(numDisparities80, blockSize15) disp_bm bm.compute(left_gray, right_gray).astype(np.float32) / 16.0注意numDisparities必須是 16 的倍數blockSize必須是奇數這兩個是硬性約束不符合會直接報錯或者行為異常。StereoBM的輸出也是定點格式需要除以 16 才能得到浮點視差細節我在后面展開。BM 的優勢是快缺點是視差圖質量相對一般弱紋理區域容易產生大面積空洞深度不連續處也容易出現“胖邊界”現象。但在資源受限的嵌入式設備上它往往是唯一現實的選擇。3.2 SGBM從局部到半全局的能量優化SGBM 全稱 Semi-Global Block Matching核心思想來自 Hirschmuller 的經典論文。它不再滿足于對每個像素獨立做 WTA而是把整個視差圖看作一個能量函數的解E(D) Σ C(p, d_p) Σ P1 * [|d_p - d_q| 1] Σ P2 * [|d_p - d_q| 1]第一項是數據項也就是像素 p 在視差 d_p 下的匹配代價第二三項是平滑項用于懲罰相鄰像素 p 和 q 之間視差不連續的情況。如果視差只差 1懲罰 P1如果視差跳變大于 1懲罰 P2。P2 要顯著大于 P1因為深度不連續處通常意味著物體邊界邊界上的視差跳變是合理的但也不能一點代價都不付否則會出現大量條紋狀誤匹配。“半全局”這三個字是理解 SGBM 的關鍵。全局優化在整個二維平面上求解能量函數理論上效果最好但計算復雜度不可接受。SGBM 的折中做法是沿多個一維方向通常是 8 或 16 個方向做動態規劃的代價累積再把所有方向的累積代價加起來。一維路徑上的動態規劃很高效多個方向疊加后又能在一定程度上模擬二維平滑約束這就是它在效果和性能之間取得平衡的原因。3.3 代碼調用和參數細節OpenCV 里對應的是StereoSGBM創建方式如下sgbm cv2.StereoSGBM_create( minDisparity0, numDisparities80, blockSize11, P18 * 3 * blockSize ** 2, P232 * 3 * blockSize ** 2, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange32, modecv2.STEREO_SGBM_MODE_SGBM_3WAY ) disp_sgbm sgbm.compute(left_gray, right_gray).astype(np.float32) / 16.0參數說明參數作用備注numDisparities最大視差范圍必須是 16 的倍數動態范圍約為 16 的整數倍blockSize匹配窗口邊長必須為奇數常見 3 到 21P1視差變化 1 時的小懲罰值越大視差圖越平滑但細節越弱P2視差跳變時的大懲罰通常取 P1 的 4 到 10 倍disp12MaxDiff左右一致性檢查閾值超限標記為無效視差uniquenessRatio最小代價與次小代價的差異比例低于閾值視為匹配不唯一speckleWindowSize斑點濾波窗口濾除孤立小區域spearackRange斑點內視差波動范圍過大容易誤濾真實邊緣mode算法模式SGBM_3WAY 更快HH 更精確但更慢官方示例里的P18 * 3 * blockSize^2、P232 * 3 * blockSize^2是很好的起點后面調參時可以在這個基礎上縮放。3.4 后處理和可視化SGBM 和 BM 的compute輸出類型是int16里面存的其實是乘以 16 后的定點視差。為什么要乘 16因為 OpenCV 想保留四位小數精度的亞像素信息又不使用浮點存儲所以先做定點放大。拿到輸出后必須除以 16否則視差圖數值看起來全都是放大的后續點云計算也會全部出錯??梢暬霸僮鲆淮螝w一化def disp_to_visual(disp): return cv2.normalize(disp, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) cv2.imshow(BM, disp_to_visual(disp_bm)) cv2.imshow(SGBM, disp_to_visual(disp_sgbm))如果視差圖里有大量負值那些是無效視差歸一化時應先屏蔽否則會把顯示范圍壓扁disp_valid disp.copy() disp_valid[disp_valid 0] 04. 跑實驗時踩過的坑與調參心得基礎算法不難真正讓新手崩潰的是那些藏在細節里的坑。這一節集中記錄我實際跑實驗時反復踩過、又花了不少時間才繞過的問題。直接照做可以幫你少走很多彎路。4.1 極線校正是匹配的地基第一個大坑拿沒有校正過的雙目圖像直接喂給 BM 或 SGBM結果十有八九是一團糟。匹配算法為了效率假設同名點在同一水平線上搜索時才敢只在水平方向掃描。沒有校正的圖像同名點有垂直偏移水平搜索根本找不到正確匹配。這個問題在自拍圖像里特別常見很多人以為雙目匹配算法“能自動處理”實際上不行。正確流程是先標定相機、畸變校正、極線校正再把校正后的圖像輸入匹配算法。哪怕你用cv2.StereoBM_create這樣的高抽像 API 也一樣預處理這一關不過后面全是白搭。4.2 numDisparities和blockSize的硬性約束numDisparities必須是 16 的倍數不是的話 OpenCV 計算時會自動調整或者直接報錯。實際設值時我習慣根據目標深度范圍先反推如果基線和焦距已知最小可測距離對應最大視差那就把numDisparities設成剛好覆蓋需要的最大視差再取整到 16 的倍數。blockSize必須是奇數因為窗口要對稱。窗口過小噪聲影響明顯弱紋理區域全是洞窗口過大視差圖過度平滑物體邊緣會被磨掉。從 5x5 開始往上試通常比較合理我常用 9 到 15 之間的值。4.3 SGBM參數調試順序與常見問題SGBM 參數多一起調很難定位問題。我的習慣是按順序來先調numDisparities和blockSize讓整體輪廓出來再調P1、P2控制平滑度最后用uniquenessRatio和斑點參數清理噪點。實際中常見的現象和應對現象可能原因調整策略大片橫條紋或空洞P1、P2 太小平滑約束不足增大 P1/P2物體邊緣被磨平P2 過大減小 P2或減小 P2 與 P1 的比值孤立亮點、白色噪點遮擋或誤匹配增大 speckleWindowSize檢查 disp12MaxDiff弱紋理區域全是洞blockSize 過小增大 blockSize視差整體偏小或偏大numDisparities 沒覆蓋到位重新估計視差范圍還有一個很多教程沒講清的點modecv2.STEREO_SGBM_MODE_HH在理論上能做更復雜的優化效果通常更好但耗時是SGBM_MODE_SGBM_3WAY的幾倍。如果你的場景不是離線處理先用SGBM_3WAY它速度更快參數調整到位的程度下質量差距沒那么夸張。4.4 手寫算法太慢的優化思路純 Python 的 SAD、SSD、ZNCC 實現跑標準測試圖怎么說呢速度慢到可以泡杯咖啡。這不是算法本身的問題是解釋型語言三層 for 循環的原罪。如果你只是為了理解原理建議縮小輸入圖像或者把max_disp限制在較小范圍比如 32保證實驗能跑完。如果確實需要用 Python 實現但有性能要求幾個方向用 NumPy 向量化窗口滑動用膨脹后的 shift 技巧批量計算 SAD或者用scipy.ndimage做滑動窗口聚合。更進一步可以用 Numba 對循環做 JIT 加速1 到 2 個數量級的提升是有的。OpenCV 自己的 BM 已經高度優化如果你不是要復現論文而是做應用直接用庫就好不要重復造輪子。4.5 無效視差和后處理是提質的最后一步SGBM 里disp12MaxDiff是左右一致性檢查的閾值。正常做法是從左圖算一次視差從右圖也算一次視差同名點互查如果兩個方向得到的視差差值超過閾值就認為這個點是誤匹配或遮擋點直接置為無效。disp12MaxDiff1時左右一致性差超過 1 就判為無效比較嚴格如果無效區域太多可以放寬到 2 或 3。斑點濾波參數speckleWindowSize和speckleRange也很實用。前者指定多大的孤立區域算斑點后者指定斑點內部允許的視差波動范圍。比如speckleWindowSize100表示面積小于 100 像素的區域如果跟周圍視差不一致就會被濾掉并填充周圍的值。我最初直接復制網上參數時發現真實裂縫邊緣也被誤濾了后來把speckleRange調小才保住邊緣這類參數一定要結合自己的數據微調。5. 五個算法放在一起怎么選五種算法的核心思想已經講完最后落到選型。選型沒有絕對的對錯關鍵是匹配場景和資源約束。5.1 橫向對比指標SADSSDZNCCBMSGBM匹配策略局部窗口局部窗口局部窗口局部塊半全局優化速度慢手寫慢手寫最慢手寫快中等精度一般一般較好中等高抗光照變化弱弱強中中邊界保持一般一般一般一般好實現方式手寫手寫手寫OpenCVOpenCV典型場景教學理解教學理解光照復雜研究實時初步估計離線/高精度前面三列是“學習向”算法后面兩列是“工程向”算法。從學習到工程本質上是同一套匹配思想的不同實現深度。5.2 按場景選型實時視覺定位、機器人避障這類對幀率敏感的場合BM 是首選。它速度快、資源占用低雖然視差圖質量一般但配合后續濾波和置信度判斷已經足夠支撐很多決策任務。在嵌入式設備上我通常還會配合降采樣圖像分辨率降到 640 或更低再把numDisparities限制在 64 以內幀率可以跑到非常理想。離線三維重建、工業尺寸測量、高精度深度估計首選 SGBM。它需要更多計算時間但換來的視差圖平滑度和邊緣保持能力對重建質量影響很大。尤其是物體輪廓要求嚴格的場景SGBM 的邊界表現明顯優于 BM。光照變化強烈的室外場景ZNCC 這類歸一化相關系數的優勢最大但手寫性能差。你可以在 BM 或 SGBM 基礎上自己做預處理比如直方圖均衡化也能部分緩解光照問題。真正的工業場景里我一般建議在采集端就盡量統一曝光比算法端硬扛要可靠得多。5.3 做完基礎算法之后還可以干什么如果你已經把這五個算法跑通了下一步的擴展方向很明確。一是亞像素細化。WTA 得到的視差是整數級用代價曲線在最小值附近做拋物線擬合可以插值出亞像素精度。對三維重建來說亞像素細化帶來的深度精度提升非常可觀。二是置信度評估。匹配代價曲線的峰值尖銳程度可以反映這個視差猜測的置信度。代價曲線越尖銳說明匹配越唯一代價曲線非常平緩說明這個像素可能落在弱紋理區域對應的匹配結果要打問號。給視差圖附帶置信度是后續濾波和融合的好依據。三是結合相機內參生成三維點云。有了視差圖、焦距、基線和主點每對匹配點都能反投影出三維坐標。這一步做完你的立體匹配才算真正接入三維視覺的完整鏈路。四是往深度學習的立體匹配方向走。現在基于 cost volume 的深度學習方法在精度上已經明顯超過傳統算法但它依然沿用“匹配代價計算、代價聚合、視差優化”這個框架只是把這些模塊換成了神經網絡。傳統算法的理解積累在看深度學習論文時會發揮很大作用。最后談一點個人體會。立體匹配知識點乍看很散但它其實是二維圖像走向三維空間的一條必經之路。我自己的學習建議是先手寫一遍 SAD哪怕跑得慢也要把窗口匹配、代價最小化的直覺建立起來然后用 OpenCV 的 SGBM 把參數一個一個試過去觀察每個參數對結果的影響最后回頭對照四步框架你會發現所有算法都能裝進同一個骨架里。我自己踩過最深的坑是過早迷戀高精度算法反而忽略了基礎代價函數和預處理的作用。先把基礎打牢再談復雜的優化這條路看起來慢實際最快。本文還有配套的精品資源點擊獲取