作者:盛盛
作者简介:盛盛目前在西工大学习,接触机器人2年。学习使用ROS大概1年多,玩过履带式全地型机器人、参加RoboCup Rescue项目,事无巨细、负责机器人上与图像有关的一切,2013年所在团队全国冠军。
本文发表于exbot.net,如需转载请注明出处!
0. 前言
因为觉得教程里面没有介绍从视觉传感器入手介绍的文章我就写一下吧。(古月居前辈的http://www.guyuehome.com/262这个是2013年的版本,但是因为是fuerte版本,有一些就不合适放在现在使用了,但是大部分都是ok的,当然本文也不会只有前辈的那一些东西,有一些扩展。至于包的安装等问题,可以看前辈的教程,下文不会太多提及安装问题。如有错误,请务必指出来,谢谢!下面主要介绍是单目的传感器,双目传感器暂未涉及。
本文将从相机的驱动开始介绍,然后标定,再是图像压缩,再是ROS与OpenCV,最后是由此扩展的两个包 pluginlib 以及nodelet。其实也没多少东西,全部是围绕视觉传感器来讲的,可能扯得有一点远,但也是正常的关联。当然,ROS提供了更多的图像处理的包,他们主要位于APIs(http://wiki.ros.org/APIs)
1. 驱动
说详细点就是介绍ROS中对于普通的相机常用的两个驱动——usb与uvc以及uvc的一个未来维护版本libuvc_camera,由于libuvc在wiki上的教程是无法使用的,所以下面会介绍一个可用的教程,这个教程也可以解决多个相机因为usb多次插拔导致的问题。此外包括支持RGBD相机的驱动的使用,包括一些遇到的问题。以及对于image_transport进行图像压缩的一些使用的讲解,还有就是与视觉传感器相关的一些初级的教程,包括pluginlib、nodelet,因为exbot的文本教程中貌似也没有介绍他们。个人的经验是初学者要么跑完20篇基础之后就不知道要干什么了,要么就是有各种前辈的指导可以从项目中练手来获取深一些的认知,当然还有就是根本无从下手。
那些昂贵的相机我反正是没用过,我就介绍一下可以使用在价格亲民的相机上的一些驱动,至于在昂贵的上面是否可以使用,没试过,不知道。
下面这个部分主要比较usb_cam (http://wiki.ros.org/usb_cam)与 uvc_camera(http://wiki.ros.org/uvc_camera)因为uvc_cam在indigo版本之后已经不再维护了,转为libuvc_camera(http://wiki.ros.org/libuvc_camera),所以也介绍一下libuvc_camera,附带一个可用的教程。
———————-usb_cam—-START—————————
usb_cam可以驱动标准的USB相机,像罗技的一些相机,这个大家都明白如何使用,我就不多说了,直接放出命令行以及launch吧。
命令行
launch文件
因为默认打开的都是/dev/video0,所以这边就小小修改一下。
对于命令行,由于usb_cam的video_device选择必须改为_video_device,而对于launch文件则不需要。可以看到在(http://wiki.ros.org/usb_cam)中的参数栏目明确显示了如下信息:
但是我们发现,一旦设置过这个参数,在系统没有关闭roscore的情况下,之后其他的终端默认打开的将是这个被我们修改了的参数。也就是说,在其他的终端中,默认打开的是/dev/video1而不是/dev/video0.同样的对于其他的参数,比如图片的宽、高、像素格式、输入输出模式、相机的ID、图像的锐度、对比度、明度等等也将遵循这个规律。(一般也没人修改这一些参数吧)。但是这个规律的优势貌似不是很明显,单纯方便我们操作。
至于在launch中的name设置为your_image_node_name 纯粹告知这个node的名字随便修改,launch文件的配置可以看(http://blog.exbot.net/archives/1413)以及(http://wiki.ros.org/roslaunch/),但是launch文件这个东西是有很多坑的,光看官网的可能不够,具体的一些比较怪异的问题还得在实际中解决,比如某一些看似没什么关系的地方多加一个空格都不行。
上图的node如下图所示
一般这样使用就ok了,至于出现的WARN,比如丢失head_camera.yaml这样的配置文件;由于相机本身不支持自动对焦导致unknown control ‘focus_auto’这样的问题,都没有关系。相机本身的基本功能,提供图像,已经实现了,使用rqt工具或者在rviz中都可以查看。
———————-usb_cam—-END——————————-
———————-uvc_camera—-START————————-
对于uvc_camera,个人认为它的一些可调参数经过了选择,更常用。
不仅可以对双目相机进行操作,同时也发布了相机本身的参数信息的topic,另外还可以调整FPS,像对比度之类的就没法调整,不过一般没人调节对比度吧。
它的使用与usb_cam类似,直接放出命令行与launch文件。
命令行:
launch文件
一个明显的、常用的区别在于,uvc_camera的设备参数名为device,所以使用‘_device’即可。
另一个是uvc_cam提供了双目的节点,可以做双目的驱动。
其余的参数调节,查看http://wiki.ros.org/uvc_camera完全可以解决。
但是uvc_camera相比usb_cam的一个缺点,在我使用的过程中,对于同一个usbhub上的多个相机,使用uvc_camera无法同时驱动,而使用usb_cam则没有这个问题。
———————-uvc_camera—-END——————————-
———————-libuvc_camera–START—————————-
对于libuvc_camera,这个官网的教程(http://wiki.ros.org/libuvc_camera)实在是不敢恭维,至少无法正常下跑下来。另外他要求的rules,在未添加的情况下,还是可以正常跑下来的。所以下面介绍如何使用libuvc_camera。
使用libuvc_camera的步骤如下:
获得相机的设备参数
使用cat /proc/bus/input/devices 得到插入的usb相机的vendor ID以及product ID,vendor ID是罗技厂商的ID,product ID是设备的型号。
上图中,得到C920的vendor为0x046d,product为 0x082d;C170的vendor 为 0x046d,product 为 0x082b.
使用libuvc_camera
命令行
launch文件
上面这个使用libuvc_camera驱动C920,同样由于直接使用了这个产品的厂商编号以及设备型号,因此对于usb多次插拔的情况,可以避免视频流混乱,保证设备与视频对应。
libuvc_camera相对uvc_camera 提供了更加多的图像配置选择,同时根据设备号选择设备,所以它在三种驱动中算是最强大的一个。至于具体的选择,还看具体环境。
———————-libuvc_camera—-END——————————-
下面这个部分介绍驱动RGBD-camera的两个驱动。
现在买到的Xtion pro live一般是2代,主要区别在于红外相机的镜头那边是否存在一个1x0.3cm左右的矩形缺口,可以使用openni2进行驱动。
Launch
由于已经提供了launch文件,所以可以直接修改来配置个人的参数,launch文件的位置[/opt/ros/indigo/share/openni2_launch/launch]
对于kinect 1代,可以使用freenect_launch进行驱动。
在下图所示位置存在其launch文件,可以copy为你所用。
如果需要在自己的launch中包含openni2或者freenect_launch的启动文件,则使用如下include句式。
对于这一些设备,对于是否支持USB3.0我已经是混乱的了,基本上来得看传感器的心情,有时候可以有时候不行。
—–ok—-
上面的驱动部分我大概就讲这一些,个人理解就是对于图像获取的过程,之后我们才可以做图像的预处理等等其他的分门别类的工作。
2. 标定
说到图像之后就必须要提标定的问题了。如果单纯是拿来看周围的环境,用于人眼,那标定可能没有那么重要,但是如果需要做一些其他的工作,比如视觉里程记(高翔前辈的教程:http://www.cnblogs.com/gaoxiang12/p/4719156.html,ROS中已经有的两个包:(libviso2:http://wiki.ros.org/libviso2fovis_ros:http://wiki.ros.org/fovis_ros),RGBD-SLAM等等,那么标定就是必不可少的工作了。
一般的单目标定手法如下
详见(http://wiki.ros.org/camera_calibration/)
–size 表示将要被用于标定的黑白格需要的个数
–square 表示实际的格子的大小,单位为米
image为订阅的图像topic。
之后面对相机移动标定板,再完成标定之后在当前的目录下可以得到相应的 .yaml文件,使用camera_calibration_parsers转化为 ini文件
(http://wiki.ros.org/camera_calibration_parsers?distro=indigo)
如果得到的是ini文件,同样可以使用camera_calibration_parsers将ini文件转化为yaml文件。
在标定的过程中,如果标定板子较小的话,如上面选择的标定板的一个方块只有0.038m,也就是3.8cm,那么对标定的结果就有一定的影响。此外,也相对不容易标定,需要将标定板与相机的镜头靠的很近,那么图像的畸变也就很难直接看出来了。
对于xtion等提供了RGB图像以及Depth图像的设备,提供RGB数据的传感器可以使用上面的方法进行标定,但是对于Depth图像,由于xtion使用红外光来计算Depth,所以Depth的标定需要对红外图像进行标定,红外图像的标定,其实就是让标定板发出红外光或者反射红外光。使用红外光发射器对准标定板再进行深度图的标定即可。因为本质上他们都属于单目的标定,所以与RGB的标定是一样的。
ROS本身提供了标定程序所以就拿来直接用来了,其他的还有很多的标定工具,如matlab的工具箱等。
3. 压缩
此外上面提到过FPS这个参数,自然少不了介绍图像的压缩。一般使用的时候直接会使用没有压缩的图像,但是在需要传输的时候,这一些大图像非常占用带宽,所以对图像进行压缩也就是必然的选择了。
ROS中已有的图像压缩的包有image_transport,它提供了一系列的类以及节点允许用户自己选择特定的传输策略,同时将传输的数据统一为sensors_msgs/Image。但是它本身并不完成数据的压缩工作,本身只完成对原始数据的传输,真正的数据压缩由继承自它的插件们来完成,关于插件的问题下面再说。这一些插件构成的集合是image_transport_plugins(http://wiki.ros.org/image_transport_plugins), 内部含有对图片的JPEG/PNG格式的压缩以及对视频数据的Theroa编码,这是一个免费但是有损的编码方式。视频编码没用过就不多说了,但是视频同样也可以逐帧通过图片压缩进行传输,当然效率上的问题没有与直接对视频编码进行比较因而也不好推荐。
这一些插件可以通过apt-get安装
在具有image_transport的插件的基础上,我们才可以选择采用哪种插件进行图像的压缩,当然实际是image_transport来选择。
通过使用rqt_reconfigure工具可以动态调节压缩比,直观显示采用具体哪一种类型或者压缩比。
当然也可以直接使用rqt将各类插件集中在一起。
下面显示了不同的JPEG压缩率下的不同图像
上图使用了uvc_camera,内部已经使用了image_transport的类,所以在驱动相机之后,自然提供了压缩的一些选择。但是如果没有提供压缩的数据呢?我想对于某一些相机的驱动,应该是只提供原始数据的吧,因此,对于此类驱动,我们可以继续使用image_transport来进行数据的压缩。Image_transport 提供了republish节点,方便我们直接做处理:选择压缩格式,进行数据的重发。
在使用republish之前,需要将image_transport完全安装,如下所示。
否则无法使用republish.
对于wiki上的例子,(http://wiki.ros.org/image_transport),Nodes部分的代码是有问题的。
这样肯定没法使用啊。
但上面大概说明了republish的用法,对于视频或者图像输入,in_transport 表示接受的数据的传输形式,out_transport 表示重发数据的传输形式。in:=之后必须为原始图像的topic,out:=为重发的topic的基本名称。
wiki给出了目前可以使用的三种数据格式,如下所示。
如果in_transport 设置为 raw,即订阅原始数据;设置为compressed,则订阅压缩数据;若设置为theora,则订阅视频的theora编码的数据。如果out_transport 设置为raw,则重只发原始数据;若为compressed,则只重发压缩数据;设置为theora,则只重发theora编码的视频数据。
但是如果out_transport 缺省,则重发全部的插件提供的数据,即包括原始数据、压缩数据、深度压缩数据以及theora编码的视频数据。
如果相机本身没有提供深度数据,则订阅深度数据失败,即没有深度数据。
下面是一个具体的例子,in_transport设置为 raw,out_transport 设置为raw,通过比较左右两图发现,右图多的一个节点就是/republish_name
下图则是设置out_transport缺省的情况,发现将原来所有的节点全部重发。
此外,我们也可以自行使用image_transport作为基类来编写自己的插件,实现满足自己需求的要求。这方面的代码在wiki上已经给出。
(http://wiki.ros.org/image_transport/Tutorials/WritingNewTransport)
由于这个部分同时涉及到了pluginlib的使用,因而需要先看pluginlib的部分。
此外,我们在自定义使用image_transport时常打算自己设定压缩的比率,但是目前image_transport没有开放参数的接口。观察transport_hints.h 的代码,我们发现只有image_transport 一个参数可以调,即选择插件的选项。
关于图像压缩的部分就暂时到这里为止。
4. 在ROS中使用OpenCV
上面我们注意到,image_transport涉及到在ROS中使用OpenCV,因而也就简单介绍一下。对于上面的图像压缩,使用OpenCV同样可以做到,CV的知识就借助其他的书籍来学习了,入门可以看《OpenCV3编程入门》。OpenCV放在ROS中使用,主要由vision_opencv这个stack提供帮助。它包括了cv_bridge以及image_geometry。
cv_bridge主要提供的帮助是将OpenCV的图像数据与ROS的image消息进行转换。OpenCV提供了图像的数据可以作为ROS中的image消息的data部分,而其他的部分,需要由用户自行定义。
ROS的sensor_msgs/Image消息如下所示。其中的data数据可以由OpenCV的图像数据进行填充。当然,data数据也就可以变成OpenCV的Mat数据格式。
关于cv_bridge的使用基本没有很大的难度,使用image_transport的subscriber与publisher进行ROS节点之间的图像收发,在需要使用OpenCV的时候使用cv_bridge将消息转化为Mat类型,由此进行OpenCV的处理,处理结束之后如果还需要处理之后的数据,则再利用cv_bridge将Mat转化为sensor_msgs/Image即可。所以,cv_bridge就如他的名字一样,是bridge,为OpenCV的使用搭建桥梁,桥上面运输的就是关键的图像像素数据。
关于cv_bridge的使用,wiki上的详细的教程见此处(http://wiki.ros.org/cv_bridge/Tutorials/UsingCvBridgeToConvertBetweenROSImagesAndOpenCVImages)
当然,由于sensor_msgs/Image的data需要的数据是uint8类型的,对于opencv中提供的众多数据类型,在进行数据的使用(拷贝或是引用)时,需要指明编码的格式,防止出现图片编码的混乱。具体的使用细节还是根据现场的情况进行分析及时调试即可。
说完cv_bridge,那么vision_opencv中的image_geometry是干什么的呢?
image_geometry借助sensor_msgs/CameraInfo的参数简化对图像的几何描述,包含了C++与python的库。这么说的话就感觉和没说差不多啊,所以我们看一下头文件的位置就明白了,如下图所示。
image_geometry中包含的两个头文件 pinhole_camera_model.h和stereo_camera_model.h,由此我们便从名字上得知,它的主要功能在于对单目相机以及双目相机进行建立模型。建立模型干嘛?干什么就是用户的事情了,但是模型建立之后,便可以简化对图像的几何描述,这也就是上面的意思了吧。
wiki推荐使用这个库,原文写到“Although CameraInfo contains all the information required to rectify a raw image and project points onto it, it is highly recommended that you use this library, since performing these operations correctly over the space of all camera options can be non-trivial.”。总之用image_geometry可以让你对在相机空间进行操作感到不那么繁琐。wiki提供了在图中标注TF例子(http://wiki.ros.org/image_geometry/Tutorials/ProjectTfFrameToImage)但是这个例子对于理解image_geometry感觉不是很有帮助啊,而且需要你事先了解TF的内容。所以为了了解image_geometry,还是得从源码入手。
(http://docs.ros.org/api/image_geometry/html/c++/classimage__geometry_1_1PinholeCameraModel.html)。我个人不喜欢看源码,因为不会看….但是通过查看函数的功能,大概还是猜的出来整个头文件想要干嘛。通过查看源码,我个人的理解是:image_transport就是一个比sensor_msgs/CameraInfo更加详细、更加通用,为我们在不同的分辨率下得到相机的各类物理参数(如x,y坐标的光学中心,x,y坐标的焦距等等等)的工具。
此外,各种与CV、AR相关的C/C++资料可以查看(http://blog.exbot.net/archives/957)
视觉以及图像处理、3D重构等工具以及网站(http://blog.exbot.net/archives/949)
5. pluginlib与nodelet
Ok,那么终于到了最后面了,因为在之前的使用中,我们发现除了从0开始编写程序之外,还有一种方法是站在前人的基础上再进行编写,有一种手法叫做编写插件。在使用uvc_camera的时候,我们从他的源码查看,也发现了这个问题,uvc_camera本身带有是两个插件的,他们均继承自nodelet。nodelet下面会讲。
其中的nodelet_uvc_camera.xml如下所示。
那么我们先从pluginlib说起。
pluginlib可以向一个现有的包中动态添加需要扩展的功能,在包的package.xml中注册插件,即可使用。一大优点是我们不需要知道源码就可以对你这个包进行扩展。最后,使用pluginlib产生的是一个库。
因为个人感觉一开始plugin是比较难明白的。但是,wiki的教程中,pluginlib(http://wiki.ros.org/pluginlib)的部分,讲的比较清楚,提供的例子的确是少了一点,尤其是如何使用一个已有的plugin的部分。wiki提供的例子
(http://wiki.ros.org/pluginlib/Tutorials/Writing%20and%20Using%20a%20Simple%20Plugin)是计算矩形以及三角形的面积,的确已经说明白了,但是使用的时候还是存在一些不是很方便理解、可能存在误解的地方。比如,因为教程中的包名(pluginlib_tutorials_)、命名空间(polygon_base)、xml文件名(polygon_plugins.xml),与在xml文件内部
wiki提供的例子可以很好的帮助我们了解pluginlib,它的结构如下所示。
因为pluginlib提供了动态添加的功能,就像cfg提供对参数的动态修改而不需要再次编译,自然在很多场合都被使用,比如navigation。Ok,那么nodelet干嘛的呢?Nodelet的使用与pluginlib比较像,都需要继承一个基类,都需要注册插件,但是不同之处在于,pluginlib比nodelet更加底层,毕竟nodelet的基类已有现成的了。或者这么说吧,pluginlib是纯牛奶,nodelet是香蕉味的纯牛奶,我们想要在吃奥利奥的时候有香蕉牛奶的味道,对于nodelet这款,我们只要蘸一下就行了,而对于pluginlib,我们还需要在加一些香精搅拌一下,再蘸一蘸,才可以感受到香蕉牛奶的味道。当然,他们的具体区别用这个比喻来说就有问题了,但是如果说是文件的格式,那可以算是差不多的比喻。
nodelet主要想要实现的功能是,在单台机器上的单个进程中,同时跑多个算法,且在消息传入进程时,该进程内的算法都可以获得消息而不需要复制。nodelet为了满足零复制成本,允许不同的算法动态加载到相同节点,然而他们各自的命名空间不同,因此看上去像是多个节点一样,但是他们的确是在同一个进程中。
Nodelet的主要应用对象是高吞吐量的数据流,像视频数据这种使用nodelet就比较合适。所以上文的uvc_camera就继承了nodelet。把他们放在同一个进程中防止信息的拷贝以及网络阻塞。
Nodelet的例子在wiki上也已经比较详细了(http://wiki.ros.org/nodelet/Tutorials/Running%20a%20nodelet)
看一下一定可以明白。
所以对于nodelet,因为文件的组成上与pluginlib很像,同时又比pluginlib要小一些,所以把这两者一起看,可以加深对pluginlib使用的理解(毕竟nodelet就使用了pluginlib),如果感觉之前wiki上介绍使用pluginlib例子的polygon_loader.cpp不是很好看,那么看nodelet的例子就可以较为方便地理解。
Ok,暂时就讲这一些。
至于其他的,像直接选择硬件的,可以看下面这个
如何选择镜头以及摄像机,请看(http://blog.exbot.net/archives/1285)
编辑:dajianli