手把手教你编写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,有更新会实时通知到你的个人中心!

各位拜拜!

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