Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions src/machinevisiontoolbox/Sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -2994,7 +2994,7 @@ class ROSBag(ImageSource):

:param filename: path to a ``.bag`` file, or an ``http(s)://`` URL
:type filename: str or Path
:param release: ROS release name (or unique substring), e.g. ``"noetic"``, defaults to ``"ROS1_NOETIC"``; controls the message definitions used to parse the bag file
:param release: ROS release (distribution) whose message definitions are used to parse the bag file, a name or unique substring such as ``"noetic"`` or ``"humble"``, or ``"auto"`` to detect it, defaults to ``"auto"``
:type release: str
:param topicfilter: only yield messages from this topic or list of topics;
``None`` accepts all topics
Expand Down Expand Up @@ -3095,12 +3095,20 @@ class ROSBag(ImageSource):
referenced by their bare name, e.g. ``ROSBag("forest.bag")``.

.. note::
The ``release`` argument controls the ROS message definitions used to
parse the bag file. This is important because message definitions can
change between ROS releases, and using the wrong definitions can lead to
incorrect parsing of the data. The ``release`` string is matched
case-insensitively against the member names of :class:`~rosbags.typesys.Stores`,
so short names such as ``"noetic"`` or ``"humble"`` are accepted in addition to the full enum name (e.g. ``"ROS1_NOETIC"``).
The ``release`` argument selects the ROS release (distribution) whose
message definitions are used to parse the bag file. Message definitions
can change between releases, and using the wrong ones can lead to
incorrect parsing of the data.

The default ``"auto"`` tries the known releases on the first message of the
bag and keeps the first that decodes it, for both ROS 1 and ROS 2 (CDR)
encoded messages. The choice is then available as the ``release``
attribute. Pass ``verbose=True`` to see what was tried.

Alternatively name a release. The string is matched case-insensitively
against the member names of :class:`~rosbags.typesys.Stores`, so short names
such as ``"noetic"`` or ``"humble"`` are accepted in addition to the full
enum name (e.g. ``"ROS1_NOETIC"``).

"""

Expand All @@ -3115,7 +3123,7 @@ class ROSBag(ImageSource):
def __init__(
self,
filename: str,
release: str = "ROS1_NOETIC",
release: str = "auto",
topicfilter: str | list[str] | None = None,
msgfilter: str | list[str] | None = "Image",
dtype: np.dtype | str | dict | None = None,
Expand Down Expand Up @@ -3530,7 +3538,8 @@ def _autodetect_typestore(self, rawdata: bytes, msgtype: str) -> bool:
store.deserialize_ros1(raw, msgtype)
self.typestore = store
self.release = name
print(f" auto-detect: {name} (ros1 encoding)")
if self.verbose:
print(f" auto-detect: {name} (ros1 encoding)")
return True
except (*_LENGTH_ERRORS, KeyError, UnicodeDecodeError, struct.error) as e:
# wrong store, or the message is the wrong length for it
Expand Down Expand Up @@ -3562,7 +3571,8 @@ def _autodetect_typestore(self, rawdata: bytes, msgtype: str) -> bool:
self.typestore = store
self.release = name
self._is_ros2 = True # use CDR for all subsequent messages
print(f" auto-detect: {name} (CDR encoding)")
if self.verbose:
print(f" auto-detect: {name} (CDR encoding)")
return True
except (
*_LENGTH_ERRORS,
Expand Down
82 changes: 82 additions & 0 deletions tests/test_ros_release_auto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""ROSBag detects the ROS release by default (``release="auto"``).

``bagtool --release`` already defaulted to ``"auto"``; the ``ROSBag`` class defaulted to
``"ROS1_NOETIC"``, which cannot read ROS 2 (CDR) messages.
"""

from __future__ import annotations

from pathlib import Path

import numpy as np
import pytest

pytest.importorskip("rosbags")

from machinevisiontoolbox import ROSBag

DATA = Path(__file__).parent / "data"
ROS1_BAG = DATA / "test_ros1.bag"
ROS2_BAG = DATA / "test_ros2"

# (bag, a release that is known to be right for it)
BAGS = [(ROS1_BAG, "noetic"), (ROS2_BAG, "humble")]


def _need(bag: Path) -> None:
if not bag.exists():
pytest.skip(f"fixture {bag.name} not generated")


def test_default_release_is_auto():
_need(ROS1_BAG)
assert ROSBag(str(ROS1_BAG)).release == "auto"


@pytest.mark.parametrize("bag, release", BAGS)
def test_default_reads_the_same_images_as_the_right_release(bag, release):
_need(bag)
auto = list(ROSBag(str(bag)))
explicit = list(ROSBag(str(bag), release=release))

assert len(auto) == len(explicit) > 0
for a, e in zip(auto, explicit):
assert a.topic == e.topic and a.timestamp == e.timestamp
np.testing.assert_array_equal(a.array, e.array)


@pytest.mark.parametrize("bag, release", BAGS)
def test_detected_release_is_reported(bag, release):
_need(bag)
reader = ROSBag(str(bag))
assert reader.release == "auto"
next(iter(reader))
assert reader.release != "auto"
assert reader.release.upper().startswith("ROS")


@pytest.mark.parametrize("bag, release", BAGS)
def test_detection_is_quiet_unless_verbose(bag, release, capsys):
_need(bag)
list(ROSBag(str(bag)))
assert "auto-detect" not in capsys.readouterr().out

list(ROSBag(str(bag), verbose=True))
assert "auto-detect" in capsys.readouterr().out


def test_an_explicit_release_still_works_and_is_not_changed():
_need(ROS1_BAG)
reader = ROSBag(str(ROS1_BAG), release="noetic")
assert len(list(reader)) > 0
assert reader.release == "noetic" # nothing was detected, nothing to report


def test_default_reads_a_real_ros2_mcap_bag():
"""The old default, ROS1_NOETIC, raised a SerdeError on this recording."""
path = Path(__file__).parent.parent / "bags" / "ros2_bouncing_ball"
if not path.exists():
pytest.skip("real ROS 2 bag not available")
images = list(ROSBag(str(path)))
# The bag's metadata claims 132 + 133 images, but its files only hold 39 + 40.
assert len(images) == 79
Loading