最近在学习Kinect 2.0的SDK,并基于此进行数据获取。本人参考了优快云很多人写的博客,受益匪浅,同时我也在学习过程中发现了一些坑(也可能是我自己经验不够,没理解透彻)。这里做个记录以提醒自己,也希望能给其他人提供一些参考。
注:我主要参考了该博主的文章https://blog.youkuaiyun.com/jiaojialulu/article/details/53087988,以及“kinect2_grabber.h”(利用OPENNI2使用KinectV2获取数据的示例代码)进行了数据获取代码的编写。
1、Kinect V2的坐标系定义和OpenCV定义的坐标系是不同的。这包括:
1)以矩阵来看的图像坐标系(图像空间),包括深度、RGB、IR等数据帧对应的图像数据,其坐标都以像素为单位;
2)以点云数据xyz来看的3D坐标系(相机空间),其坐标为米(m)。
如果需要将Kinect V2的坐标系与OpenCV坐标系进行统一(可以这么思考:与OpenCV坐标系保持一致时,获取的二维图像数据,其视角与“将我们自己的眼睛放在相机位置向前看”是一致的,主要是左右关系!),可进行以下变换:
1)图像空间(坐标系)下,令矩阵列不变,行翻转(左右翻转)
例如,假设获取的(原始)纹理RGB数据(将RGB图匹配到点云数据上作为纹理的RGB图像)如下:
cv::Mat textureDepthRGB(424, 512, CV_8UC3);
用cv::imshow()显示如下:
注意到上图中红圈中的书,其实是位于相机视角(向前)的左手边,而显示中却位于了右手边,书名是镜像的现象也体现出这一点。
采用如下代码进行简单的左右翻转后,结果如下:
cv::flip(textureDepthRGB, textureDepthRGB, 1);// 点云纹理 rgb 信息
注意到,此时红圈中的书是位于左手边的,且书名显示正常。
以上用纹理RGB数据做了示例,而depth,color,infrared等数据都是类似的。如果需要将图像坐标系统一为与OpenCV一致,只需要左右翻转。
2)以点云数据xyz来考虑的3D坐标系(相机空间)时,将其统一为OpenCV坐标系定义,需要:x取反,y取反,z不变
Kinect的相机空间指的是其使用的3D空间坐标,定义如下:
- 坐标原点(x=0, y=0, z=0)位于kinect的红外相机中心;
- X 轴方向为顺着kinect的照射方向的左方向;
- Y 轴方向为顺着kinect的照射方向的上方向;
- Z 轴方向为顺着kinect的照射方向;
- 坐标单位为米(m);
注:上述Kinect的坐标系满足右手系,图片摘自博客https://blog.youkuaiyun.com/jiaojialulu/article/details/53088170
下方截图是OpenCV中定义的相机空间(同样满足右手系),来自https://docs.opencv.org/3.4.6/d9/d0c/group__calib3d.html
可以看到,如果将OpenCV的坐标系定义用在Kinect相机空间中,则:X轴方向为顺着Kinect照射方向的右方向,Y轴方向为顺着Kinect照射方向的下方向,Z轴方向为顺着Kinect的照射方向,因此需要将Kinect的默认相机空间坐标系中的 x 坐标取反,y 坐标取反,z 坐标不变。
代码示例如下:
cv::Mat xyzDepth(424, 512, CV_32FC3);// 点云 xyz 数据
for (int iRows = 0; iRows < 424; iRows++)
{
for (int iCols = 0; iCols < 512; iCols++)
{
// x 取反
xyzDepth.at<cv::Vec3f>(iRows, iCols)[0] = - xyzDepth.at<cv::Vec3f>(iRows, iCols)[0];
// y 取反
xyzDepth.at<cv::Vec3f>(iRows, iCols)[1] = - xyzDepth.at<cv::Vec3f>(iRows, iCols)[1];
// z 不变
}
}
2、参考该博主文章https://blog.youkuaiyun.com/jiaojialulu/article/details/53087988获取多源数据帧后,
// 获取新的一个多源数据帧
hr = m_pMultiFrameReader->AcquireLatestFrame(&m_pMultiFrame);
if (FAILED(hr) || !m_pMultiFrame)
{
continue;
}
---------------------
作者:jiaojialulu
来源:优快云
原文:https://blog.youkuaiyun.com/jiaojialulu/article/details/53087988
版权声明:本文为博主原创文章,转载请附上博文链接!
接下来进行“从多源数据帧中分离出彩色数据,深度数据和红外数据”。
原作者上面的代码保证了获取最新的多源数据帧一定是成功的(否则会continue直到成功),我原以为下面进行分离的时候也一定会成功(作者在分离彩色、深度等数据时用了if (SUCCEEDED(hr)),其实就表明了是有可能失败的,但我自己并未留意),直到后来我发现:即使获取最新的多源数据帧成功了,在分理处彩色数据,深度数据和红外数据时也是会失败的!这样一旦发生失败,后续的拷贝CopyFrameDataToArray()操作就并未进行。这等于说,此次循环中,多源数据帧获取成功了,但并未分离数据也并没有拷贝成功!
这一点可能在绝大多数情况下都不会有影响,但是我目前进行的工作中需要实时获取数据、处理,并记录时间戳,保证每一次获取都是同步的color,depth,infrared帧以及时间戳等信息,这就显得很重要。因为一旦在while循环中,在if (SUCCEEDED(hr))以外(比如第119行以后的代码,对拷贝得到的 i_rgb 数据进行处理时),你以为处理的是最新帧数据,但其实还是上一次获取的数据。
我认为这个小点隐藏的比较深,除非代码都用在了if (SUCCEEDED(hr))中,不然一定会在某个不经意的地方入坑。我也是在一个意外情况下才发现的。
所以,为了完备和避免时间久了忘记:我认为不仅应该使用 if 语句,还应该设置 else 的情况。例如:
if (SUCCEEDED(hrResult))
{
hrResult = m_pMultiFrame->get_ColorFrameReference(&m_pColorFrameReference);
}
else
{
SafeRelease(m_pColorFrame);
SafeRelease(m_pDepthFrame);
SafeRelease(m_pInfraredFrame);
SafeRelease(m_pColorFrameReference);
SafeRelease(m_pDepthFrameReference);
SafeRelease(m_pInfraredFrameReference);
SafeRelease(m_pMultiFrame);
continue;
}
if (SUCCEEDED(hrResult))
{
hrResult = m_pColorFrameReference->AcquireFrame(&m_pColorFrame);
}
else
{
SafeRelease(m_pColorFrame);
SafeRelease(m_pDepthFrame);
SafeRelease(m_pInfraredFrame);
SafeRelease(m_pColorFrameReference);
SafeRelease(m_pDepthFrameReference);
SafeRelease(m_pInfraredFrameReference);
SafeRelease(m_pMultiFrame);
continue;
}
对每一次拷贝和相关操作,加上相应的else条件,可以保证后续处理都是针对最新帧的。另外,还要注意else中对指针的release,否则重新回到while循环开始处进行Multi帧获取时是会出错的。
—— 后续想到什么再更新