千呼萬喚始出來,猶抱琵琶半遮面。
抱歉讓大家久等,最近事兒繁多,導致更新推遲,實在抱歉。
碼上教學系列
【恆叨立碼】【碼上教學】【立體匹配系列】經典SGM:(1)框架與類設計
【恆叨立碼】【碼上教學】【立體匹配系列】經典SGM:(2)代價計算
【恆叨立碼】【碼上教學】【立體匹配系列】經典SGM:(3)代價聚合
【恆叨立碼】【碼上教學】【立體匹配系列】經典SGM:(4)代價聚合2
【恆叨立碼】【碼上教學】【立體匹配系列】經典SGM:(5)視差優化
【恆叨立碼】【碼上教學】【立體匹配系列】經典SGM:(6)視差填充
【恆叨立碼】【碼上教學】【立體匹配系列】經典SGM:(7)弱紋理優化
代碼已同步於Github開源項目:Github/GemiGlobalMatching
上回說到,路徑聚合後,視差圖英姿初現,我想初學者到這一步大多都會產生一種溢於言表的喜悅之情,這就是編程的魅力,你日以繼夜的在鍵盤上敲打,與世隔絕甚至廢寢忘食,當程序運行,屏幕出現一個還不錯的結果時,你的體內會產生一種叫多巴胺的物質,它讓你興奮和開心,並激勵着你不斷前行。
讓我們再回顧下上篇的結果:
本篇算是進入SGM編碼教學的完結篇,即立體匹配的最後一步:視差優化(Disparity Refine)。優化的含義大家都懂,爲了得到更好的視差圖。
怎樣算更好?如何能更好?本篇就是解答這兩個問題。
我們先來看看,本篇我們將得到一個什麼樣的實驗結果,大家自行判斷是否有興趣繼續閱讀。
客觀的說,噪點剔除的很乾淨,遮擋區做了處理,我想你們應該會感點興趣。
【碼上教學】【立體匹配系列】經典SGM:(5)視差優化
1 優化目的
- 1. 提高精度
- 2. 剔除錯誤
- 3. 弱紋理區優化
- 4. 填補空洞
提高精度:前面的視差計算步驟中,我們選擇最小代價值對應的視差值,它是一個整數值(整數值我們纔能有離散化的視差空間),即整像素級精度,而實際應用中整像素精度基本無法滿足需求,必須優化到子像素精度纔有意義。
剔除錯誤:即剔除錯誤的視差值,比如a像素本應和b像素是同名點,而結果卻是a像素和c像素是同名點,這就是錯誤的視差值。造成錯誤匹配的原因有遮擋、弱紋理等,所謂錯誤是永恆的,完美是不存在的。
弱紋理區優化:弱紋理區域是所有立體匹配都會面對的難題,極端情況下,一塊白牆,怎麼找同名點?SGM提出的是一種基於圖像分割+平面擬合的處理弱紋理的方法。
填補空洞:剔除錯誤匹配後,被剔除的像素會造成無效值空洞,如何填補使視差圖更加完整也是優化所研究的內容。
優化做的策略不少,科學家都是精益求精,限於篇幅,本篇只講前兩條,後兩條我將在下一篇更新。
2 優化手段
- 1. 子像素擬合(Subpixel)
- 2. 一致性檢查(Left/Right Consistency Check)
- 3. 唯一性約束(Uniqueness)(待續)
- 4. 剔除小連通區(Remove Peaks)(待續)
- 5. 中值濾波(Median Filter)
子像素擬合和一致性檢查幾乎是所有立體匹配算法必執行的策略。子像素擬合將整像素精度提高到子像素精度,而一致性檢查可以說是剔除錯誤匹配的不二選擇。
唯一性約束和剔除小連通區可以視情況而添加,比如在GPU實現的時候,找最小值是時間複雜度較高的操作,而唯一性約束要找兩次最小值(一個最小一個次最小);而區域跟蹤用GPU也難以高效的實現,所以這兩塊一般也可省掉。
我將教大家編碼實現這5個策略。
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);
這裏只貼出了右視圖視差計算的部分代碼,大家可從Github/GemiGlobalMatching下載完整代碼。
計算出右視差圖後,執行一致性檢查:根據左影像視差圖可以算出像素在右影像中的匹配像素是,如果的視差剛好也近似等於,則滿足一致性。
【一致性檢查代碼】
// ---左右一致性檢查
void SemiGlobalMatching::LRCheck() const
{
const int width = width_;
const int height = height_;
const float32& threshold = option_.lrcheck_thres;
// ---左右一致性檢查
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.4 唯一性約束
唯一性約束的含義是:最優視差的代價值應該是所有候選視差中唯一的最小代價,換句話說它的代價值比其他視差的代價值足夠小,這樣它才能在衆多候選視差中脫穎而出。如果有另外一個視差的代價值和它一樣,那麼它就不再是最好的那一個了,而是最好的兩個之一,而我們不能接受有兩個最好,只能忍痛捨棄它。
其實,這裏面蘊含的另一層含義是:視差估計的可靠性!如果兩個最小的代價值相差很小,比如一個是30,一個是31,因爲視差估計是帶有噪聲影響的,所以其實不能肯定的說是30最好還是31最好,可能31纔是正確的那個視差,因爲兩個值相差太小了,可能是由於一些輕微的噪聲導致實際上最優的那個視差的代價值卻是次最小的。所以就乾脆把這種讓人頭疼的選擇題給PASS掉,遇到這種情況直接給它一個NO!
用程序來處理時,我們會計算最小代價和次最小代價的相對差值,如果差值小於閾值,那就表明最小的兩個代價值相差不明顯,就給這個像素賦一個無效視差。
我們來看代碼:
// 判斷唯一性約束
// 若(min-sec)/min < min*(1-uniquness),則爲無效估計
if (sec_min_cost - min_cost <= static_cast<uint16>(min_cost * (1 - uniqueness_ratio))) {
disparity[i * width + j] = Invalid_Float;
continue;
}
代碼很簡單,次最小和最小代價的相對差值小於閾值,就給個無效值。這一步也在視差計算的函數體內完成,在遍歷計算最小代價的時候,再遍歷一次計算次最小代價,然後判斷唯一性。
2.5 剔除小連通區
學過圖像處理的同學對連通區應該並不陌生,它是圖像處理中很常見的一個名詞,它的含義是通過4-鄰域或8-鄰域連通在一起的像素集合,在SGM中,這一步用來剔除連通在一起的小塊錯誤匹配像素,像這樣的:
【剔除小連通區代碼】
void sgm_util::RemoveSpeckles(float32* disparity_map, const sint32& width, const sint32& height,
const sint32& diff_insame, const uint32& min_speckle_aera, const float& invalid_val)
{
assert(width > 0 && height > 0);
if (width < 0 || height < 0) {
return;
}
// 定義標記像素是否訪問的數組
std::vector<bool> visited(uint32(width*height),false);
for(sint32 i=0;i<height;i++) {
for(sint32 j=0;j<width;j++) {
if (visited[i * width + j] || disparity_map[i*width+j] == invalid_val) {
// 跳過已訪問的像素及無效像素
continue;
}
// 廣度優先遍歷,區域跟蹤
// 把連通域面積小於閾值的區域視差全設爲無效值
std::vector<std::pair<sint32, sint32>> vec;
vec.emplace_back(i, j);
visited[i * width + j] = true;
uint32 cur = 0;
uint32 next = 0;
do {
// 廣度優先遍歷區域跟蹤
next = vec.size();
for (uint32 k = cur; k < next; k++) {
const auto& pixel = vec[k];
const sint32 row = pixel.first;
const sint32 col = pixel.second;
const auto& disp_base = disparity_map[row * width + col];
// 8鄰域遍歷
for(int r=-1;r<=1;r++) {
for(int c=-1;c<=1;c++) {
if(r==0&&c==0) {
continue;
}
int rowr = row + r;
int colc = col + c;
if (rowr >= 0 && rowr < height && colc >= 0 && colc < width) {
if(!visited[rowr * width + colc] && abs(disparity_map[rowr * width + colc] - disp_base) <= diff_insame) {
vec.emplace_back(rowr, colc);
visited[rowr * width + colc] = true;
}
}
}
}
}
cur = next;
} while (next < vec.size());
// 把連通域面積小於閾值的區域視差全設爲無效值
if(vec.size() < min_speckle_aera) {
for(auto& pix:vec) {
disparity_map[pix.first * width + pix.second] = invalid_val;
}
}
}
}
}
2.6 中值濾波
中值濾波在立體匹配中使用的還挺廣泛的,作爲一個平滑算法,它主要是用來剔除視差圖中的一些孤立的離羣外點,同時還能起到填補小洞的作用。這個部分我就不細說了,想必大家對中值濾波都不會太陌生,我會在實驗中展示中值濾波的效果。
中值濾波的代碼實現,我放到sgm_util工具集裏,如下:
void sgm_util::MedianFilter(const float32* in, float32* out, const sint32& width, const sint32& height,
const sint32 wnd_size)
{
const sint32 radius = wnd_size / 2;
const sint32 size = wnd_size * wnd_size;
// 存儲局部窗口內的數據
std::vector<float32> wnd_data;
wnd_data.reserve(size);
for (sint32 i = 0; i < height; i++) {
for (sint32 j = 0; j < width; j++) {
wnd_data.clear();
// 獲取局部窗口數據
for (sint32 r = -radius; r <= radius; r++) {
for (sint32 c = -radius; c <= radius; c++) {
const sint32 row = i + r;
const sint32 col = j + c;
if (row >= 0 && row < height && col >= 0 && col < width) {
wnd_data.push_back(in[row * width + col]);
}
}
}
// 排序
std::sort(wnd_data.begin(), wnd_data.end());
// 取中值
out[i * width + j] = wnd_data[wnd_data.size() / 2];
}
}
}
3 實驗
枯燥而或許有趣的理論部分介紹完畢,我們喜迎實驗環節。
【實驗1 子像素擬合對比圖】
【實驗2 一致性檢查效果圖】
圖中我們可以看到,非重疊區、遮擋區的匹配錯誤已經大部分都被剔除了,當然也讓視差圖有了很多黑色的無效區。
【實驗3 唯一性約束+去小連通區&中值濾波效果】
唯一性約束和去小連通區可以進一步剔除不可靠的視差以及連通成塊的錯誤視差區;最後通過中值濾波,進一步對噪點進行剔除,且填補一些小的無效區域,效果還不錯。
夜已深,春困秋乏,博主也該歇息了,今天就介紹到這,不出意外,後面博主還會寫視差填充及弱紋理區域優化的優化模塊。
代碼已同步於Github開源項目:Github/GemiGlobalMatching,大家可自行下載,點擊項目右上角的star,有更新會實時通知到你的個人中心!
各位拜拜!
下篇:視差填充
理論恆叨系列
【恆叨立碼】【理論恆叨】【立體匹配系列】經典SGM:(1)匹配代價計算之互信息(MI))
【恆叨立碼】【理論恆叨】【立體匹配系列】經典SGM:(2)匹配代價計算之Census變換
【恆叨立碼】【理論恆叨】【立體匹配系列】經典SGM:(3)代價聚合(Cost Aggregation)
【恆叨立碼】【理論恆叨】【立體匹配系列】經典SGM:(4)視差計算、視差優化