Skip to main content
Share

How to Store Images in ROS 2

· 11 min read
Anthony Cavin
Data Scientist - ML/AI, Python, TypeScript

ROS with ReductStore

The Robot Operating System (ROS) stands as a versatile framework for developing sophisticated robotic applications with various sensors, including cameras. These cameras are relatively inexpensive and widely used as they can provide a wealth of information about the robot's environment.

Processing camera output with computer vision requires efficient solutions to handle massive amounts of data in real time. ROS 2 is designed with this in mind, but it is a communication middleware and does not provide a built-in solution for storing and managing large volumes of image data.

Addressing this challenge, this blog post will guide you through setting up ROS 2 with ReductStore—a time-series database for unstructured data optimized for edge computing, ensuring your robotic applications can process and store camera outputs effectively.

ROS 2 distributions

To install ROS 2, you'll need to select a distribution that aligns with your project requirements and system compatibility. As of now, multiple distributions are available for ROS 2 here.

Among them, two distributions are currently actively maintained :

  • Iron Irwini (iron) (Release date: May 23rd, 2023; End of life: November 2024)

  • Humble Hawksbill (humble) (Release date: May 23rd, 2022; End of life: May 2027)

We recommend the humble distribution for its long-term support until May 2027. This is the eighth release of ROS 2 and caters to various platforms including Ubuntu Linux, Windows, RHEL (Red Hat Enterprise Linux), or macOS.

To install the Humble Hawksbill distribution on your preferred operating system, follow the instructions provided in their installation guide. The installation process involves a series of commands specific to each platform and may require certain prerequisites like Python or C++ dependencies depending on your OS.

Understanding the advantages of using ReductStore with ROS

Integrating ReductStore with ROS provides many benefits for robotic applications:

  • Best performance: ReductStore's time-series design is tailored to the sequential nature of robotic applications and optimized for unstructured data, such as images, audio, and sensor readings.

  • Real-time data management: ReductStore provides real-time First In First Out (FIFO) quota management, which is critical for maintaining a balance between storage space and continuous data flow on edge devices.

  • Metadata association: It supports the association of labels or AI-generated analytics results directly with each stored record, enriching the dataset and facilitating subsequent processing or machine learning tasks.

  • Replication: ReductStore offers the ability to replicate data across a distributed network based on user-defined filters. For instance, to copy important data to a central server or cloud storage for further analysis.

Example to capture and store raw camera images

To capture and store raw camera images with ROS 2, you need to create a node that subscribes to an image topic, processes the image, and stores it in ReductStore.

You can use the reduct-ros-example as a guideline. This example demonstrates how to subscribe to a ROS topic, such as /image_raw, serialize the message in binary format, and store it in a bucket called ros-bucket under the entry image.

Create a custom ROS 2 Node

To set up your node for integrating ReductStore, you will need to create a custom ROS 2 Node that listens to image messages and uses ReductStore client for data storage.

Below is an example demonstrating this integration within a Python class:

import asyncio
from reduct import Client, Bucket
from sensor_msgs.msg import Image
from rclpy.node import Node

class ImageListener(Node):
"""Node for listening to image messages and storing them in ReductStore."""
def __init__(self, reduct_client: Client, loop: asyncio.AbstractEventLoop) -> None:
"""
Initialize the image listener node.

:param reduct_client: Client instance for interacting with ReductStore.
:param loop: The asyncio event loop.
"""
super().__init__("image_listener")
self.reduct_client: Client = reduct_client
self.loop: asyncio.AbstractEventLoop = loop
self.bucket: Bucket = None
self.subscription = self.create_subscription(
Image, "/image_raw", self.image_callback, 10
)

In this example ImageListener is a subclass of Node, which is part of ROS 2's client library (rclpy). It sets up a subscription to listen for incoming images from the /image_raw topic. When an image message is received by the node via self.subscription, it triggers image_callback.

In this callback function, each received frame is stored in the designated bucket in ReductStore using Python's built-in asyncio module, more on this later.

Initialize a new ReductStore bucket

This process involves setting up configuration parameters such as the bucket name and its storage quota settings.

In our example, we create a bucket named ros-bucket with a FIFO quota type, which is suitable for making sure that the disk doesn't run out of space.

The exist_ok parameter ensures that if the bucket already exists, it won't raise an exception but rather reuse the existing one.

Here's how we can define this initialization within our Python class:

from reduct_py import BucketSettings, QuotaType

class ImageListener(Node):
# ... [other parts of ImageListener class] ...

async def init_bucket(self) -> None:
"""Asynchronously initialize the Reduct bucket for storing images."""
self.get_logger().info("Initializing Reduct bucket")
self.bucket = await self.reduct_client.create_bucket(
"ros-bucket",
BucketSettings(quota_type=QuotaType.FIFO, quota_size=1_000_000_000),
exist_ok=True,
)

This code snippet should be called within our existing ImageListener class during the node's initialization or before storing the first image. This ensures that the storage bucket is ready.

Handling images in callbacks

When an image message from a ROS topic is received, it triggers the image_callback method. This method's role is to serialize the image data and organize its storage without blocking the main thread.

Serialization converts the ROS message data into a binary format that can be stored in ReductStore. This step is necessary because ReductStore is designed to handle binary data, and the image message is a complex data structure that needs to be converted into a simple byte stream.

Here's an example code snippet demonstrating how to handle images in callbacks for storing them in ReductStore:

class ImageListener(Node):
# ... [previous parts of ImageListener class] ...

@staticmethod
def get_timestamp(msg: Image) -> int:
"""
Extract the timestamp from a ROS message.

:param msg: The ROS message.
:return: The timestamp in microseconds.
"""
return int(msg.header.stamp.sec * 1e6 + msg.header.stamp.nanosec / 1e3)

@staticmethod
def serialize_message(msg: Image) -> bytes:
"""
Serialize a ROS message to bytes.

:param msg: The ROS message.
:return: The serialized message.
"""
return bytes(msg.data)

def image_callback(self, msg: Image) -> None:
"""
Handle incoming image messages by scheduling storage.

This callback is triggered by ROS message processing. It schedules
the image storage coroutine to be executed in the asyncio event loop.
"""
self.get_logger().info(f'Received image, storing to database')
timestamp = self.get_timestamp(msg)
binary_data = self.serialize_message(msg)
asyncio.run_coroutine_threadsafe(self.store_data(timestamp, binary_data), self.loop)

In this context, serialize_message is used to convert the Image message object into a byte stream that can subsequently be passed along for storage.

Following serialization, an asynchronous coroutine (store_data) is scheduled on the event loop (self.loop) using asyncio.run_coroutine_threadsafe.

This function is particularly useful for integrating asynchronous operations within primarily synchronous ROS 2 callback handlers, ensuring that the processing doesn't block the executor.

Store images in a ReductStore bucket entry

The store_data method is designed to receive timestamped image data and write it into a specific ReductStore bucket.

To store data, we need at least two parameters:

  • Timestamp Parameter: The timestamp argument ensures that each piece of data can be associated with the exact time it was captured.

  • Data Serialization: The data parameter expects a byte stream, which implies that image messages from ROS need to be serialized before being passed to this function.

With these considerations in mind, here's how you can define the store_data method within the ImageListener class:

class ImageListener(Node):
# ... [previous parts of ImageListener class] ...

async def store_data(self, timestamp: int, data: bytes) -> None:
"""
Store unstructured data in the Reduct bucket.

:param timestamp: The timestamp for the data.
:param data: The serialized data.
"""
if not self.bucket:
await self.init_bucket()

await self.bucket.write("image", data, timestamp)

When the store_data method is called, it first checks if the bucket has been initialized. If not, it calls the init_bucket method to create the bucket. Then, it writes the serialized data to the bucket entry image with the provided timestamp.

As you can see, the store_data method is designed to be non-blocking, ensuring that the main thread can continue processing other tasks without waiting for the data to be stored.

Testing the image storage system

To test the image storage system, you can run the ROS 2 node and publish image messages to the /image_raw topic.

For example, we can use the usb_cam package to capture images from a USB camera (such as a web camera) and publish them to the /image_raw topic.

To install the usb_cam package, you can use the following command:

sudo apt-get install ros-<ros2-distro>-usb-cam

Replace <ros2-distro> with the ROS 2 distribution you are using, such as humble or iron.

Our custom ROS 2 node directly stores the binary data from the Image message to ReductStore. This means that we can control the format of the images we store by configuring the usb_cam package to publish images in the desired format.

MotionJPEG (MJPEG) is a common format for video compression and is often used for video streaming. This format compresses each frame of video as a separate JPEG image, which can be useful for easily storing and processing images.

If your camera supports MJPEG, you can set the pixel_format parameter to raw_mjpeg to publish JPEG images Here's an example of how to do this with a config file usb_cam_params.yaml:

usb_cam:
ros__parameters:
video_device: "/dev/video0"
image_width: 640
image_height: 480
pixel_format: "raw_mjpeg"

We can run the usb_cam node to start capturing images and publishing them to the /image_raw topic using the following command:

ros2 run usb_cam usb_cam_node_exe --ros-args --params-file ./usb_cam_params.yaml

Once the usb_cam node is running, we can start the custom ROS 2 node that listens to the /image_raw topic and stores the images in ReductStore.

ros2 run reduct_camera capture_and_store

The capture_and_store node will start listening to the /image_raw topic and store the images in ReductStore as they are received.

Use ReductStore CLI to inspect stored images

To get started, you need to install the ReductStore CLI by following the instructions here.

Once installed, you can create an alias for the URL of your ReductStore instance using the rcli alias add command. For example:

rcli alias add -L  https://play.reduct.store play

You can then use the rcli command to inspect the stored images. For instance, to export the image data from the ros-bucket bucket to a local directory, you can use the following command:

rcli export folder --ext jpeg play/ros-bucket ./exported-data

This command exports all images from the ros-bucket bucket to the ./exported-data folder with the JPEG file extension.

Best practices

There are several best practices to consider when integrating ReductStore. Here are a few to keep in mind:

  • Use non-blocking operations to avoid blocking the main thread of the ROS 2 node. This ensures that the node can continue processing other tasks while waiting for data to be stored.
  • Serialize data before storing it in ReductStore in a cross-platform binary format to ensure compatibility with different systems and programming languages.
  • Create a ReductStore bucket with a FIFO quota to prevent disk overwriting in the future.
  • Use token authentication to protect your information. You can use either the Web Console or the CLI client to generate an access token.
  • Set up data replication between two instances using the Web Console or the CLI client.
  • Use ReductCLI to perform manual replication or backup.

Conclusion

In conclusion, this blog post has demonstrated how to capture and store raw camera images from a ROS topic in ReductStore. The provided code snippets serve as a practical guide for setting up such a system, highlighting the importance of non-blocking operations and proper serialization to maintain system performance and compatibility.

Using ReductStore is straightforward to deploy and provides a robust solution for managing large volumes of image data in real time. The FIFO quota management, metadata association, and replication features make it an ideal choice for managing unstructured data in ROS-based computer vision applications.


I hope this tutorial has been helpful. If you have any questions or feedback, don’t hesitate to use the ReductStore Community forum.