当前位置: 代码迷 >> 综合 >> kinect 2.0 学习笔记_实时平滑Kinect深度帧(像素滤波器)
  详细解决方案

kinect 2.0 学习笔记_实时平滑Kinect深度帧(像素滤波器)

热度:92   发布时间:2023-11-25 05:58:46.0

参考:

https://blog.csdn.net/jiaojialulu/article/details/53192887?locationnum=15&fps=1(jiaojialulu)

https://www.codeproject.com/Articles/317974/KinectDepthSmoothing 

    今天我们解决一个问题:如何平滑深度图的噪声点(深度值为0)。

    解决办法:像素滤波器加权移动平均两种方法。这两种方法可以单独使用,也可以串联使用,以产生平滑的输出。虽然这个解决方案并不能完全消除所有的噪音,但它确实带来了明显的不同。使用的解决方案不会降低帧速率,并且能够产生实时的结果输出到屏幕或记录。

    一直在看jiaojialulu的博客学习kinect 2.0的使用,虽然现在离我本身要完成的东西已经走偏了,但是还是觉得有意思,博主很优秀,很热心,所以跟着玩玩,对自己也是成长。

图1

   

图2


图3

    离Kinect更近的物体颜色更浅,更远的物体更暗。它的有效距离是0.8-4m。在这个范围之外的物体就会表现为无数据,即深度值为0。

     正如您已经知道的,即使没有视频提要的闪烁,质量也很低。从Kinect获得深度数据的最高分辨率是512x424,但即使是这个分辨率,质量看起来也很差。数据中的噪声表现为连续不断地出现在图像中的白点。数据中的一些噪音来自于被它撞击的物体散射的红外线。

     可能今天所在环境灯光太亮,导致的,感觉噪声点及其多,多的让人发指。还以为是我代码写错了,吓得我呀。

     先看下像素滤波器这种方法及其原理:

 step 1:第一步是将深度数据帧转换为我们方便处理的形式,比如UINT16[]。(如图1效果,是不是看不清,哈哈哈)

 Mat i_depth(424, 512, CV_16UC1);//注意呀是16位
// 实际是16位unsigned int数据
hr = m_pDepthFrame->CopyFrameDataToArray(424 * 512, reinterpret_cast<UINT16*>(i_depth.data))

          当然为了表现的明显:也可以这么写代码

 Mat i_depth(424, 512, CV_8UC1);//注意呀是8位
hr = m_pDepthFrame->CopyFrameDataToArray(424 * 512, depthData);
for (int i = 0; i < 512 * 424; i++){// 0-255深度图,为了显示明显,只取深度数据的低8位BYTE intensity = static_cast<BYTE>(depthData[i] % 256);reinterpret_cast<BYTE*>(i_depth.data)[i] = intensity;}

 SDK 2.0demo中就是这么写的,但是我个人觉得要实际的深度还是第一种写法吧。

        当然如果还是想表现的更加明显点,可以这么写(图3):

// 1. 深度图并显示Mat i_depth(424, 512, CV_16UC1);// 16位无符号的单通道-- - 灰度图片i_depth = imread("depth.png", IMREAD_ANYDEPTH);//IMREAD_ANYDEPTH等价于2,如果图像深度为16位则读出为16位,32位则读出为32位,其余的转化为8位Mat i_before(424, 512, CV_8UC4);				// 为了显示方便  一般的图像文件格式使用的是 Unsigned 8bits -8位无符号的四通道---带透明色的RGB图像 Mat i_after(424, 512, CV_8UC4);				        // 为了显示方便Mat i_result(424, 512, CV_16UC1);				// 滤波结果 16位无符号的单通道---灰度图片unsigned short maxDepth = 0;unsigned short* depthArray = (unsigned short*)i_depth.data;//图像矩阵是一个二维数组,不论是灰度图像还是彩色图像,在计算机内存中都是以一维数组的形式存储的unsigned short iZeroCountBefore = 0;unsigned short iZeroCountAfter = 0;for (int i = 0; i < 512 * 424; i++){int row = i / 512;int col = i % 512;unsigned short depthValue = depthArray[row * 512 + col];if (depthValue == 0){i_before.data[i * 4] = 255;i_before.data[i * 4 + 1] = 0;i_before.data[i * 4 + 2] = 0;i_before.data[i * 4 + 3] = depthValue / 256;iZeroCountBefore++;}else{i_before.data[i * 4] = depthValue / 8000.0f * 256;i_before.data[i * 4 + 1] = depthValue / 8000.0f * 256;i_before.data[i * 4 + 2] = depthValue / 8000.0f * 256;i_before.data[i * 4 + 3] = depthValue / 8000.0f * 256;}maxDepth = depthValue > maxDepth ? depthValue : maxDepth;}

     上面时重新定义了一个深度图,不是实际的深度图,只是用于显示而已。按字节改变bgra 。注意深度图保存是后缀必须是.png.

   step 2:

     现在我们有了一个更容易处理的数组,我们可以开始对它应用实际的过滤器。我们扫描整个数组,逐像素,寻找零值。这些是Kinect无法正常处理的值。我们希望在不降低性能或减少数据的其他特性的情况下尽可能多地删除这些特性(稍后详细介绍)。当我们在数组中找到一个零值时,它被认为是过滤的候选,我们必须仔细观察。特别地,我们想看看相邻的像素。过滤器有效地在候选像素周围有两个“波段”,用于搜索其他像素中的非零值。过滤器创建这些值的频率分布,并注意在每个频段中找到了多少。然后,它会将这些值与每个频带的任意阈值进行比较,以确定是否应该筛选候选者。如果任何一个频段的阈值被打破,那么所有非零值的统计模式都将应用于该候选项,否则它将被单独保留。

   

     对于这种方法,最大的考虑是确保过滤器的带实际上包围像素,因为它们将显示在呈现的图像中,而不仅仅是深度数组中相邻的值。应用这个过滤器的代码如下所示:原文使用的是C#,这里用的全部是jiaojialulu的代码,非常感谢博主的分享,如下的C++:

// 滤波后深度图的缓存unsigned short* smoothDepthArray = (unsigned short*)i_result.data;// 我们用这两个值来确定索引在正确的范围内int widthBound = 512 - 1;int heightBound = 424 - 1;// 内(8个像素)外(16个像素)层阈值int innerBandThreshold = 3;int outerBandThreshold = 7;// 处理每行像素for (int depthArrayRowIndex = 0; depthArrayRowIndex<424;depthArrayRowIndex++){// 处理一行像素中的每个像素for (int depthArrayColumnIndex = 0; depthArrayColumnIndex < 512; depthArrayColumnIndex++){int depthIndex = depthArrayColumnIndex + (depthArrayRowIndex * 512);// 我们认为深度值为0的像素即为候选像素if (depthArray[depthIndex] == 0){// 通过像素索引,我们可以计算得到像素的横纵坐标int x = depthIndex % 512;int y = (depthIndex - x) / 512;// filter collection 用来计算滤波器内每个深度值对应的频度,在后面// 我们将通过这个数值来确定给候选像素一个什么深度值。unsigned short filterCollection[24][2] = {0};// 内外层框内非零像素数量计数器,在后面用来确定候选像素是否滤波int innerBandCount = 0;int outerBandCount = 0;// 下面的循环将会对以候选像素为中心的5 X 5的像素阵列进行遍历。这里定义了两个边界。如果在// 这个阵列内的像素为非零,那么我们将记录这个深度值,并将其所在边界的计数器加一,如果计数器// 高过设定的阈值,那么我们将取滤波器内统计的深度值的众数(频度最高的那个深度值)应用于候选// 像素上for (int yi = -2; yi < 3; yi++){for (int xi = -2; xi < 3; xi++){// yi和xi为操作像素相对于候选像素的平移量// 我们不要xi = 0&&yi = 0的情况,因为此时操作的就是候选像素if (xi != 0 || yi != 0){// 确定操作像素在深度图中的位置int xSearch = x + xi;int ySearch = y + yi;// 检查操作像素的位置是否超过了图像的边界(候选像素在图像的边缘)if (xSearch >= 0 && xSearch <= widthBound &&ySearch >= 0 && ySearch <= heightBound){int index = xSearch + (ySearch * 512);// 我们只要非零量if (depthArray[index] != 0){// 计算每个深度值的频度for (int i = 0; i < 24; i++){if (filterCollection[i][0] == depthArray[index]){// 如果在 filter collection中已经记录过了这个深度// 将这个深度对应的频度加一filterCollection[i][1]++;break;}else if (filterCollection[i][0] == 0){// 如果filter collection中没有记录这个深度// 那么记录filterCollection[i][0] = depthArray[index];filterCollection[i][1]++;break;}}// 确定是内外哪个边界内的像素不为零,对相应计数器加一if (yi != 2 && yi != -2 && xi != 2 && xi != -2)innerBandCount++;elseouterBandCount++;}}}}}// 判断计数器是否超过阈值,如果任意层内非零像素的数目超过了阈值,// 就要将所有非零像素深度值对应的统计众数if (innerBandCount >= innerBandThreshold || outerBandCount >= outerBandThreshold){short frequency = 0;short depth = 0;// 这个循环将统计所有非零像素深度值对应的众数for (int i = 0; i < 24; i++){// 当没有记录深度值时(无非零深度值的像素)if (filterCollection[i][0] == 0)break;if (filterCollection[i][1] > frequency){depth = filterCollection[i][0];frequency = filterCollection[i][1];}}smoothDepthArray[depthIndex] = depth;}else{smoothDepthArray[depthIndex] = 0;}}else{// 如果像素的深度值不为零,保持原深度值smoothDepthArray[depthIndex] = depthArray[depthIndex];}}}

      作者最近更新了这个过滤器,与我原来的帖子相比更加准确。在作者最初的文章中,如果任何一个带阈值被破坏,则将滤波器矩阵中所有非零像素的统计平均值赋给候选像素;我把它改成了统计模式(也就是上面的代码)。为什么这很重要?

   

    上图主要表明了采用众数,即滤波器框内频数最高的一个深度值来作为候选像素的深度值,要比直接采用框内所有深度值的平均要更加符合实际(我觉得如果改成内层的众数更好)。

    考虑前面的图,表示深度值的理论过滤矩阵。通过观察这些值,我们可以直观地识别出在我们的过滤器矩阵中可能存在某个对象的边。如果我们将所有这些值的平均值应用到候选像素上,它会从X,Y的角度去除噪声但它会引入Z,Y的角度的噪声;将候选像素的深度放在两个独立特征之间的中间位置。通过使用统计模式,我们可以确保为匹配滤波器矩阵中最主要特征的候选像素分配深度。作者觉得:说“大多数”是因为仍然有机会识别一个顺从的特性作为主导,因为深度读数中有很小的差异;但这对结果的影响微乎其微。

   step3 滤波效果:

    右图是滤波后的效果图:可以看到物体边缘散乱的深度值为0的点已经减少了不少。


                                  

从上面的结果看呀:

     原来的深度值为0的值为 19971 平滑后为11008

     但是有个不明白的点哈,就是最大深度值既然为0.7959m,有点不可思议!!!why?

彩蛋:

对【OpenCV】访问Mat图像中每个像素的值有疑惑的可以看看:

         https://blog.csdn.net/xiaowei_cqu/article/details/7771760

对OpenCV中Mat的data成员解析有疑惑,可以看看:

         https://blog.csdn.net/zy122121cs/article/details/47029431。主要就是:

       在访问和修改图像矩阵像素值的时候,我们经常会用到at,ptr,以及迭代器MatIterator等,对于用Mat存储的图像的像素值的访问方法,此篇博文已经介绍得颇为详细http://blog.csdn.net/xiaowei_cqu/article/details/7771760,本文的重点在于用data访问图像元素值的时候遇到的一些问题。图像矩阵是一个二维数组,不论是灰度图像还是彩色图像,在计算机内存中都是以一维数组的形式存储的。用Mat存储一幅图像时,若图像在内存中是连续存储的(Mat对象的isContinuous == true),则可以将图像的数据看成是一个一维数组,而其data(uchar*)成员就是指向图像数据的第一个字节的,因此可以用data指针访问图像的数据,那么问题来了,OpenCV中将data定义为uchar*,而当我们用构造函数创建一个Mat对象的时候,可以指定图像的数据类型有CV_8UC1、CV_8UC3、CV_32FC1、CV_32FC3等多种,那么我们如何通过data指针去访问和修改图像的某一个像素值呢,对于数据为uchar类型的Mat对象,可以直接用data访问和修改,对于数据为float或double类型的Mat对象,我们同样可以用data对图像的某个像素值进行访问和修改操作,方法就是将data强制转换成指向Mat对象对应数据类型的指针。
  相关解决方案