Bootstrap

立体匹配视差图生成的PCL点云分层、放射状等杂乱效果的解决方法

背景

    博主最近在学习三维重建中的立体匹配,在OpenCV中使用sgbm和bm算法时,碰到了一个棘手的问题,PCL点云出现分层的效果,让我百思不得其解,csdn翻了好多页,也没比较明确的解答(可能我的问题比较笨吧…)。在我翻了不少博客、代码例程和向前辈请教之后,最后修改了一些细节,算是能显示出点云图。
在这里插入图片描述

    在搜索解决方法时,发现还有挺多人碰到过类似的问题,这种感受很难受,就像你检查了很多遍,感觉没什么问题 代码就该这样写,但效果就是出不来,搁置不前,原地打转。所以我觉得,我还是记录下解决这个问题的过程,给以后掉进坑里的老铁们有一个参考的方向(本菜鸡真的在这个问题上耽误了好多时间),不说废话了,进入正题——


问题1:点云分层、断层

在这里插入图片描述

    如果出现以上类似效果,PCL点云像是很多2d平面图像层叠出来的问题,首先给出结论:转换点云时使用的视差图格式不对
    以OpenCV-sgbm为例,先调sgbm中的参数、至视差图既平滑又少空洞。
    在阅读网上许多代码后发现,用于计算距离的视差图(CV_32F)和用于肉眼看的视差图(CV_8U)使用的格式不同,并且用于计算的视差图无需进行裁剪和归一化,这些只是为了显示的可读性和美观。所以,在对sgbm进行compute之后得到视差图disparity_sgbm,除以16得到用于计算的视差图disparity(除以16是因为每个像素值由一个16bit表示,其中低位的4位存储的是视差值得小数部分,所以真实视差值应该是该值除以16)。

	cv::Mat disparity_sgbm, disparity, disparityCV_8U;
	sgbm->compute(rectifyImageL, rectifyImageR, disparity_sgbm);
	disparity_sgbm.convertTo(disparity, CV_32F, 1.0 / 16.0f);     //用来测距

    将compute后的视差图,转为8位,得到可视的视差图disparityCV_8U,可以作为sgbm调参的直观依据,没有后续的作用了。

	disparity_sgbm.convertTo(disparityCV_8U, CV_8U, 255 / (NumDisparities * 16.));   //用来调参直观参考
    imshow("disparityCV_8U",disparityCV_8U);

    转换了正确的视差图格式之后,在计算点云时还要注意计算深度值的取点语句,数据类型也要对应好。

	……
	for (int i = 0; i < disparity.rows; i++){
        for (int j = 0; j < disparity.cols; j++) {
            PointXYZRGB p;
            double d = disparity.ptr<float>(i)[j];  //注意数据类型
    ……

    ok,关于点云分层、深度不连续的问题就解决啦,附上以上代码涉及到的两个知识点:图像格式(矩阵类型)和convertTo函数,配合阅读有助于深入认识。

图像格式(矩阵类型)

type()=0CV_8U八位无符号整型 (uchar)0–255
1CV_8S八位有符号整型 (schar)-128–127
2CV_16U十六位无符号整型 (ushort)0–65535
3CV_16S十六位有符号整型 (short)-32768–32767
4CV_32S三十二位有符号整型 (int)0–65535
5CV_32F三十二位浮点数 (float)0.0–1.0
6CV_64F六十四位浮点数 (double)0.0–1.0

在这里插入图片描述

convertTo函数
xxx.convertTo(dst, type, scale, shift)

dst:目的矩阵;
type:需要的输出矩阵类型,或者更明确的,是输出矩阵的深度,如果是负值(常用-1)则输出矩阵和输入矩阵类型相同。转换位深度本质上就是对原深度下的数据做线性变换,使原位深度下的最小值和最大值分别对应转换后位深度下的最小值和最大值。
scale:范围比例因子;eg. 原来的图是255的灰度图,转成float型数据并归一化到0~1,那么就是scale=1.0/255
shift:将输入数组元素按比例缩放后添加的值;

    目前OpenCV主要只支持单通道和3通道的图像,并要求其深度为8bit和16bit无符号(即CV_16U),所以其他是不支持的,比如说float型等。
    如果Mat类型数据的深度不满足上面的要求,则需要使用convertTo()函数来进行转换。convertTo()函数负责转换数据类型不同的Mat,即可以将类似float型的Mat转换到imwrite()函数能够接受的类型。


问题2:点云放射、发散、锥形,总之不能看

在这里插入图片描述
在这里插入图片描述
    如果出现以上类似效果,PCL点云是发散、放射、圆锥形的问题,先上结论:点云可能是对的,但发散出来大多都是无效点,除掉即可
    这个问题太过于细节,以致于排查时无从下手,反而去检查了许多点云坐标的计算和PCL的设置语句,也花了不少时间。其实仔细观察这个问题和上一类还不一样,这里的点云锥顶部份比较稠密,而且分层也不是那么规律。再考虑到相机硬件本身的局限性,基线的长度决定了测距的有效范围,超过范围的点误差就会很大,这些点也是没有意义的。
    所以,解决的方法也很简单,在计算点云的双for循环中添加以下语句,把视差值小于80或者深度大于60cm的点剔除掉即可。(这里的数字不一定适配你的硬件,自行修改)

	if (d<=80)continue; //d是double d = disparity.ptr<float>(i)[j]

结束

在这里插入图片描述

    好了,我碰到的问题到此就解决了,点云终于是——能看了!当然这只是最低标准,后续还应做更多的优化处理,让点云更漂亮。其实事后发现都是细节问题,微调即可解决,但没排查出来时可真要命。
    在解决PCL生成点云分层过程中,也得知了其他解决方法。其一是标定参数出现问题,但我的标定参数是从MATLAB中取出的,所以应该没问题。其二是改用cv::stereoRectify(cameraMatrixL, distCoeffL, cameraMatrixR, distCoeffR,rectifyImageL.size(), R, t, Rl, Rr, Pl, Pr, Q )生成的Q重投影矩阵计算点云中的点,避免求点繁杂的计算,感觉也不是很关键。附上Q重投影矩阵的含义:
在这里插入图片描述
Cx:左相机光心在像素坐标系的x
Cy:左相机光心在像素坐标系的y
Cx’:右相机光心在像素坐标系的x
Tx:立体校正后光心距离
d:视差
f:焦距


    如果大家觉得文章对你有所帮助,麻烦大家帮忙点个赞。O(∩_∩)O
    欢迎大家在评论区交流讨论。

;