手把手教你編寫SGM雙目立體匹配(基於C++,Github同步更新)(六)(視差填充)

科學是一個精益求精的過程。

抱歉同學們,視差優化的完結篇被我安排成了一個系列篇,實在是SGM太過經典,每一點我都想拿出來單獨成篇,作者在視差優化模塊的確安排了很多子模塊,每個模塊都有其存在的意義。上一篇我們學習了怎麼做一些常規的視差優化操作:一致性檢查、唯一性約束、去除小連通區等。本篇我們來說一說視差填充。
我們再次整理下整個優化的子模塊,讓大家思路更清晰一些:

  • 1. 子像素擬合(Subpixel)
  • 2. 一致性檢查(Left/Right Consistency Check)
  • 3. 唯一性約束(Uniqueness)
  • 4. 剔除小連通區(Remove Peaks)
  • 5. 中值濾波(Median Filter)
  • 6. 弱紋理區優化
  • 7. 填補空洞

本篇我們講的是第7點。即填補空洞,也就是視差圖填充。

先看看本篇最後的實驗成果,讓大家有個初探的興趣:

視差填充前
視差填充後

上圖一看,大家就明白What is 視差填充了,那我們就進主題吧。

視差填充,即給視差圖的無效區域像素分配一個有效值。填充之前要問兩個問題:

  • 1. 無效區是否一視同仁?
  • 2. 有效值哪裏來?

本文前兩個小節就是回答這兩個問題。

遮擋區和誤匹配區

回答第一個問題,無效區是否一視同仁? 答案是NO!需要區分遮擋區和誤匹配區。

遮擋區:由於前景遮擋而在左視圖上可見但在右視圖上不可見的像素區域。

誤匹配區:位於非遮擋區域的錯誤匹配像素區域。

從定義可以看出,其實兩者都是錯誤匹配像素區域,我們的主要目的是爲了把遮擋區拿出來單獨成一類,因爲遮擋區比較特殊,它們位於視差非連續區域,一側是前景,視差值較大,一側是背景,視差值較小,它理應和背景像素視差更爲接近,而和前景視差相差較大,所以填補時應該儘量選擇周圍背景像素的視差,避免選擇前景像素。

誤匹配區則不同,它們並不在遮擋區,鄰域像素都位於一個連續的視差表面,視差是連續的,所以填補時可以考慮鄰域內的所有像素。

總之,在填充前,我們要做個判斷,哪些像素是遮擋區,哪些像素是誤匹配區。

作者是通過如下方法來判斷遮擋區的:

(1)像素pp是通過各種優化操作而判定的無效像素。
(2)左影像像素pp在右影像上的匹配像素爲q=pdq=p-d,像素qq在右視差圖上的值爲drd_r,通過drdr找到左影像的匹配點pp',獲取pp'的視差dd',若d>dd'>d,則pp爲遮擋區。

第二條有點繞,因爲是兩次對應,左找到右,右又反過來找左,換句話描述:假設qqpp通過視差dd找到的同名點,如果在左影像存在另外一個像素pp'也和qq是同名點而且它的視差比dd要大,那麼pp就是遮擋區。

原文描述:A pixel p is occluded, if another pixel with higher disparity maps to the same pixel q in the match image1.

這個判定所基於的兩個假設是:

(1)pp的視差值和周圍的背景像素視差值比較接近。
(2)pp因爲遮擋而在右影像上不可見,所以它會匹配到右影像上的前景像素,而前景像素的視差值必定比背景像素大,即比pp的視差大。

有人問,既然像素pp是無效像素,那麼爲啥還有視差值呢,答案就是在給pp無效值之前判斷,這一步可以在一致性檢查步驟裏完成,實際上遮擋區主要都是通過一致性檢查來使其無效的,因爲遮擋區存在明顯的左右差異性(左可見,右不可見),所以一致性檢查大概率會讓這些區域的像素無效。我們在一致性檢查讓pp的視差值無效之前,判斷p是否是遮擋區和誤匹配區。

遮擋區和誤匹配區判斷代碼

// 判斷兩個視差值是否一致(差值在閾值內)
if (abs(disp - disp_r) > threshold) {
	// 區分遮擋區和誤匹配區
	// 通過右影像視差算出在左影像的匹配像素,並獲取視差disp_rl
	// if(disp_rl > disp) 
    //		pixel in occlusions
	// else 
    //		pixel in mismatches
	const sint32 col_rl = static_cast<sint32>(col_right + disp_r + 0.5);
	if(col_rl > 0 && col_rl < width){
		const auto& disp_l = disp_left_[i*width + col_rl];
		if(disp_l > disp) {
			occlusions.emplace_back(i, j);
		}
		else {
			mismatches.emplace_back(i, j);
		}
	}
	else{
		mismatches.emplace_back(i, j);
	}

    // 讓視差值無效
	disp = Invalid_Float;
}

有區別填充

回答第二個問題:有效視差從哪裏來?

我們已經把無效像素分爲了遮擋區和誤匹配區,填充的有效視差來源也要區別分析。

首先,兩者的共同點是,有效視差都來自於周圍有效像素的視差值,區別在於如何從周圍的有效視差中選出最合適的一個。

對於遮擋區像素,因爲它的身份是背景像素,所以它是不能選擇周圍的前景像素視差值的,應該選擇周圍背景像素的視差值。由於背景像素視差值比前景像素小,所以在收集周圍的有效視差值後,應選擇較小的幾個,具體哪一個呢?SGM作者選擇的是次最小視差

對於誤匹配像素,它並不位於遮擋區,所以周圍的像素都是可見的,而且沒有遮擋導致的視差非連續的情況,它就像一個連續的表面凸起的一小塊噪聲,這時周圍的視差值都是等價的,沒有哪個應選哪個不應選,這時取中值就很適合。

文章中的公式是這樣的:

另一個關鍵點是如何找到周圍的有效像素,博主提供一種思路:以像素爲中心,等角度往外發射8條射線,收集每條射線碰到的第一個有效像素,如圖所示:

至此,可以編寫視差填充代碼:

void SemiGlobalMatching::FillHolesInDispMap()
{
	const sint32 width = width_;
	const sint32 height = height_;

	std::vector<float32> disp_collects;

	// 定義8個方向
	float32 pi = 3.1415926;
	float32 angle1[8] = { pi, 3 * pi / 4, pi / 2, pi / 4, 0, 7 * pi / 4, 3 * pi / 2, 5 * pi / 4 };
	float32 angle2[8] = { pi, 5 * pi / 4, 3 * pi / 2, 7 * pi / 4, 0, pi / 4, pi / 2, 3 * pi / 4 };
	float32 *angle = angle1;

	float32* disp_ptr = disp_left_;
	for (int k = 0; k < 3; k++) {
		// 第一次循環處理遮擋區,第二次循環處理誤匹配區
		auto& trg_pixels = (k == 0) ? occlusions_ : mismatches_;

		std::vector<std::pair<int, int>> inv_pixels;
		if (k == 2) {
			//  第三次循環處理前兩次沒有處理乾淨的像素
			for (int i = 0; i < height; i++) {
				for (int j = 0; j < width; j++) {
					if (disp_ptr[i * width + j] == Invalid_Float) {
						inv_pixels.emplace_back(i, j);
					}
				}
			}
			trg_pixels = inv_pixels;
		}

		// 遍歷待處理像素
		for (auto& pix : trg_pixels) {
			int y = pix.first;
			int x = pix.second;

			if (y == height / 2) {
				angle = angle2;
			}

			// 收集8個方向上遇到的首個有效視差值
			disp_collects.clear();
			for (sint32 n = 0; n < 8; n++) {
				const float32 ang = angle[n];
				const float32 sina = sin(ang);
				const float32 cosa = cos(ang);
				for (sint32 n = 1; ; n++) {
					const sint32 yy = y + n * sina;
					const sint32 xx = x + n * cosa;
					if (yy<0 || yy >= height || xx<0 || xx >= width) {
						break;
					}
					auto& disp = *(disp_ptr + yy*width + xx);
					if (disp != Invalid_Float) {
						disp_collects.push_back(disp);
						break;
					}
				}
			}
			if(disp_collects.empty()) {
				continue;
			}

			std::sort(disp_collects.begin(), disp_collects.end());

			// 如果是遮擋區,則選擇第二小的視差值
			// 如果是誤匹配區,則選擇中值
			if (k == 0) {
				if (disp_collects.size() > 1) {
					disp_ptr[y*width + x] = disp_collects[1];
				}
				else {
					disp_ptr[y*width + x] = disp_collects[0];
				}
			}
			else{
				disp_ptr[y*width + x] = disp_collects[disp_collects.size() / 2];
			}
		}
	}
}

實驗

實驗如前言所見,博主做了視差填充前後的對比,如圖:

視差填充前
視差填充後

可以看到,填充後視差圖更加完整,視差估計也更加稠密。

但不得不討論的是,視差填充始終是不精確的,無論是取最小值還是取中值,它只能說是通過周圍的有效值來預測,所以精確程度是有限的,換句話說,遮擋區像素都看不見,何以預測出十分精確的值?所以我們通常會根據應用需求來決定是否執行視差填充,如果實際要求每個點足夠準確,而不太要求是否足夠完整,那麼就不需要做視差填充;而如果要求視差圖足夠完整,而對填充精度要求不高,則可以執行視差填充。

代碼已同步於Github開源項目:Github/GemiGlobalMatching,大家可自行下載,點擊項目右上角的star,有更新會實時通知到你的個人中心!


  1. HIRSCHMÜLLER H. Hirschmüller, H.: Stereo processing by semiglobal matching and mutual information. IEEE PAMI 30(2), 328-341[J]. IEEE Transactions on Pattern Analysis & Machine Intelligence, 2008,30(2):328-341. ↩︎

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