清华大学无人机课程第二次作业

清华大学无人机课程第二次作业

在第一次作业中,我们能够用话题topic和服务service两种服务方式传输数据。在ROS实际应用中,来自相机传感器的图片经常作为传输所需的数据类型,本次作业首先讲解图片的传递&RVIZ可视化,随后学习ROS系统中的tf的使用,最终手动实现2个turtle追踪同一个turtle的代码设计。


实验准备:安装OpenCV库

 sudo apt install libopencv-dev

任务一:怎么使用ROS的话题传输图片?

如果想使用话题topic传输图片,则需要首先定义图片的数据类型,而ROS系统中内建了一个图片数据类型sensor_msgs/Image:其中:

Header记录的图片的时间信息、坐标系信息等等;

height记录图片的行数;width记录图片的列数;

encoding为图排尿的编码方式,可以中include/sensor_msgs/image_encodings.h查看候选数值;

step为图片中的一整行的数据长度(以Byte为单位)。

data记录了具体的像素信息(按照行进行索引)。

我们可以读取外部图片每个像素点的数值并且手动定义每一个内部成员变量,然后创建一个以该消息为内容的消息 topic将其publish出去,但这样操作过于繁琐。一个较为简单的做法是使用开源库OpenCV读取外部图片。由于OpenCV读取的外部图片为内建的Mat类型,该类型与sensor_msgs/Image类型不同,因此需要从Mat转换称为sensor_msgs/Image。ROS内部也提供了格式转换所需的可函数CvBridge。因此接下来我们采用OpenCV读取外部图片存储为Mat格式,通过CvBridge将其转换为sensor_msgs/Image并创建消息topic将其发出。具体代码如下:

$ cd ~/catkin_ws/src/
$ catkin_create_pkg image_tran rospy roscpp std_msgs image_transport cv_bridge
$ cd image_tran
$ mkdir scripts && cd scripts
$ gedit image_pub_node.py
$ sudo chmod +x ~/catkin_ws/src/image_tran/scripts/image_pub_node.py

将下列内容写入image_pub_node.py

#!/usr/bin/env python
#coding:utf-8

import rospy
import sys
import cv2
import os
import numpy as np
from sensor_msgs.msg import Image
from cv_bridge import CvBridge, CvBridgeError

def pubImage():
    rospy.init_node('pubImage', anonymous = True)
    pub = rospy.Publisher('ShowImage', Image, queue_size = 10)
    rate = rospy.Rate(10)
    bridge = CvBridge()
    get_image_name = []
    path = "/home/jimmy/work/ROS_work/test_ws/src/image_tran/dataset/"
    for item in os.listdir(path):
        get_image_name.append(os.path.join(path, item))
    while not rospy.is_shutdown():
        for imagepath in get_image_name:
            image = cv2.imread(imagepath)
            image = cv2.resize(image, (900, 450))
            pub.publish(bridge.cv2_to_imgmsg(image, "bgr8"))

            rate.sleep()

if __name__ == '__main__':
    try:
        pubImage()
    except rospy.ROSInterruptException:
        pass

修改文件中path为同学电脑中图片的路径位置。
代码解读:

  1. 修改下面的路径为本地存储图片的路径,图片在资料中picture_data.zip中。
path = "/home/jimmy/work/ROS_work/test_ws/src/image_tran/dataset/"
  1. 以下代码实现从path指定的路径中获取所有图片的文件名称。
for item in os.listdir(path):
    get_image_name.append(os.path.join(path,item))
  1. 以下代码从path指定的路径中读取每一张图片并将图片放缩成宽900,高450的格式。
for imagepath in get_image_name:
    image = cv2.imread(imagepath)
    image = cv2.resize(image, (900, 450))
  1. bridge创建了CvBridge()的对象,并且通过调用cv2_to_imgmsg函数将OpenCV读取的Mat类型图片转换为sensor_msgs/Image类型,最终发布。
bridge = CvBridge()
pub.publish(bridge.cv2_to_imgmsg(image, "bgr8"))

至此,我们完成了话题发布者脚本的撰写。
话题接受者如下:

$ cd ~/catkin_ws/src/image_tran/scripts
$ gedit image_sub_node.py
$ sudo chmod +x ~/catkin_ws/src/image_tran/scripts/image_sub_node.py

将下列内容输入image_sub_node.py

#!/usr/bin/env python
#coding:utf-8

import rospy
import cv2
from sensor_msgs.msg import Image
from cv_bridge import CvBridge,CvBridgeError

def callback(data):
    bridge = CvBridge()
    cv_image = bridge.imgmsg_to_cv2(data, "bgr8")
    cv2.imshow("view", cv_image)
 
def showImage():
    rospy.init_node('showImage', anonymous = True)
    cv2.namedWindow("view", cv2.WINDOW_AUTOSIZE)
    cv2.startWindowThread()
    rospy.Subscriber('ShowImage', Image, callback)
    rospy.spin()
    cv2.destroyWindow("view")

if __name__ == '__main__':
    showImage()

代码解读:

  1. 主函数调用进入showImage()函数。该函数中下面两行代码创建了OpenCV用于显示图片名为“view”的窗口并创建一个新的线程运行窗口的更新。该窗口一直存在直至调用cv2.destroyWindow("view")
cv2.namedWindow("view", cv2.WINDOW_AUTOSIZE)
cv2.startWindowThread()
  1. 回调函数中的代码创建了一个CvBridge()并且调用imgmsg_to_cv2将接受的sensor_msgs/Image类型消息转化为Mat并用于后续imshow的图片显示。imshow第一参数指定了显示的窗口名称。
    bridge = CvBridge()
    cv_image = bridge.imgmsg_to_cv2(data, "bgr8")
    cv2.imshow("view", cv_image)

至此,所有准备工作就绪。编译运行

终端1

$ cd ~/catkin_ws
$ catkin_make
$ roscore

终端2

$ source ~/catkin_ws/devel/setup.bash
$ rosrun image_tran image_pub_node.py

终端3

$ source ~/catkin_ws/devel/setup.bash
$ rosrun image_tran image_sub_node.py

此时,可以看到终端3运行后会新创建一个窗口循环显示path路径下的图片。

运行后视频.png

我们也可以使用RVIZ来观察当前ROS系统中传递的图片消息。

rosrun rviz rviz

选中窗口中左下角add选项->By topic->/ShowImage->Image,左下角会显示传输的图片。

rviz显示图片.png

双击图片的Image状态栏可视图片自动放缩到合适的大小。

rviz显示图片.png

任务二:tf能做什么?

本任务通过一系列演示程序旨在帮助大家理解tf能够完成什么功能。

安装tf相关的依赖包,如下:

$ sudo apt-get install ros-melodic-ros-tutorials ros-melodic-geometry-tutorials ros-melodic-rviz ros-melodic-rosbash ros-melodic-rqt-tf-tree

终端1运行demo

$ roslaunch turtle_tf turtle_tf_demo.launch

在终端1中使用键盘方向键操作乌龟移动,发现另一只乌龟紧随其后进行跟踪。


turtlesim.png

这个demo是如何做到的呢?
此demo使用的是tf包提供的独特的通信方式(广播/监听:broadcaster/listener),即tf中的话题。
为了实现turtle2追踪turtle1,需要控制turtle2向turtle1的位置移动。那么问题来了,如何使turtle2得知turtle1的位置?这就需要用到tf包。

tf全称叫做transform,该库提供了坐标变换所需的常用函数。在上述例子中一共存在3个坐标系:
ROS内建了名为RVIZ的可视化工具

rosrun rviz rviz

打开后左侧便是当前显示的话题名称和内容。Global Options中的Fixed Frame需要更改为world。因为Fixed Frame为了当前RVIZ显示的参考系。初始化时为map frame,但在本例中并不存在map。

通过左下角add按钮添加By display type中的rviz文件夹中的Axes并按照下图中配置每个Axes中的Refernce Frame分别为turtle1, turtle2和world,我们可以直观地看到一下三个坐标系。

配置好的RVIZ.png

RVIZ三个坐标系的坐标.png
  • turtle1的坐标系(frame ID: turtle1):以turtle1中心为原点,turtle前方为x轴正方向,turtle左手边为y轴正方向,采用右手螺旋定则,四指从x正向转到y轴正向时拇指的指向为z轴正方向。
  • turtle2的坐标系(frame ID: turtle2):以turtle2中心为原点,正方向同上。
  • 世界坐标系(frame ID: world)。

由于Global Options中Fixed Frame选择为world,因此只有世界坐标系固定,而两只turtle的坐标系都在移动。

注意:turtle1的世界坐标系下坐标 就是 world frame->turtle1 frame的坐标转换。而上述例子中turtle2追踪turtle1即计算了turtle1在turtle2的坐标,即turtle2->turtle1的坐标变换。

为了更直观地展示turtle2->turtle1的坐标变换,我们还可以监听tf的数据$ rosrun tf tf_echo [reference_frame] [target_frame]

$ rosrun tf tf_echo turtle2 turtle1

已知turtle1在turtle2坐标系中的坐标(即turtle1->turtle2的坐标变换),我们即可控制turtle2直接向turtle1位置移动,从而实现追踪。
最后,我们也可以通过下述命令查看当前ROS系统中的各个坐标系之间的变换关系

rosrun rqt_tf_tree rqt_tf_tree
rqt_tf_tree.png

任务三:手写tf脚本实现turtle跟踪

任务一底层的实现方式为:turtle1通过tf广播/监听通信方式广播world->turtle1的坐标变换(turtle1的世界坐标系下坐标);同时turtle2也通过tf广播/监听通信方式广播world->turtle2的坐标变换(turtle2的世界坐标系下坐标)。turtle1/turtle2 tf广播的作用就是将turtle1/turtle2发送到tf树中。而另外编写一个监听脚本,该脚本负责在tf树中监听是否存在turtle2->turtle1的坐标变换(实际上只要rqt_tf_tree中从turtle1节点能够走到turtle2节点即存在turtle2->turtle1的坐标变换)。

部分一:广播脚本编写

$ cd ~/catkin_ws/src
$ catkin_create_pkg learning_tf rospy roscpp tf turtlesim
$ cd learning_tf
$ mkdir scripts
$ gedit scripts/turtle_tf_broadcaster.py

将下列内容放入该python脚本

#!/usr/bin/env python  
import roslib
roslib.load_manifest('learning_tf')
import rospy

import tf
import turtlesim.msg

def handle_turtle_pose(msg, turtlename):
    br = tf.TransformBroadcaster()
    br.sendTransform((msg.x, msg.y, 0),
                     tf.transformations.quaternion_from_euler(0, 0, msg.theta),
                     rospy.Time.now(),
                     turtlename,
                     "world")

if __name__ == '__main__':
    rospy.init_node('turtle_tf_broadcaster')
    turtlename = rospy.get_param('~turtle')
    rospy.Subscriber('/%s/pose' % turtlename,
                     turtlesim.msg.Pose,
                     handle_turtle_pose,
                     turtlename)
    rospy.spin()

为python脚本文件增加可执行权限。

$ chmod +x scripts/turtle_tf_broadcaster.py

代码解读(仅解释先前任务中未涉及的代码):

  1. turtlename = rospy.get_param('~turtle')即从参数服务器中获取名为turtle参数的数值,该数值用于配置后续监听话题名称。
  2. rospy.Subscriber('/%s/pose' % turtlename, turtlesim.msg.Pose, handle_turtle_pose, turtlename)使该节点监听名为'<turtlename>/pose'的话题(其中<turtlename>为参数服务器中该参数的数值),每当从该话题中收到一个消息后即调用一次回调函数handle_turtle_pose。
  3.      br.sendTransform((msg.x, msg.y, 0), tf.transformations.quaternion_from_euler(0, 0, msg.theta), rospy.Time.now(), turtlename, "world")```广播了<turtlename>所指定world->turtlename的坐标变换(平移+旋转)。
    
    

完成了广播脚本的撰写,下面准备完成启动文件的撰写,即启动乌龟仿真节点,键盘控制节点以及两个坐标广播节点。

$ mkdir -p ~/catkin_ws/src/learning_tf/launch/
$ gedit ~/catkin_ws/src/learning_tf/launch/start_demo.launch

将下列内容写入start_demo.launch 文件

  <launch>
    <!-- Turtlesim Node-->
    <node pkg="turtlesim" type="turtlesim_node" name="sim"/>
    <node pkg="turtlesim" type="turtle_teleop_key" name="teleop" output="screen"/>

    <node name="turtle1_tf_broadcaster" pkg="learning_tf" type="turtle_tf_broadcaster.py" respawn="false" output="screen" >
      <param name="turtle" type="string" value="turtle1" />
    </node>
    <node name="turtle2_tf_broadcaster" pkg="learning_tf" type="turtle_tf_broadcaster.py" respawn="false" output="screen" >
      <param name="turtle" type="string" value="turtle2" /> 
    </node>

  </launch>

编译运行

$ cd  ~/catkin_ws
$ catkin_make
$ source ./devel/setup.bash
$ roslaunch learning_tf start_demo.launch

用户可以看到如下结果


result.png

并且可以通过下面代码来查看turtle1是否成功广播插入tf树中。

 $ rosrun tf tf_echo /world /turtle1

部分二:监听脚本撰写

$ gedit ~/catkin_ws/src/learning_tf/scripts/turtle_tf_listener.py

将下列内容写入文件中

#!/usr/bin/env python  
import roslib
roslib.load_manifest('learning_tf')
import rospy
import math
import tf
import geometry_msgs.msg
import turtlesim.srv

if __name__ == '__main__':
    rospy.init_node('turtle_tf_listener')

    listener = tf.TransformListener()

    rospy.wait_for_service('spawn')
    spawner = rospy.ServiceProxy('spawn', turtlesim.srv.Spawn)
    spawner(4, 2, 0, 'turtle2')

    turtle_vel = rospy.Publisher('turtle2/cmd_vel', geometry_msgs.msg.Twist,queue_size=1)

    rate = rospy.Rate(10.0)
    while not rospy.is_shutdown():
        try:
            (trans,rot) = listener.lookupTransform('/turtle2', '/turtle1', rospy.Time(0))
        except (tf.LookupException, tf.ConnectivityException, tf.ExtrapolationException):
            continue

        angular = 4 * math.atan2(trans[1], trans[0])
        linear = 0.5 * math.sqrt(trans[0] ** 2 + trans[1] ** 2)
        cmd = geometry_msgs.msg.Twist()
        cmd.linear.x = linear
        cmd.angular.z = angular
        turtle_vel.publish(cmd)

        rate.sleep()

增加文件的执行权限。

$ chmod +x scripts/turtle_tf_listener.py

代码解读:

  1. import tflistener = tf.TransformListener()创建tf的监听,一旦tf完成创建,就开始接受tf变换,并且在本地缓存10s。
  2. void tf::TransformListener::lookupTransform (const std::string &target_frame, const std::string &source_frame, const ros::Time &time, StampedTransform &transform) constlookupTransform的API如此所示,在本例中监听turtle1->turtle2的坐标变换。
  3. rospy.Time(0)控制返回当前系统中可使用的最新的变换。

将下列代码加入刚刚撰写start_demo.launch中

<launch>
    ...
    <node pkg="learning_tf" type="turtle_tf_listener.py" 
          name="listener" />
  </launch>

启动launch文件

$ roslaunch learning_tf start_demo.launch

成功后应该在窗口中具有两个turtle,turtle2跟踪turtle1.
至此,我们完成了tf跟踪设计。


任务四:自主任务

请同学们施展才华实现turtle3追踪turtle2,turtle2追踪turtle1,通过终端按键控制turtle1移动。

任务示意图.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352