科學是一個精益求精的過程。
抱歉同學們,視差優化的完結篇被我安排成了一個系列篇,實在是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)像素是通過各種優化操作而判定的無效像素。
(2)左影像像素在右影像上的匹配像素爲,像素在右視差圖上的值爲,通過找到左影像的匹配點,獲取的視差,若,則爲遮擋區。
第二條有點繞,因爲是兩次對應,左找到右,右又反過來找左,換句話描述:假設是通過視差找到的同名點,如果在左影像存在另外一個像素也和是同名點而且它的視差比要大,那麼就是遮擋區。
原文描述:A pixel p is occluded, if another pixel with higher disparity maps to the same pixel q in the match image1.
這個判定所基於的兩個假設是:
(1)的視差值和周圍的背景像素視差值比較接近。
(2)因爲遮擋而在右影像上不可見,所以它會匹配到右影像上的前景像素,而前景像素的視差值必定比背景像素大,即比的視差大。
有人問,既然像素是無效像素,那麼爲啥還有視差值呢,答案就是在給無效值之前判斷,這一步可以在一致性檢查步驟裏完成,實際上遮擋區主要都是通過一致性檢查來使其無效的,因爲遮擋區存在明顯的左右差異性(左可見,右不可見),所以一致性檢查大概率會讓這些區域的像素無效。我們在一致性檢查讓的視差值無效之前,判斷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,有更新會實時通知到你的個人中心!
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. ↩︎