一起做RGB-D SLAM (2)

第二讲 从图像到点云



  本讲中,我们将带领读者,编写一个将图像转换为点云的程序。该程序是后期处理地图的基础。最简单的点云地图即是把不同位置的点云进行拼接得到的。

  当我们使用RGB-D相机时,会从相机里读到两种数据:彩色图像和深度图像。如果你有Kinect和ros,可以运行:

1 roslaunch openni_launch openni.launch

  使Kinect工作。随后,如果PC连接上了Kinect,彩色图像与深度图像就会发布在 /camera/rgb/image_color 和 /camera/depth_registered/image_raw 中。你可以通过:

1 rosrun image_view image_view image:=/camera/rgb/image_color

  来显示彩色图像。或者,你也可以在Rviz里看到图像与点云的可视化数据。

  小萝卜:可是师兄!我现在手边没有Kinect,该怎么办啊!

  师兄:没关系!你可以下载我们给你提供的数据。实际上就是下面两张图片啦!

  小萝卜:怎么深度图是一团黑的呀!

  师兄:请睁大眼睛仔细看!怎么可能是黑的!

  小萝卜:呃……可是确实是黑的啊!

  师兄:对!这是由于画面里的物体离我们比较近,所以看上去比较黑。但是你实际去读的话可是有数据的哦!

  重要的备注:

  1. 这两张图来自于nyuv2数据集:http://cs.nyu.edu/~silberman/datasets/ 原图格式是ppm和pgm的,被我转成了png格式(否则博客园不让传……)。
  2. 你可以直接另存为这两个图,也可以到我的git里面获取这两个图。
  3. 实际Kinect里(或其他rgb-d相机里)直接采到的RGB图和深度图可能会有些小问题:
    • 有一些时差(约几到十几个毫秒)。这个时差的存在,会产生“RGB图已经向右转了,怎么深度图还没转”的感觉哦。
    • 光圈中心未对齐。因为深度毕竟是靠另一个相机获取的,所以深度传感器和彩色传感器参数可能不一致。
    • 深度图里有很多“洞”。因为RGB-D相机不是万能的,它有一个探测距离的限制啦!太远或太近的东西都是看不见的呢。关于这些“洞”,我们暂时睁一只眼闭一只眼,不去理它。以后我们也可以靠双边bayes滤波器去填这些洞。但是!这是RGB-D相机本身的局限性。软件算法顶多给它修修补补,并不能完全弥补它的缺陷。

  不过请你放心,在我们给出的这两个图中,都进行了预处理。你可以认为“深度图就是彩色图里每个像素距传感器的距离”啦!

  师兄:现在,我们要把这两个图转成点云啦,因为计算每个像素的空间点位置,可是后面配准、拼图等一系列事情的基础呢。比如,在配准时,必须知道特征点的3D位置呢,这时候就要用到我们这里讲到的知识啦!

  小萝卜:听起来很重要的样子!

  师兄:对!所以请读者朋友务必掌握好这部分的内容啦!


从2D到3D(数学部分)

  上面两个图像给出了机器人外部世界的一个局部的信息。假设这个世界由一个点云来描述:$X=\{ x_1, \ldots, x_n \}$. 其中每一个点呢,有 $r,g,b,x,y,z$一共6个分量,表示它们的颜色与空间位置。颜色方面,主要由彩色图像记录; 而空间位置,可以由图像和相机模型、姿态一起计算出来。

  对于常规相机,SLAM里使用针孔相机模型(图来自http://www.comp.nus.edu.sg/~cs4243/lecture/camera.pdf ):

  简而言之,一个空间点$[ x,y,z ]$和它在图像中的像素坐标$[ u, v, d ]$ ($d$指深度数据) 的对应关系是这样的:

$$ u = \frac{ x \cdot f_x }{z} + c_x $$

$$ v = \frac{ y \cdot f_y }{z} + c_y $$

$$ d = z \cdot s $$

  其中,$f_x, f_y$指相机在$x,y$两个轴上的焦距,$c_x, c_y$指相机的光圈中心,$s$指深度图的缩放因子。

  小萝卜:好晕啊!突然冒出这么多个变量!

  师兄:别急啊,这已经是很简单的模型了,等你熟悉了就不觉得复杂了。

  这个公式是从$(x,y,z)$推到$(u,v,d)$的。反之,我们也可以把它写成已知$(u,v,d)$,推导$(x,y,z)$的方式。请读者自己推导一下。

  不,还是我们来推导吧……公式是这样的:

$$ z = d/s $$

$$ x = (u-c_x) \cdot z/f_x $$

$$ y = (v-c_y) \cdot f_y $$

  怎么样,是不是很简单呢?事实上根据这个公式就可以构建点云啦。

  通常,我们会把$f_x, f_y, c_x, c_y$这四个参数定义为相机的内参矩阵$C$,也就是相机做好之后就不会变的参数。相机的内参可以用很多方法来标定,详细的步骤比较繁琐,我们这里就不提了。给定内参之后呢,每个点的空间位置与像素坐标就可以用简单的矩阵模型来描述了:

$$ s \cdot \left[ \begin{array}{l} u \\ v \\ 1 \end{array} \right] = R \cdot C \cdot \left[ \begin{array}{l} x \\ y \\ z \end{array} \right] + t $$

  其中,$R$和$t$是相机的姿态。$R$代表旋转矩阵,$t$代表位移矢量。因为我们现在做的是单幅点云,认为相机没有旋转和平移。所以,把$R$设成单位矩阵$I$,把$t$设成了零。

  小萝卜:于是就有了上面那个$(u,v,d$转$(x,y,z)$的公式?

  师兄:对!真聪明!如果相机发生了位移和旋转,那么只要对这些点进行位移和旋转操作即可。


从2D到3D (编程部分)

  下面,我们来实现一个程序,完成从图像到点云的转换。请在上一节讲到的 代码根目录/src/ 文件夹中新建一个generatePointCloud.cpp文件:

1 touch src/generatePointCloud.cpp

  小萝卜:师兄!一个工程里可以有好几个main函数么?

  师兄:对呀,cmake允许你自己定义编译的过程。我们会把这个cpp也编译成一个可执行的二进制,只要在cmakelists.txt里作相应的更改便行了。

  接下来,请在刚建的文件里输入下面的代码。为保证行文的连贯性,我们先给出完整的代码,然后在重要的地方加以解释。建议新手逐字自己敲一遍,你会掌握得更牢固。

 1 // C++ 标准库
 2 #include <iostream>
 3 #include <string>
 4 using namespace std;
 5
 6 // OpenCV 库
 7 #include <opencv2/core/core.hpp>
 8 #include <opencv2/highgui/highgui.hpp>
 9
10 // PCL 库
11 #include <pcl/io/pcd_io.h>
12 #include <pcl/point_types.h>
13
14 // 定义点云类型
15 typedef pcl::PointXYZRGBA PointT;
16 typedef pcl::PointCloud<PointT> PointCloud;
17
18 // 相机内参
19 const double camera_factor = 1000;
20 const double camera_cx = 325.5;
21 const double camera_cy = 253.5;
22 const double camera_fx = 518.0;
23 const double camera_fy = 519.0;
24
25 // 主函数
26 int main( int argc, char** argv )
27 {
28     // 读取./data/rgb.png和./data/depth.png,并转化为点云
29
30     // 图像矩阵
31     cv::Mat rgb, depth;
32     // 使用cv::imread()来读取图像
33     // API: http://docs.opencv.org/modules/highgui/doc/reading_and_writing_images_and_video.html?highlight=imread#cv2.imread
34     rgb = cv::imread( "./data/rgb.png" );
35     // rgb 图像是8UC3的彩色图像
36     // depth 是16UC1的单通道图像,注意flags设置-1,表示读取原始数据不做任何修改
37     depth = cv::imread( "./data/depth.png", -1 );
38
39     // 点云变量
40     // 使用智能指针,创建一个空点云。这种指针用完会自动释放。
41     PointCloud::Ptr cloud ( new PointCloud );
42     // 遍历深度图
43     for (int m = 0; m < depth.rows; m++)
44         for (int n=0; n < depth.cols; n++)
45         {
46             // 获取深度图中(m,n)处的值
47             ushort d = depth.ptr<ushort>(m)[n];
48             // d 可能没有值,若如此,跳过此点
49             if (d == 0)
50                 continue;
51             // d 存在值,则向点云增加一个点
52             PointT p;
53
54             // 计算这个点的空间坐标
55             p.z = double(d) / camera_factor;
56             p.x = (n - camera_cx) * p.z / camera_fx;
57             p.y = (m - camera_cy) * p.z / camera_fy;
58
59             // 从rgb图像中获取它的颜色
60             // rgb是三通道的BGR格式图,所以按下面的顺序获取颜色
61             p.b = rgb.ptr<uchar>(m)[n*3];
62             p.g = rgb.ptr<uchar>(m)[n*3+1];
63             p.r = rgb.ptr<uchar>(m)[n*3+2];
64
65             // 把p加入到点云中
66             cloud->points.push_back( p );
67         }
68     // 设置并保存点云
69     cloud->height = 1;
70     cloud->width = cloud->points.size();
71     cout<<"point cloud size = "<<cloud->points.size()<<endl;
72     cloud->is_dense = false;
73     pcl::io::savePCDFile( "./data/pointcloud.pcd", *cloud );
74     // 清除数据并退出
75     cloud->points.clear();
76     cout<<"Point cloud saved."<<endl;
77     return 0;
78 }

  程序运行需要数据。请把上面的那两个图存放在 代码根目录/data 下(没有这个文件夹就新建一个)。

  我们使用OpenCV的imread函数读取图片。在OpenCV2里,图像是以矩阵(cv::MAt)作为基本的数据结构。Mat结构既可以帮你管理内存、像素信息,还支持一些常见的矩阵运算,是非常方便的结构。彩色图像含有R,G,B三个通道,每个通道占8个bit(也就是unsigned char),故称为8UC3(8位unsigend char, 3通道)结构。而深度图则是单通道的图像,每个像素由16个bit组成(也就是C++里的unsigned short),像素的值代表该点离传感器的距离。通常1000的值代表1米,所以我们把camera_factor设置成1000. 这样,深度图里每个像素点的读数除以1000,就是它离你的真实距离了。

  接下来,我们按照“先列后行”的顺序,遍历了整张深度图。在这个双重循环中:

1 for (int m = 0; m < depth.rows; m++)
2      for (int n=0; n < depth.cols; n++)

  m指图像的行,n是图像的列。它和空间点的坐标系关系是这样的:

  深度图第m行,第n行的数据可以使用depth.ptr<ushort>(m) [n]来获取。其中,cv::Mat的ptr函数会返回指向该图像第m行数据的头指针。然后加上位移n后,这个指针指向的数据就是我们需要读取的数据啦。

  计算三维点坐标的公式我们已经给出过了,代码里原封不动地实现了一遍。我们根据这个公式,新增了一个空间点,并放入了点云中。最后,把整个点云存储为 ./data/pointcloud.pcd 文件。


编译并运行

  最后,我们在src/CMakeLists.txt里加入几行代码,告诉编译器我们希望编译这个程序。请在此文件中加入以下几行:

# 增加PCL库的依赖
FIND_PACKAGE( PCL REQUIRED COMPONENTS common io )

# 增加opencv的依赖
FIND_PACKAGE( OpenCV REQUIRED )

# 添加头文件和库文件
ADD_DEFINITIONS( ${PCL_DEFINITIONS} )
INCLUDE_DIRECTORIES( ${PCL_INCLUDE_DIRS}  )
LINK_LIBRARIES( ${PCL_LIBRARY_DIRS} )

ADD_EXECUTABLE( generate_pointcloud generatePointCloud.cpp )
TARGET_LINK_LIBRARIES( generate_pointcloud ${OpenCV_LIBS}
    ${PCL_LIBRARIES} )

  然后,编译新的工程:

1 cd build
2 cmake ..
3 make
4 cd ..

  如果编译通过,就可在bin目录下找到新写的二进制:generate_pointcloud 运行它:

bin/generate_pointcloud

  即可在data目录下生成点云文件。现在,你肯定希望查看一下新生成的点云了。如果已经安装了pcl,就可以通过:

1 pcl_viewer data/pointcloud.pcd

  来查看新生成的点云。


课后作业

  本讲中,我们实现了一个从2D图像到3D点云的转换程序。下一讲,我们将探讨图像的特征点提取与配准。配准过程中,我们需要计算2D图像特征点的空间位置。因此,请你编写一个头文件与一个源文件,实现一个point2dTo3d函数。请在头文件里写这个函数的声明,源文件里给出它的实现,并在cmake中把它编译成一个叫做slam_base的库。(你需要考虑如何定义一个比较好的接口。)这样一来,今后当我们需要计算它时,就只需调用这个函数就可以了。

  小萝卜:师兄!这个作业看起来有些难度啊!

  师兄:是呀,不能把读者想的太简单嘛。

  最后呢,本节用到的源代码仍然可以从我的git里下载到。如果您关于本文有什么问题,也欢迎给我发邮件:[email protected] 读者的鼓励就是对我最好的支持!

时间: 2024-12-09 21:26:33

一起做RGB-D SLAM (2)的相关文章

一起做RGB-D SLAM (4)

第四讲 点云拼接 广告:“一起做”系列的代码网址:https://github.com/gaoxiang12/rgbd-slam-tutorial-gx 当博客更新时代码也会随着更新. SLAM技术交流群:254787961 读者朋友们大家好!尽管还没到一周,我们的教程又继续更新了,因为暑假实在太闲了嘛! 上讲回顾 上一讲中,我们理解了如何利用图像中的特征点,估计相机的运动.最后,我们得到了一个旋转向量与平移向量.那么读者可能会问:这两个向量有什么用呢?在这一讲里,我们就要使用这两个向量,把两张

QT使用QgraphicView/Qpainter展示RGB/YUV图像

本文记载说明使用Qt做RGB的渲染流程和问题.笔者认为:做媒体渲染这种数据量大而对象少的应用场景,使用Qpainter更佳,在多种RGB的转换中,YUV转RGB565最小而且字节对齐问题不容易发生,故推荐之. 加入与opengl  ddraw sdl 的 对比. RGB->QImage->QPixMap->Scene->GrapView 图像的数据是以字节为单位保存的,每一行的字节数必须是4的整数倍,不足的补0. (因为我们使用的是32操作系统,因此数据是按照32位对齐的,所以每行

Camera图像处理原理及实例分析-重要图像概念

Camera图像处理原理及实例分析 作者:刘旭晖  [email protected]  转载请注明出处 BLOG:http://blog.csdn.net/colorant/ 主页:http://rgbbones.googlepages.com/ 做为拍照手机的核心模块之一,camera sensor 效果的调整,涉及到众多的参数,如果对基本的光学原理及 sensor 软/硬件对图像处理的原理能有深入的理解和把握的话,对我们的工作将会起到事半功倍的效果.否则,缺乏了理论的指导,只能是凭感觉和经

Camera图像处理原理及实例分析

Camera图像处理原理及实例分析 作者:刘旭晖  [email protected]  转载请注明出处 BLOG:http://blog.csdn.net/colorant/ 主页:http://rgbbones.googlepages.com/ 做为拍照手机的核心模块之一,camera sensor 效果的调整,涉及到众多的参数,如果对基本的光学原理及 sensor 软/硬件对图像处理的原理能有深入的理解和把握的话,对我们的工作将会起到事半功倍的效果.否则,缺乏了理论的指导,只能是凭感觉和经

Unity -----一些可能存在的错误

关于Unity中的资源管理,你可能遇到这些问题 张鑫 8 个月前 原文链接:关于Unity中的资源管理,你可能遇到这些问题 - Blog 在优化Unity项目时,对资源的管理可谓是个系统纷繁的大工程.鉴于Unity独特又绝(cao)妙(dan)资源打包的AssetBundle管理机制,不同资源的属性适合于不同的存储和加载方式.此外,要处理好成百上千个资源之间的相互依赖关系也非易事.谁说良好的资源管理不是个艺术呢?:) ------------------ 关键字 AssetBundle资源制作

乐学成语项目的实现

首先给软件取名为HappyIdiom,在开始编码之前,需要先对程序进行需求分析,想一想HappyIdiom中应该具备哪些功能.将这些功能全部整理出来之后,我们才好动手一一实现.这里我认为HappyIdiom中至少应该具备以下功能: 1.成语分类学习:你可以根据自己喜欢的分类(动物类.人物类.季节类.自然类.数字类.语言类.其他类)来进行学习. 2.乐猜成语:学习之余玩玩猜成语游戏,寓教于乐. 3.成语收藏:当用户遇到自己想要保存的成语,点击保存按钮,即保存到收藏页面以方便用户再次查看,对已经掌握

一起做RGB-D SLAM (5)

第五讲 Visual Odometry (视觉里程计) 读者朋友们大家好,又到了我们开讲rgbd slam的时间了.由于前几天博主在忙开会拍婚纱照等一系列乱七八糟的事情,这一讲稍微做的慢了些,先向读者们道个歉! 上几讲中,我们详细讲了两张图像间的匹配与运动估计.然而一个实际的机器人总不可能只有两个图像数据吧?那该多么寂寞呀.所以,本讲开始,我们要处理一个视频流,包含八百左右的数据啦.这才像是在做SLAM嘛! 小萝卜:那我们去哪里下载这些数据呢? 师兄:可以到我的百度云里去:http://yun.

一起做RGB-D SLAM 第二季 (一)

小萝卜:师兄!过年啦!是不是很无聊啊!普通人的生活就是赚钱花钱,实在是很没意思啊! 师兄:是啊…… 小萝卜:他们都不懂搞科研和码代码的乐趣呀! 师兄:可不是嘛…… 小萝卜:所以今年过年,我们再做一个SLAM吧!之前写的那个太烂了啦,我都不好意思说是我做的了! 师兄:嗯那可真是对不住你啊…… 小萝卜:没事!你再写一个好一点的,我就原谅你了!写完再请我吃饭吧! 师兄:啊,好的…… 小萝卜:师兄你别这么没精神啊!加油咯! 前言 在经过了一番激烈的思想斗争之后呢,师兄厌倦了年假的无聊生活,开始写<一起做

一起做RGB-D SLAM (6)

第六讲 图优化工具g2o的入门 在上一讲中,我们介绍了如何使用两两匹配,搭建一个视觉里程计.那么,这个里程计有什么不足呢? 一旦出现了错误匹配,整个程序就会跑飞. 误差会累积.常见的现象是:相机转过去的过程能够做对,但转回来之后则出现明显的偏差. 效率方面不尽如人意.在线的点云显示比较费时. 累积误差是里程计中不可避免的,后续的相机姿态依赖着前面的姿态.想要保证地图的准确,必须要保证每次匹配都精确无误,而这是难以实现的.所以,我们希望用更好的方法来做slam.不仅仅考虑两帧的信息,而要把所有整的