#
Data Visualization
This tutorial aims to provide a basis for displaying ROS2 data. It can be applied for any topic or message.
#
Prerequisites
- Setup, tutorial 1, and tutorial 2 must be completed to understand the following.
- Have an understanding on what topics are, how to publish and subscribe to them (tutorial 1), and how messages are related to topics.
#
Markers
ROS markers are a data type that allow renderers (foxglove studio, rviz, etc) to display a wide variety of elements ranging from fundamentals, such as lines, to meshes that form avocados (https://foxglove.dev/blog/how-to-visualize-ros-mesh-markers).
#
File Structure
When it comes to file organization in ROS, it is typically best to keep packages and nodes as modular as possible. In the case of data visualization, this means that you should keep node fundamentals separate from its helpers. An example would be: you are trying to show a line between the vehicle and its target waypoint. Intuitively, it is simpler to display the marker in the waypoint following node. However, for the sake of modularity, this should be avoided. Instead, publish the target waypoint from the waypoint following node, create a separate "painter" package, and then subscribe and display. This method avoids node cluttering and allows you to not display the visuals by default (which is typically desired behavior, as most visualization will be used for debugging and testing). Additionally, publishing more data is more beneficial in the long term when more nodes are added. On the other hand, if your node has a very simple purpose, say publishing waypoints from a file, and you know that you want the visual to be constantly enabled, then publishing the markers directly through the waypoint publisher is correct.
TLDR: keep code modular and clean. If drawing stuff makes your node ugly, move it to a separate "painting" package.
#
Create a node
Or don't... read above on "file structure" and decide.
Refer to tutorial 1 on how to set up a package and a node. It should be named "painter", "visualizer", or something else informative.
#
Line displaying
To publish lines, we will use a helper function:
def draw_line(self, c1, c2, id=0, color=(0.0, 1.0, 0.0)):
# p1, p2 to Points
p1 = Point()
p1.x = c1[0]
p1.y = c1[1]
p1.z = 0.1
p2 = Point()
p2.x = c2[0]
p2.y = c2[1]
p2.z = 0.1
marker = Marker()
marker.header.frame_id = "map"
marker.id = id
marker.type = marker.LINE_STRIP
marker.action = marker.ADD
marker.points = [p1, p2]
marker.pose.orientation.w = 1.0
scale = 0.1 # adjust if needed
marker.scale.x = scale
marker.scale.y = scale
marker.scale.z = scale
marker.color.a = 1.0
marker.color.r = color[0]
marker.color.g = color[1]
marker.color.b = color[2]
self._line_publisher.publish(marker)
The function creates two Points (another ROS type), assigns it to a marker, sets its parameters, and publishes. To make a line with many points, appened more Points to marker.points.
If the function is run multiple times, setting a separate id is essential otherwise it will try to overwrite the previous line.
#
Painter Node
Next we will subscribe to odom and call our draw_line function to display the car's direction as a line.
class PainterNode(Node):
def __init__(self):
super().__init__("painter_node")
self._line_publisher = self.create_publisher(Marker, "/line_test", 10)
# subscribe to /odom
self.subscription = self.create_subscription(
Odometry, "/odom", self.odom_callback, 10
)
self._i = 0
def odom_callback(self, msg):
self._i += 1
self.get_logger().info("Publishing line %d" % self._i)
pose = msg.pose.pose
car_pos = (pose.position.x, pose.position.y)
# car direction
quaternion = (
pose.orientation.x,
pose.orientation.y,
pose.orientation.z,
pose.orientation.w,
)
euler = tf_transformations.euler_from_quaternion(quaternion)
car_direction = euler[2] # radians
draw_point = (
car_pos[0]
+ math.cos(car_direction),
car_pos[1]
+ math.sin(car_direction),
)
# draws from car to car movement direction
self.draw_line(
car_pos,
draw_point,
id=0,
color=(1.0, 1.0, 0.0),
)
Make sure to add a main function and make necessary imports.
If compiled and run, the node should output Publishing line {number}
. Enabling the /line_test
topic on foxglove studio should display a line in front of the car. Good job!
#
Drawing the car's instantaneous movement
Add:
car_speed = msg.twist.twist.linear.x
car_steering = msg.twist.twist.angular.z
below car_pos to obtain the car's speed and steering angle (in radians).
then modify draw_point to:
draw_point = (
car_pos[0]
+ math.cos(car_direction + car_steering / math.pi) * car_speed * 0.5,
car_pos[1]
+ math.sin(car_direction + car_steering / math.pi) * car_speed * 0.5,
)
This will adust the line's length based on the car speed and steering angle. In autonomous vehicles, continous steering and acceleration is required for a smooth driving experience. Our painter node allows us to analyze whether our steering and velocity are abrupt or not.
#
Visualizing an array of markers
Simply publish a MarkerArray (https://docs.ros2.org/galactic/api/visualization_msgs/msg/MarkerArray.html). This is useful to publish different marker types (see types: https://docs.ros2.org/galactic/api/visualization_msgs/msg/Marker.html). However, if you're seeking to display waypoints, a single Marker using "Sphere list" (type 7) will be more efficient.
#
Final code
import rclpy
from rclpy.node import Node
from visualization_msgs.msg import Marker
from geometry_msgs.msg import Point
from nav_msgs.msg import Odometry
import tf_transformations # should be installed otherwise apt install ros-iron-tf-transformations
import math
class PainterNode(Node):
def __init__(self):
super().__init__("painter_node")
self._line_publisher = self.create_publisher(Marker, "/line_test", 10)
# subscribe to /odom
self.subscription = self.create_subscription(
Odometry, "/odom", self.odom_callback, 10
)
self._i = 0
def odom_callback(self, msg):
self._i += 1
self.get_logger().info("Publishing line %d" % self._i)
pose = msg.pose.pose
car_pos = (pose.position.x, pose.position.y)
car_speed = msg.twist.twist.linear.x
car_steering = msg.twist.twist.angular.z
# car direction
quaternion = (
pose.orientation.x,
pose.orientation.y,
pose.orientation.z,
pose.orientation.w,
)
euler = tf_transformations.euler_from_quaternion(quaternion)
car_direction = euler[2] # radians
draw_point = (
car_pos[0]
+ math.cos(car_direction + car_steering / math.pi) * car_speed * 0.5,
car_pos[1]
+ math.sin(car_direction + car_steering / math.pi) * car_speed * 0.5,
)
# draws from car to car movement direction
self.draw_line(
car_pos,
draw_point,
id=0,
color=(1.0, 1.0, 0.0),
)
def draw_line(self, c1, c2, id=0, color=(0.0, 1.0, 0.0)):
# p1, p2 to Points
p1 = Point()
p1.x = c1[0]
p1.y = c1[1]
p1.z = 0.1
p2 = Point()
p2.x = c2[0]
p2.y = c2[1]
p2.z = 0.1
marker = Marker()
marker.header.frame_id = "map"
marker.id = id
marker.type = marker.LINE_STRIP
marker.action = marker.ADD
marker.points = [p1, p2]
marker.pose.orientation.w = 1.0
scale = 0.1 # adjust if needed
marker.scale.x = scale
marker.scale.y = scale
marker.scale.z = scale
marker.color.a = 1.0
marker.color.r = color[0]
marker.color.g = color[1]
marker.color.b = color[2]
self._line_publisher.publish(marker)
def main(args=None):
rclpy.init(args=args)
painter_node = PainterNode()
rclpy.spin(painter_node)
painter_node.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()
#
Challenges
- Display the car's active path using a kinematic bicycle model
- Give the car's visual an upgrade using a mesh marker (avocado?)
- Create and visualize a set of obstacles that are visible to the car