Skip to content

Commit 228694c

Browse files
hakuturu583claude
andauthored
fix(rosbag): widen Reader.messages start by 1ns to dodge rosbags MCAP chunk-filter off-by-one (#279)
* fix(rosbag): widen Reader.messages start by 1ns to dodge rosbags MCAP chunk-filter off-by-one Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(pre-commit): autofix --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: hakuturu583 <10348912+hakuturu583@users.noreply.github.com>
1 parent a17e67e commit 228694c

3 files changed

Lines changed: 48 additions & 10 deletions

File tree

t4_devkit/rosbag/pandar_decoder.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ def __post_init__(self) -> None:
8282
object.__setattr__(self, "cos_el", np.cos(el_rad))
8383
object.__setattr__(self, "sin_el", np.sin(el_rad))
8484
object.__setattr__(
85-
self, "azimuth_offset_rad",
85+
self,
86+
"azimuth_offset_rad",
8687
np.radians(np.array(self.azimuth_offset_deg, dtype=np.float32)),
8788
)
8889

@@ -137,11 +138,13 @@ def __post_init__(self) -> None:
137138
# Model lookup by sensor type name.
138139
HESAI_MODELS: dict[str, _HesaiModelConfig] = {
139140
"XT32": _HesaiModelConfig(
140-
name="XT32", elevation_deg=_XT32_ELEVATION_DEG,
141+
name="XT32",
142+
elevation_deg=_XT32_ELEVATION_DEG,
141143
azimuth_offset_deg=_XT32_AZIMUTH_OFFSET_DEG,
142144
),
143145
"OT128": _HesaiModelConfig(
144-
name="OT128", elevation_deg=_OT128_ELEVATION_DEG,
146+
name="OT128",
147+
elevation_deg=_OT128_ELEVATION_DEG,
145148
azimuth_offset_deg=_OT128_AZIMUTH_OFFSET_DEG,
146149
),
147150
}

t4_devkit/rosbag/reader.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@
3737

3838
def _quat_to_matrix(x: float, y: float, z: float, w: float) -> np.ndarray:
3939
"""Convert quaternion (x, y, z, w) to a 3x3 rotation matrix."""
40-
return np.array([
41-
[1 - 2 * (y * y + z * z), 2 * (x * y - w * z), 2 * (x * z + w * y)],
42-
[2 * (x * y + w * z), 1 - 2 * (x * x + z * z), 2 * (y * z - w * x)],
43-
[2 * (x * z - w * y), 2 * (y * z + w * x), 1 - 2 * (x * x + y * y)],
44-
])
40+
return np.array(
41+
[
42+
[1 - 2 * (y * y + z * z), 2 * (x * y - w * z), 2 * (x * z + w * y)],
43+
[2 * (x * y + w * z), 1 - 2 * (x * x + z * z), 2 * (y * z - w * x)],
44+
[2 * (x * z - w * y), 2 * (y * z + w * x), 1 - 2 * (x * x + y * y)],
45+
]
46+
)
4547

4648

4749
def _make_transform(R: np.ndarray, t: np.ndarray) -> np.ndarray:
@@ -223,7 +225,8 @@ def __init__(
223225
continue
224226
try:
225227
self._channel_sensor2ego[m.channel] = _resolve_chain(
226-
self._tf_tree, m.frame_id,
228+
self._tf_tree,
229+
m.frame_id,
227230
)
228231
except ValueError:
229232
logger.warning(
@@ -366,9 +369,15 @@ def get_pointcloud(
366369
raise ValueError(f"No connections found for topic '{topic}' (channel '{channel}')")
367370

368371
with self._lock:
372+
# Workaround for a rosbags MCAP off-by-one (storage_mcap.py:591):
373+
# `start < x.message_end_time` excludes the chunk when start equals
374+
# the chunk's last-message ts (which is inclusive), causing 1ns-window
375+
# queries at chunk boundaries to return empty. Widening start by 1ns
376+
# selects the correct chunk; the [start, stop) interval still uniquely
377+
# identifies the target packet.
369378
for conn, ts_ns, rawdata in self._reader.messages(
370379
connections=conns_for_topic,
371-
start=target_ns,
380+
start=target_ns - 1,
372381
stop=target_ns + 1,
373382
):
374383
msg = self._typestore.deserialize_cdr(rawdata, conn.msgtype)

tests/rosbag/test_reader.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,32 @@ def test_context_manager(self, bag_with_pointclouds: Path) -> None:
136136
with Rosbag2Reader(str(bag_with_pointclouds)) as reader:
137137
assert len(reader.channels) > 0
138138

139+
def test_get_pointcloud_start_widened(
140+
self,
141+
bag_with_pointclouds: Path,
142+
monkeypatch: pytest.MonkeyPatch,
143+
) -> None:
144+
"""Regression: start is widened by 1ns to dodge rosbags MCAP chunk-filter off-by-one."""
145+
mapping = [TopicMapping(channel="LIDAR_TOP", topic="/sensing/lidar/top/pointcloud")]
146+
with Rosbag2Reader(str(bag_with_pointclouds), topic_mapping=mapping) as reader:
147+
captured_starts: list[int] = []
148+
original_messages = reader._reader.messages
149+
150+
def patched_messages(*args, **kwargs):
151+
if "start" in kwargs:
152+
captured_starts.append(kwargs["start"])
153+
return original_messages(*args, **kwargs)
154+
155+
monkeypatch.setattr(reader._reader, "messages", patched_messages)
156+
157+
target_ts_us = 1_704_067_200_000_000
158+
target_ts_ns = target_ts_us * 1_000
159+
pc = reader.get_pointcloud("LIDAR_TOP", target_ts_us)
160+
161+
assert pc.points.shape == (4, 3)
162+
assert len(captured_starts) == 1
163+
assert captured_starts[0] == target_ts_ns - 1
164+
139165
def test_multiple_timestamps(self, bag_with_pointclouds: Path) -> None:
140166
mapping = [TopicMapping(channel="LIDAR_TOP", topic="/sensing/lidar/top/pointcloud")]
141167
with Rosbag2Reader(str(bag_with_pointclouds), topic_mapping=mapping) as reader:

0 commit comments

Comments
 (0)