转载自:https://zhuanlan.zhihu.com/p/30754263
紧接着上次的文章,这次将详细的说明一下立体匹配(这里需要说明一样,这里所有的图片都是存在对极约束的,也就是完成过校正,可以说对于左右两幅图像上的匹配点,它们的y值相等,而x之间的差别就是视差d),并且会给出最近看的一些文章,以及对文章的一些理解。(PS:目前,立体匹配领域,主要有两个评测网站,一个是KITTI(http://www.cvlibs.net/datasets/kitti/eval_stereo_flow.php?benchmark=stereo),另一个是middlebury(http://vision.middlebury.edu/stereo/),两个网站上的算法都交叉,但是又不完全一样)
对于图像的匹配我们都知道,就是对两幅图片或者更多图片,找到它们相似的地方。那么对于立体匹配而言,就是基于同一场景得到的多张二维图,通过找到相同点进一步还原场景的三维信息,一般采用的图像是双目图像,下面给出Middlebury上的一组标准图,第一幅图和第二幅图分别是双目相机得到的左图和右图,第三幅图就是视差图。可以很明显的看出来越亮的地方表示视差越大,也就离相机越近,反之离相机也就越远。
下面给出立体匹配的四个最基本的步骤:
- 匹配代价计算(Matching Cost Computation:CC)
- 代价聚合(Cost Aggregation:CA)
- 视差计算(Disparity Computation )
- 视差精化(Disparity Refinement ):对上一步得到的粗估计的视差图进行精确计算,策略有很多,例如plane fitting,BP,动态规划等。这里不再熬述。
同样的,立体匹配也分为全局匹配和局部匹配两种,这两种在匹配步骤上也有所差异。
- 全局匹配
全局立体匹配算法一般来说会省略第二步,主要是采用了全局的优化理论方法估计视差,建立全局能量函数,通过最小化全局能量函数得到最优视差值。其能量函数由数据项和平滑项构成。
全局匹配算法得到的结果比较准确,但是其运行时间比较长,不适合实时运行。主要的算法有图割(graph cuts)、信念传播(belief propagation)、动态规划(Dynamic Programming )等算法。
- 局部匹配
基本原理是给定在一幅图像上的某一点,选取该像素点邻域内的一个子窗口,在另一幅图像中的一个区域内,根据某种相似性判断依据,寻找与子窗口图像最为相似的子图,而其匹配的子图中对应的像素点就为该像素的匹配点。通常方法有SAD、SSD、NCC等等。
下面来详细的一步一步的介绍立体匹配的每一个步骤,并且结合一个具体的例子来进行说明。
匹配代价计算
一般来说,匹配代价的计算是对左右两幅图像的每一个像素点而言的,可以认为定义了一个处理左右两幅图像中匹配像素点的函数 ,这里的 是我们定义的视差范围,也就是对于左图中的一个像素点 ,我们给它在右图中定义一个寻找视差的范围 。
举个例子:假设我们对于匹配图像定义了视差的寻找范围为0~16,也就是说 ( 是整数),对于左图的一个像素点 在右图中能够计算匹配代价的区域就是 这之间。
我们是不是可以这样想,对于每一个像素点,在给定的视差范围内,我们找到 ,我们是不是就可以认为这个最小值就代表这是正确的匹配点,而这个最小值对应的 就是我们要找的视差(这一步运用的是Winner Take All: WTA策略)。如果对每一个像素都这样进行操作的话,我们是不是就可以得到最后的视差图。显然这是成立的,但是这样做的最后的视差图效果非常的差,在这里可以给大家展示一下这样简单操作后的结果。
左图(标准图) 右图(简单处理后的视差图)代价聚合
我们从上图中基于点之间的匹配很容易受噪声的影响,所以我们需要在点的周围建立一个window,让像素块和像素块之间进行比较,这样肯定靠谱些。代价聚合往往是局部算法或者半全局算法才会使用,全局算法抛弃了window,采用基于全图信息的方式建立能量函数。
或者说,我们换一种看法,可以看做是对匹配代价计算的结果进行滤波的一个过程。
举个例子:假设我们有一个3*3的窗,在这个窗上进行代价聚合的函数为 ,首先看对原始左图像一个像素点的聚合: (结果是窗口的中心点的值),同理对于右边的图像,需要在视差范围内做聚合: 。然后,下一步就需要对处理之后的像素计算匹配代价,并且找到它们的代价最小值: 。我们可以看到,最后同一的公式是一个关于视差 的函数。
再看一看以滤波的方式来解释,这里我们直接对得到的视差图进行代价聚合: ( 表示视差图坐标),这里的 就是经过匹配代价计算后的像素值。可以看到最后两种方法的结果都是一样的(可以使用最简单的Box filter去验证)。
视差计算
其实在上面两个步骤的时候,就已经提及到了视差计算。视差计算最常用的策略就是WTA策略。这一步也可以分为局部算法与全局算法,局部算法直接优化代价聚合模型。而全局算法,要建立一个能量函数。输出的是一个粗略的视差图。
视差精化
这一步也就是对得到的视差进行优化的过程,通常方法都是“亚像素求精+均值滤波”。但是不可否认的是,立体匹配最关键的步骤仍然是代价计算和代价聚合步骤。
下面以局部匹配中的SAD做为例子来更好的理解立体匹配,其中着重考虑的是代价计算和代价聚合。
这里简单的介绍一下SAD,SAD就是Sum of Absolute differences,下面给出SAD计算公式:
下面给出实现的代码:
#include<iostream>
#include <opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
using namespace std;
using namespace cv;
int main(){
int hWin = 3;//窗的大小为3*3
cout<<"hWin: "<<hWin<<"; "<<"compare length: "<<compareLength<<endl;
cout<<"SAD test"<<endl;
Mat left_image = imread("disp2.png");
Mat right_image = imread("disp6.png");
int imageWidth = left_image.cols;
int imageHeight =left_image.rows;
//cout << left_image.size();
Mat SAD_image = Mat(left_image.size(),CV_8UC1,1);
Mat MatchLevel_image = Mat(left_image.size(),CV_8UC1,1);
//设定视差寻找范围d为0~7
int minDBounds = 0;
int maxDBounds = 7;
namedWindow("Left");
namedWindow("Right");
namedWindow("SAD");
namedWindow("MatchLevel");
imshow("Left",left_image);
imshow("Right",right_image);
/*SAD Transform */
int i,j ,m,n,k;
unsigned char centerPixel = 0;
unsigned char neighborPixel = 0;
int bitCount = 0;
unsigned int bigger = 0;
int sum = 0;
unsigned int *match_level = new unsigned int[maxDBounds - minDBounds + 1];
int temp_min = 0;
int temp_index = 0;
unsigned char* dst;
unsigned char* left_src = NULL;
unsigned char* right_src = NULL;
unsigned char left_pixel = 0;
unsigned char right_pixel =0;
unsigned char sub_pixel = 0;
for(i = 0 ; i < imageHeight;i++)
{
for(j = 0; j< imageWidth;j++)
{
//计算每一个像素的SAD
for (k = minDBounds;k <= maxDBounds;k++)
{
sum = 0;
for (m = i-hWin; m <= i + hWin;m++)
{
for (n = j - hWin; n <= j + hWin;n++)
{
if (m < 0 || m >= imageHeight || n <0 || n >= imageWidth )
{
sub_pixel = 0;
}else if (n + k >= imageWidth)
{
sub_pixel = 0;
}else
{
left_src = (unsigned char*)left_image.data
+ m*left_image.step + n + k;
right_src = (unsigned char*)right_image.data
+ m*right_image.step + n;
left_pixel = *left_src;
right_pixel = *right_src;
if (left_pixel > right_pixel)
{
sub_pixel = left_pixel - right_pixel;
}else
{
sub_pixel = right_pixel -left_pixel;
}
}
sum += sub_pixel;
}
}
match_level[k] = sum;
//cout<<"SUM:"<<sum<<endl;
}
/*寻找最佳匹配点*/
temp_min = 0;
temp_index = 0;
for ( m = 1;m < maxDBounds - minDBounds + 1;m++)
{
//cout<<"match_level[m]:"<<match_level[m]<<endl;
if (match_level[m] < match_level[temp_index])
{
temp_min = match_level[m];
temp_index = m;
}
}
dst = (unsigned char *)SAD_image.data + i*SAD_image.step + j;
*dst = temp_index*8;
dst = (unsigned char *)MatchLevel_image.data + i*MatchLevel_image.step + j;
*dst = temp_min;
}
}
imshow("SAD",SAD_image);
imshow("MatchLevel",MatchLevel_image);
imwrite("MatchLevel.jpg", MatchLevel_image);
imwrite("depth.jpg", SAD_image);
waitKey(0);
return 0;
}
下面是标准视差图和SAD的结果图
左图(标准图) 右图(SAD的视差图)我们可以很明显的看到,SAD的结果和最终的标准图还是有很大的差别,图中的有些特征也没有很好的还原出来。
后面将开始着重于对立体匹配文章的解读,将一些文章的读后感跟大家分享。