mcap-codec-support 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcap_codec_support/__init__.py +1 -0
- mcap_codec_support/_messages.py +56 -0
- mcap_codec_support/_protocols.py +94 -0
- mcap_codec_support/_schemas.py +17 -0
- mcap_codec_support/pointcloud/__init__.py +45 -0
- mcap_codec_support/pointcloud/compression.py +340 -0
- mcap_codec_support/pointcloud/factories.py +473 -0
- mcap_codec_support/pointcloud/schemas.py +111 -0
- mcap_codec_support/py.typed +0 -0
- mcap_codec_support/video/__init__.py +55 -0
- mcap_codec_support/video/common.py +283 -0
- mcap_codec_support/video/compression.py +269 -0
- mcap_codec_support/video/factories.py +123 -0
- mcap_codec_support/video/ffmpeg.py +944 -0
- mcap_codec_support/video/file_writer.py +411 -0
- mcap_codec_support/video/pyav.py +413 -0
- mcap_codec_support/video/schemas.py +52 -0
- mcap_codec_support-0.2.0.dist-info/METADATA +53 -0
- mcap_codec_support-0.2.0.dist-info/RECORD +20 -0
- mcap_codec_support-0.2.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"""Point cloud decoder factories.
|
|
2
|
+
|
|
3
|
+
Also exposes :class:`Pointcloud2DecoderFactory`, a small_mcap decoder
|
|
4
|
+
factory that decodes ``sensor_msgs/PointCloud2`` payloads directly into a
|
|
5
|
+
structured numpy array via :func:`pointcloud2.read_points`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from types import SimpleNamespace
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
12
|
+
|
|
13
|
+
from mcap_codec_support.pointcloud.schemas import (
|
|
14
|
+
COMPRESSED_POINTCLOUD2_SCHEMA,
|
|
15
|
+
FOXGLOVE_COMPRESSED_POINTCLOUD_SCHEMA,
|
|
16
|
+
POINTCLOUD2_SCHEMAS,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
from pointcloud2 import HeaderMsg, PointFieldDict, PointFieldMsg
|
|
24
|
+
from pointcloud2.messages import Stamp
|
|
25
|
+
from pureini import PointcloudDecoder
|
|
26
|
+
from small_mcap import Schema
|
|
27
|
+
|
|
28
|
+
from mcap_codec_support._messages import Header, Pointcloud2Dict
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _RosCompressedPointcloud2Msg(Protocol):
|
|
32
|
+
"""point_cloud_interfaces/msg/CompressedPointCloud2."""
|
|
33
|
+
|
|
34
|
+
header: HeaderMsg
|
|
35
|
+
height: int
|
|
36
|
+
width: int
|
|
37
|
+
fields: list[PointFieldMsg]
|
|
38
|
+
is_bigendian: bool
|
|
39
|
+
point_step: int
|
|
40
|
+
row_step: int
|
|
41
|
+
is_dense: bool
|
|
42
|
+
format: str | bytes
|
|
43
|
+
compressed_data: bytes
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _FoxgloveCompressedPointcloudMsg(Protocol):
|
|
47
|
+
"""foxglove_msgs/msg/CompressedPointCloud — flattened header."""
|
|
48
|
+
|
|
49
|
+
timestamp: Stamp
|
|
50
|
+
frame_id: str
|
|
51
|
+
format: str | bytes
|
|
52
|
+
data: bytes
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_COMPRESSED_POINTCLOUD_SCHEMAS = {
|
|
56
|
+
COMPRESSED_POINTCLOUD2_SCHEMA,
|
|
57
|
+
FOXGLOVE_COMPRESSED_POINTCLOUD_SCHEMA,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _pointcloud_dict_to_array(cloud_dict: Pointcloud2Dict) -> np.ndarray:
|
|
62
|
+
from pointcloud2 import read_points # noqa: PLC0415
|
|
63
|
+
|
|
64
|
+
ns = SimpleNamespace(**{k: v for k, v in cloud_dict.items() if k != "header"})
|
|
65
|
+
return read_points(ns, skip_nans=True)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _decode_format(fmt: str | bytes) -> str:
|
|
69
|
+
if isinstance(fmt, bytes):
|
|
70
|
+
fmt = fmt.decode()
|
|
71
|
+
return fmt.strip().lower()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _as_bytes(payload: bytes | bytearray | memoryview) -> bytes:
|
|
75
|
+
return payload if isinstance(payload, bytes) else bytes(payload)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _header_from_ros_msg(msg: _RosCompressedPointcloud2Msg) -> Header:
|
|
79
|
+
stamp = msg.header.stamp
|
|
80
|
+
return {
|
|
81
|
+
"stamp": {"sec": stamp.sec, "nanosec": stamp.nanosec},
|
|
82
|
+
"frame_id": msg.header.frame_id,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _header_from_foxglove_msg(msg: _FoxgloveCompressedPointcloudMsg) -> Header:
|
|
87
|
+
return {
|
|
88
|
+
"stamp": {"sec": msg.timestamp.sec, "nanosec": msg.timestamp.nanosec},
|
|
89
|
+
"frame_id": msg.frame_id,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _fields_from_msg(fields: list[PointFieldMsg]) -> list[PointFieldDict]:
|
|
94
|
+
return [
|
|
95
|
+
{
|
|
96
|
+
"name": field.name,
|
|
97
|
+
"offset": field.offset,
|
|
98
|
+
"datatype": field.datatype,
|
|
99
|
+
"count": field.count,
|
|
100
|
+
}
|
|
101
|
+
for field in fields
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _fields_from_cloudini_info(info: Any) -> list[PointFieldDict]:
|
|
106
|
+
return [
|
|
107
|
+
{
|
|
108
|
+
"name": field.name,
|
|
109
|
+
"offset": field.offset,
|
|
110
|
+
"datatype": int(field.type),
|
|
111
|
+
"count": 1,
|
|
112
|
+
}
|
|
113
|
+
for field in info.fields
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _pointcloud2_from_cloudini_ros(
|
|
118
|
+
msg: _RosCompressedPointcloud2Msg, pc_decoder: PointcloudDecoder
|
|
119
|
+
) -> Pointcloud2Dict:
|
|
120
|
+
raw_bytes, _info = pc_decoder.decode(_as_bytes(msg.compressed_data))
|
|
121
|
+
return {
|
|
122
|
+
"header": _header_from_ros_msg(msg),
|
|
123
|
+
"height": int(msg.height),
|
|
124
|
+
"width": int(msg.width),
|
|
125
|
+
"fields": _fields_from_msg(msg.fields),
|
|
126
|
+
"is_bigendian": bool(msg.is_bigendian),
|
|
127
|
+
"point_step": int(msg.point_step),
|
|
128
|
+
"row_step": int(msg.row_step),
|
|
129
|
+
"data": raw_bytes,
|
|
130
|
+
"is_dense": bool(msg.is_dense),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _pointcloud2_from_cloudini_foxglove(
|
|
135
|
+
msg: _FoxgloveCompressedPointcloudMsg, pc_decoder: PointcloudDecoder
|
|
136
|
+
) -> Pointcloud2Dict:
|
|
137
|
+
raw_bytes, info = pc_decoder.decode(_as_bytes(msg.data))
|
|
138
|
+
point_step = info.point_step
|
|
139
|
+
width = info.width
|
|
140
|
+
return {
|
|
141
|
+
"header": _header_from_foxglove_msg(msg),
|
|
142
|
+
"height": info.height,
|
|
143
|
+
"width": width,
|
|
144
|
+
"fields": _fields_from_cloudini_info(info),
|
|
145
|
+
"is_bigendian": False,
|
|
146
|
+
"point_step": point_step,
|
|
147
|
+
"row_step": point_step * width,
|
|
148
|
+
"data": raw_bytes,
|
|
149
|
+
"is_dense": True,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _decode_draco_payload(payload: bytes, header: Header) -> Pointcloud2Dict:
|
|
154
|
+
import DracoPy # noqa: PLC0415
|
|
155
|
+
import numpy as np # noqa: PLC0415
|
|
156
|
+
from pointcloud2 import fields_from_dtype # noqa: PLC0415
|
|
157
|
+
|
|
158
|
+
decoded = DracoPy.decode(_as_bytes(payload))
|
|
159
|
+
positions = np.asarray(decoded.points, dtype=np.float32)
|
|
160
|
+
if positions.ndim != 2 or positions.shape[1] < 2:
|
|
161
|
+
raise ValueError("Draco point cloud does not contain at least two coordinate fields")
|
|
162
|
+
|
|
163
|
+
point_count = int(positions.shape[0])
|
|
164
|
+
columns: list[tuple[str, np.ndarray]] = [
|
|
165
|
+
("x", positions[:, 0].astype(np.float32, copy=False)),
|
|
166
|
+
("y", positions[:, 1].astype(np.float32, copy=False)),
|
|
167
|
+
]
|
|
168
|
+
if positions.shape[1] >= 3:
|
|
169
|
+
columns.append(("z", positions[:, 2].astype(np.float32, copy=False)))
|
|
170
|
+
else:
|
|
171
|
+
columns.append(("z", np.zeros(point_count, dtype=np.float32)))
|
|
172
|
+
|
|
173
|
+
for index, attr in enumerate(decoded.attributes):
|
|
174
|
+
if attr.get("attribute_type") == DracoPy.AttributeType.POSITION:
|
|
175
|
+
continue
|
|
176
|
+
name = attr.get("name") or f"attribute_{attr.get('unique_id', index)}"
|
|
177
|
+
if name in {"x", "y", "z"}:
|
|
178
|
+
continue
|
|
179
|
+
values = attr.get("data")
|
|
180
|
+
if values is None:
|
|
181
|
+
continue
|
|
182
|
+
arr = np.asarray(values)
|
|
183
|
+
if arr.shape[0] != point_count:
|
|
184
|
+
continue
|
|
185
|
+
if arr.dtype.kind == "f":
|
|
186
|
+
arr = arr.astype(np.float32, copy=False)
|
|
187
|
+
elif arr.dtype == np.uint8:
|
|
188
|
+
arr = arr.astype(np.uint8, copy=False)
|
|
189
|
+
elif arr.dtype == np.uint16:
|
|
190
|
+
arr = arr.astype(np.uint16, copy=False)
|
|
191
|
+
elif arr.dtype == np.uint32:
|
|
192
|
+
arr = arr.astype(np.uint32, copy=False)
|
|
193
|
+
else:
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
if arr.ndim == 1 or arr.shape[1] == 1:
|
|
197
|
+
columns.append((str(name), arr.reshape(point_count)))
|
|
198
|
+
else:
|
|
199
|
+
columns.extend(
|
|
200
|
+
(f"{name}_{component}", arr[:, component]) for component in range(arr.shape[1])
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
dtype = np.dtype([(name, values.dtype) for name, values in columns])
|
|
204
|
+
point_data = np.empty(point_count, dtype=dtype)
|
|
205
|
+
for name, values in columns:
|
|
206
|
+
point_data[name] = values
|
|
207
|
+
|
|
208
|
+
fields: list[PointFieldDict] = [
|
|
209
|
+
{"name": field.name, "offset": field.offset, "datatype": field.datatype, "count": 1}
|
|
210
|
+
for field in fields_from_dtype(point_data.dtype)
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
"header": header,
|
|
215
|
+
"height": 1,
|
|
216
|
+
"width": point_count,
|
|
217
|
+
"fields": fields,
|
|
218
|
+
"is_bigendian": False,
|
|
219
|
+
"point_step": point_data.dtype.itemsize,
|
|
220
|
+
"row_step": point_data.dtype.itemsize * point_count,
|
|
221
|
+
"data": point_data.tobytes(),
|
|
222
|
+
"is_dense": True,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _pointcloud2_from_draco_ros(msg: _RosCompressedPointcloud2Msg) -> Pointcloud2Dict:
|
|
227
|
+
return _decode_draco_payload(msg.compressed_data, _header_from_ros_msg(msg))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _pointcloud2_from_draco_foxglove(msg: _FoxgloveCompressedPointcloudMsg) -> Pointcloud2Dict:
|
|
231
|
+
return _decode_draco_payload(msg.data, _header_from_foxglove_msg(msg))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# The two compressed-pointcloud schemas share ``format`` but differ in the
|
|
235
|
+
# payload field name (``compressed_data`` vs ``data``); branching on attribute
|
|
236
|
+
# presence is the only stable runtime discriminator.
|
|
237
|
+
def _is_ros_style_compressed_msg(
|
|
238
|
+
msg: _RosCompressedPointcloud2Msg | _FoxgloveCompressedPointcloudMsg,
|
|
239
|
+
) -> bool:
|
|
240
|
+
return hasattr(msg, "compressed_data")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class CloudiniPointCloudDecompressFactory:
|
|
244
|
+
"""Schema-only decoder factory: Cloudini compressed point cloud → PointCloud2.
|
|
245
|
+
|
|
246
|
+
Stateless — safe to share across all channels.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
def __init__(self) -> None:
|
|
250
|
+
from mcap_ros2_support_fast.decoder import DecoderFactory # noqa: PLC0415
|
|
251
|
+
from pureini import PointcloudDecoder # noqa: PLC0415
|
|
252
|
+
|
|
253
|
+
self._cdr_factory = DecoderFactory()
|
|
254
|
+
self._pc_decoder: PointcloudDecoder = PointcloudDecoder()
|
|
255
|
+
|
|
256
|
+
def _decompress(
|
|
257
|
+
self,
|
|
258
|
+
msg: _RosCompressedPointcloud2Msg | _FoxgloveCompressedPointcloudMsg,
|
|
259
|
+
) -> Pointcloud2Dict:
|
|
260
|
+
if _decode_format(msg.format) != "cloudini":
|
|
261
|
+
raise ValueError(f"unsupported compressed point cloud format: {msg.format!r}")
|
|
262
|
+
if _is_ros_style_compressed_msg(msg):
|
|
263
|
+
return _pointcloud2_from_cloudini_ros(msg, self._pc_decoder) # ty: ignore[invalid-argument-type]
|
|
264
|
+
return _pointcloud2_from_cloudini_foxglove(msg, self._pc_decoder) # ty: ignore[invalid-argument-type]
|
|
265
|
+
|
|
266
|
+
def decoder_for(
|
|
267
|
+
self,
|
|
268
|
+
message_encoding: str,
|
|
269
|
+
schema: Schema | None,
|
|
270
|
+
) -> Callable[[bytes | memoryview], Pointcloud2Dict] | None:
|
|
271
|
+
if schema is None or schema.name not in _COMPRESSED_POINTCLOUD_SCHEMAS:
|
|
272
|
+
return None
|
|
273
|
+
cdr_decoder = self._cdr_factory.decoder_for(message_encoding, schema)
|
|
274
|
+
if cdr_decoder is None:
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
def _decode(data: bytes | memoryview) -> Pointcloud2Dict:
|
|
278
|
+
return self._decompress(cdr_decoder(data))
|
|
279
|
+
|
|
280
|
+
return _decode
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class DracoPointCloudDecompressFactory:
|
|
284
|
+
"""Schema-only decoder factory: Draco compressed point cloud → PointCloud2."""
|
|
285
|
+
|
|
286
|
+
def __init__(self) -> None:
|
|
287
|
+
from mcap_ros2_support_fast.decoder import DecoderFactory # noqa: PLC0415
|
|
288
|
+
|
|
289
|
+
self._cdr_factory = DecoderFactory()
|
|
290
|
+
|
|
291
|
+
def _decompress(
|
|
292
|
+
self,
|
|
293
|
+
msg: _RosCompressedPointcloud2Msg | _FoxgloveCompressedPointcloudMsg,
|
|
294
|
+
) -> Pointcloud2Dict:
|
|
295
|
+
if _decode_format(msg.format) != "draco":
|
|
296
|
+
raise ValueError(f"unsupported compressed point cloud format: {msg.format!r}")
|
|
297
|
+
if _is_ros_style_compressed_msg(msg):
|
|
298
|
+
return _pointcloud2_from_draco_ros(msg) # ty: ignore[invalid-argument-type]
|
|
299
|
+
return _pointcloud2_from_draco_foxglove(msg) # ty: ignore[invalid-argument-type]
|
|
300
|
+
|
|
301
|
+
def decoder_for(
|
|
302
|
+
self,
|
|
303
|
+
message_encoding: str,
|
|
304
|
+
schema: Schema | None,
|
|
305
|
+
) -> Callable[[bytes | memoryview], Pointcloud2Dict] | None:
|
|
306
|
+
if schema is None or schema.name not in _COMPRESSED_POINTCLOUD_SCHEMAS:
|
|
307
|
+
return None
|
|
308
|
+
cdr_decoder = self._cdr_factory.decoder_for(message_encoding, schema)
|
|
309
|
+
if cdr_decoder is None:
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
def _decode(data: bytes | memoryview) -> Pointcloud2Dict:
|
|
313
|
+
return self._decompress(cdr_decoder(data))
|
|
314
|
+
|
|
315
|
+
return _decode
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class CompressedPointCloudDecompressFactory:
|
|
319
|
+
"""Decode either compressed point cloud schema by dispatching on ``format``."""
|
|
320
|
+
|
|
321
|
+
def __init__(self) -> None:
|
|
322
|
+
from mcap_ros2_support_fast.decoder import DecoderFactory # noqa: PLC0415
|
|
323
|
+
|
|
324
|
+
self._cdr_factory = DecoderFactory()
|
|
325
|
+
self._cloudini_decoder: PointcloudDecoder | None = None
|
|
326
|
+
|
|
327
|
+
def _ensure_cloudini_decoder(self) -> PointcloudDecoder:
|
|
328
|
+
if self._cloudini_decoder is None:
|
|
329
|
+
from pureini import PointcloudDecoder # noqa: PLC0415
|
|
330
|
+
|
|
331
|
+
self._cloudini_decoder = PointcloudDecoder()
|
|
332
|
+
return self._cloudini_decoder
|
|
333
|
+
|
|
334
|
+
def _decompress(
|
|
335
|
+
self,
|
|
336
|
+
msg: _RosCompressedPointcloud2Msg | _FoxgloveCompressedPointcloudMsg,
|
|
337
|
+
) -> Pointcloud2Dict:
|
|
338
|
+
fmt = _decode_format(msg.format)
|
|
339
|
+
is_ros = _is_ros_style_compressed_msg(msg)
|
|
340
|
+
if fmt == "cloudini":
|
|
341
|
+
decoder = self._ensure_cloudini_decoder()
|
|
342
|
+
if is_ros:
|
|
343
|
+
return _pointcloud2_from_cloudini_ros(msg, decoder) # ty: ignore[invalid-argument-type]
|
|
344
|
+
return _pointcloud2_from_cloudini_foxglove(msg, decoder) # ty: ignore[invalid-argument-type]
|
|
345
|
+
if fmt == "draco":
|
|
346
|
+
if is_ros:
|
|
347
|
+
return _pointcloud2_from_draco_ros(msg) # ty: ignore[invalid-argument-type]
|
|
348
|
+
return _pointcloud2_from_draco_foxglove(msg) # ty: ignore[invalid-argument-type]
|
|
349
|
+
raise ValueError(f"unsupported compressed point cloud format: {msg.format!r}")
|
|
350
|
+
|
|
351
|
+
def decoder_for(
|
|
352
|
+
self,
|
|
353
|
+
message_encoding: str,
|
|
354
|
+
schema: Schema | None,
|
|
355
|
+
) -> Callable[[bytes | memoryview], Pointcloud2Dict] | None:
|
|
356
|
+
if schema is None or schema.name not in _COMPRESSED_POINTCLOUD_SCHEMAS:
|
|
357
|
+
return None
|
|
358
|
+
cdr_decoder = self._cdr_factory.decoder_for(message_encoding, schema)
|
|
359
|
+
if cdr_decoder is None:
|
|
360
|
+
return None
|
|
361
|
+
|
|
362
|
+
def _decode(data: bytes | memoryview) -> Pointcloud2Dict:
|
|
363
|
+
return self._decompress(cdr_decoder(data))
|
|
364
|
+
|
|
365
|
+
return _decode
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class CloudiniCompressedPointcloud2DecoderFactory:
|
|
369
|
+
"""Decode Cloudini compressed point clouds to a structured numpy array.
|
|
370
|
+
|
|
371
|
+
Chains :class:`CloudiniPointCloudDecompressFactory` with
|
|
372
|
+
:func:`pointcloud2.read_points`, so the output has the same shape as the
|
|
373
|
+
regular PointCloud2 path — a numpy structured array ready to be packed
|
|
374
|
+
into an Arrow ``LIST<STRUCT<...>>``.
|
|
375
|
+
|
|
376
|
+
Requires ``pureini``; construction raises ``ImportError`` if missing.
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
def __init__(self) -> None:
|
|
380
|
+
self._decompress_factory = CloudiniPointCloudDecompressFactory()
|
|
381
|
+
|
|
382
|
+
def decoder_for(
|
|
383
|
+
self,
|
|
384
|
+
message_encoding: str,
|
|
385
|
+
schema: Schema | None,
|
|
386
|
+
) -> Callable[[bytes | memoryview], np.ndarray] | None:
|
|
387
|
+
decoder = self._decompress_factory.decoder_for(message_encoding, schema)
|
|
388
|
+
if decoder is None:
|
|
389
|
+
return None
|
|
390
|
+
|
|
391
|
+
def _decode(data: bytes | memoryview) -> np.ndarray:
|
|
392
|
+
return _pointcloud_dict_to_array(decoder(data))
|
|
393
|
+
|
|
394
|
+
return _decode
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class DracoCompressedPointcloudDecoderFactory:
|
|
398
|
+
"""Decode Draco compressed point clouds to a structured numpy array."""
|
|
399
|
+
|
|
400
|
+
def __init__(self) -> None:
|
|
401
|
+
self._decompress_factory = DracoPointCloudDecompressFactory()
|
|
402
|
+
|
|
403
|
+
def decoder_for(
|
|
404
|
+
self,
|
|
405
|
+
message_encoding: str,
|
|
406
|
+
schema: Schema | None,
|
|
407
|
+
) -> Callable[[bytes | memoryview], np.ndarray] | None:
|
|
408
|
+
decoder = self._decompress_factory.decoder_for(message_encoding, schema)
|
|
409
|
+
if decoder is None:
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
def _decode(data: bytes | memoryview) -> np.ndarray:
|
|
413
|
+
return _pointcloud_dict_to_array(decoder(data))
|
|
414
|
+
|
|
415
|
+
return _decode
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class CompressedPointCloudDecoderFactory:
|
|
419
|
+
"""Decode either compressed point cloud schema to a structured numpy array."""
|
|
420
|
+
|
|
421
|
+
def __init__(self) -> None:
|
|
422
|
+
self._decompress_factory = CompressedPointCloudDecompressFactory()
|
|
423
|
+
|
|
424
|
+
def decoder_for(
|
|
425
|
+
self,
|
|
426
|
+
message_encoding: str,
|
|
427
|
+
schema: Schema | None,
|
|
428
|
+
) -> Callable[[bytes | memoryview], np.ndarray] | None:
|
|
429
|
+
decoder = self._decompress_factory.decoder_for(message_encoding, schema)
|
|
430
|
+
if decoder is None:
|
|
431
|
+
return None
|
|
432
|
+
|
|
433
|
+
def _decode(data: bytes | memoryview) -> np.ndarray:
|
|
434
|
+
return _pointcloud_dict_to_array(decoder(data))
|
|
435
|
+
|
|
436
|
+
return _decode
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
PointCloudDecompressFactory = CompressedPointCloudDecompressFactory
|
|
440
|
+
CompressedPointcloud2DecoderFactory = CompressedPointCloudDecoderFactory
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
_SCHEMA_ENCODING_ROS2 = "ros2msg"
|
|
444
|
+
_MESSAGE_ENCODING_CDR = "cdr"
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
class Pointcloud2DecoderFactory:
|
|
448
|
+
def __init__(self) -> None:
|
|
449
|
+
from mcap_ros2_support_fast.decoder import DecoderFactory # noqa: PLC0415
|
|
450
|
+
|
|
451
|
+
self._ros2_decoder_factory = DecoderFactory()
|
|
452
|
+
|
|
453
|
+
def decoder_for(
|
|
454
|
+
self, message_encoding: str, schema: Schema | None
|
|
455
|
+
) -> Callable[[bytes | memoryview], np.ndarray] | None:
|
|
456
|
+
if (
|
|
457
|
+
message_encoding != _MESSAGE_ENCODING_CDR
|
|
458
|
+
or schema is None
|
|
459
|
+
or schema.encoding != _SCHEMA_ENCODING_ROS2
|
|
460
|
+
or schema.name not in POINTCLOUD2_SCHEMAS
|
|
461
|
+
):
|
|
462
|
+
return None
|
|
463
|
+
|
|
464
|
+
decoder = self._ros2_decoder_factory.decoder_for(message_encoding, schema)
|
|
465
|
+
if decoder is None:
|
|
466
|
+
return None
|
|
467
|
+
|
|
468
|
+
from pointcloud2 import read_points # noqa: PLC0415
|
|
469
|
+
|
|
470
|
+
def _decode(data: bytes | memoryview) -> np.ndarray:
|
|
471
|
+
return read_points(decoder(data), skip_nans=True)
|
|
472
|
+
|
|
473
|
+
return _decode
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Point cloud ROS schema names and definitions."""
|
|
2
|
+
|
|
3
|
+
POINTCLOUD2_SCHEMAS = {"sensor_msgs/msg/PointCloud2", "sensor_msgs/PointCloud2"}
|
|
4
|
+
COMPRESSED_POINTCLOUD2_SCHEMA = "point_cloud_interfaces/msg/CompressedPointCloud2"
|
|
5
|
+
CLOUDINI_COMPRESSED_POINTCLOUD2_SCHEMA = COMPRESSED_POINTCLOUD2_SCHEMA
|
|
6
|
+
FOXGLOVE_COMPRESSED_POINTCLOUD_SCHEMA = "foxglove_msgs/msg/CompressedPointCloud"
|
|
7
|
+
|
|
8
|
+
COMPRESSED_POINTCLOUD2 = """\
|
|
9
|
+
std_msgs/Header header
|
|
10
|
+
uint32 height
|
|
11
|
+
uint32 width
|
|
12
|
+
sensor_msgs/PointField[] fields
|
|
13
|
+
bool is_bigendian
|
|
14
|
+
uint32 point_step
|
|
15
|
+
uint32 row_step
|
|
16
|
+
uint8[] compressed_data
|
|
17
|
+
bool is_dense
|
|
18
|
+
string format
|
|
19
|
+
|
|
20
|
+
================================================================================
|
|
21
|
+
MSG: sensor_msgs/PointField
|
|
22
|
+
uint8 INT8 = 1
|
|
23
|
+
uint8 UINT8 = 2
|
|
24
|
+
uint8 INT16 = 3
|
|
25
|
+
uint8 UINT16 = 4
|
|
26
|
+
uint8 INT32 = 5
|
|
27
|
+
uint8 UINT32 = 6
|
|
28
|
+
uint8 FLOAT32 = 7
|
|
29
|
+
uint8 FLOAT64 = 8
|
|
30
|
+
string name
|
|
31
|
+
uint32 offset
|
|
32
|
+
uint8 datatype
|
|
33
|
+
uint32 count
|
|
34
|
+
|
|
35
|
+
================================================================================
|
|
36
|
+
MSG: std_msgs/Header
|
|
37
|
+
builtin_interfaces/Time stamp
|
|
38
|
+
string frame_id
|
|
39
|
+
|
|
40
|
+
================================================================================
|
|
41
|
+
MSG: builtin_interfaces/Time
|
|
42
|
+
int32 sec
|
|
43
|
+
uint32 nanosec"""
|
|
44
|
+
|
|
45
|
+
CLOUDINI_COMPRESSED_POINTCLOUD2 = COMPRESSED_POINTCLOUD2
|
|
46
|
+
|
|
47
|
+
POINTCLOUD2 = """\
|
|
48
|
+
std_msgs/Header header
|
|
49
|
+
uint32 height
|
|
50
|
+
uint32 width
|
|
51
|
+
sensor_msgs/PointField[] fields
|
|
52
|
+
bool is_bigendian
|
|
53
|
+
uint32 point_step
|
|
54
|
+
uint32 row_step
|
|
55
|
+
uint8[] data
|
|
56
|
+
bool is_dense
|
|
57
|
+
|
|
58
|
+
================================================================================
|
|
59
|
+
MSG: sensor_msgs/PointField
|
|
60
|
+
uint8 INT8 = 1
|
|
61
|
+
uint8 UINT8 = 2
|
|
62
|
+
uint8 INT16 = 3
|
|
63
|
+
uint8 UINT16 = 4
|
|
64
|
+
uint8 INT32 = 5
|
|
65
|
+
uint8 UINT32 = 6
|
|
66
|
+
uint8 FLOAT32 = 7
|
|
67
|
+
uint8 FLOAT64 = 8
|
|
68
|
+
string name
|
|
69
|
+
uint32 offset
|
|
70
|
+
uint8 datatype
|
|
71
|
+
uint32 count
|
|
72
|
+
|
|
73
|
+
================================================================================
|
|
74
|
+
MSG: std_msgs/Header
|
|
75
|
+
builtin_interfaces/Time stamp
|
|
76
|
+
string frame_id
|
|
77
|
+
|
|
78
|
+
================================================================================
|
|
79
|
+
MSG: builtin_interfaces/Time
|
|
80
|
+
int32 sec
|
|
81
|
+
uint32 nanosec"""
|
|
82
|
+
|
|
83
|
+
FOXGLOVE_COMPRESSED_POINTCLOUD = """\
|
|
84
|
+
builtin_interfaces/Time timestamp
|
|
85
|
+
string frame_id
|
|
86
|
+
geometry_msgs/Pose pose
|
|
87
|
+
uint8[] data
|
|
88
|
+
string format
|
|
89
|
+
|
|
90
|
+
================================================================================
|
|
91
|
+
MSG: geometry_msgs/Pose
|
|
92
|
+
geometry_msgs/Point position
|
|
93
|
+
geometry_msgs/Quaternion orientation
|
|
94
|
+
|
|
95
|
+
================================================================================
|
|
96
|
+
MSG: geometry_msgs/Point
|
|
97
|
+
float64 x
|
|
98
|
+
float64 y
|
|
99
|
+
float64 z
|
|
100
|
+
|
|
101
|
+
================================================================================
|
|
102
|
+
MSG: geometry_msgs/Quaternion
|
|
103
|
+
float64 x
|
|
104
|
+
float64 y
|
|
105
|
+
float64 z
|
|
106
|
+
float64 w
|
|
107
|
+
|
|
108
|
+
================================================================================
|
|
109
|
+
MSG: builtin_interfaces/Time
|
|
110
|
+
int32 sec
|
|
111
|
+
uint32 nanosec"""
|
|
File without changes
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Video MCAP factories, schema constants, and backend helpers."""
|
|
2
|
+
|
|
3
|
+
from mcap_codec_support._protocols import VideoCompressionBackend
|
|
4
|
+
from mcap_codec_support.video.common import (
|
|
5
|
+
EncoderBackend,
|
|
6
|
+
EncoderConfig,
|
|
7
|
+
EncoderMode,
|
|
8
|
+
VideoCodec,
|
|
9
|
+
VideoEncoderError,
|
|
10
|
+
calculate_downscale_dimensions,
|
|
11
|
+
get_software_encoder,
|
|
12
|
+
raw_image_to_array,
|
|
13
|
+
)
|
|
14
|
+
from mcap_codec_support.video.compression import (
|
|
15
|
+
create_video_compression_backend,
|
|
16
|
+
encode_raw_image_to_jpeg,
|
|
17
|
+
prefetch_image_decodes,
|
|
18
|
+
)
|
|
19
|
+
from mcap_codec_support.video.factories import VideoDecompressFactory
|
|
20
|
+
from mcap_codec_support.video.file_writer import (
|
|
21
|
+
VideoFileWriterSession,
|
|
22
|
+
create_video_file_writer,
|
|
23
|
+
)
|
|
24
|
+
from mcap_codec_support.video.schemas import (
|
|
25
|
+
COMPRESSED_IMAGE,
|
|
26
|
+
COMPRESSED_VIDEO_SCHEMA,
|
|
27
|
+
FOXGLOVE_COMPRESSED_VIDEO,
|
|
28
|
+
IMAGE,
|
|
29
|
+
IMAGE_SCHEMAS,
|
|
30
|
+
RAW_SCHEMAS,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"COMPRESSED_IMAGE",
|
|
35
|
+
"COMPRESSED_VIDEO_SCHEMA",
|
|
36
|
+
"FOXGLOVE_COMPRESSED_VIDEO",
|
|
37
|
+
"IMAGE",
|
|
38
|
+
"IMAGE_SCHEMAS",
|
|
39
|
+
"RAW_SCHEMAS",
|
|
40
|
+
"EncoderBackend",
|
|
41
|
+
"EncoderConfig",
|
|
42
|
+
"EncoderMode",
|
|
43
|
+
"VideoCodec",
|
|
44
|
+
"VideoCompressionBackend",
|
|
45
|
+
"VideoDecompressFactory",
|
|
46
|
+
"VideoEncoderError",
|
|
47
|
+
"VideoFileWriterSession",
|
|
48
|
+
"calculate_downscale_dimensions",
|
|
49
|
+
"create_video_compression_backend",
|
|
50
|
+
"create_video_file_writer",
|
|
51
|
+
"encode_raw_image_to_jpeg",
|
|
52
|
+
"get_software_encoder",
|
|
53
|
+
"prefetch_image_decodes",
|
|
54
|
+
"raw_image_to_array",
|
|
55
|
+
]
|