Getting Started With OpenCV in ROS 2 Galactic (Python)

In this tutorial, we’ll learn the basics of how to interface ROS 2 Galactic with OpenCV, the popular computer vision library. These basics will provide you with the foundation to add vision to your robotics applications.

We’ll create an image publisher node to publish webcam data (i.e. video frames) to a topic, and we’ll create an image subscriber node that subscribes to that topic. 

If you’re interested in integrating OpenCV with ROS 2 Foxy, check out this tutorial.

Prerequisites

You can find all the code for this project at this link on my Google Drive.

Connect Your Built-in Webcam to Ubuntu 20.04 on a VirtualBox and Test OpenCV

Follow this tutorial to connect your built-in webcam to Ubuntu 20.04 on a Virtual Box and to test OpenCV on your machine.

Create a Package

Open a new terminal window, and navigate to the src directory of your workspace:

cd ~/dev_ws/src

Now let’s create a package named opencv_tools (you can name the package anything you want).

Type this command (this is all a single command):

ros2 pkg create --build-type ament_python opencv_tools --dependencies rclpy image_transport cv_bridge sensor_msgs std_msgs opencv2

Modify Package.xml

Go to the dev_ws/src/cv_basics folder.

cd ~/dev_ws/src/cv_basics

Make sure you have a text editor installed. I like to use gedit.

sudo apt-get install gedit

Open the package.xml file. 

gedit package.xml

Fill in the description of the cv_basics package, your email address and name on the maintainer line, and the license you desire (e.g. Apache License 2.0).

<description>Basic OpenCV-ROS2 nodes</description>
<maintainer email="automaticaddison@todo.todo">automaticaddison</maintainer>
<license>Apache License 2.0</license>

Save and close the file.

Build a Package

Return to the root of your workspace:

cd ~/dev_ws/

Build all packages in the workspace.

colcon build

Create the Image Publisher Node (Python)

Open a new terminal window, and type:

cd ~/dev_ws/src/opencv_tools/opencv_tools

Open a new Python file named basic_image_publisher.py.

gedit basic_image_publisher.py

Type the code below into it:

# Basic ROS 2 program to publish real-time streaming 
# video from your built-in webcam
# Author:
# - Addison Sears-Collins
# - https://automaticaddison.com
  
# Import the necessary libraries
import rclpy # Python Client Library for ROS 2
from rclpy.node import Node # Handles the creation of nodes
from sensor_msgs.msg import Image # Image is the message type
from cv_bridge import CvBridge # Package to convert between ROS and OpenCV Images
import cv2 # OpenCV library
 
class ImagePublisher(Node):
  """
  Create an ImagePublisher class, which is a subclass of the Node class.
  """
  def __init__(self):
    """
    Class constructor to set up the node
    """
    # Initiate the Node class's constructor and give it a name
    super().__init__('image_publisher')
      
    # Create the publisher. This publisher will publish an Image
    # to the video_frames topic. The queue size is 10 messages.
    self.publisher_ = self.create_publisher(Image, 'video_frames', 10)
      
    # We will publish a message every 0.1 seconds
    timer_period = 0.1  # seconds
      
    # Create the timer
    self.timer = self.create_timer(timer_period, self.timer_callback)
         
    # Create a VideoCapture object
    # The argument '0' gets the default webcam.
    self.cap = cv2.VideoCapture(0)
         
    # Used to convert between ROS and OpenCV images
    self.br = CvBridge()
   
  def timer_callback(self):
    """
    Callback function.
    This function gets called every 0.1 seconds.
    """
    # Capture frame-by-frame
    # This method returns True/False as well
    # as the video frame.
    ret, frame = self.cap.read()
          
    if ret == True:
      # Publish the image.
      # The 'cv2_to_imgmsg' method converts an OpenCV
      # image to a ROS 2 image message
      self.publisher_.publish(self.br.cv2_to_imgmsg(frame))
 
    # Display the message on the console
    self.get_logger().info('Publishing video frame')
  
def main(args=None):
  
  # Initialize the rclpy library
  rclpy.init(args=args)
  
  # Create the node
  image_publisher = ImagePublisher()
  
  # Spin the node so the callback function is called.
  rclpy.spin(image_publisher)
  
  # Destroy the node explicitly
  # (optional - otherwise it will be done automatically
  # when the garbage collector destroys the node object)
  image_publisher.destroy_node()
  
  # Shutdown the ROS client library for Python
  rclpy.shutdown()
  
if __name__ == '__main__':
  main()

Save and close the editor.

Modify Setup.py

Go to the following directory.

cd ~/dev_ws/src/opencv_tools/

Open setup.py.

gedit setup.py

Add the following line between the ‘console_scripts’: brackets:

'img_publisher = opencv_tools.basic_image_publisher:main',

The whole block should look like this:

entry_points={
    'console_scripts': [
      'img_publisher = opencv_tools.basic_image_publisher:main',
    ],
},

Save the file, and close it.

Create the Image Subscriber Node (Python)

Open a new terminal window, and type:

cd ~/dev_ws/src/opencv_tools/opencv_tools

Open a new Python file named basic_image_subscriber.py.

gedit basic_image_subscriber.py

Type the code below into it:

# Basic ROS 2 program to subscribe to real-time streaming 
# video from your built-in webcam
# Author:
# - Addison Sears-Collins
# - https://automaticaddison.com
  
# Import the necessary libraries
import rclpy # Python library for ROS 2
from rclpy.node import Node # Handles the creation of nodes
from sensor_msgs.msg import Image # Image is the message type
from cv_bridge import CvBridge # Package to convert between ROS and OpenCV Images
import cv2 # OpenCV library
 
class ImageSubscriber(Node):
  """
  Create an ImageSubscriber class, which is a subclass of the Node class.
  """
  def __init__(self):
    """
    Class constructor to set up the node
    """
    # Initiate the Node class's constructor and give it a name
    super().__init__('image_subscriber')
      
    # Create the subscriber. This subscriber will receive an Image
    # from the video_frames topic. The queue size is 10 messages.
    self.subscription = self.create_subscription(
      Image, 
      'video_frames', 
      self.listener_callback, 
      10)
    self.subscription # prevent unused variable warning
      
    # Used to convert between ROS and OpenCV images
    self.br = CvBridge()
   
  def listener_callback(self, data):
    """
    Callback function.
    """
    # Display the message on the console
    self.get_logger().info('Receiving video frame')
 
    # Convert ROS Image message to OpenCV image
    current_frame = self.br.imgmsg_to_cv2(data)
    
    # Display image
    cv2.imshow("camera", current_frame)
    
    cv2.waitKey(1)
  
def main(args=None):
  
  # Initialize the rclpy library
  rclpy.init(args=args)
  
  # Create the node
  image_subscriber = ImageSubscriber()
  
  # Spin the node so the callback function is called.
  rclpy.spin(image_subscriber)
  
  # Destroy the node explicitly
  # (optional - otherwise it will be done automatically
  # when the garbage collector destroys the node object)
  image_subscriber.destroy_node()
  
  # Shutdown the ROS client library for Python
  rclpy.shutdown()
  
if __name__ == '__main__':
  main()

Save and close the editor.

Modify Setup.py

Go to the following directory.

cd ~/dev_ws/src/opencv_tools/

Open setup.py.

gedit setup.py

Make sure the entry_points block looks like this:

entry_points={
    'console_scripts': [
      'img_publisher = opencv_tools.basic_image_publisher:main',
      'img_subscriber = opencv_tools.basic_image_subscriber:main',
    ],
},

Save the file, and close it.

Build the Package 

Return to the root of your workspace:

cd ~/dev_ws/

We need to double check that all the dependencies needed are already installed.

rosdep install -i --from-path src --rosdistro galactic -y

If you get the following error:

ERROR: the following packages/stacks could not have their rosdep keys resolved to system dependencies: cv_basics: Cannot locate rosdep definition for [opencv2]

To resolve the error, open your package.xml file.

cd ~/dev_ws/src/opencv_tools/
gedit package.xml

Change “opencv2” in your package.xml to “opencv-python” so that rosdep can find it. 

Save the file, and close it.

Build the package:

cd ~/dev_ws/
colcon build 

Run the Nodes

To run the nodes, open a new terminal window.

Make sure you are in the root of your workspace:

cd ~/dev_ws/

Run the publisher node. If you recall, its name is img_publisher.

ros2 run opencv_tools img_publisher
1-publishing-video-frame

Open a new terminal, and run the subscriber node.

ros2 run opencv_tools img_subscriber
2-receiving-video-frame

A window will pop up with the streaming video.

3-frame-appears

Check out the active topics.

ros2 topic list -t
4-ros2-topic-list

See the relationship between the active nodes.

rqt_graph
5-rqt_graph

Click the refresh button. The refresh button is that circular arrow at the top of the screen.

refresh

Press CTRL+C in all terminal windows when you’re ready to shut everything down.

That’s it! Keep building!

Connect Your Built-in Webcam to Ubuntu 20.04 on a VirtualBox

If you’re using Ubuntu 20.04 on a Virtual Box on a Windows PC, follow the instructions below to make sure your installation recognizes your built-in webcam.

Go to this page, and make sure you download and install the latest version of Virtual Box.

Once you have done that, go to this page, and download the Virtual Box Oracle VM VirtualBox Extension Pack.

1-virtual-box-extension-pack

Click on the Oracle VM VirtualBox Manager.

Go to File -> Preferences.

Select “Extensions” in the Preferences menu.

2-select-extensions

Click the “Add a New Package” icon on the right-hand side.

3-add-a-new-package

Select the Virtual Box Oracle VM VirtualBox Extension Pack you downloaded earlier.

4-virtualbox-extension-packJPG

Click Open.

Click Install at the prompt.

5-click-install

Scroll to the bottom of the VirtualBox License and click “I Agree”.

Click Yes to make changes to your system.

After it has installed, you should see this message.

6-extension-pack-installed

Click OK twice to go back to the main panel.

Close the Oracle VM VirtualBox Manager.

Go back to Windows.

Launch the Command Prompt. Press the Win + R keys, then, type ‘cmd’ and Enter.

Go to the VirtualBox folder.

cd c:\Program Files\Oracle\VirtualBox

Check the list of available cameras by typing this command.

VBoxManage list webcams
7-list-of-webcams

I have 1 webcam available.

Attach the webcam (or webcams) you want to use. The number at the end of the line indicates the camera.

VboxManage controlvm "Ubuntu 20.04" webcam attach .1
8-attach-webcamJPG

Open the Oracle VM VirtualBox Manager.

Launch Ubuntu.

In the top of the VirtualBox in the upper-left, there is a Devices dropdown menu. See the image below, towards the top (it is a bit tough to see in this photo, but it is there in faint text)

9-devices-menu

Click Devices -> Webcams. Enable your webcam(s). When you enable it, a check mark should appear next to it. You’ll have to do this step each time you launch Ubuntu.

Open up a new terminal window in Ubuntu. Copy the code from this post into a new Python program. That code uses the OpenCV library, a popular library for computer vision. You can save it to your Desktop in Ubuntu.

To run the code, type:

echo 'export OPENCV_LOG_LEVEL=ERROR' >> ~/.bashrc

You only need to type that command once. It keeps a bunch of error messages from popping up each time you run the program. You are adding a line to your .bashrc file.

Now type:

chmod +x object_tracker.py 

Now open a new terminal window, and type:

python3 object_tracker.py

*Replace object_tracker.py with whatever you named your program.

You should see yourself on the webcam. Don’t worry about all the error messages that pop up in the terminal. If you see yourself on the webcam, everything is working fine.

10-webcam-on-linuxJPG

Press CTRL+C in the terminal window to close everything down when you’re done.

That’s it! Keep building!

URDF vs. SDF – Link Pose, Joint Pose, Visual & Collision

One of the most confusing concepts creating a robot for simulation using ROS 2 and Gazebo is how to set up the coordinate frames for links and joints for your robot using URDF and SDF.

In this tutorial, I will compare and contrast how poses of links and joints are defined inside a typical URDF file vs. an SDF file.

Prerequisites

You understand coordinate frames for mobile robots.

What is a URDF File?

URDF (Unified Robotic Description Format) is an XML file format used in ROS to describe all elements (links and joints) of a robot.

You can find my instructions for setting up a URDF file in this tutorial.

What is an SDF File?

SDF (Simulation Description Format) is an XML file format used in Gazebo to describe all elements (links and joints) of a robot.

You can find my instructions for setting up an SDF file in this tutorial.

Links and Joints: URDF

Let’s look at an example to make this easier to undestand.

We will create a base for a basic mobile robot (like the one in the cover image of this tutorial) using URDF format. We will assume that the base_link coordinate frame is located 0.2 meters above the base_footprint coordinate frame. A sample schematic of the architecture is below.

coordsystems_img

Below is sample URDF code. We define the base_footprint link and the base_link.

<?xml version="1.0" ?>
<robot name="my_robot" xmlns:xacro="http://ros.org/wiki/xacro"> 
<!-- ****************** ROBOT BASE FOOTPRINT ***************************  -->
  <!-- Define the center of the main robot chassis projected on the ground -->	
  <link name="base_footprint">
  </link>

  <!-- ********************** ROBOT BASE *********************************  -->
  <link name="base_link">
    <visual>
      <origin xyz="-0.10 0 0" rpy="${pi/2} 0 ${pi/2}"/>
      <geometry>
        <mesh filename="file://$(find my_robot)/meshes/my_robot/robot_base.stl" />
      </geometry>
      <material name="White">
        <color rgba="${255/255} ${255/255} ${255/255} 1.0"/>
      </material>
    </visual>

    <collision>
      <origin xyz="0 0 -0.15" rpy="0 0 0"/>
      <geometry>
        <box size="0.70 0.30 0.01"/>
      </geometry>
    </collision>
  
  </link>

  <gazebo reference="base_link">
    <material>Gazebo/White</material>
  </gazebo>  
  
  <!-- The base footprint of the robot is located underneath the chassis -->
  <joint name="base_joint" type="fixed">
    <parent link="base_footprint"/>
    <child link="base_link" />
    <origin xyz="0 0 0.2" rpy="0 0 0"/>
  </joint>
</robot>

Looking at the joint definition in the URDF file above, you can see that the pose of the base_link inside the base_footprint coordinate frame is:

  • (x=0, y=0, z=0.2, roll = 0 radians, pitch = 0 radians, yaw = 0 radians)

This information must be contained within the <origin></origin> tag of the joint.

In other words, the base_link and base_footprint have the exact same orientation…the x, y, and z axes are all pointed in the same direction. However, the base_link is translated 0.2 meters in the positive z-direction (i.e. above).

Now, if you take a look inside the link definition for the base_link, you can see we defined the visual and collision properties of the link.

The visual properties define what the robot should look like in simulation.

The collision properties define the physical properties that govern the robot’s collisions with other objects in a Gazebo simulation environment.

Often the visual and collision properties are the same. However, if we want simulation engines like Gazebo to run faster, we might decide to represent the robot’s base_link as a cube rather than a more complex shape or mesh.

You can learn all about visual and collision properties in this tutorial.

The visual and collision properties may have their own pose defined inside separate <origin></origin> tags. The pose defined inside these tags is with respect to the link’s pose (i.e. x=0, y=0, z=0.2, roll = 0 radians, pitch = 0 radians, yaw = 0 radians). You can think of the pose inside the visual, collision, and inertial <origin></origin> tags as an offset from that specific link’s coordinate frame.

For example, take a look at the collision geometry again:

    <collision>
      <origin xyz="0 0 -0.15" rpy="0 0 0"/>
      <geometry>
        <box size="0.70 0.30 0.01"/>
      </geometry>
    </collision>

What this code above means is that the robot’s base collision geometry is a cube 0.1 meters in thickness. The center of mass of the cube is located 0.15 meters below the base_link pose. And since the base_link is located 0.2 meters above the base_footprint, the center of mass of the collision geometry is located 0.05 meters (i.e. 0.20 – 0.15) above the base footprint.

As you can see in this section below, the visual geometry is located 0.10 meters behind the origin of the base_link. It is also rotated 90 degrees (i.e. pi/2) around the x and z axes, with respect to the base_link coordinate frame.

    <visual>
      <origin xyz="-0.10 0 0" rpy="${pi/2} 0 ${pi/2}"/>
      <geometry>
        <mesh filename="file://$(find my_robot)/meshes/my_robot/robot_base.stl" />
      </geometry>
      <material name="White">
        <color rgba="${255/255} ${255/255} ${255/255} 1.0"/>
      </material>
    </visual>

So, in summary, in a URDF file, the main work horse for defining the coordinate frames is the <joint>…<origin></origin></joint> tag. The <origin></origin> tab inside the link definitions is used solely to make minor adjustments/offsets to the visual, collision, and inertial geometry.

Links and Joints: SDF

Now let’s look at the exact same robot base defined in SDF format. Here is the code:

<?xml version="1.0" ?>
<sdf version="1.6">
  <model name="my_robot">  
  <static>false</static>

  <!-- ****************** ROBOT BASE FOOTPRINT ***************************  -->
  <link name="base_footprint"/>

  <!-- ********************** ROBOT BASE *********************************  -->
  <link name="base_link">
    <!-- The pose below is always the global pose with respect to the 
    origin (i.e. base_footprint) of the robot model -->
    <pose>0 0 0.2 0 0 0</pose>
    <collision name="base_link_collision">
      <!-- The pose below is the local pose with respect to this link -->      
      <pose>0 0 -0.15 0 0 0</pose>
      <geometry>
        <box>
          <size>0.7 0.3 0.1</size>
        </box>
      </geometry>
    </collision>

    <visual name="base_link_visual">
      <pose>-0.10 0 0 1.5708 0 1.5708</pose>
      <geometry>
        <mesh>
          <uri>model://my_robot/meshes/robot_base.stl</uri>
          <scale>1.0 1.0 1.0</scale>
        </mesh>
      </geometry>
      <material>
        <ambient>1.0 1.0 1.0 1.0</ambient>
        <diffuse>1.0 1.0 1.0 1.0</diffuse>
        <specular>0.0 0.0 0.0 1.0</specular>
        <emissive>0.0 0.0 0.0 1.0</emissive>
      </material>
    </visual>
  </link>
  <!-- ************************ JOINTS ***********************************  -->
  <!-- Pose of the joint is the same as the child link frame -->
  <joint name="base_joint" type="fixed">
    <parent>base_footprint</parent>
    <child>base_link</child>
    <pose>0 0 0 0 0 0</pose>
  </joint>
  </model>
</sdf>

You can see that the base_link’s pose is set just inside the <link><pose></pose>…</link> tag. No matter what link this is inside a robot, this pose is always a global pose, relative to the origin (i.e. often the base_footprint in the case of a mobile robot) of the model.

The pose inside the collision and visual geometry tags work exactly the same as the <origin></origin> tags inside the collision and visual geometry definition in URDF file. These poses are offsets with respect to that specific link’s pose.

The pose inside the <joint></joint> tags in an offset from the child link’s pose. I almost always set this to all zeros for every joint in a robot model since the child link’s pose is set inside the link definition (i.e. inside the initial <pose></pose> tag) like below:

 <link name="base_link">
    <!-- The pose below is always the global pose with respect to the 
    origin (i.e. base_footprint) of the robot model -->
    <pose>0 0 0.2 0 0 0</pose>

So, in summary, in an SDF file, the main work horse for defining the coordinate frames is the <link><pose></pose>…</link> tag rather than the <joint>…<origin></origin></joint> tag.

The link-specific visual, collision, and inertial pose adjustments for an SDF file are defined inside the <pose></pose> tag rather than the <origin></origin> tag.

That’s it! I hope this information will help you out when you are trying to convert a URDF file to an SDF file.

Keep building!