RGBD深度相机数据格式-存储读取-可视化

设备

此处RGBD相机主要指利用Light Coding(结构光)或TOF进行成像的深度相机,主动发光,主要适用于室内环境。

比较常见的RGBD相机如下表:

相机 原理 SDK 资料
PrimeSense Carmine 1.08/1.09 (2012) Light Coding(散斑) OpenNI 链接
Xbox One Kinect(Kinect v2,2013) TOF OpenNI/Kinect SDK
ASUS XTION 2 / XTION PRO LIVE(CARMINE 1.08的马甲) Light Coding OpenNI 链接
Intel RealSense(D435 etc.) Light Coding(编码) OpenNI/Realsense Camera SDK 链接
ZED (2016) Stereo

数据格式

通常深度数据使用整形数进行表示,范围在0-65535之间,类型为UINT16,即unsigned short,无符号2字节
单位为毫米(mm),但是某些SDK也支持使用100微米(um)来进行表示,如OpenNI支持的深度数据格式有PIXEL_FORMAT_DEPTH_1_MM,PIXEL_FORMAT_DEPTH_100_UM等。至于分辨率则通常是万年的640x480,有些甚至是320x240插值上去的。

由于Depth有一定的有效工作范围,以PrimeSense Carmine 1.09为例,其工作范围只有0.35-1.4m,即数据范围是350-1400,通常会使用一个数值来表示无效的数据,如0或65535。

通常RGB Sensor和Depth Sensor的FOV并不一致,所以需要进行配准,配准的目的是使得RGB的像素和Depth的像素一一对应。谁向谁来配准倒是没有那么重要,深度图也可以被插值,与彩色图类似。

以OpenNI为例,里面有一个这样的接口,setImageRegistrationMode(IMAGE_REGISTRATION_DEPTH_TO_COLOR),其效果是裁剪缩放深度图使得其FOV与RGB完全相同(但其实边缘已经没有了,全部都是无效的Depth像素)。

存储读取

根据之前的描述,深度图的表示也很简单,如下:

char RGBData[640][480][3];
typedef unsigned short UINT16,*PUINT16;
UINT16 DepthData[640][480];

我们可以自己实现保存与读取的方法,也可以借助一些第三方SDK,如OpenCV进行读写,
在操作深度图时,OpenCV使用的格式是CV_16U,举个例子:

inline cv::Mat getMat(const VideoFrameRef &frame,int dataStride,int openCVFormat){
     
    //深度图PNG16类型(short),Little-Endian(低位在低字节)
    //RGB图BGR24类型
    int h=frame.getHeight();
    int w=frame.getWidth();
    cv::Mat image=cv::Mat(h,w,openCVFormat);
    memcpy(image.data,frame.getData(),h*w*dataStride);
    //cv::flip(image,image,1);
    return image;
}

getMat(frame,2,CV_16U); //For Depth
getMat(frame,3,CV_8UC3); //For RGB

深度图的Mat可以保存为PNG16格式,直接保存即可,读取时记得加上cv::IMREAD_UNCHANGED的FLAG

可视化

深度图的可视化有多种方法,如三维点云,Mesh等,但是通常获取的深度数据是一张二维的图像,需要转换成三维数据才可以显示,二维图像则采用colormap来进行显示。

获取三维坐标

在OpenNI中,有这样一个接口,如下:

UINT16 deep=depthImage.at<UINT16>(row,col);
float fx, fy, fz;
openni::CoordinateConverter::convertDepthToWorld(depth, col, row, deep, &fx, &fy, &fz);

这个是什么意思呢,我们来看一下具体实现:

OniStatus VideoStream::convertDepthToWorldCoordinates(float depthX, float depthY, float depthZ, float* pWorldX, float* pWorldY, float* pWorldZ)
{
     
	if (m_pSensorInfo->sensorType != ONI_SENSOR_DEPTH)
	{
     
		m_errorLogger.Append("convertDepthToWorldCoordinates: Stream is not from DEPTH\n");
		return ONI_STATUS_NOT_SUPPORTED;
	}

	float normalizedX = depthX / m_worldConvertCache.resolutionX - .5f;
	float normalizedY = .5f - depthY / m_worldConvertCache.resolutionY;

	*pWorldX = normalizedX * depthZ * m_worldConvertCache.xzFactor;
	*pWorldY = normalizedY * depthZ * m_worldConvertCache.yzFactor;
	*pWorldZ = depthZ;
	return ONI_STATUS_OK;
}

OniStatus VideoStream::convertWorldToDepthCoordinates(float worldX, float worldY, float worldZ, float* pDepthX, float* pDepthY, float* pDepthZ)
{
     
	if (m_pSensorInfo->sensorType != ONI_SENSOR_DEPTH)
	{
     
		m_errorLogger.Append("convertWorldToDepthCoordinates: Stream is not from DEPTH\n");
		return ONI_STATUS_NOT_SUPPORTED;
	}

	*pDepthX = m_worldConvertCache.coeffX * worldX / worldZ + m_worldConvertCache.halfResX;
	*pDepthY = m_worldConvertCache.halfResY - m_worldConvertCache.coeffY * worldY / worldZ;
	*pDepthZ = worldZ;
	return ONI_STATUS_OK;
}

一般来说我们直接将深度表示为Z坐标,而X与Y则与Z坐标有一个对应关系,未必是正比,可能是一个系数,m_worldConvertCache的数据结构如下,同时标注出了一个可能的参数列表,不同的相机可能不同:

struct WorldConversionCache {
     
    float xzFactor=1.342392;
    float yzFactor=1.006794;
    float coeffX=476.760925;
    float coeffY=476.760956;
    float distanceScale=1.000000;
    int resolutionX=640;
    int resolutionY=480;
    int halfResX=320;
    int halfResY=240;
};

有时相机厂家提供的SDK可能封装了三维坐标的计算,我们可以根据获取到XYZ值进行反推coeffX与coeffY,从而得到相机相关的参数,具体的计算方式则如下:

m_worldConvertCache.xzFactor = tan(horizontalFov / 2) * 2;
m_worldConvertCache.yzFactor = tan(verticalFov / 2) * 2;
m_worldConvertCache.resolutionX = videoMode.resolutionX;
m_worldConvertCache.resolutionY = videoMode.resolutionY;
m_worldConvertCache.halfResX = m_worldConvertCache.resolutionX / 2;
m_worldConvertCache.halfResY = m_worldConvertCache.resolutionY / 2;
m_worldConvertCache.coeffX = m_worldConvertCache.resolutionX / m_worldConvertCache.xzFactor;
m_worldConvertCache.coeffY = m_worldConvertCache.resolutionY / m_worldConvertCache.yzFactor;

要计算出这些参数,我们需要知道相机在两个方向的视场角,并且知道图像的分辨率,一种可能的计算视场角的方式如下:

float focusLength = (m_stLensParam.stDepthLensParam.dFocalLengthX * m_stLensParam.stSensorParam.dPixelWidth / 1000 + m_stLensParam.stDepthLensParam.dFocalLengthY * m_stLensParam.stSensorParam.dPixelHeight / 1000) / 2;
float fovH = 2 * atan(m_stLensParam.stSensorParam.dPixelWidth * m_worldConvertCache.resolutionX / 2 / 1000 / focusLength);
float fovV = 2 * atan(m_stLensParam.stSensorParam.dPixelHeight * m_worldConvertCache.resolutionY / 2 / 1000 / focusLength);
			

有了三维坐标我们就可以绘制点云或者Mesh,同时进行2D纹理贴图了,在此不赘述。

ColorMap

colormap有很多种,主要思想是使用平滑过渡的不同颜色来表示不同范围的数值。
以常见的MATLAB Jet为例,我们可以指定越远的深度越蓝,越近的越红。
RGBD深度相机数据格式-存储读取-可视化_第1张图片
参考链接中给出的代码,我们可以在一个指定的深度范围内对图像进行可视化(这样子可视化的结果,又称伪彩图):

typedef struct {
     
    double r,g,b;
} COLOUR;
COLOUR CommonUtils::getColorMapJet(double v,double vmin,double vmax)
{
     
    COLOUR c = {
     1.0,1.0,1.0}; // white

    double dv;
    if (v < vmin)
        v = vmin;
    if (v > vmax)
        v = vmax;
    dv = vmax - vmin;

    //hot to cold
//    if (v < (vmin + 0.25 * dv)) {
     
//        c.r = 0;
//        c.g = 4 * (v - vmin) / dv;
//    } else if (v < (vmin + 0.75 * dv)) {
     
//        c.r = 2 * (v - vmin - 0.25 * dv) / dv;
//        c.b = 1 + 2 * (vmin + 0.25 * dv - v) / dv;
//    } else {
     
//        c.g = 1 + 4 * (vmin + 0.75 * dv - v) / dv;
//        c.b = 0;
//    }

    //colormap jet
    double t=((v - vmin) / dv)*2-1.0;
    c.r = clamp(1.5 - std::abs(2.0 * t - 1.0));
    c.g = clamp(1.5 - std::abs(2.0 * t));
    c.b = clamp(1.5 - std::abs(2.0 * t + 1.0));
    return(c);
}

RGBD深度相机数据格式-存储读取-可视化_第2张图片

你可能感兴趣的:(RGBD)