OpenCV学习笔记(19)双目测距与三维重建的OpenCV实现问题集锦(四)三维重建与OpenGL显



 

四、双目匹配与视差计算

立体匹配主要是通过找出每对图像间的对应关系,根据三角测量原理,得到视差图;在获得了视差信息后,根据投影模型很容易地可以得到原始图像的深度信息和三维信息。立体匹配技术被普遍认为是立体视觉中最困难也是最关键的问题,主要是以下因素的影响:

 

(1) 光学失真和噪声(亮度、色调、饱和度等失衡)

OpenCV学习笔记(19)双目测距与三维重建的OpenCV实现问题集锦(四)三维重建与OpenGL显_第1张图片

(2) 平滑表面的镜面反射

OpenCV学习笔记(19)双目测距与三维重建的OpenCV实现问题集锦(四)三维重建与OpenGL显_第2张图片

(3) 投影缩减(Foreshortening)

OpenCV学习笔记(19)双目测距与三维重建的OpenCV实现问题集锦(四)三维重建与OpenGL显_第3张图片

(4) 透视失真(Perspective distortions)

OpenCV学习笔记(19)双目测距与三维重建的OpenCV实现问题集锦(四)三维重建与OpenGL显_第4张图片

(5) 低纹理(Low texture)

OpenCV学习笔记(19)双目测距与三维重建的OpenCV实现问题集锦(四)三维重建与OpenGL显_第5张图片

(6) 重复纹理(Repetitive/ambiguous patterns)

OpenCV学习笔记(19)双目测距与三维重建的OpenCV实现问题集锦(四)三维重建与OpenGL显_第6张图片

(7) 透明物体

OpenCV学习笔记(19)双目测距与三维重建的OpenCV实现问题集锦(四)三维重建与OpenGL显_第7张图片

(8) 重叠和非连续

OpenCV学习笔记(19)双目测距与三维重建的OpenCV实现问题集锦(四)三维重建与OpenGL显_第8张图片

 

 

目前立体匹配算法是计算机视觉中的一个难点和热点,算法很多,但是一般的步骤是:

 

A、匹配代价计算

匹配代价计算是整个立体匹配算法的基础,实际是对不同视差下进行灰度相似性测量。常见的方法有灰度差的平方SD(squared intensity differences),灰度差的绝对值AD(absolute intensity differences)等。另外,在求原始匹配代价时可以设定一个上限值,来减弱叠加过程中的误匹配的影响。以AD法求匹配代价为例,可用下式进行计算,其中T为设定的阈值。

OpenCV学习笔记(19)双目测距与三维重建的OpenCV实现问题集锦(四)三维重建与OpenGL显_第9张图片

图18

B、 匹配代价叠加

一般来说,全局算法基于原始匹配代价进行后续算法计算。而区域算法则需要通过窗口叠加来增强匹配代价的可靠性,根据原始匹配代价不同,可分为:

OpenCV学习笔记(19)双目测距与三维重建的OpenCV实现问题集锦(四)三维重建与OpenGL显_第10张图片

图19

 

C、 视差获取

对于区域算法来说,在完成匹配代价的叠加以后,视差的获取就很容易了,只需在一定范围内选取叠加匹配代价最优的点(SAD和SSD取最小值,NCC取最大值)作为对应匹配点,如胜者为王算法WTA(Winner-take-all)。而全局算法则直接对原始匹配代价进行处理,一般会先给出一个能量评价函数,然后通过不同的优化算法来求得能量的最小值,同时每个点的视差值也就计算出来了。

 

D、视差细化(亚像素级)

大多数立体匹配算法计算出来的视差都是一些离散的特定整数值,可满足一般应用的精度要求。但在一些精度要求比较高的场合,如精确的三维重构中,就需要在初始视差获取后采用一些措施对视差进行细化,如匹配代价的曲线拟合、图像滤波、图像分割等。

 

有关立体匹配的介绍和常见匹配算法的比较,推荐大家看看Stefano Mattoccia 的讲义 Stereo Vision: algorithms and applications,190页的ppt,讲解得非常形象详尽。

1. opencv2.1和opencv2.0在做stereo vision方面有什么区别了?

2.1版增强了Stereo Vision方面的功能:

(1) 新增了 SGBM 立体匹配算法(源自Heiko Hirschmuller的《Stereo Processing by Semi-global Matching and Mutual Information》),可以获得比 BM 算法物体轮廓更清晰的视差图(但低纹理区域容易出现横/斜纹路,在 GCstate->fullDP 选项使能时可消减这种异常纹路,但对应区域视差变为0,且运行速度会有所下降),速度比 BM 稍慢, 352*288的帧处理速度大约是 5 帧/秒;

(2) 视差效果:BM < SGBM < GC;处理速度:BM > SGBM > GC ;

(3) BM 算法比2.0版性能有所提升,其状态参数新增了对左右视图感兴趣区域 ROI 的支持(roi1 和 roi2,由stereoRectify函数产生);

(4) BM 算法和 GC 算法的核心代码改动不大,主要是面向多线程运算方面的(由 OpenMP 转向 Intel TBB);

(5) cvFindStereoCorrespondenceBM 函数的disparity参数的数据格式新增了 CV_32F 的支持,这种格式的数据给出实际视差,而 2.0 版只支持 CV_16S,需要除以 16.0 才能得到实际的视差数值。

 

 

2. 用于立体匹配的图像可以是彩色的吗?

在OpenCV2.1中,BM和GC算法只能对8位灰度图像计算视差,SGBM算法则可以处理24位(8bits*3)彩色图像。所以在读入图像时,应该根据采用的算法来处理图像:

 

int color_mode = alg == STEREO_SGBM ? 1 : 0;
//////////////////////////////////////////////////////////////////////////
// 载入图像
cvGrabFrame( lfCam );
cvGrabFrame( riCam );
frame1 = cvRetrieveFrame( lfCam );
frame2 = cvRetrieveFrame( riCam );
if(frame1.empty()) break;
resize(frame1, img1, img_size, 0, 0);
resize(frame2, img2, img_size, 0, 0);
// 选择彩色或灰度格式作为双目匹配的处理图像
if (!color_mode && cn>1)
{
cvtColor(img1, img1gray, CV_BGR2GRAY);
cvtColor(img2, img2gray, CV_BGR2GRAY);
img1p = img1gray;
img2p = img2gray;
}
else
{
img1p = img1;
img2p = img2;
}


 

 

3. 怎样获取与原图像有效像素区域相同的视差图?

OpenCV2.0及以前的版本中,所获取的视差图总是在左侧和右侧有明显的黑色区域,这些区域没有有效的视差数据。视差图有效像素区域与视差窗口(ndisp,一般取正值且能被16整除)和最小视差值(mindisp,一般取0或负值)相关,视差窗口越大,视差图左侧的黑色区域越大,最小视差值越小,视差图右侧的黑色区域越大。其原因是为了保证参考图像(一般是左视图)的像素点能在目标图像(右视图)中按照设定的视差匹配窗口匹配对应点,OpenCV 只从参考图像的第 (ndisp - 1 + mindisp) 列开始向右计算视差,第 0 列到第 (ndisp - 1 + mindisp) 列的区域视差统一设置为 (mindisp - 1) *16;视差计算到第 width + mindisp 列时停止,余下的右侧区域视差值也统一设置为 (mindisp - 1) *16。  

00177 static const int DISPARITY_SHIFT = 4;
…
00411     int ndisp = state->numberOfDisparities;
00412     int mindisp = state->minDisparity;
00413     int lofs = MAX(ndisp - 1 + mindisp, 0);
00414     int rofs = -MIN(ndisp - 1 + mindisp, 0);
00415     int width = left->cols, height = left->rows;
00416     int width1 = width - rofs - ndisp + 1;
…
00420     short FILTERED = (short)((mindisp - 1) << DISPARITY_SHIFT);
…
00466     // initialize the left and right borders of the disparity map
00467     for( y = 0; y < height; y++ )
00468     {
00469         for( x = 0; x < lofs; x++ )
00470             dptr[y*dstep + x] = FILTERED;
00471         for( x = lofs + width1; x < width; x++ )
00472             dptr[y*dstep + x] = FILTERED;
00473     }
00474     dptr += lofs;
00475
00476     for( x = 0; x < width1; x++, dptr++ )

…


 

这样的设置很明显是不符合实际应用的需求的,它相当于把摄像头的视场范围缩窄了。因此,OpenCV2.1 做了明显的改进,不再要求左右视图和视差图的大小(size)一致,允许对视差图进行左右边界延拓,这样,虽然计算视差时还是按上面的代码思路来处理左右边界,但是视差图的边界得到延拓后,有效视差的范围就能够与对应视图完全对应。具体的实现代码范例如下:

 

//////////////////////////////////////////////////////////////////////////
// 对左右视图的左边进行边界延拓,以获取与原始视图相同大小的有效视差区域
copyMakeBorder(img1r, img1b, 0, 0, m_nMaxDisp, 0, IPL_BORDER_REPLICATE);
copyMakeBorder(img2r, img2b, 0, 0, m_nMaxDisp, 0, IPL_BORDER_REPLICATE);

//////////////////////////////////////////////////////////////////////////
// 计算视差
if( alg == STEREO_BM )
{
	bm(img1b, img2b, dispb);
	// 截取与原始画面对应的视差区域(舍去加宽的部分)
	displf = dispb.colRange(m_nMaxDisp, img1b.cols);	
}
else if(alg == STEREO_SGBM)
{
	sgbm(img1b, img2b, dispb);
	displf = dispb.colRange(m_nMaxDisp, img1b.cols);
}


 

4. cvFindStereoCorrespondenceBM的输出结果好像不是以像素点为单位的视差?

“@scyscyao:在OpenCV2.0中,BM函数得出的结果是以16位符号数的形式的存储的,出于精度需要,所有的视差在输出时都扩大了16倍(2^4)。其具体代码表示如下:

dptr[y*dstep] = (short)(((ndisp - mind - 1 + mindisp)*256 + (d != 0 ? (p-n)*128/d : 0) + 15) >> 4);

可以看到,原始视差在左移8位(256)并且加上一个修正值之后又右移了4位,最终的结果就是左移4位。

因此,在实际求距离时,cvReprojectTo3D出来的X/W,Y/W,Z/W都要乘以16 (也就是W除以16),才能得到正确的三维坐标信息。”

 

OpenCV2.1中,BM算法可以用 CV_16S 或者 CV_32F 的方式输出视差数据,使用32位float格式可以得到真实的视差值,而CV_16S 格式得到的视差矩阵则需要 除以16 才能得到正确的视差。另外,OpenCV2.1另外两种立体匹配算法 SGBM 和 GC 只支持 CV_16S 格式的 disparity 矩阵

 

 

5. 如何设置BM、SGBM和GC算法的状态参数?

(1)StereoBMState

// 预处理滤波参数

  • preFilterType:预处理滤波器的类型,主要是用于降低亮度失真(photometric distortions)、消除噪声和增强纹理等, 有两种可选类型:CV_STEREO_BM_NORMALIZED_RESPONSE(归一化响应) 或者 CV_STEREO_BM_XSOBEL(水平方向Sobel算子,默认类型), 该参数为 int 型;
  • preFilterSize:预处理滤波器窗口大小,容许范围是[5,255],一般应该在 5x5..21x21 之间,参数必须为奇数值, int 型
  • preFilterCap:预处理滤波器的截断值,预处理的输出值仅保留[-preFilterCap, preFilterCap]范围内的值,参数范围:1 - 31(文档中是31,但代码中是 63), int

// SAD 参数

  • SADWindowSize:SAD窗口大小,容许范围是[5,255],一般应该在 5x5 至 21x21 之间,参数必须是奇数,int 型
  • minDisparity:最小视差默认值为 0, 可以是负值,int 型
  • numberOfDisparities:视差窗口,即最大视差值与最小视差值之差, 窗口大小必须是 16 的整数倍,int 型

// 后处理参数

  • textureThreshold:低纹理区域的判断阈值。如果当前SAD窗口内所有邻居像素点的x导数绝对值之和小于指定阈值,则该窗口对应的像素点的视差值为 0(That is, if the sum of absolute values of x-derivatives computed over SADWindowSize by SADWindowSize pixel neighborhood is smaller than the parameter, no disparity is computed at the pixel),该参数不能为负值,int 型
  • uniquenessRatio:视差唯一性百分比, 视差窗口范围内最低代价是次低代价的(1 + uniquenessRatio/100)倍时,最低代价对应的视差值才是该像素点的视差,否则该像素点的视差为 0 (the minimum margin in percents between the best (minimum) cost function value and the second best value to accept the computed disparity, that is, accept the computed disparity d^ only if SAD(d) >= SAD(d^) x (1 + uniquenessRatio/100.) for any d != d*+/-1 within the search range ),该参数不能为负值,一般5-15左右的值比较合适,int 型
  • speckleWindowSize:检查视差连通区域变化度的窗口大小, 值为 0 时取消 speckle 检查,int 型
  • speckleRange:视差变化阈值,当窗口内视差变化大于阈值时,该窗口内的视差清零,int 型

// OpenCV2.1 新增的状态参数

  • roi1, roi2:左右视图的有效像素区域,一般由双目校正阶段的 cvStereoRectify 函数传递,也可以自行设定。一旦在状态参数中设定了 roi1 和 roi2,OpenCV 会通过cvGetValidDisparityROI 函数计算出视差图的有效区域,在有效区域外的视差值将被清零。
  • disp12MaxDiff:左视差图(直接计算得出)和右视差图(通过cvValidateDisparity计算得出)之间的最大容许差异。超过该阈值的视差值将被清零。该参数默认为 -1,即不执行左右视差检查。int 型。注意在程序调试阶段最好保持该值为 -1,以便查看不同视差窗口生成的视差效果。具体请参见《使用OpenGL动态显示双目视觉三维重构效果示例》一文中的讨论。

在上述参数中,对视差生成效果影响较大的主要参数是 SADWindowSize、numberOfDisparities 和 uniquenessRatio 三个,一般只需对这三个参数进行调整,其余参数按默认设置即可

在OpenCV2.1中,BM算法有C和C++ 两种实现模块。

 

(2)StereoSGBMState

SGBM算法的状态参数大部分与BM算法的一致,下面只解释不同的部分:

  • SADWindowSize:SAD窗口大小,容许范围是[1,11],一般应该在 3x3 至 11x11 之间,参数必须是奇数,int 型
  • P1, P2:控制视差变化平滑性的参数。P1、P2的值越大,视差越平滑。P1是相邻像素点视差增/减 1 时的惩罚系数;P2是相邻像素点视差变化值大于1时的惩罚系数。P2必须大于P1。OpenCV2.1提供的例程 stereo_match.cpp 给出了 P1 和 P2 比较合适的数值
  • fullDP:布尔值,当设置为 TRUE 时,运行双通道动态编程算法(full-scale 2-pass dynamic programming algorithm),会占用O(W*H*numDisparities)个字节,对于高分辨率图像将占用较大的内存空间。一般设置为 FALSE

注意OpenCV2.1的SGBM算法是用C++ 语言编写的,没有C实现模块。与H. Hirschmuller提出的原算法相比,主要有如下变化:

  1. 算法默认运行单通道DP算法,只用了5个方向,而fullDP使能时则使用8个方向(可能需要占用大量内存)。
  2. 算法在计算匹配代价函数时,采用块匹配方法而非像素匹配(不过SADWindowSize=1时就等于像素匹配了)。
  3. 匹配代价的计算采用BT算法("Depth Discontinuities by Pixel-to-Pixel Stereo" by S. Birchfield and C. Tomasi),并没有实现基于互熵信息的匹配代价计算。
  4. 增加了一些BM算法中的预处理和后处理程序。

(3)StereoGCState

GC算法的状态参数只有两个:numberOfDisparities 和 maxIters ,并且只能通过 cvCreateStereoGCState 在创建算法状态结构体时一次性确定,不能在循环中更新状态信息。GC算法并不是一种实时算法,但可以得到物体轮廓清晰准确的视差图,适用于静态环境物体的深度重构。

注意GC算法只能在C语言模式下运行,并且不能对视差图进行预先的边界延拓,左右视图和左右视差矩阵的大小必须一致。

 

 

6. 如何实现视差图的伪彩色显示?

首先要将16位符号整形的视差矩阵转换为8位无符号整形矩阵,然后按照一定的变换关系进行伪彩色处理。我的实现代码如下: 

 

// 转换为 CV_8U 格式,彩色显示
dispLfcv = displf, dispRicv = dispri, disp8cv = disp8;
if (alg == STEREO_GC)
{
	cvNormalize( &dispLfcv, &disp8cv, 0, 256, CV_MINMAX );
} 
else
{
	displf.convertTo(disp8, CV_8U, 255/(m_nMaxDisp*16.));
}
F_Gray2Color(&disp8cv, vdispRGB);


 

 

灰度图转伪彩色图的代码,主要功能是使灰度图中 亮度越高的像素点,在伪彩色图中对应的点越趋向于 红色;亮度越低,则对应的伪彩色越趋向于 蓝色;总体上按照灰度值高低,由红渐变至蓝,中间色为绿色。其对应关系如下图所示:

OpenCV学习笔记(19)双目测距与三维重建的OpenCV实现问题集锦(四)三维重建与OpenGL显_第11张图片 

图20

 

 

void F_Gray2Color(CvMat* gray_mat, CvMat* color_mat)
{
	if(color_mat)
		cvZero(color_mat);
		
	int stype = CV_MAT_TYPE(gray_mat->type), dtype = CV_MAT_TYPE(color_mat->type);
	int rows = gray_mat->rows, cols = gray_mat->cols;

	// 判断输入的灰度图和输出的伪彩色图是否大小相同、格式是否符合要求
	if (CV_ARE_SIZES_EQ(gray_mat, color_mat) && stype == CV_8UC1 && dtype == CV_8UC3)
	{
		CvMat* red = cvCreateMat(gray_mat->rows, gray_mat->cols, CV_8U);
		CvMat* green = cvCreateMat(gray_mat->rows, gray_mat->cols, CV_8U);
		CvMat* blue = cvCreateMat(gray_mat->rows, gray_mat->cols, CV_8U);
		CvMat* mask = cvCreateMat(gray_mat->rows, gray_mat->cols, CV_8U);

		// 计算各彩色通道的像素值
		cvSubRS(gray_mat, cvScalar(255), blue);	// blue(I) = 255 - gray(I)
		cvCopy(gray_mat, red);			// red(I) = gray(I)
		cvCopy(gray_mat, green);			// green(I) = gray(I),if gray(I) < 128
		cvCmpS(green, 128, mask, CV_CMP_GE );	// green(I) = 255 - gray(I), if gray(I) >= 128
		cvSubRS(green, cvScalar(255), green, mask);
		cvConvertScale(green, green, 2.0, 0.0);

		// 合成伪彩色图
		cvMerge(blue, green, red, NULL, color_mat);

		cvReleaseMat( &red );
		cvReleaseMat( &green );
		cvReleaseMat( &blue );
		cvReleaseMat( &mask );
	}
}


 

7. 如何将视差数据保存为 txt 数据文件以便在 Matlab 中读取分析?

由于OpenCV本身只支持 xml、yml 的数据文件读写功能,并且其xml文件与构建网页数据所用的xml文件格式不一致,在Matlab中无法读取。我们可以通过以下方式将视差数据保存为txt文件,再导入到Matlab中。 

 

void saveDisp(const char* filename, const Mat& mat)		
{
	FILE* fp = fopen(filename, "wt");
	fprintf(fp, "%02d/n", mat.rows);
	fprintf(fp, "%02d/n", mat.cols);
	for(int y = 0; y < mat.rows; y++)
	{
		for(int x = 0; x < mat.cols; x++)
		{
			short disp = mat.at<short>(y, x); // 这里视差矩阵是CV_16S 格式的,故用 short 类型读取
			fprintf(fp, "%d/n", disp); // 若视差矩阵是 CV_32F 格式,则用 float 类型读取
		}
	}
	fclose(fp);
}


 

相应的Matlab代码为:

  

function img = txt2img(filename)
data = importdata(filename);
r = data(1);    % 行数
c = data(2);    % 列数
disp = data(3:end); % 视差
vmin = min(disp);
vmax = max(disp);
disp = reshape(disp, [c,r])'; % 将列向量形式的 disp 重构为 矩阵形式
%  OpenCV 是行扫描存储图像,Matlab 是列扫描存储图像
%  故对 disp 的重新排列是首先变成 c 行 r 列的矩阵,然后再转置回 r 行 c 列
img = uint8( 255 * ( disp - vmin ) / ( vmax - vmin ) );
mesh(disp);
set(gca,'YDir','reverse');  % 通过 mesh 方式绘图时,需倒置 Y 轴方向
axis tight; % 使坐标轴显示范围与数据范围相贴合,去除空白显示区


 

显示效果如下:

OpenCV学习笔记(19)双目测距与三维重建的OpenCV实现问题集锦(四)三维重建与OpenGL显_第12张图片 

图21



五、三维重建与 OpenGL 显示

.

在获取到视差数据后,利用 OpenCV reProjectImageTo3D 函数结合 Bouquet 校正方法得到的 Q 矩阵就可以得到环境的三维坐标数据,然后利用 OpenGL 来实现三维重构。 OpenCV OpenGL 的编程范例,我在 学习笔记( 15 中有详细的讨论,这里就不重复了,下面补充一些细节问题:

.

1.             reProjectImageTo3D 是怎样计算出三维坐标数据的?

 

OpenCV学习笔记(19)双目测距与三维重建的OpenCV实现问题集锦(四)三维重建与OpenGL显_第13张图片

22

.

 

相信看过 OpenCV 12 章的朋友对上图中的 Q 矩阵不会陌生,根据以上变换公式,按理说 OpenCV 应该也是通过矩阵运算的方式来计算出三维坐标数据的,但实际上仔细查看源代码,会发现 cvReprojectImageTo3D 用了比较奇怪的方法来实现,主要代码如下:

 

[cpp] view plain copy print ?
  1. 02737     for( y = 0; y < rows; y++ )  
  2. 02738     {  
  3. 02739         const float* sptr = (const float*)(src->data.ptr + src->step*y);   // 视差矩阵指针  
  4. 02740         float* dptr0 = (float*)(dst->data.ptr + dst->step*y), *dptr = dptr0;   // 三维坐标矩阵指针  
  5. // 每一行运算开始时,用 当前行号y 乘以Q阵第2列、再加上Q阵第4列,作为初始值  
  6. // 记 qq=[qx, qy, qz, qw]’  
  7. 02741         double qx = q[0][1]*y + q[0][3], qy = q[1][1]*y + q[1][3];      
  8. 02742         double qz = q[2][1]*y + q[2][3], qw = q[3][1]*y + q[3][3];     
  9. …   
  10. // 每算完一个像素的三维坐标,向量qq 累加一次q阵第1列  
  11. // 即:qq = qq + q(:,1)  
  12. 02769         for( x = 0; x < cols; x++, qx += q[0][0], qy += q[1][0], qz += q[2][0], qw += q[3][0] )  
  13. 02770         {  
  14. 02771             double d = sptr[x];  
  15. // 计算当前像素三维坐标  
  16. // 将向量qq 加上 Q阵第3列与当前像素视差d的乘积,用所得结果的第4元素除前三位元素即可  
  17. // [X,Y,Z,W]’ = qq + q(:,3) * d;   iW = 1/W; X=X*iW; Y=Y*iW; Z=Z*iW;  
  18. 02772             double iW = 1./(qw + q[3][2]*d);  
  19. 02773             double X = (qx + q[0][2]*d)*iW;  
  20. 02774             double Y = (qy + q[1][2]*d)*iW;  
  21. 02775             double Z = (qz + q[2][2]*d)*iW;  
  22. 02776             if( fabs(d-minDisparity) <= FLT_EPSILON )  
  23. 02777                 Z = bigZ;   // 02713     const double bigZ = 10000.;  
  24. 02778   
  25. 02779             dptr[x*3] = (float)X;  
  26. 02780             dptr[x*3+1] = (float)Y;  
  27. 02781             dptr[x*3+2] = (float)Z;  
  28. 02782         }  
02737 for( y = 0; y < rows; y++ ) 02738 { 02739 const float* sptr = (const float*)(src->data.ptr + src->step*y); // 视差矩阵指针 02740 float* dptr0 = (float*)(dst->data.ptr + dst->step*y), *dptr = dptr0; // 三维坐标矩阵指针 // 每一行运算开始时,用 当前行号y 乘以Q阵第2列、再加上Q阵第4列,作为初始值 // 记 qq=[qx, qy, qz, qw]’ 02741 double qx = q[0][1]*y + q[0][3], qy = q[1][1]*y + q[1][3]; 02742 double qz = q[2][1]*y + q[2][3], qw = q[3][1]*y + q[3][3]; … // 每算完一个像素的三维坐标,向量qq 累加一次q阵第1列 // 即:qq = qq + q(:,1) 02769 for( x = 0; x < cols; x++, qx += q[0][0], qy += q[1][0], qz += q[2][0], qw += q[3][0] ) 02770 { 02771 double d = sptr[x]; // 计算当前像素三维坐标 // 将向量qq 加上 Q阵第3列与当前像素视差d的乘积,用所得结果的第4元素除前三位元素即可 // [X,Y,Z,W]’ = qq + q(:,3) * d; iW = 1/W; X=X*iW; Y=Y*iW; Z=Z*iW; 02772 double iW = 1./(qw + q[3][2]*d); 02773 double X = (qx + q[0][2]*d)*iW; 02774 double Y = (qy + q[1][2]*d)*iW; 02775 double Z = (qz + q[2][2]*d)*iW; 02776 if( fabs(d-minDisparity) <= FLT_EPSILON ) 02777 Z = bigZ; // 02713 const double bigZ = 10000.; 02778 02779 dptr[x*3] = (float)X; 02780 dptr[x*3+1] = (float)Y; 02781 dptr[x*3+2] = (float)Z; 02782 }

 

OpenCV 的这种计算方式比较令人费解,我的理解是可能这种方式的计算速度比较快。理论上,直接通过矩阵 Q 与向量 [x,y,d,1]’ 的乘积就可以得到相同的结果,下面用 Matlab 来验证一下两种方式是异曲同工的,用 Matlab 按照 OpenCV 计算方式得到的结果称为“ OpenCV method ”,直接按公式计算得到的结果称为“ Equation method ”,用 OpenCV 本身算出的三维坐标作为参考,程序代码如下  

 

 

[c-sharp] view plain copy print ?
  1. close all;clear all;clc   
  2. im = imread('C:/Stereo IO Data/lfFrame_01.jpg');  
  3. data = importdata('C:/Stereo IO Data/disparity_01.txt');  
  4. r = data(1);    % 行数  
  5. c = data(2);    % 列数  
  6. disp = data(3:end); % 视差  
  7. vmin = min(disp);  
  8. vmax = max(disp);  
  9. disp = reshape(disp, [c,r])'; % 将列向量形式的 disp 重构为 矩阵形式  
  10. %  OpenCV 是行扫描存储图像,Matlab 是列扫描存储图像  
  11. %  故对 disp 的重新排列是首先变成 c 行 r 列的矩阵,然后再转置回 r 行 c 列  
  12. img = uint8( 255 * ( disp - vmin ) / ( vmax - vmin ) );  
  13. q = [1. 0. 0. -1.5690376663208008e+002;...  
  14.     0. 1. 0. -1.4282237243652344e+002;...      
  15.     0. 0. 0. 5.2004731331639300e+002;...  
  16.     0. 0. 1.0945105843175637e-002 0.]; % q(4,3) 原为负值,现修正为正值  
  17. big_z = 1e5;  
  18. pos1 = zeros(r,c,3);  
  19. pos2 = zeros(r,c,3);  
  20. for i = 1:r  
  21.     qq = q*[0 i 0 1]';  
  22.     for j = 1:c  
  23.         if disp(i,j)>0  
  24.         % OpenCV method  
  25.             vec = qq + q(:,3)*disp(i,j);  
  26.             vec = vec/vec(4);  
  27.             pos1(i,j,:) = vec(1:3);  
  28.         % Textbook method  
  29.             tmp = q*[j,i,disp(i,j),1]'; % j 是列数,i 是行数,分别对应公式中的 x 和 y  
  30.             pos2(i,j,:) = tmp(1:3)/tmp(4);  
  31.         else  
  32.             pos1(i,j,3) = big_z;  
  33.             pos2(i,j,3) = big_z;  
  34.         end  
  35.         qq = qq + q(:,1);  
  36.     end  
  37. end  
  38. subplot(221);  
  39. imshow(im); title('Left Frame');  
  40. subplot(222);  
  41. imshow(img); title('Disparity map');  
  42. % Matlab按OpenCV计算方式得到的三维坐标  
  43. x = pos1(:,:,1);   
  44. y = -pos1(:,:,2);  % 图像坐标系Y轴是向下为正方向,因此需添加负号来修正  
  45. z = pos1(:,:,3);   
  46. ind = find(z>10000);  % 以毫米为量纲  
  47. x(ind)=NaN; y(ind)=NaN; z(ind)=NaN;  
  48. subplot(234);  
  49. mesh(x,z,y,double(im),'FaceColor','texturemap');  % Matlab 的 mesh、surf 函数支持纹理映射  
  50. colormap(gray);   
  51. axis equal;   
  52. axis([-1000 1000 0 9000 -500 2000]);  
  53. xlabel('Horizonal');ylabel('Depth');zlabel('Vertical'); title('OpenCV method');  
  54. view([0 0]);  % 正视图  
  55. % view([0 90]);   % 俯视图  
  56. % view([90 0]);   % 侧视图  
  57. % Matlab 按公式直接计算得到的三维坐标  
  58. x = pos2(:,:,1);   
  59. y = -pos2(:,:,2);   
  60. z = pos2(:,:,3);   
  61. ind = find(z>10000);  % 以毫米为量纲  
  62. x(ind)=NaN; y(ind)=NaN; z(ind)=NaN;  
  63. subplot(235);  
  64. mesh(x,z,y,double(im),'FaceColor','texturemap');   
  65. colormap(gray);   
  66. axis equal;   
  67. axis([-1000 1000 0 9000 -500 2000]);  
  68. xlabel('Horizonal');ylabel('Depth');zlabel('Vertical'); title('Equation method');  
  69. view([0 0]);  
  70. % 读入OpenCV计算保存到本地的三维坐标作为参考  
  71. data=importdata('C:/Stereo IO Data/xyz.txt');  
  72. x=data(:,1); y=data(:,2); z=data(:,3);  
  73. ind=find(z>1000);  % 以厘米为量纲  
  74. x(ind)=NaN; y(ind)=NaN; z(ind)=NaN;  
  75. x=reshape(x,[352 288])'; % 数据写入时是逐行进行的,而Matlab是逐列读取  
  76. y=-reshape(y,[352 288])';   
  77. z=reshape(z,[352 288])';  
  78. subplot(236)  
  79. mesh(x,z, y,double(im),'FaceColor','texturemap');  
  80. colormap(gray);   
  81. axis equal;axis([-100 100 0 900 -50 200]);  
  82. xlabel('Horizonal');ylabel('Depth');zlabel('Vertical'); title('OpenCV result');  
  83. view([0 0]);  
close all;clear all;clc im = imread('C:/Stereo IO Data/lfFrame_01.jpg'); data = importdata('C:/Stereo IO Data/disparity_01.txt'); r = data(1); % 行数 c = data(2); % 列数 disp = data(3:end); % 视差 vmin = min(disp); vmax = max(disp); disp = reshape(disp, [c,r])'; % 将列向量形式的 disp 重构为 矩阵形式 % OpenCV 是行扫描存储图像,Matlab 是列扫描存储图像 % 故对 disp 的重新排列是首先变成 c 行 r 列的矩阵,然后再转置回 r 行 c 列 img = uint8( 255 * ( disp - vmin ) / ( vmax - vmin ) ); q = [1. 0. 0. -1.5690376663208008e+002;... 0. 1. 0. -1.4282237243652344e+002;... 0. 0. 0. 5.2004731331639300e+002;... 0. 0. 1.0945105843175637e-002 0.]; % q(4,3) 原为负值,现修正为正值 big_z = 1e5; pos1 = zeros(r,c,3); pos2 = zeros(r,c,3); for i = 1:r qq = q*[0 i 0 1]'; for j = 1:c if disp(i,j)>0 % OpenCV method vec = qq + q(:,3)*disp(i,j); vec = vec/vec(4); pos1(i,j,:) = vec(1:3); % Textbook method tmp = q*[j,i,disp(i,j),1]'; % j 是列数,i 是行数,分别对应公式中的 x 和 y pos2(i,j,:) = tmp(1:3)/tmp(4); else pos1(i,j,3) = big_z; pos2(i,j,3) = big_z; end qq = qq + q(:,1); end end subplot(221); imshow(im); title('Left Frame'); subplot(222); imshow(img); title('Disparity map'); % Matlab按OpenCV计算方式得到的三维坐标 x = pos1(:,:,1); y = -pos1(:,:,2); % 图像坐标系Y轴是向下为正方向,因此需添加负号来修正 z = pos1(:,:,3); ind = find(z>10000); % 以毫米为量纲 x(ind)=NaN; y(ind)=NaN; z(ind)=NaN; subplot(234); mesh(x,z,y,double(im),'FaceColor','texturemap'); % Matlab 的 mesh、surf 函数支持纹理映射 colormap(gray); axis equal; axis([-1000 1000 0 9000 -500 2000]); xlabel('Horizonal');ylabel('Depth');zlabel('Vertical'); title('OpenCV method'); view([0 0]); % 正视图 % view([0 90]); % 俯视图 % view([90 0]); % 侧视图 % Matlab 按公式直接计算得到的三维坐标 x = pos2(:,:,1); y = -pos2(:,:,2); z = pos2(:,:,3); ind = find(z>10000); % 以毫米为量纲 x(ind)=NaN; y(ind)=NaN; z(ind)=NaN; subplot(235); mesh(x,z,y,double(im),'FaceColor','texturemap'); colormap(gray); axis equal; axis([-1000 1000 0 9000 -500 2000]); xlabel('Horizonal');ylabel('Depth');zlabel('Vertical'); title('Equation method'); view([0 0]); % 读入OpenCV计算保存到本地的三维坐标作为参考 data=importdata('C:/Stereo IO Data/xyz.txt'); x=data(:,1); y=data(:,2); z=data(:,3); ind=find(z>1000); % 以厘米为量纲 x(ind)=NaN; y(ind)=NaN; z(ind)=NaN; x=reshape(x,[352 288])'; % 数据写入时是逐行进行的,而Matlab是逐列读取 y=-reshape(y,[352 288])'; z=reshape(z,[352 288])'; subplot(236) mesh(x,z, y,double(im),'FaceColor','texturemap'); colormap(gray); axis equal;axis([-100 100 0 900 -50 200]); xlabel('Horizonal');ylabel('Depth');zlabel('Vertical'); title('OpenCV result'); view([0 0]);

 

 

OpenCV学习笔记(19)双目测距与三维重建的OpenCV实现问题集锦(四)三维重建与OpenGL显_第14张图片

23

.

.  

2.             为什么利用修正了的 Q 矩阵所计算得到的三维数据中, Y 坐标数据是正负颠倒的?

 

 

OpenCV学习笔记(19)双目测距与三维重建的OpenCV实现问题集锦(四)三维重建与OpenGL显_第15张图片

24

.

 

这个问题我觉得可以从图像坐标系与摄像机坐标系的关系这一角度来解释。如上图所示,一般图像坐标系和摄像机坐标系都是以从左至右为 X 轴正方向,从上至下为 Y 轴正方向 ,摄像机坐标系的 Z 轴正方向则是从光心到成像平面的垂线方向。因此,我们得到的三维坐标数据中 Y 轴数据的正负与实际是相反的,在应用时要添加负号来修正。

.

3.             如何画出三维重建图像和景深图像?

 

.

利用 cvReprojectImageTo3D 计算出的三维坐标数据矩阵一般是三通道浮点型的,需要注意的是这个矩阵存储的是三维坐标数据,而不是 RGB 颜色值,所以是不能调用 cvShowImage() 或者 OpenCV2.1 版的 imshow() 等函数来显示这个矩阵,否则就会看到这种图像:

.

 

OpenCV学习笔记(19)双目测距与三维重建的OpenCV实现问题集锦(四)三维重建与OpenGL显_第16张图片

25

.

 

这里出现的明显的四个色块,其实应该是由三维坐标数据中的 X 轴和 Y 轴数据造成,不同象限的数据形成相应的色块。

要画出正确的三维重建图像,可以结合 OpenGL (可参考我的 学习笔记( 15 )或者 Matlab (例如保存三维数据到本地然后用 Matlab mesh 函数画出,例程见本文问题 1 ;也可以考虑在 OpenCV 中调用 Matlab 混合编程)来实现。

深度图像的显示相对比较简单,只要从三维坐标数据中分离出来(可用 cvSplit() 函数),经过适当的格式转换(例如转换为 CV_8U 格式),就可用 cvShowImage() 或者 OpenCV2.1 版的 imshow() 等函数来显示了,伪彩色的深度图 也可以参考我的 学习笔记( 18 问题 6 给出的例程 稍作修改即可实现。

.

4.             怎样把 OpenGL 窗口的图像复制到 OpenCV 中用 IplImage 格式显示和保存?

 

.

学习笔记( 15 中详细给出了将 OpenCV 生成的 IplImage 图像和三维坐标数据复制到 OpenGL 中显示的例程,而在应用中,我们有时候也需要把 OpenGL 实时显示的三维图像复制到 OpenCV 中,用 IplImage 格式保存,以便和其它图像组合起来显示或保存为视频文件。这里给出相应的例程以供参考:

首先在创建 OpenGL 窗口时,显示模式要如下设置:

 

[c-sharp] view plain copy print ?
  1. //***OpenGL Window  
  2. glutInit(&argc, argv);  
  3. glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGB);  
  4. glutInitWindowPosition(10,420);  
  5. glutInitWindowSize(glWinWidth, glWinHeight);  
  6. glutCreateWindow("3D disparity image");  
//***OpenGL Window glutInit(&argc, argv); glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGB); glutInitWindowPosition(10,420); glutInitWindowSize(glWinWidth, glWinHeight); glutCreateWindow("3D disparity image");

 

在循环中的调用:

 

[c-sharp] view plain copy print ?
  1.     //////////////////////////////////////////////////////////////////////////  
  2. // OpenGL显示  
  3.         img3dIpl = img3d;  
  4. load3dDataToGL(&img3dIpl);      // 载入需要显示的图像(视差数据)  
  5. loadTextureToGL(&img1roi);      // 显示纹理  
  6. glutReshapeFunc (reshape);          // 窗口变化时重绘图像  
  7. glutDisplayFunc(renderScene);       // 显示三维图像  
  8. glutPostRedisplay();                // 刷新画面(不用此语句则不能动态更新图像)  
  9. loadPixel2IplImage(imgGL);          // 将 OpenGL 生成的像素值存储到 IplImage 中  
////////////////////////////////////////////////////////////////////////// // OpenGL显示 img3dIpl = img3d; load3dDataToGL(&img3dIpl); // 载入需要显示的图像(视差数据) loadTextureToGL(&img1roi); // 显示纹理 glutReshapeFunc (reshape); // 窗口变化时重绘图像 glutDisplayFunc(renderScene); // 显示三维图像 glutPostRedisplay(); // 刷新画面(不用此语句则不能动态更新图像) loadPixel2IplImage(imgGL); // 将 OpenGL 生成的像素值存储到 IplImage 中   

 

loadGLPixelToIplImage 函数定义:

 

 

[c-sharp] view plain copy print ?
  1. //////////////////////////////////////////////////////////////////////////  
  2. // 将OpenGL窗口像素存储到 IplImage 中  
  3. void  loadGLPixelToIplImage(IplImage* img)  
  4. {  
  5.     const int n = 3*glWinWidth*glWinHeight;   
  6.     float *pixels = (float *)malloc(n * sizeof(GL_FLOAT));   
  7.     IplImage *tmp = cvCreateImage(cvSize(glWinWidth, glWinHeight), 8, 3);  
  8.     tmp->origin = CV_ORIGIN_BL;  
  9. /* 后台缓存的图像数据才是我们需要复制的,若复制前台缓存会把可能的叠加在OpenGL窗口上的对象(其它窗口或者鼠标指针)也复制进去*/  
  10.     glReadBuffer(GL_BACK);        
  11.     glReadPixels(0, 0, glWinWidth, glWinHeight, GL_RGB, GL_FLOAT, pixels);  
  12.     int k = 0;  
  13.     for(int i = 0 ; i < glWinHeight; i++)  
  14.     {  
  15.         for(int j = 0 ; j < glWinWidth; j++,k+=3)  
  16.         {  
  17.             CvPoint pt = {j, glWinHeight - i - 1};  
  18.             uchar* temp_ptr = &((uchar*)(tmp->imageData + tmp->widthStep*pt.y))[pt.x*3];  
  19.             //OpenGL采用的是BGR格式,所以,读出来以后,还要换一下R<->B,才能得到RGB  
  20.             temp_ptr[0] = pixels[k+2] * 255; //blue   
  21.             temp_ptr[1] = pixels[k+1] * 255; //green  
  22.             temp_ptr[2] = pixels[k] * 255;   //red  
  23.         }  
  24.     }     
  25.     cvResize(tmp, img);  
  26.     // 释放内存  
  27.     free(pixels);  
  28.     cvReleaseImage(&tmp);  
  29. }  
////////////////////////////////////////////////////////////////////////// // 将OpenGL窗口像素存储到 IplImage 中 void loadGLPixelToIplImage(IplImage* img) { const int n = 3*glWinWidth*glWinHeight; float *pixels = (float *)malloc(n * sizeof(GL_FLOAT)); IplImage *tmp = cvCreateImage(cvSize(glWinWidth, glWinHeight), 8, 3); tmp->origin = CV_ORIGIN_BL; /* 后台缓存的图像数据才是我们需要复制的,若复制前台缓存会把可能的叠加在OpenGL窗口上的对象(其它窗口或者鼠标指针)也复制进去*/ glReadBuffer(GL_BACK); glReadPixels(0, 0, glWinWidth, glWinHeight, GL_RGB, GL_FLOAT, pixels); int k = 0; for(int i = 0 ; i < glWinHeight; i++) { for(int j = 0 ; j < glWinWidth; j++,k+=3) { CvPoint pt = {j, glWinHeight - i - 1}; uchar* temp_ptr = &((uchar*)(tmp->imageData + tmp->widthStep*pt.y))[pt.x*3]; //OpenGL采用的是BGR格式,所以,读出来以后,还要换一下R<->B,才能得到RGB temp_ptr[0] = pixels[k+2] * 255; //blue temp_ptr[1] = pixels[k+1] * 255; //green temp_ptr[2] = pixels[k] * 255; //red } } cvResize(tmp, img); // 释放内存 free(pixels); cvReleaseImage(&tmp); }

 

 

显示效果如下:

 

OpenCV学习笔记(19)双目测距与三维重建的OpenCV实现问题集锦(四)三维重建与OpenGL显_第17张图片

图26 

 

 

 

(待续)

.

.

你可能感兴趣的:(OpenCV学习笔记(19)双目测距与三维重建的OpenCV实现问题集锦(四)三维重建与OpenGL显)