手把手教你編寫SGM雙目立體匹配代碼(基於C++,Github同步更新)(五)(視差優化)

千呼萬喚始出來,猶抱琵琶半遮面。
抱歉讓大家久等,最近事兒繁多,導致更新推遲,實在抱歉。

上回說到,路徑聚合後,視差圖英姿初現,我想初學者到這一步大多都會產生一種溢於言表的喜悅之情,這就是編程的魅力,你日以繼夜的在鍵盤上敲打,與世隔絕甚至廢寢忘食,當程序運行,屏幕出現一個還不錯的結果時,你的體內會產生一種叫多巴胺的物質,它讓你興奮和開心,並激勵着你不斷前行。

讓我們再回顧下上篇的結果:

4路徑-1
4路徑-2
8-路徑

前文鏈接
手把手教你編寫SGM雙目立體匹配代碼(基於C++,Github同步更新)(一)
手把手教你編寫SGM雙目立體匹配代碼(基於C++,Github同步更新)(二)
手把手教你編寫SGM雙目立體匹配代碼(基於C++,Github同步更新)(三)(代價聚合)
手把手教你編寫SGM雙目立體匹配代碼(基於C++,Github同步更新)(四)(代價聚合2)

本篇算是進入SGM編碼教學的完結篇,即立體匹配的最後一步:視差優化(Disparity Refine)。優化的含義大家都懂,爲了得到更好的視差圖。

怎樣算更好?如何能更好?本篇就是解答這兩個問題。

1 優化目的

  • 1. 提高精度
  • 2. 剔除錯誤
  • 3. 填補空洞

提高精度:前面的視差計算步驟中,我們選擇最小代價值對應的視差值,它是一個整數值(整數值我們纔能有離散化的視差空間WHDWHD),即整像素級精度,而實際應用中整像素精度基本無法滿足需求,必須優化到子像素精度纔有意義。

剔除錯誤:即剔除錯誤的視差值,比如a像素本應和b像素是同名點,而結果卻是a像素和c像素是同名點,這就是錯誤的視差值。造成錯誤匹配的原因有遮擋、弱紋理等,所謂錯誤是永恆的,完美是不存在的。

填補空洞:剔除錯誤匹配後,被剔除的像素會造成無效值空洞,如何填補使視差圖更加完整也是優化所研究的內容。

限於篇幅,本篇只講前兩條,第三條我將在下一篇更新。

2 優化手段

  • 1. 子像素擬合
  • 2. 一致性檢查

以上兩個幾乎是所有立體匹配算法必執行的策略。子像素擬合將整像素精度提高到子像素精度,而一致性檢查可以說是剔除錯誤匹配的不二選擇。

我將教大家編碼實現這兩個策略。

2.1 子像素擬合

看過我之前博客的同學們應該對這個圖還有點印象,左邊表示視差爲18時代價最小,那麼18就是我們得到的整像素視差值,而右邊則是把最小代價左右兩邊的代價值也記錄下來,3個代價值做一個一元二次曲線擬合,曲線的極值點橫座標就是視差值的子像素位置。

很簡單對不對,一元二次擬合誰不會!

一元二次求解圖

我們來看代碼實現(只貼子像素部分):

// 最優視差best_disparity前一個視差的代價值cost_1,後一個視差的代價值cost_2
const sint32 idx_1 = best_disparity - 1 - min_disparity;
const sint32 idx_2 = best_disparity + 1 - min_disparity;
const uint16 cost_1 = cost_local[idx_1];
const uint16 cost_2 = cost_local[idx_2];
// 解一元二次曲線極值
const uint16 denom = std::max(1, cost_1 + cost_2 - 2 * min_cost);
disparity[i * width + j] = best_disparity + (cost_1 - cost_2) / (denom * 2.0f);

嗯,確實很簡單!

2.2 一致性檢查

我們先了解下一致性檢查是什麼東東。

我們在立體匹配裏會區分左右影像,這是模擬人眼立體,左視圖對應左眼,右視圖對應右眼,對人來說左是左右是右,可不能搞反。但對計算機來說,無所謂,左可以是右,右可以是左,我纔不管是否是合理,別讓我死機就行。

確實,我們讓左右對調,也可以形成雙目立體,兩個視圖也有重疊區,只不過重疊區不在中間,在兩邊,但管他呢,只要告訴我怎麼搜同名點就行。

正常核線對,重疊區在中間
左右對調後,重疊區在兩邊

一致性檢查就是:把左右影像位置對調,再計算一個右影像視差圖,對照兩個視差圖來看同名點對是否能夠相互匹配成功。我這裏有以下兩個描述,你們看哪個能夠理解。

  1. 對調前,左影像像素aa匹配右影像像素bb;則對調後,bb也匹配aa爲一致,否則爲不一致(比如對調後bb匹配cc)。
  2. 對調前,左影像像素aa的視差爲dd;則對調後右影像像素ada-d的視差爲dd爲一致,否則爲不一致。

一致性檢查的一般性操作步驟是

  1. 獲取左右視差圖。
  2. 對左視差圖的每個像素aa,計算出同名點在右視差圖中的像素位置bb
  3. 判斷aabb的視差值之差的絕對值是否小於一個閾值(通常爲1個像素)。
  4. 如果超過閾值,則一致性檢查不通過,把對應位置的視差變爲無效值。

借用下SGM作者老爺子的圖,這裏的bb就是左視圖,mm是右視圖,ppqq爲同名點對。

一致性檢查有兩種策略,一種是內部型,一種是外部型。

  • 1. 內部型檢查
  • 2. 外部型檢查

我會實現內部型(比較有意思),描述外部型(沒技術含量)。

2.2.1 內部型檢查

內部型就是直接通過左影像的代價數組,來推算右影像的代價數組,從而計算右影像的視差圖。所以你只用代價聚合一次就可以做一致性檢查。

聽起來很爽,怎麼辦到的?

祕訣就是:右影像(i,j)(i,j)視差爲dd的代價 = 左影像(i,j+d)(i,j+d)視差爲dd的代價

能理解不,就是對於右影像的像素(i,j)(i,j),根據視差值dd可算出左影像的對應像素位置爲(i,j+d)(i,j+d),然後把左影像(i,j+d)(i,j+d)同樣視差值dd下的代價值取出來賦給(i,j,d)(i,j,d)

似乎有點繞,但是大家讀幾遍應該能理解。不理解的話肯定是博主描述的不夠清晰,歡迎和我交流。

根據此祕訣,我們可以將右影像每個像素的所有候選視差dd的代價值Cost(i,j,d)Cost(i,j,d)都得到,進而尋找最小代價值對應的視差,並做子像素優化,得到右影像視差圖。

右影像視差圖計算代碼

// ---逐像素計算最優視差
// 通過左影像的代價,獲取右影像的代價
// 右cost(xr,yr,d) = 左cost(xr+d,yl,d)
for (sint32 i = 0; i < height; i++) {
    for (sint32 j = 0; j < width; j++) {
        uint16 min_cost = UINT16_MAX;
        sint32 best_disparity = 0;

        // ---統計候選視差下的代價值
    	for (sint32 d = min_disparity; d < max_disparity; d++) {
            const sint32 d_idx = d - min_disparity;
    		const sint32 col_left = j + d;
    		if (col_left >= 0 && col_left < width) {
                const auto& cost = cost_local[d_idx] = cost_ptr[i * width * disp_range + col_left * disp_range + d_idx];
                if (min_cost > cost) {
                    min_cost = cost;
                    best_disparity = d;
                }
    		}
            else {
                cost_local[d_idx] = UINT16_MAX;
            }
        }
    }
}

// ---子像素擬合
if (best_disparity == min_disparity || best_disparity == max_disparity - 1) {
    disparity[i * width + j] = Invalid_Float;
    continue;
}
// 最優視差前一個視差的代價值cost_1,後一個視差的代價值cost_2
const sint32 idx_1 = best_disparity - 1 - min_disparity;
const sint32 idx_2 = best_disparity + 1 - min_disparity;
const uint16 cost_1 = cost_local[idx_1];
const uint16 cost_2 = cost_local[idx_2];
// 解一元二次曲線極值
const uint16 denom = std::max(1, cost_1 + cost_2 - 2 * min_cost);
disparity[i * width + j] = static_cast<float32>(best_disparity) + static_cast<float32>(cost_1 - cost_2 ) / (denom * 2.0f);

計算出右視差圖後,執行一致性檢查:根據左影像視差圖可以算出像素(ileft,jleft)(i_{left},j_{left})在右影像中的匹配像素是(ileft,jleftd)(i_{left},j_{left}-d),如果(ileft,jleftd)(i_{left},j_{left}-d)的視差剛好也近似等於dd,則滿足一致性。

一致性檢查代碼

// ---左右一致性檢查
for (int i = 0; i < height; i++) {
    for (int j = 0; j < width; j++) {
        
        // 左影像視差值
    	auto& disp = disp_left_[i * width + j];

        // 根據視差值找到右影像上對應的同名像素
    	const auto col_right = static_cast<sint32>(j - disp + 0.5);
        
    	if(col_right >= 0 && col_right < width) {

            // 右影像上同名像素的視差值
            const auto& disp_r = disp_right_[i * width + col_right];
            
    		// 判斷兩個視差值是否一致(差值在閾值內)
    		if (abs(disp - disp_r) > threshold) {
                // 左右不一致,把視差值變爲無效
                disp = Invalid_Float;
            }
        }
        else{
            // 通過視差值在右影像上找不到同名像素(超出影像範圍)
            disp = Invalid_Float;
        }
    }
}

這部分代碼還是比較簡單的,理解起來比較容易。

2.2.2 外部型檢查

相比內部型檢查,外部型檢查是比較笨的辦法,就是在算法輸入時把左右圖像數據對調,再執行一次完整的立體匹配,得到右影像視差圖,一致性檢查則是採用同樣的策略。

這裏需要注意的是,左右對調後,視差在意義上和左影像是相反的,而立體匹配算法的設定是:視差=左減右,如果只做對調(就是簡單的把右影像數據作爲左影像數據傳進算法),是得不到正確結果的,因爲對調後重疊區在兩邊,不符合算法的設定,所以這裏我一般會在對調後,把左右影像的像素來個水平鏡像翻轉,這樣兩張影像的重疊區到了中間,視差就等於左減右了。

外部型檢查需要執行兩次完整的匹配流程,所以時間效率不如內部型檢查。

2.3 中值濾波

中值濾波在立體匹配中使用的還挺廣泛的,作爲一個平滑算法,它主要是用來剔除視差圖中的一些孤立的離羣外點,同時還能起到填補小洞的作用。這個部分我就不細說了,想必大家對中值濾波都不會太陌生,我會在實驗中展示中值濾波的效果。

3 實驗

枯燥而或許有趣的理論部分介紹完畢,我們喜迎實驗環節。

實驗1 子像素擬合對比圖

取視差圖某一行的數據,上圖爲擬合前,下圖爲擬合後,可看到整像素的階梯形被子像素擬合優化

實驗2 一致性檢查效果圖

一致性檢查前
一致性檢查後

圖中我們可以看到,非重疊區、遮擋區的匹配錯誤已經大部分都被剔除了,當然也讓視差圖有了很多黑色的無效區。

一致性檢查後
一致性檢查+中值濾波

通過中值濾波,進一步對噪點進行剔除,且填補了一些小的無效區域,效果不錯。

夜已深,所謂春困秋乏,博主也該歇息了,代碼已同步於Github/GemiGlobalMatching,大家可自行下載,點擊右上角的star,有更新會實時通知到你的個人中心!

各位拜拜!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章