深度图与彩色图的配准与对齐
深度图与彩色纹理的对齐与贴合
自从有了Kinect,根据深度图提取前景就非常方便了。因此出现了很多虚拟现实、视频融合等应用。但是,Kinect自身的RGB摄像头分辨率有限,清晰度也不及一些专业摄像头,因此有了用第三方摄像头代替Kinect摄像头的想法。现在的问题是,如何将Kinect的深度图与第三方摄像头的RGB图像对准?
我们知道,当使用Kinect的RGB时,有方便的MapColorCoordinatesToDepth()和MapDepthCoordinatesToColor()方法可以使用,这些函数将深度图和RGB对准到一起,从而可根据深度图准确的提取出RGB中的前景。但打算使用第三方摄像头时,这些函数都没有用了,它们不可能知道我们所用摄像头的参数以及空间位置,因此只能靠自己标定的方法解决这一问题。
在标定之前,先要固定好Kinect和摄像头的位置,让深度摄像头和RGB摄像头的像平面尽量平行,距离也不要隔得太远,就像下面这样(做得很丑,请见谅-_-!!):
一、RGB摄像头的标定
RGB摄像头的标定想必大家都很熟悉,最常用的就是棋盘法。用待标定的摄像头拍摄多幅不同视角下的棋盘图片,将这些图片扔给OpenCV或Matlab,从而计算出该摄像头的内参以及对应于每一幅图像的外参。这里就写写我在标定过程中的一些感受和经验吧。
1、标定所用的棋盘要尽量大,至少要有A3纸的大小;
2、棋盘平面与摄像头像平面之间的夹角不要太大,控制在45度以下;
3、棋盘的姿势与位置尽可能多样化,但相互平行的棋盘对结果没有贡献;
4、用于标定的图片要多于10张;
5、注意设置好摄像头的分辨率,长宽比最好和深度图的相同,比如1280x960(4:3)。
以下是一些用于标定的样图:
二、深度摄像头的标定
深度摄像头看起来和RGB摄像头差别很大,实际上有很多相似之处。就Kinect而言,其通过一个红外散斑发射器发射红外光束,光束碰到障碍物后反射回深度摄像头,然后通过返回散斑之间的几何关系计算距离。其实,Kinect的深度摄像头就是一个装了滤波片的普通摄像头,只对红外光成像的摄像头(可以这么认为)。因此要对其标定,只需用红外光源照射物体即可,LED红外光源在淘宝上就20元一个。还有一点必须注意,在拍摄红外照片时,要用黑胶带(或其他东西)将Kinect的红外发射器完全挡住,否则其发出的散斑会在红外照片中产生很多亮点,不利于棋盘角点的检测。以下是对应于上面RGB图像的红外图:
三、计算内参
得到以上图片之后,就可以分别对RGB摄像头和深度摄像头计算内参了。可以使用OpenCV,自己写一小段程序,然后把图片扔进去。也可以使用著名的Matlab Camera Calibration Toolbox。自己写代码累,Matlab我没装,因此我使用 GML Calibration Toolbox,可以在这里下载 http://graphics.cs.msu.ru/en/node/909 。这是一个C++写的标定程序,有友好的用户界面,精度也不错,使用非常方便。
分别将RGB和红外的照片扔进去,得到RGB摄像头的内参(包括畸变参数):
=== Intrinsic ===
554.952628 0.000000 327.545377
0.000000 555.959694 248.218614
0.000000 0.000000 1.000000
=== Distortion ===
0.025163 -0.118850 -0.006536 -0.001345
和Kinect深度摄像头的内参(这个对所有Kinect应该都是差不多的):
=== Intrinsic ===
597.599759 0.000000 322.978715
0.000000 597.651554 239.635289
0.000000 0.000000 1.000000
=== Distortion ===
-0.094718 0.284224 -0.005630 -0.001429
四、配准
现在说说怎么配准,由于Kinect可以得到真实点的三维坐标,因此深度图的配准可以用一些简单特殊的方法。
设P_ir为在深度摄像头坐标下某点的空间坐标,p_ir为该点在像平面上的投影坐标(x、y单位为像素,z等于深度值,单位为毫米),H_ir为深度摄像头的内参矩阵,由小孔成像模型可知,他们满足以下关系:
又设P_rgb为在RGB摄像头坐标下同一点的空间坐标,p_rgb为该点在RGB像平面上的投影坐标,H_rgb为RGB摄像头的内参矩阵。由于深度摄像头的坐标和RGB摄像头的坐标不同,他们之间可以用一个旋转平移变换联系起来,即:
其中R为旋转矩阵,T为平移向量。最后再用H_rgb对P_rgb投影,即可得到该点对应的RGB坐标:
需要注意的是,p_ir和p_rgb使用的都是齐次坐标,因此在构造p_ir时,应将原始的像素坐标(x,y)乘以深度值,而最终的RGB像素坐标必须将p_rgb除以z分量,即(x/z,y/z),且z分量的值即为该点到RGB摄像头的距离(单位为毫米)。
现在的问题是,如何求联系两个坐标系的旋转矩阵和平移向量。这就要用到摄像头的外参了。
外参矩阵实际上也是由一个旋转矩阵R_ir(R_rgb)和平移向量T_ir(T_rgb)构成的,它表示将一个全局坐标系下的点P变换到摄像头坐标系下,分别对深度摄像头和RGB摄像头进行变换,有以下关系:
在第一式中,将P用P_ir、R_ir和T_ir表示,并带入第二式,可得:
从上式可以看出,这是在将P_ir变换为P_rgb,对比之前的式子:
可得:
因此,我们只需在同一场景下,得到棋盘相对于深度摄像头和RGB摄像头的外参矩阵,即可算出联系两摄像头坐标系的变换矩阵(注意,所有旋转矩阵都是正交阵,因此可用转置运算代替求逆运算)。虽然不同场景下得到的外参矩阵都不同,计算得到的R和T也有一些变化,但根据实际实验结果来看,使用一个正面棋盘的标定图像就可达到较好的效果,如下图:
注意,这两幅图像必须来自于同一场景,否则没有意义。当然你也可以使用多个场景下的外参,然后使用OpenCV的StereoCalibration函数求得两个摄像头的最佳相对变换矩阵,由于时间关系,我没有做这个测试。
使用GML Calibration Toolbox得到以上两图的外参(在菜单栏的Calibration->Export Calibration Data菜单中选择导出),然后根据上式,扔进Mathematica里面去做矩阵运算,得到最终的R和T:
R={ {0.999853, -0.00340388, 0.0167495},
{0.00300206, 0.999708, 0.0239986},
{-0.0168257, -0.0239459, 0.999571} }
T={ {15.2562}, {70.2212}, {-10.9926} }
五、代码
/*
将深度图映射到彩色图上,生成和深度图匹配的对齐彩色图
*/
#include <opencv2\core\core.hpp>
#include <opencv2\highgui\highgui.hpp>
#include <fstream>
#include <iostream>
#include <sstream>
#include <Eigen/Core>
#include <Eigen/LU>
#include <thread>
using namespace cv;
using namespace std;
struct KinectParm
{
float fx_rgb;
float fy_rgb;
float cx_rgb;
float cy_rgb;
float fx_ir;
float fy_ir;
float cx_ir;
float cy_ir;
Eigen::Matrix3f R_ir2rgb;
Eigen::Vector3f T_ir2rgb;
};
bool loadParm(KinectParm* kinectParm)
{
// 加载参数
ifstream parm("registration.txt");
string stringLine;
if (parm.is_open())
{
// rgb相机参数:fx,fy,cx,cy
getline(parm, stringLine);
stringstream lin(stringLine);
string s1, s2, s3, s4, s5, s6, s7, s8, s9;
lin >> s1 >> s2 >> s3 >> s4;
kinectParm->fx_rgb = atof(s1.c_str());
kinectParm->fy_rgb = atof(s2.c_str());
kinectParm->cx_rgb = atof(s3.c_str());
kinectParm->cy_rgb = atof(s4.c_str());
stringLine.clear();
// ir相机参数:fx,fy,cx,cy
getline(parm, stringLine);
stringstream lin2(stringLine);
lin2 << stringLine;
lin2 >> s1 >> s2 >> s3 >> s4;
kinectParm->fx_ir = atof(s1.c_str());
kinectParm->fy_ir = atof(s2.c_str());
kinectParm->cx_ir = atof(s3.c_str());
kinectParm->cy_ir = atof(s4.c_str());
stringLine.clear();
// R_ir2rgb
getline(parm, stringLine);
stringstream lin3(stringLine);
lin3 << stringLine;
lin3 >> s1 >> s2 >> s3 >> s4 >> s5 >> s6 >> s7 >> s8 >> s9;
kinectParm->R_ir2rgb <<
atof(s1.c_str()), atof(s2.c_str()), atof(s3.c_str()),
atof(s4.c_str()), atof(s5.c_str()), atof(s6.c_str()),
atof(s7.c_str()), atof(s8.c_str()), atof(s9.c_str());
stringLine.clear();
// T_ir2rgb
getline(parm, stringLine);
stringstream lin4(stringLine);
lin4 << stringLine;
lin4 >> s1 >> s2 >> s3;
kinectParm->T_ir2rgb << atof(s1.c_str()), atof(s2.c_str()), atof(s3.c_str());
}
else
{
cout << "parm.txt not right!!!";
return false;
}
cout << "******************************************" << endl;
cout << "fx_rgb: " << kinectParm->fx_rgb << endl;
cout << "fy_rgb: " << kinectParm->fy_rgb << endl;
cout << "cx_rgb: " << kinectParm->cx_rgb << endl;
cout << "cy_rgb: " << kinectParm->cy_rgb << endl;
cout << "******************************************" << endl;
cout << "fx_ir: " << kinectParm->fx_ir << endl;
cout << "fy_ir: " << kinectParm->fy_ir << endl;
cout << "cx_ir: " << kinectParm->cx_ir << endl;
cout << "cy_ir: " << kinectParm->cy_ir << endl;
cout << "******************************************" << endl;
cout << "R_ir2rgb:" << endl << kinectParm->R_ir2rgb << endl;
cout << "******************************************" << endl;
cout << "T_ir2rgb:" << endl << kinectParm->T_ir2rgb << endl;
cout << "******************************************" << endl;
return true;
}
int main()
{
// 1. 读取参数
KinectParm *parm = new KinectParm();
if(!loadParm(parm))
return 0;
// 2. 载入rgb图片和深度图并显示
Mat bgr(1080, 1920, CV_8UC4);
bgr = imread("color.jpg");
Mat depth(424, 512, CV_16UC1);
depth = imread("depth.png", IMREAD_ANYDEPTH); // 图片读入后的格式不一定和定义时候的一样,比如这里读入后的格式就是8UC3
Mat depth2rgb = imread("depth2rgb.jpg");
// 3. 显示
thread th = std::thread([&]{
while (true)
{
imshow("原始彩色图", bgr);
waitKey(1);
imshow("原始深度图", depth);
waitKey(1);
imshow("原始投影图", depth2rgb);
waitKey(1);
}
});
// 4. 变换
// 4.1 计算各个矩阵
#pragma region 非齐次
Eigen::Matrix3f K_ir; // ir内参矩阵
K_ir <<
parm->fx_ir, 0, parm->cx_ir,
0, parm->fy_ir, parm->cy_ir,
0, 0, 1;
Eigen::Matrix3f K_rgb; // rgb内参矩阵
K_rgb <<
parm->fx_rgb, 0, parm->cx_rgb,
0, parm->fy_rgb, parm->cy_rgb,
0, 0, 1;
Eigen::Matrix3f R;
Eigen::Vector3f T;
R = K_rgb*parm->R_ir2rgb*K_ir.inverse();
T = K_rgb*parm->T_ir2rgb;
cout << "K_rgb:\n" << K_rgb << endl;
cout << "K_ir:\n" << K_ir << endl;
cout << "R:\n" << R << endl;
cout << "T:\n" << T << endl;
cout << depth.type() << endl;
// 4.2 计算投影
Mat result(424, 512, CV_8UC3);
int i = 0;
for (int row = 0; row < 424; row++)
{
for (int col = 0; col < 512; col++)
{
unsigned short* p = (unsigned short*)depth.data;
unsigned short depthValue = p[row * 512 + col];
//cout << "depthValue " << depthValue << endl;
if (depthValue != -std::numeric_limits<unsigned short>::infinity() && depthValue != -std::numeric_limits<unsigned short>::infinity() && depthValue != 0 && depthValue != 65535)
{
// 投影到彩色图上的坐标
Eigen::Vector3f uv_depth(col, row,1.0f); // !!!p_ir
Eigen::Vector3f uv_color = depthValue/1000.f*R*uv_depth + T/1000; // !!!Z_rgb*p_rgb=R*Z_ir*p_ir+T; (除以1000,是为了从毫米变米)
int X = static_cast<int>(uv_color[0] / uv_color[2]); // !!!Z_rgb*p_rgb -> p_rgb
int Y = static_cast<int>(uv_color[1] / uv_color[2]); // !!!Z_rgb*p_rgb -> p_rgb
//cout << "X: " << X << " Y: " << Y << endl;
if ((X >= 0 && X < 1920) && (Y >= 0 && Y < 1080))
{
//cout << "X: " << X << " Y: " << Y << endl;
result.data[i * 3] = bgr.data[3 * (Y * 1920 + X)];
result.data[i * 3 + 1] = bgr.data[3 * (Y * 1920 + X) + 1];
result.data[i * 3 + 2] = bgr.data[3 * (Y * 1920 + X) + 2];
}
else
{
result.data[i * 3] = 0;
result.data[i * 3 + 1] = 0;
result.data[i * 3 + 2] = 0;
}
}
else
{
result.data[i * 3] = 0;
result.data[i * 3 + 1] = 0;
result.data[i * 3 + 2] = 0;
}
i++;
}
}
imwrite("registrationResult.png", result);
thread th2 = std::thread([&]{
while (true)
{
imshow("结果图", result);
waitKey(1);
}
});
th.join();
th2.join();
#pragma endregion
system("pause");
return 0;
}
六、测试
最后写一个小程序测试一下,看看配准前(左)和配准后(右)的区别:
从图像上看,配准已经很精确了。若还要更好,可以手动微调一下两个摄像头的平移向量T,主要改x分量和y分量,这样可以控制RGB和深度图的左右对齐和上下对齐。另外,还可以加入对畸变系数的处理,不过由于Kinect的摄像头以及我使用的RGB摄像头本身质量较高,畸变影响不大,这里就全部忽略了。
================================分割线=====================================
由于之前换电脑,一时疏忽将最后配准的代码丢了,所以没法提供给大家,万分抱歉。感谢ctgushiwei朋友的提出,在此说一下这个测试程序的思路。
1、获取Kinect的深度图像;
2、获取RGB摄像头的图像;
3、为深度图像中的每一个像素附上对应的RGB颜色,比如你要给坐标为(x, y)的深度图像素附上颜色,具体步骤如下;
1)构造一个三维向量p_ir = (x, y, z),其中x,y是该点的像素坐标,z是该像素的深度值;
2)用Kinect内参矩阵H_ir的逆,乘以p_ir得到对应的空间点坐标P_ir,具体公式见上文第四部分(配准);
3)由于P_ir是该点在Kinect坐标系下的坐标,我们需要将其转换到RGB摄像头的坐标系下,具体的,就是乘以一个旋转矩阵R,再加上一个平移向量T,得到P_rgb;
4)用RGB摄像头的内参矩阵H_rgb乘以P_rgb,得到p_rgb,p_rgb也是一个三维向量,其x和y坐标即为该点在RGB图像中的像素坐标,取出该像素的颜色,作为深度图像中对应像素的颜色;
5)对深度图像中的每一个像素都做上述操作,得到配准后的深度图。
更多推荐
所有评论(0)