From f5d3494cbbfc0e341754ee6c853620173a350f53 Mon Sep 17 00:00:00 2001 From: Marko Durkovic Date: Mon, 31 Jan 2022 13:35:42 +0100 Subject: [PATCH] Add common usage examples to docs --- docs/examples/edit_rosbags_edit_timestamps.py | 42 ++++++++++++ docs/examples/edit_rosbags_remove_topic.py | 36 ++++++++++ docs/examples/edit_rosbas.rst | 16 +++++ docs/examples/register_types.rst | 22 +++++++ docs/examples/register_types_files.py | 24 +++++++ docs/examples/register_types_rosbag1.py | 29 ++++++++ docs/examples/register_types_string.py | 25 +++++++ docs/examples/save_images.rst | 16 +++++ docs/examples/save_images_rosbag1.py | 46 +++++++++++++ docs/examples/save_images_rosbag2.py | 43 ++++++++++++ docs/examples/use_with_native.py | 66 +++++++++++++++++++ docs/examples/use_with_native.rst | 10 +++ docs/index.rst | 7 ++ 13 files changed, 382 insertions(+) create mode 100644 docs/examples/edit_rosbags_edit_timestamps.py create mode 100644 docs/examples/edit_rosbags_remove_topic.py create mode 100644 docs/examples/edit_rosbas.rst create mode 100644 docs/examples/register_types.rst create mode 100644 docs/examples/register_types_files.py create mode 100644 docs/examples/register_types_rosbag1.py create mode 100644 docs/examples/register_types_string.py create mode 100644 docs/examples/save_images.rst create mode 100644 docs/examples/save_images_rosbag1.py create mode 100644 docs/examples/save_images_rosbag2.py create mode 100644 docs/examples/use_with_native.py create mode 100644 docs/examples/use_with_native.rst diff --git a/docs/examples/edit_rosbags_edit_timestamps.py b/docs/examples/edit_rosbags_edit_timestamps.py new file mode 100644 index 00000000..36e2cdc1 --- /dev/null +++ b/docs/examples/edit_rosbags_edit_timestamps.py @@ -0,0 +1,42 @@ +"""Example: Edit timestamps.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from rosbags.rosbag2 import Reader, Writer +from rosbags.serde import deserialize_cdr, serialize_cdr + +if TYPE_CHECKING: + from pathlib import Path + + +def offset_timestamps(src: Path, dst: Path, offset: int) -> None: + """Offset timestamps. + + Args: + src: Source path. + dst: Destination path. + offset: Amount of nanoseconds to offset timestamps. + + """ + with Reader(src) as reader, Writer(dst) as writer: + conn_map = {} + for conn in reader.connections.values(): + conn_map[conn.id] = writer.add_connection( + conn.topic, + conn.msgtype, + conn.serialization_format, + conn.offered_qos_profiles, + ) + + for conn, timestamp, data in reader.messages(): + # Adjust header timestamps, too + msg = deserialize_cdr(data, conn.msgtype) + if head := getattr(msg, 'header', None): + headstamp = head.stamp.sec * 10**9 + head.stamp.nanosec + offset + head.stamp.sec = headstamp // 10**9 + head.stamp.nanosec = headstamp % 10**9 + data = serialize_cdr(msg, conn.msgtype) + + writer.write(conn_map[conn.id], timestamp + offset, data) diff --git a/docs/examples/edit_rosbags_remove_topic.py b/docs/examples/edit_rosbags_remove_topic.py new file mode 100644 index 00000000..0e416a84 --- /dev/null +++ b/docs/examples/edit_rosbags_remove_topic.py @@ -0,0 +1,36 @@ +"""Example: Remove topic.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from rosbags.rosbag2 import Reader, Writer + +if TYPE_CHECKING: + from pathlib import Path + + +def remove_topic(src: Path, dst: Path, topic: str) -> None: + """Remove topic from rosbag2. + + Args: + src: Source path. + dst: Destination path. + topic: Name of topic to remove. + + """ + with Reader(src) as reader, Writer(dst) as writer: + conn_map = {} + for conn in reader.connections.values(): + if conn.topic == topic: + continue + conn_map[conn.id] = writer.add_connection( + conn.topic, + conn.msgtype, + conn.serialization_format, + conn.offered_qos_profiles, + ) + + rconns = [reader.connections[x] for x in conn_map] + for conn, timestamp, data in reader.messages(connections=rconns): + writer.write(conn_map[conn.id], timestamp, data) diff --git a/docs/examples/edit_rosbas.rst b/docs/examples/edit_rosbas.rst new file mode 100644 index 00000000..7e90cfb6 --- /dev/null +++ b/docs/examples/edit_rosbas.rst @@ -0,0 +1,16 @@ +Edit rosbags +============ + +Rosbags does not support opening files in read-write mode, but implicitly enforces copy-on-write semantics. Apart from the mapping of reader to writer connections the process is fairly straightforward. + + +Remove topic +------------ + +.. literalinclude:: ./edit_rosbags_remove_topic.py + + +Edit timestamps +--------------- + +.. literalinclude:: ./edit_rosbags_edit_timestamps.py diff --git a/docs/examples/register_types.rst b/docs/examples/register_types.rst new file mode 100644 index 00000000..c946b245 --- /dev/null +++ b/docs/examples/register_types.rst @@ -0,0 +1,22 @@ +Register custom message types +============================= + +Out of the box rosbags only supports the message types that ship with a default ROS2 distribution. If you want to (de)serialize custom messages you need to add them to the type system manually. + + +From rosbag1 +------------ + +.. literalinclude:: ./register_types_rosbag1.py + + +From definition string +---------------------- + +.. literalinclude:: ./register_types_string.py + + +From multiple files +------------------- + +.. literalinclude:: ./register_types_files.py diff --git a/docs/examples/register_types_files.py b/docs/examples/register_types_files.py new file mode 100644 index 00000000..3cf1fd57 --- /dev/null +++ b/docs/examples/register_types_files.py @@ -0,0 +1,24 @@ +"""Example: Register types from msg files.""" + +from pathlib import Path + +from rosbags.typesys import get_types_from_msg, register_types + +add_types = {} + +msgdef = Path('/path/to/custom_msgs/msg/Speed.msg').read_text(encoding='utf-8') +add_types.update(get_types_from_msg(msgdef, 'custom_msgs/msg/Speed.msg')) + +msgdef = Path('/path/to/custom_msgs/msg/Accel.msg').read_text(encoding='utf-8') +add_types.update(get_types_from_msg(msgdef, 'custom_msgs/msg/Accel.msg')) + +register_types(add_types) + +# Type import works only after the register_types call, +# the classname is derived from the msgtype names above + +# pylint: disable=no-name-in-module,wrong-import-position +from rosbags.typesys.types import custom_msgs__msg__Speed as Speed # type: ignore # noqa +from rosbags.typesys.types import custom_msgs__msg__Accel as Accel # type: ignore # noqa + +# pylint: enable=no-name-in-module,wrong-import-position diff --git a/docs/examples/register_types_rosbag1.py b/docs/examples/register_types_rosbag1.py new file mode 100644 index 00000000..0be64334 --- /dev/null +++ b/docs/examples/register_types_rosbag1.py @@ -0,0 +1,29 @@ +"""Example: Register rosbag1 types.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from rosbags.rosbag1 import Reader +from rosbags.typesys import get_types_from_msg, register_types + +if TYPE_CHECKING: + from pathlib import Path + + +def process_bag(src: Path) -> None: + """Register contained messages types before processing bag. + + Args: + src: Bag to process. + + """ + with Reader(src) as reader: + typs = {} + for conn in reader.connections.values(): + typs.update(get_types_from_msg(conn.msgdef, conn.msgtype)) + register_types(typs) + + # Now all message types used in the bag are registered + # for conn, timestamp, data in reader.messages(): + # ... diff --git a/docs/examples/register_types_string.py b/docs/examples/register_types_string.py new file mode 100644 index 00000000..21f56934 --- /dev/null +++ b/docs/examples/register_types_string.py @@ -0,0 +1,25 @@ +"""Example: Register type from definition string.""" + +from rosbags.serde import serialize_cdr +from rosbags.typesys import get_types_from_msg, register_types + +# Your custom message definition +STRIDX_MSG = """ +string string +uint32 index +""" + +register_types(get_types_from_msg(STRIDX_MSG, 'custom_msgs/msg/StrIdx')) + +# Type import works only after the register_types call, +# the classname is derived from the msgtype name above + +# pylint: disable=no-name-in-module,wrong-import-position +from rosbags.typesys.types import custom_msgs__msg__StrIdx as StrIdx # type: ignore # noqa + +# pylint: enable=no-name-in-module,wrong-import-position + +message = StrIdx(string='foo', index=42) + +# Rawdata that can be passed to rosbag2.Writer.write +rawdata = serialize_cdr(message, message.__msgtype__) diff --git a/docs/examples/save_images.rst b/docs/examples/save_images.rst new file mode 100644 index 00000000..9296f634 --- /dev/null +++ b/docs/examples/save_images.rst @@ -0,0 +1,16 @@ +Save images as rosbag +===================== + +The following examples show how to create new ROS bags from images. + + +Save rosbag1 +------------ + +.. literalinclude:: ./save_images_rosbag1.py + + +Save rosbag2 +------------ + +.. literalinclude:: ./save_images_rosbag2.py diff --git a/docs/examples/save_images_rosbag1.py b/docs/examples/save_images_rosbag1.py new file mode 100644 index 00000000..60acc8e2 --- /dev/null +++ b/docs/examples/save_images_rosbag1.py @@ -0,0 +1,46 @@ +"""Example: Save images as rosbag1.""" + +import numpy + +from rosbags.rosbag1 import Writer +from rosbags.serde import cdr_to_ros1, serialize_cdr +from rosbags.typesys.types import builtin_interfaces__msg__Time as Time +from rosbags.typesys.types import sensor_msgs__msg__CompressedImage as CompressedImage +from rosbags.typesys.types import std_msgs__msg__Header as Header + +TOPIC = '/camera' +FRAMEID = 'map' + +# Contains filenames and their timestamps +IMAGES = [ + ('homer.jpg', 42), + ('marge.jpg', 43), +] + + +def save_images() -> None: + """Iterate over IMAGES and save to output bag.""" + with Writer('output.bag') as writer: + conn = writer.add_connection(TOPIC, CompressedImage.__msgtype__) + + for path, timestamp in IMAGES: + message = CompressedImage( + Header( + stamp=Time( + sec=int(timestamp // 10**9), + nanosec=int(timestamp % 10**9), + ), + frame_id=FRAMEID, + ), + format='jpeg', # could also be 'png' + data=numpy.fromfile(path, dtype=numpy.uint8), # type: ignore + ) + + writer.write( + conn, + timestamp, + cdr_to_ros1( + serialize_cdr(message, CompressedImage.__msgtype__), + CompressedImage.__msgtype__, + ), + ) diff --git a/docs/examples/save_images_rosbag2.py b/docs/examples/save_images_rosbag2.py new file mode 100644 index 00000000..673d4da5 --- /dev/null +++ b/docs/examples/save_images_rosbag2.py @@ -0,0 +1,43 @@ +"""Save multiple images in rosbag2.""" + +import numpy + +from rosbags.rosbag2 import Writer +from rosbags.serde import serialize_cdr +from rosbags.typesys.types import builtin_interfaces__msg__Time as Time +from rosbags.typesys.types import sensor_msgs__msg__CompressedImage as CompressedImage +from rosbags.typesys.types import std_msgs__msg__Header as Header + +TOPIC = '/camera' +FRAMEID = 'map' + +# Contains filenames and their timestamps +IMAGES = [ + ('homer.jpg', 42), + ('marge.jpg', 43), +] + + +def save_images() -> None: + """Iterate over IMAGES and save to output bag.""" + with Writer('output') as writer: + conn = writer.add_connection(TOPIC, CompressedImage.__msgtype__, 'cdr', '') + + for path, timestamp in IMAGES: + message = CompressedImage( + Header( + stamp=Time( + sec=int(timestamp // 10**9), + nanosec=int(timestamp % 10**9), + ), + frame_id=FRAMEID, + ), + format='jpeg', # could also be 'png' + data=numpy.fromfile(path, dtype=numpy.uint8), # type: ignore + ) + + writer.write( + conn, + timestamp, + serialize_cdr(message, message.__msgtype__), + ) diff --git a/docs/examples/use_with_native.py b/docs/examples/use_with_native.py new file mode 100644 index 00000000..f484bfe3 --- /dev/null +++ b/docs/examples/use_with_native.py @@ -0,0 +1,66 @@ +"""Example: Message instance conversion.""" + +from __future__ import annotations + +import importlib +from typing import TYPE_CHECKING + +import numpy + +if TYPE_CHECKING: + from typing import Any + +NATIVE_CLASSES: dict[str, Any] = {} + + +def to_native(msg: Any) -> Any: + """Convert rosbags message to native message. + + Args: + msg: Rosbags message. + + Returns: + Native message. + + """ + msgtype: str = msg.__msgtype__ + if msgtype not in NATIVE_CLASSES: + pkg, name = msgtype.rsplit('/', 1) + NATIVE_CLASSES[msgtype] = getattr(importlib.import_module(pkg.replace('/', '.')), name) + + fields = {} + for name, field in msg.__dataclass_fields__.items(): + if 'ClassVar' in field.type: + continue + value = getattr(msg, name) + if '__msg__' in field.type: + value = to_native(value) + elif isinstance(value, numpy.ndarray): + value = value.tolist() + fields[name] = value + + return NATIVE_CLASSES[msgtype](**fields) + + +if __name__ == '__main__': + from rosbags.typesys.types import ( + builtin_interfaces__msg__Time, + sensor_msgs__msg__Image, + std_msgs__msg__Header, + ) + + image = sensor_msgs__msg__Image( + std_msgs__msg__Header( + builtin_interfaces__msg__Time(42, 666), + '/frame', + ), + 4, + 4, + 'rgb8', + False, + 4 * 3, + numpy.zeros(4 * 4 * 3, dtype=numpy.uint8), + ) + + native_image = to_native(image) + # native_image can now be passed to the ROS stack diff --git a/docs/examples/use_with_native.rst b/docs/examples/use_with_native.rst new file mode 100644 index 00000000..a245e13e --- /dev/null +++ b/docs/examples/use_with_native.rst @@ -0,0 +1,10 @@ +Use with native stack +===================== + +Messages read with rosbags are simple dataclasses that mimic the native ROS2 interface. If you want to pass those messages to the native ROS2 stack, you need to convert them into native objects first. + + +Message instance conversion +--------------------------- + +.. literalinclude:: ./use_with_native.py diff --git a/docs/index.rst b/docs/index.rst index 904b22ff..d609e6df 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,6 +16,13 @@ topics/rosbag1 topics/convert +.. toctree:: + :caption: Usage examples + :maxdepth: 0 + :hidden: + :glob: + + examples/* .. toctree:: :caption: API