ROS高效入门第九章 -- 掌握rosbag工具,编写python包录制、读取、截断和回放样例

1 资料

本章是本人的ROS高效入门系列的最后一章,也是上一章节 ROS高效入门第八章 – 掌握rosbag工具,编写c++包录制、读取、截断和回放样例 的姊妹篇,使用 python 重新实现 bag录制,读取,截断和回放四个样例。参考资料是一样的:
(1)《机器人操作系统(ROS)浅析》[美] Jason M. O’Kane 著 肖军浩 译,第9章
(2)ros Tutorials 初级教程的10,17和18节: ros Tutorials
(3)chatgpt 4.0 (chatGPT少不了)
本系列博客汇总:ROS高效入门系列

2 正文

2.1 python 录包样例

(1)创建 rosbag_test 软件包和文件,这个软件包将实现bag录制,读取,截断和回放四个功能,这里先讲解bag录制,实现功能与上一章节c++版是完全一样的。

cd ~/catkin_ws/src/cpp
// topic_tools 是用来写rosbag play程序用的
catkin_create_pkg rosbag_test rosbag turtlesim roslib roscpp rospy
cd rosbag_test 
mkdir scripts launch
touch launch/start_record.launch scripts/bag_record.py

(2)编写bag_record.py

#! /usr/bin/env python3

import rosbag
import rospy
import argparse
import logging
from datetime import datetime
import roslib.message
from threading import Lock
import signal, sys

logging.basicConfig(level=logging.DEBUG)

class BagRecord(object):
  def __init__(self, output_bag, topic_infos):
    self.__output_bag = output_bag
    self.__topic_infos = topic_infos
    self.__subscribers = []
    # __Lock is to protect bag write and bag close in two thread
    # in python, this is necessary !
    self.__Lock = Lock()
    self.__bag = rosbag.Bag(self.__output_bag, "w")
    signal.signal(signal.SIGINT, self.signal_handler)

  // 不同于cpp,python中多个回调函数是并发执行的,因此需要加上互斥锁保护__bag句柄
  # msg must be placed before topic !!
  def topic_callback(self, msg, topic):
    self.__Lock.acquire()
    try:
      self.__bag.write(topic, msg, rospy.Time.now())
    except Exception as e:
      // 当CTRL+C关闭__bag后,这里会发生写错误,所以需要捕捉下,避免影响程序正常结束
      rospy.logerr("%s" %e)
    self.__Lock.release()
  
  // 使用 rospy.get_published_topics() 和 roslib.message.get_message_class() 可以在不知道消息类型的情况下,创建subscriber,关键!!
  def start_recorder(self):
    for topic, topic_type in self.__topic_infos:
      msg_class = roslib.message.get_message_class(topic_type)
      self.__subscribers.append(rospy.Subscriber(topic, msg_class, self.topic_callback, topic))
 
  // 由于python中写bag是并发执行的,因此当用户ctrl+c结束录包时,也是异步关闭__bag,因此也需要加锁保护,否则bag就是残损的,无法正确读出来
  def signal_handler(self, sig, frame):
    rospy.loginfo("ctrl+c, close bag")
    self.__Lock.acquire()
    self.__bag.close()
    self.__Lock.release()
    sys.exit(0)

def main():
  // 支持录制特定topic,或者录制当前活跃的所有topic
  parser = argparse.ArgumentParser(description="record topic to rosbag")
  parser.add_argument("-O", "--output_bag", type=str, help="specify rosbag")
  parser.add_argument("-t", "--topics", nargs="+", type=str, help="specify topic")
  parser.add_argument("-a", "--all", action="store_true", help="record all topic")
  parser.add_argument('extra_args', nargs='*', help=argparse.SUPPRESS)
  args=parser.parse_args()  
  // -t 和 -a 必须给出一个
  if not args.all and not args.topics:
    logging.error("please specify topic using -t or -a")
    return

  rospy.init_node("bag_recorder")
  // 默认以时间戳作为bag名
  output_bag = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + ".bag"
  if args.output_bag:
    output_bag = args.output_bag
  // 如果 -a 和 -t 同时出现,以 -a 为准
  topic_infos = []
  if args.all:
    topic_infos = rospy.get_published_topics()
  elif args.topics:
    topic_infos = [(topic, msg_class) for topic, msg_class in rospy.get_published_topics() if topic in args.topics]

  recorder = BagRecord(output_bag, topic_infos)
  rospy.loginfo("start recorder...")
  recorder.start_recorder()
  rospy.spin()

if __name__ == "__main__":
  main()

(3)编写start_record.launch

<launch>
<node
    pkg="turtlesim"
    type="turtlesim_node"
    name="turtlesim"
    required="true"
/>
<node
    pkg="turtlesim"
    type="draw_square"
    name="draw_square"
    required="true"
/>
<node
    pkg="rosbag_test"
    type="bag_record.py"
    name="bag_recorder"
    output="screen"
    launch-prefix="bash -c 'sleep 5; $0 $@'"
    args="-O /home/ycao/Study/ros_noetic/bag_dir/square_1.bag -a"
/>
</launch>

(4)编译并运行

cd ~/catkin_ws/
catkin_make --source src/python/rosbag_test/
source devel/setup.bash
rosbag_test start_record.launch

在这里插入图片描述
在这里插入图片描述

2.2 python 查看包样例

(1)创建并编写bag_echo.py

cd ~/catkin_ws/src/python/rosbag_test
touch scripts/bag_echo.py

bag_echo.py

#! /usr/bin/env python3

import os
import argparse
import logging
import rosbag

logging.basicConfig(level=logging.DEBUG)

class BagEcho(object):
  def __init__(self, bag, topics):
      self.__bag = bag
      self.__topics = topics
  
  def echo_bag(self):
      msg_cnts = {}
      bag = rosbag.Bag(self.__bag, "r")
      for topic, msg, t in bag.read_messages(topics=self.__topics, raw=False):
          if topic not in msg_cnts:  
            msg_cnts[topic] = 1
          else:
            msg_cnts[topic] += 1

          print("----------[%s]---------" %topic)
          // python查看消息内容远比c++方便的多,使用bag.read_messages(),直接就能打印出来,不需要引入任何消息类型
          // 而且速度远快于命令行工具:rostopic echo -b ***.bag,但略慢于c++版。
          print(msg)

      bag.close()

      logging.info("----------[rosbag summary]---------")
      for topic in msg_cnts:  
        logging.info("topic %s msg cnt is %d" %(topic, msg_cnts[topic]))

def main():
  // 支持查看指定topic,如果不指定,默认查看所有topic内容。c++版要求必须给出topic,没有默认选项。
  parser = argparse.ArgumentParser(description="echo rosbag depends topic")
  parser.add_argument("-b", "--bag", type=str, required=True, help="specify rosbag")
  parser.add_argument("-t", "--topics", nargs="+", type=str, help="specify topic")
  parser.add_argument('extra_args', nargs='*', help=argparse.SUPPRESS)
  args=parser.parse_args()

  if not os.path.isfile(args.bag):
      logging.error("%s is not found!" %args.bag)
      return

  echo = BagEcho(args.bag, args.topics)
  echo.echo_bag()

if __name__ == "__main__":
    main()

(2)编译并运行

cd ~/catkin_ws/
catkin_make --source src/python/rosbag_test/
source devel/setup.bash
./devel/lib/rosbag_test/bag_echo.py -b ../bag_dir/square_1.bag
./devel/lib/rosbag_test/bag_echo.py -b ../bag_dir/square_1.bag -t /rosout

在这里插入图片描述

2.3 python 截取包样例

(1)创建并编写bag_filter.py

cd ~/catkin_ws/src/python/rosbag_test
touch scripts/bag_filter.py

bag_filter.py

#! /usr/bin/env python3

import os
import argparse
import rosbag
import logging

logging.basicConfig(level=logging.DEBUG)

class BagFilter(object):
  def __init__(self, input_bag, output_bag, start_offset, end_offset, topics):
    self.__input_bag = input_bag
    self.__output_bag = output_bag
    self.__start_offset = start_offset
    self.__end_offset = end_offset
    self.__topics = topics

  def filter_bag(self):
    inbag = rosbag.Bag(self.__input_bag, "r")
    start_time = inbag.get_start_time() + self.__start_offset
    end_time = inbag.get_end_time() - self.__end_offset

    with rosbag.Bag(self.__output_bag, "w") as outbag:
      // 如果不指定 -t ,这里的self.__topics就是空,read_messages()就会把全量topic读出来,很方便
      for topic, msg, t in inbag.read_messages(topics=self.__topics, raw=False):
        if start_time <= t.to_sec() <= end_time:
          outbag.write(topic, msg, t)

      logging.info("------------[%s summary]------------" %self.__output_bag)
      logging.info("time span %f [origin %f]" %(end_time-start_time, inbag.get_end_time()-inbag.get_start_time()))
      topic_list = [topic for topic in outbag.get_type_and_topic_info()[1].keys()]
      logging.info("topic list %s" %topic_list)

    inbag.close()

def main():
  // 支持前后时间偏移截取,支持提取指定topic
  parser = argparse.ArgumentParser(description="filter rosbag depends time offset and topics")
  parser.add_argument("-i", "--input_bag", type=str, required=True, help="specify input rosbag")
  parser.add_argument("-o", "--output_bag", type=str, required=True, help="specify output rosbag")
  parser.add_argument("-s", "--start_offset", type=int, default=0, help="specify start offset time")
  parser.add_argument("-e", "--end_offset", type=int, default=0, help="specify end offset time")
  parser.add_argument("-t", "--topics", nargs="+", type=str, help="specify topic")
  parser.add_argument('extra_args', nargs='*', help=argparse.SUPPRESS)
  args=parser.parse_args()

  if not os.path.isfile(args.input_bag):
    logging.error("%s is no found!" %args.input_bag)
    return

  filter = BagFilter(args.input_bag, args.output_bag, args.start_offset, args.end_offset, args.topics)
  filter.filter_bag()

if __name__ == "__main__":
    main()

(2)编译并运行

cd ~/catkin_ws/
catkin_make --source src/python/rosbag_test/
source devel/setup.bash
./devel/lib/rosbag_test/bag_filter.py -i ../bag_dir/square_1.bag -o filter.bag -s 1 -e 1

在这里插入图片描述
在这里插入图片描述

2.4 python 包播放样例

(1)创建并编写bag_play.py

cd ~/catkin_ws/src/python/rosbag_test
touch scripts/bag_play.py

bag_play.py

#! /usr/bin/env python3

import os, sys
import rospy
import rosbag
import argparse

class BagPlay(object):
  def __init__(self, bag, start_offset, rate, topic_list=[]):
    self.__bag = bag
    self.__start_ofset = start_offset
    self.__rate = rate
    self.__topic_list = topic_list
    self.__publisher = {}
    
  // 必须持久化publisher
  def create_publisher(self):
    bag = rosbag.Bag(self.__bag, 'r')

    for topic, msg, t in bag.read_messages(topics=self.__topic_list, raw=False):
      if topic not in self.__publisher:
        self.__publisher[topic] = rospy.Publisher(topic, type(msg), queue_size=10)

    bag.close()

  def start_play(self):
    bag = rosbag.Bag(self.__bag, "r")

    bag_start_time = bag.get_start_time() + self.__start_ofset
    rospy_start_time =rospy.get_time()
    for topic, msg, t in bag.read_messages(topics=self.__topic_list, start_time=rospy.Time.from_sec(bag_start_time)):
      // 这里实现了倍速播放的机制,比c++版逻辑更清晰简介 !!
      relative_time = (t.to_sec() - bag_start_time) / self.__rate
      while rospy.get_time() < (rospy_start_time + relative_time):
        rospy.sleep(0.01)

      self.__publisher[topic].publish(msg)

      // 这里是进度显示功能
      cur = t.to_sec()
      du = rospy.Duration(cur - bag_start_time).to_sec()
      sys.stdout.write("[RUNNING] Bag Time: %f Duration: %f\r" %(cur, du))
      sys.stdout.flush()

      if rospy.is_shutdown():
        break

    print("\n")
    bag.close

def main():
  // 支持偏移起点播放,支持倍速播放,支持播放指定topic
  parser = argparse.ArgumentParser(description="play topic from rosbag")
  parser.add_argument("-b", "--bag", type=str, required=True, help="specify rosbag")
  parser.add_argument("-s", "--start_offset", type=int, default=0, help="offset time from bag start")
  parser.add_argument("-r", "--rate", type=float, default=1.0, help="specify play rate")
  parser.add_argument("-t", "--topic_list", nargs="+", type=str, help="specify play topics")
  parser.add_argument("extra_args", nargs='*', help=argparse.SUPPRESS)
  args = parser.parse_args()

  if not os.path.isfile(args.bag):
    print("%s is not found!" %args.bag)
    return

  rospy.init_node("bag_player")

  player = BagPlay(args.bag, args.start_offset, args.rate, args.topic_list)
  player.create_publisher()

  print("start play bag after 2 sec...")
  rospy.sleep(2.0)

  player.start_play()

if __name__ == "__main__":
  main()

(2)创建并编写start_play.launch

cd ~/catkin_ws/src/python/rosbag_test
touch launch/start_play.launch

start_play.launch

<launch>
<node
    pkg="turtlesim"
    type="turtlesim_node"
    name="turtlesim"
    required="true"
/>
<node
    pkg="rosbag_test"
    type="bag_play.py"
    name="bag_player"
    args="-b /home/ycao/Study/ros_noetic/bag_dir/square_1.bag -t /turtle1/cmd_vel"
    output="screen"
/>
</launch>

(3)这里给出覆盖四个样例的CMakeLists.txt

cmake_minimum_required(VERSION 3.0.2)
project(rosbag_test)
find_package(catkin REQUIRED COMPONENTS
  rosbag
  roscpp
  rospy
  turtlesim
  roslib
)
catkin_package()
include_directories(
  ${catkin_INCLUDE_DIRS}
)
catkin_install_python(PROGRAMS
  scripts/bag_echo.py
  scripts/bag_filter.py
  scripts/bag_record.py
  scripts/bag_play.py
  DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)

(4)编译并运行

cd ~/catkin_ws/
catkin_make --source src/python/rosbag_test/
source devel/setup.bash
roslaunch rosbag_test start_play.launch

在这里插入图片描述

3 总结

本章用python重新实现了bag录制,读取,截断和回放四个样例,相比于c++版,代码明显更短,实现起来更容易。本文中的例子放在了本人的github上: ros_src

3.1 chatGPT4.0使用经验

写代码的过程中,大量使用了chatGPT4.0,从一开始无脑信任它,到最后不知道该不该信任它。多次受骗以后,我逐步摸索出了一套沟通方法,记录在此:
(1)对于较为复杂的代码,chatGPT给出来的往往是有bug的,而且很隐蔽,需要你调试半天,所以千万不要无脑相信它。
(2)对于编译运行出现的问题,chatGPT的回答也是模棱两可的,所以调试代码,千万不要无脑依赖chatGPT。
(3)因此,使用chatGPT,需要开发者自己要有清晰的思路,当遇到问题的时候,也要有自己的思考,用自己的思路引导它帮你。
(4)对于chatGPT给出的不准确的地方,一定要使用google及时进行校验,不要一直无脑的问它,否则肯定会浪费大量时间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值