千呼萬喚始出來,猶抱琵琶半遮面。
抱歉讓大家久等,最近事兒繁多,導致更新推遲,實在抱歉。
上回說到,路徑聚合後,視差圖英姿初現,我想初學者到這一步大多都會產生一種溢於言表的喜悅之情,這就是編程的魅力,你日以繼夜的在鍵盤上敲打,與世隔絕甚至廢寢忘食,當程序運行,屏幕出現一個還不錯的結果時,你的體內會產生一種叫多巴胺的物質,它讓你興奮和開心,並激勵着你不斷前行。
讓我們再回顧下上篇的結果:
前文鏈接
手把手教你編寫SGM雙目立體匹配代碼(基於C++,Github同步更新)(一)
手把手教你編寫SGM雙目立體匹配代碼(基於C++,Github同步更新)(二)
手把手教你編寫SGM雙目立體匹配代碼(基於C++,Github同步更新)(三)(代價聚合)
手把手教你編寫SGM雙目立體匹配代碼(基於C++,Github同步更新)(四)(代價聚合2)
本篇算是進入SGM編碼教學的完結篇,即立體匹配的最後一步:視差優化(Disparity Refine)。優化的含義大家都懂,爲了得到更好的視差圖。
怎樣算更好?如何能更好?本篇就是解答這兩個問題。
1 優化目的
- 1. 提高精度
- 2. 剔除錯誤
- 3. 填補空洞
提高精度:前面的視差計算步驟中,我們選擇最小代價值對應的視差值,它是一個整數值(整數值我們纔能有離散化的視差空間),即整像素級精度,而實際應用中整像素精度基本無法滿足需求,必須優化到子像素精度纔有意義。
剔除錯誤:即剔除錯誤的視差值,比如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個像素)。
- 如果超過閾值,則一致性檢查不通過,把對應位置的視差變爲無效值。
借用下SGM作者老爺子的圖,這裏的就是左視圖,是右視圖,和爲同名點對。
一致性檢查有兩種策略,一種是內部型,一種是外部型。
- 1. 內部型檢查
- 2. 外部型檢查
我會實現內部型(比較有意思),描述外部型(沒技術含量)。
2.2.1 內部型檢查
內部型就是直接通過左影像的代價數組,來推算右影像的代價數組,從而計算右影像的視差圖。所以你只用代價聚合一次就可以做一致性檢查。
聽起來很爽,怎麼辦到的?
祕訣就是:右影像視差爲的代價 = 左影像視差爲的代價
能理解不,就是對於右影像的像素,根據視差值可算出左影像的對應像素位置爲,然後把左影像同樣視差值下的代價值取出來賦給。
似乎有點繞,但是大家讀幾遍應該能理解。不理解的話肯定是博主描述的不夠清晰,歡迎和我交流。
根據此祕訣,我們可以將右影像每個像素的所有候選視差的代價值都得到,進而尋找最小代價值對應的視差,並做子像素優化,得到右影像視差圖。
【右影像視差圖計算代碼】
// ---逐像素計算最優視差
// 通過左影像的代價,獲取右影像的代價
// 右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);
計算出右視差圖後,執行一致性檢查:根據左影像視差圖可以算出像素在右影像中的匹配像素是,如果的視差剛好也近似等於,則滿足一致性。
【一致性檢查代碼】
// ---左右一致性檢查
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,有更新會實時通知到你的個人中心!
各位拜拜!