techno_memo

個人用の技術メモ。python・ROS・AI系のソフトウェア・ツールなどの情報を記載

ROS2 navigation stack odometryの出力

この記事の目的

 ROS2のnavigation stack用にpython scriptからodometryを出力する方法をまとめる。  ROS2とnavigagiton2 は下記記事に従って導入済みであることを前提とする。   sd08419ttic.hatenablog.com

1. スクリプトの構成

ROS2のpythonスクリプトの記述の実装方法は下記のチュートリアルに記載されている。

docs.ros.org

ROSと異なり、rclpyのNodeを継承して処理を記述し、一定周期に呼び出す処理をcallback関数で記述するように変更されている。 簡潔に記述するため、ROS2のノード処理(ノード立ち上げ、周期処理、終了)およびトピック通信(publish/subscribe)と制御に関する処理(自己位置の計算など)を別ファイルに記載する。

1. publish/subscribe処理の実装

ROS2でodometryを10msecでpublishし続けるスクリプトは下記のように記述できる。 下記スクリプトはOdomSimulatorNode生成時のinit関数でodometryのpublisher初期化、cmd_velのsubscribe設定、周期処理呼び出しのtimer設定をしている。 その後、ノード実行中はcmd_velのsubscribe成功時にsub_cmdvel_callbackで受信内容の保存、20msec周期でtimer_callbackから位置情報の更新とpublish、TFのbroadcastを行う。位置情報・TFの計算はodom_simulator_func.pyに記述する。

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

import pandas as pd
# import for ros function
import rclpy
from rclpy.node import Node
import tf2_py
from geometry_msgs.msg import PoseStamped, Twist
from std_msgs.msg import Header
from nav_msgs.msg import Path, Odometry
from odom_simulator_func import OdomSimlatorFunc
from tf2_ros.transform_broadcaster import TransformBroadcaster

class OdomSimulatorNode(Node):

    ##################
    # Initialization #
    ##################
    def __init__(self):
        super().__init__('Odom_Simulator')

        #initialize publisher
        self.OdometryPub_ = self.create_publisher(Odometry, '/odom', 10)
        self._tf_Odompublisher = TransformBroadcaster(self)

        #initialize subscriber
        self.CmdvelSub = self.create_subscription(
            Twist,
            '/cmd_vel',
            self.sub_cmdvel_callback,
            10)

        self.CmdvelSub  # prevent unused variable warning

        #timer configuration
        timer_period = 0.02  # seconds
        self.timer = self.create_timer(timer_period, self.timer_callback)
        #self.path_follow_func = Path_follow_func()
        self.cnt = 0

        self.odomSimFunc = OdomSimlatorFunc()

    def timer_callback(self):
        # publish odom data
        #cmdvel = self.pathfollowerFunc.update_cmd_vel()
        #odom = Odometry()
        odom, tf_odom = self.odomSimFunc.update_odom()
        #calc cmdvel
        odom.header.stamp = self.get_clock().now().to_msg()
        self.OdometryPub_.publish(odom)
        tf_odom.header.stamp = self.get_clock().now().to_msg()
        self._tf_Odompublisher.sendTransform(tf_odom)
        self.cnt += 1
        if self.cnt %10 ==0:
            self.get_logger().info('Publishing Count: "%s"' % self.cnt)

    #subscriber call back for Odometry
    def sub_cmdvel_callback(self, msg):
        self.odomSimFunc.set_cmdvel_from_subscriber(msg)
        test = Twist()
        self.get_logger().info('I heard: "%s"' % msg.linear)

def main(args=None):
    rclpy.init(args=args)

    odomSimNode = OdomSimulatorNode()
    rclpy.spin(odomSimNode)

    # Destroy the node explicitly
    # (optional - otherwise it will be done automatically
    # when the garbage collector destroys the node object)
    odomSimNode.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

2. 位置情報・TFの計算方法

位置情報・TFの計算処理は下記のように記述できる。cmd_velで受信した目標速度・ヨーレートから座標を更新する。 ROS2ではTFからクォータニオンオイラー角を変換する関数がなくなってしまったのでtf_lib ファイルに変換処理を記述する。

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

import math
# import for ros function
from geometry_msgs.msg import Point, Pose, Quaternion, Twist, Vector3
from std_msgs.msg import Header
from nav_msgs.msg import Odometry
from geometry_msgs.msg import TransformStamped
from tf_lib import euler_from_quaternion, quaternion_from_euler


class OdomSimlatorFunc():

    ##################
    # Initialization #
    ##################
    def __init__(self):

        #Save path_plan flag
        self.save_path_as_csv = True

        #Cmd_vel receive flag (auto control)
        self.subscribe_cmd_vel = False

        #Parameter
        self.MAX_SPEED_KMH = 30.0                       # MAX vehicle speed [km/h]
        self.MIN_SPEED_KMH = -20.0                      # MINIMUM vehicle speed [km/h]
        self.MAX_SPEED_MPS = self.MAX_SPEED_KMH / 3.6   # MAX vehicle speed [km/h]
        self.MIN_SPEED_MPS = self.MIN_SPEED_KMH / 3.6   # MAX vehicle speed [km/h]
        self.MAX_YAW_RATE = 1.57                        # MAX YAWRATE [rad/s]

        #initiali position
        initPosx = 0.0
        initPosy = 0.0

        #Initialize odometry header
        self.odom_header = Header()
        self.odom_header.frame_id = "map"

        # Initialize pose info
        self.sim_pose = Pose()
        self.sim_pose.position.x = initPosx
        self.sim_pose.position.y = initPosy
        self.sim_pose.position.z = 0.0
        self.sim_pose.orientation.x = 0.0
        self.sim_pose.orientation.y = 0.0
        self.sim_pose.orientation.z = 0.0

        # initialize twist info
        self.sim_twist = Twist()

        # Initialize odometry info
        self.sim_odom = Odometry()
        self.sim_odom.header = self.odom_header
        self.sim_odom.child_frame_id = "base_link"
        self.sim_odom.pose.pose = self.sim_pose
        self.sim_odom.twist.twist = self.sim_twist

        self.cmdvel_linear_x = 0.0
        self.cmdvel_linear_y = 0.0
        self.cmdvel_angular_z = 0.0

    #############################################
    # Update odometry form User request cmd_vel #
    #############################################
    def update_odom(self):
        #Update Vehicle Pose

        sampletime = 0.01    #calculate by 10msec
        e = euler_from_quaternion(self.sim_pose.orientation.x, self.sim_pose.orientation.y, self.sim_pose.orientation.z, self.sim_pose.orientation.w)
        yaw_euler = e[2]

        #update pose from user request
        self.sim_pose.position.x = self.sim_pose.position.x + self.cmdvel_linear_x*sampletime*math.cos(yaw_euler)
        self.sim_pose.position.y = self.sim_pose.position.y + self.cmdvel_linear_x*sampletime*math.sin(yaw_euler)
        updated_yaw = e[2] + self.cmdvel_angular_z*sampletime

        updated_quaternion = quaternion_from_euler(0, 0, updated_yaw)
        self.sim_pose.orientation.x = updated_quaternion[0]
        self.sim_pose.orientation.y = updated_quaternion[1]
        self.sim_pose.orientation.z = updated_quaternion[2]
        self.sim_pose.orientation.w = updated_quaternion[3]

        #update timestamp
        #self.odom_header.stamp = rospy.Time.now()
        self.sim_odom.header = self.odom_header
        self.sim_odom.pose.pose = self.sim_pose
        self.sim_odom.twist.twist = self.sim_twist

        #update TF
        map_frame = TransformStamped()
        map_frame.header.frame_id = 'map'
        map_frame.child_frame_id = 'odom'
        map_frame.transform.translation.x = self.sim_pose.position.x
        map_frame.transform.translation.y = self.sim_pose.position.y
        map_frame.transform.translation.z = 0.0
        map_frame.transform.rotation.w = self.sim_pose.orientation.w
        map_frame.transform.rotation.x = self.sim_pose.orientation.x
        map_frame.transform.rotation.y = self.sim_pose.orientation.y
        map_frame.transform.rotation.z = self.sim_pose.orientation.z

        return self.sim_odom, map_frame

    #####################
    # cmdvel subscriber #
    #####################
    def set_cmdvel_from_subscriber(self, msg):
        self.cmdvel_linear_x =msg.linear.x
        self.cmdvel_angular_z =msg.angular.z

tf_lib.py (座標変換処理)

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

import numpy as np
import math

def euler_from_quaternion(x,y,z,w):
    """
    Converts quaternion (w in last place) to euler roll, pitch, yaw
    quaternion = [x, y, z, w]
    Bellow should be replaced when porting for ROS 2 Python tf_conversions is done.
    """

    sinr_cosp = 2 * (w * x + y * z)
    cosr_cosp = 1 - 2 * (x * x + y * y)
    roll = np.arctan2(sinr_cosp, cosr_cosp)

    sinp = 2 * (w * y - z * x)
    pitch = np.arcsin(sinp)

    siny_cosp = 2 * (w * z + x * y)
    cosy_cosp = 1 - 2 * (y * y + z * z)
    yaw = np.arctan2(siny_cosp, cosy_cosp)

    return roll, pitch, yaw


def quaternion_from_euler(roll, pitch, yaw):
    """
    Converts euler roll, pitch, yaw to quaternion (w in last place)
    quat = [x, y, z, w]
    Bellow should be replaced when porting for ROS 2 Python tf_conversions is done.
    """
    cy = math.cos(yaw * 0.5)
    sy = math.sin(yaw * 0.5)
    cp = math.cos(pitch * 0.5)
    sp = math.sin(pitch * 0.5)
    cr = math.cos(roll * 0.5)
    sr = math.sin(roll * 0.5)

    q = [0] * 4
    q[0] = cy * cp * cr + sy * sp * sr
    q[1] = cy * cp * sr - sy * sp * cr
    q[2] = sy * cp * sr + cy * sp * cr
    q[3] = sy * cp * cr - cy * sp * sr

    return q

上記処理を実行し、cmd_velを出力するとodomの出力・TFの発行がされていることをrviz2などで確認できる。 cmd_velは下記コマンド等で発行可能

ros2 topic pub /cmd_vel geometry_msgs/Twist '{linear: {x: 0.1}, angular: {z: 0.3}}'

ソースコードは下記にまとめている。

github.com