此处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有很多种,主要思想是使用平滑过渡的不同颜色来表示不同范围的数值。
以常见的MATLAB Jet为例,我们可以指定越远的深度越蓝,越近的越红。
参考链接中给出的代码,我们可以在一个指定的深度范围内对图像进行可视化(这样子可视化的结果,又称伪彩图):
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);
}