千呼万唤始出来,犹抱琵琶半遮面。
抱歉让大家久等,最近事儿繁多,导致更新推迟,实在抱歉。
上回说到,路径聚合后,视差图英姿初现,我想初学者到这一步大多都会产生一种溢于言表的喜悦之情,这就是编程的魅力,你日以继夜的在键盘上敲打,与世隔绝甚至废寝忘食,当程序运行,屏幕出现一个还不错的结果时,你的体内会产生一种叫多巴胺的物质,它让你兴奋和开心,并激励着你不断前行。
让我们再回顾下上篇的结果:
前文链接
手把手教你编写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,有更新会实时通知到你的个人中心!
各位拜拜!