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,413 @@
|
|
|
1
|
+
"""PyAV-based video compression and decompression backend.
|
|
2
|
+
|
|
3
|
+
All ``av`` (PyAV) usage is confined to this module. Importing this file
|
|
4
|
+
requires PyAV and numpy to be installed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import threading
|
|
10
|
+
from fractions import Fraction
|
|
11
|
+
from io import BytesIO
|
|
12
|
+
from typing import TYPE_CHECKING, cast
|
|
13
|
+
|
|
14
|
+
import av
|
|
15
|
+
import av.error
|
|
16
|
+
from av import Packet, VideoFrame
|
|
17
|
+
from typing_extensions import Self
|
|
18
|
+
|
|
19
|
+
from mcap_codec_support.video.common import (
|
|
20
|
+
DecompressedFrame,
|
|
21
|
+
EncoderConfig,
|
|
22
|
+
VideoEncoderError,
|
|
23
|
+
build_encoder_options,
|
|
24
|
+
)
|
|
25
|
+
from mcap_codec_support.video.common import (
|
|
26
|
+
resolve_encoder as _resolve_encoder,
|
|
27
|
+
)
|
|
28
|
+
from mcap_codec_support.video.common import (
|
|
29
|
+
resolve_encoder_for_backend as _resolve_encoder_for_backend,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from av.container import InputContainer
|
|
34
|
+
from av.video.codeccontext import VideoCodecContext
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Encoder probing
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_encoder(encoder_name: str) -> bool:
|
|
43
|
+
"""Test if an encoder is available via PyAV."""
|
|
44
|
+
try:
|
|
45
|
+
av.CodecContext.create(encoder_name, "w")
|
|
46
|
+
except (av.error.FFmpegError, ValueError):
|
|
47
|
+
return False
|
|
48
|
+
else:
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def resolve_encoder(codec: str, *, use_hardware: bool = True) -> str:
|
|
53
|
+
"""Pick the best available encoder for *codec* using PyAV to probe."""
|
|
54
|
+
return _resolve_encoder(codec, test_fn=test_encoder, use_hardware=use_hardware)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def resolve_encoder_for_backend(codec: str, backend: str) -> str:
|
|
58
|
+
"""Pick the encoder for *codec* using the specified *backend* (PyAV probe)."""
|
|
59
|
+
return _resolve_encoder_for_backend(codec, backend, test_fn=test_encoder)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Image decoding (JPEG / PNG → VideoFrame)
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class _DecoderLocal(threading.local):
|
|
68
|
+
"""Thread-local persistent codec contexts for JPEG and PNG decoding."""
|
|
69
|
+
|
|
70
|
+
mjpeg_ctx: VideoCodecContext | None = None
|
|
71
|
+
png_ctx: VideoCodecContext | None = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
_decoder_local = _DecoderLocal()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _get_mjpeg_ctx() -> VideoCodecContext:
|
|
78
|
+
ctx = _decoder_local.mjpeg_ctx
|
|
79
|
+
if ctx is None:
|
|
80
|
+
ctx = cast("VideoCodecContext", av.CodecContext.create("mjpeg", "r"))
|
|
81
|
+
ctx.open()
|
|
82
|
+
_decoder_local.mjpeg_ctx = ctx
|
|
83
|
+
return ctx
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _get_png_ctx() -> VideoCodecContext:
|
|
87
|
+
ctx = _decoder_local.png_ctx
|
|
88
|
+
if ctx is None:
|
|
89
|
+
ctx = av.CodecContext.create("png", "r")
|
|
90
|
+
ctx.open()
|
|
91
|
+
_decoder_local.png_ctx = ctx
|
|
92
|
+
return ctx
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _detect_image_format(data: bytes) -> str:
|
|
96
|
+
if data[:2] == b"\xff\xd8":
|
|
97
|
+
return "mjpeg"
|
|
98
|
+
if data[:8] == b"\x89PNG\r\n\x1a\n":
|
|
99
|
+
return "png"
|
|
100
|
+
return "unknown"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _decode_via_container(data: bytes) -> VideoFrame:
|
|
104
|
+
try:
|
|
105
|
+
with cast("InputContainer", av.open(BytesIO(data))) as container:
|
|
106
|
+
for frame in container.decode(video=0):
|
|
107
|
+
return frame
|
|
108
|
+
except av.error.FFmpegError as exc:
|
|
109
|
+
raise VideoEncoderError(f"Failed to decode compressed image: {exc}") from exc
|
|
110
|
+
raise VideoEncoderError("Decoder produced no frames")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def decode_compressed_frame(compressed_data: bytes) -> VideoFrame:
|
|
114
|
+
"""Decode a compressed image (JPEG/PNG) to a VideoFrame.
|
|
115
|
+
|
|
116
|
+
Uses persistent thread-local codec contexts for performance.
|
|
117
|
+
"""
|
|
118
|
+
fmt = _detect_image_format(compressed_data)
|
|
119
|
+
if fmt == "unknown":
|
|
120
|
+
return _decode_via_container(compressed_data)
|
|
121
|
+
|
|
122
|
+
ctx = _get_mjpeg_ctx() if fmt == "mjpeg" else _get_png_ctx()
|
|
123
|
+
try:
|
|
124
|
+
for frame in ctx.decode(Packet(compressed_data)):
|
|
125
|
+
return frame
|
|
126
|
+
except av.error.FFmpegError as exc:
|
|
127
|
+
raise VideoEncoderError(f"Failed to decode compressed image: {exc}") from exc
|
|
128
|
+
|
|
129
|
+
raise VideoEncoderError("Decoder produced no frames")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
# VideoEncoder (H.264/H.265 frame encoder)
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class VideoEncoder:
|
|
138
|
+
"""PyAV-based video encoder for converting images to compressed video."""
|
|
139
|
+
|
|
140
|
+
_YUV420P_COMPAT = frozenset({"yuv420p", "yuvj420p"})
|
|
141
|
+
|
|
142
|
+
def __init__(
|
|
143
|
+
self,
|
|
144
|
+
width: int,
|
|
145
|
+
height: int,
|
|
146
|
+
codec_name: str,
|
|
147
|
+
quality: int = 28,
|
|
148
|
+
target_fps: float = 30.0,
|
|
149
|
+
gop_size: int = 30,
|
|
150
|
+
*,
|
|
151
|
+
preset: str | None = None,
|
|
152
|
+
) -> None:
|
|
153
|
+
self.config = EncoderConfig(width=width, height=height, codec_name=codec_name)
|
|
154
|
+
self._target_fps = max(target_fps, 1.0)
|
|
155
|
+
self._frame_index = 0
|
|
156
|
+
self._quality = quality
|
|
157
|
+
self._gop_size = gop_size
|
|
158
|
+
self._context: VideoCodecContext | None = None
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
self._context = cast("VideoCodecContext", av.CodecContext.create(codec_name, "w"))
|
|
162
|
+
except av.error.FFmpegError as exc:
|
|
163
|
+
raise VideoEncoderError(f"Failed to create encoder {codec_name}: {exc}") from exc
|
|
164
|
+
|
|
165
|
+
fps_int = max(round(self._target_fps), 1)
|
|
166
|
+
self._context.width = width
|
|
167
|
+
self._context.height = height
|
|
168
|
+
self._context.pix_fmt = "yuv420p"
|
|
169
|
+
self._context.time_base = Fraction(1, fps_int)
|
|
170
|
+
self._context.framerate = Fraction(fps_int, 1)
|
|
171
|
+
self._context.gop_size = gop_size
|
|
172
|
+
self._context.max_b_frames = 0
|
|
173
|
+
|
|
174
|
+
options, bit_rate = build_encoder_options(codec_name, quality, width, height, preset=preset)
|
|
175
|
+
if bit_rate is not None:
|
|
176
|
+
self._context.bit_rate = bit_rate
|
|
177
|
+
if options:
|
|
178
|
+
self._context.options = options
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
self._context.open()
|
|
182
|
+
except av.error.FFmpegError as exc:
|
|
183
|
+
self._context = None
|
|
184
|
+
raise VideoEncoderError(f"Failed to open encoder {codec_name}: {exc}") from exc
|
|
185
|
+
|
|
186
|
+
def close(self) -> None:
|
|
187
|
+
"""Release the native codec context."""
|
|
188
|
+
if self._context is not None:
|
|
189
|
+
del self._context
|
|
190
|
+
self._context = None
|
|
191
|
+
|
|
192
|
+
def __enter__(self) -> Self:
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
def __exit__(self, *_: object) -> None:
|
|
196
|
+
self.close()
|
|
197
|
+
|
|
198
|
+
def encode(self, frame: VideoFrame) -> bytes | None:
|
|
199
|
+
"""Encode a single frame and return compressed video bytes, or None if buffered."""
|
|
200
|
+
needs_resize = frame.width != self.config.width or frame.height != self.config.height
|
|
201
|
+
needs_fmt = frame.format.name not in self._YUV420P_COMPAT
|
|
202
|
+
if needs_resize or needs_fmt:
|
|
203
|
+
frame = frame.reformat(
|
|
204
|
+
width=self.config.width, height=self.config.height, format=self._context.pix_fmt
|
|
205
|
+
)
|
|
206
|
+
frame.pts = self._frame_index
|
|
207
|
+
self._frame_index += 1
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
packets = list(self._context.encode(frame))
|
|
211
|
+
except av.error.FFmpegError as exc:
|
|
212
|
+
raise VideoEncoderError(f"Encoding error: {exc}") from exc
|
|
213
|
+
|
|
214
|
+
if not packets:
|
|
215
|
+
return None
|
|
216
|
+
return b"".join(bytes(packet) for packet in packets)
|
|
217
|
+
|
|
218
|
+
def flush_packets(self) -> list[bytes]:
|
|
219
|
+
"""Flush remaining buffered frames as one bytes blob per packet."""
|
|
220
|
+
try:
|
|
221
|
+
packets = list(self._context.encode(None))
|
|
222
|
+
except av.error.FFmpegError:
|
|
223
|
+
return []
|
|
224
|
+
return [bytes(packet) for packet in packets]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
# JpegEncoder (single-frame MJPEG)
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class JpegEncoder:
|
|
233
|
+
"""PyAV-based MJPEG encoder for converting individual frames to JPEG.
|
|
234
|
+
|
|
235
|
+
JPEG is intra-only, so each call to ``encode`` produces exactly one
|
|
236
|
+
JPEG-encoded blob with no buffering.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
def __init__(self, width: int, height: int, quality: int = 90) -> None:
|
|
240
|
+
if not 1 <= quality <= 100:
|
|
241
|
+
raise VideoEncoderError(f"JPEG quality must be in [1, 100], got {quality}")
|
|
242
|
+
self.config = EncoderConfig(width=width, height=height, codec_name="mjpeg")
|
|
243
|
+
self._quality = quality
|
|
244
|
+
self._context: VideoCodecContext | None = None
|
|
245
|
+
try:
|
|
246
|
+
self._context = cast("VideoCodecContext", av.CodecContext.create("mjpeg", "w"))
|
|
247
|
+
except av.error.FFmpegError as exc:
|
|
248
|
+
raise VideoEncoderError(f"Failed to create mjpeg encoder: {exc}") from exc
|
|
249
|
+
|
|
250
|
+
self._context.width = width
|
|
251
|
+
self._context.height = height
|
|
252
|
+
self._context.pix_fmt = "yuvj420p"
|
|
253
|
+
self._context.time_base = Fraction(1, 1000)
|
|
254
|
+
# PyAV's mjpeg encoder maps q:v 1..31 (lower = better). Convert from 1..100.
|
|
255
|
+
qv = max(1, min(31, 32 - quality * 31 // 100))
|
|
256
|
+
self._context.options = {"q:v": str(qv)}
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
self._context.open()
|
|
260
|
+
except av.error.FFmpegError as exc:
|
|
261
|
+
self._context = None
|
|
262
|
+
raise VideoEncoderError(f"Failed to open mjpeg encoder: {exc}") from exc
|
|
263
|
+
|
|
264
|
+
self._frame_index = 0
|
|
265
|
+
|
|
266
|
+
def encode(self, frame: VideoFrame) -> bytes:
|
|
267
|
+
"""Encode a single frame to a complete JPEG blob."""
|
|
268
|
+
if (
|
|
269
|
+
frame.width != self.config.width
|
|
270
|
+
or frame.height != self.config.height
|
|
271
|
+
or frame.format.name != "yuvj420p"
|
|
272
|
+
):
|
|
273
|
+
frame = frame.reformat(
|
|
274
|
+
width=self.config.width, height=self.config.height, format="yuvj420p"
|
|
275
|
+
)
|
|
276
|
+
frame.pts = self._frame_index
|
|
277
|
+
self._frame_index += 1
|
|
278
|
+
try:
|
|
279
|
+
packets = list(self._context.encode(frame))
|
|
280
|
+
except av.error.FFmpegError as exc:
|
|
281
|
+
raise VideoEncoderError(f"JPEG encoding error: {exc}") from exc
|
|
282
|
+
if not packets:
|
|
283
|
+
raise VideoEncoderError("MJPEG encoder produced no output")
|
|
284
|
+
return b"".join(bytes(p) for p in packets)
|
|
285
|
+
|
|
286
|
+
def close(self) -> None:
|
|
287
|
+
"""Release the native codec context."""
|
|
288
|
+
if self._context is not None:
|
|
289
|
+
del self._context
|
|
290
|
+
self._context = None
|
|
291
|
+
|
|
292
|
+
def __enter__(self) -> Self:
|
|
293
|
+
return self
|
|
294
|
+
|
|
295
|
+
def __exit__(self, *_: object) -> None:
|
|
296
|
+
self.close()
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
# PyAVVideoDecompressor (H.264/H.265 → Image)
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class PyAVVideoDecompressor:
|
|
305
|
+
"""Decompresses H.264/H.265 video to JPEG or raw RGB using PyAV.
|
|
306
|
+
|
|
307
|
+
Implements ``VideoDecompressorProtocol``.
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
def __init__(
|
|
311
|
+
self,
|
|
312
|
+
video_format: str = "compressed",
|
|
313
|
+
jpeg_quality: int = 90,
|
|
314
|
+
) -> None:
|
|
315
|
+
self._video_format = video_format
|
|
316
|
+
self._jpeg_quality = jpeg_quality
|
|
317
|
+
self._decoder: VideoCodecContext | None = None
|
|
318
|
+
self._jpeg_encoder: VideoCodecContext | None = None
|
|
319
|
+
|
|
320
|
+
def _ensure_decoder(self, codec: str) -> VideoCodecContext:
|
|
321
|
+
if self._decoder is not None:
|
|
322
|
+
return self._decoder
|
|
323
|
+
codec_name = "h264" if codec == "h264" else "hevc"
|
|
324
|
+
self._decoder = av.CodecContext.create(codec_name, "r")
|
|
325
|
+
self._decoder.open()
|
|
326
|
+
return self._decoder
|
|
327
|
+
|
|
328
|
+
def _ensure_jpeg_encoder(self, width: int, height: int) -> VideoCodecContext:
|
|
329
|
+
if self._jpeg_encoder is not None:
|
|
330
|
+
return self._jpeg_encoder
|
|
331
|
+
self._jpeg_encoder = cast("VideoCodecContext", av.CodecContext.create("mjpeg", "w"))
|
|
332
|
+
self._jpeg_encoder.width = width
|
|
333
|
+
self._jpeg_encoder.height = height
|
|
334
|
+
self._jpeg_encoder.pix_fmt = "yuvj420p"
|
|
335
|
+
self._jpeg_encoder.time_base = Fraction(1, 1000)
|
|
336
|
+
self._jpeg_encoder.options = {
|
|
337
|
+
"q:v": str(max(1, 31 - self._jpeg_quality * 31 // 100)),
|
|
338
|
+
}
|
|
339
|
+
self._jpeg_encoder.open()
|
|
340
|
+
return self._jpeg_encoder
|
|
341
|
+
|
|
342
|
+
def decompress(self, video_data: bytes, codec: str) -> DecompressedFrame | None:
|
|
343
|
+
decoder = self._ensure_decoder(codec)
|
|
344
|
+
frames = decoder.decode(Packet(video_data))
|
|
345
|
+
if not frames:
|
|
346
|
+
return None
|
|
347
|
+
frame = frames[-1]
|
|
348
|
+
|
|
349
|
+
if self._video_format == "compressed":
|
|
350
|
+
encoder = self._ensure_jpeg_encoder(frame.width, frame.height)
|
|
351
|
+
reformatted = frame.reformat(format="yuvj420p")
|
|
352
|
+
reformatted.pts = 0
|
|
353
|
+
packets = encoder.encode(reformatted)
|
|
354
|
+
jpeg_data = b"".join(bytes(p) for p in packets)
|
|
355
|
+
return DecompressedFrame(
|
|
356
|
+
data=jpeg_data,
|
|
357
|
+
width=frame.width,
|
|
358
|
+
height=frame.height,
|
|
359
|
+
is_jpeg=True,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
rgb_frame = frame.reformat(format="rgb24")
|
|
363
|
+
raw_data = rgb_frame.to_ndarray().tobytes()
|
|
364
|
+
return DecompressedFrame(
|
|
365
|
+
data=raw_data,
|
|
366
|
+
width=rgb_frame.width,
|
|
367
|
+
height=rgb_frame.height,
|
|
368
|
+
is_jpeg=False,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def close(self) -> None:
|
|
372
|
+
"""Release native codec contexts."""
|
|
373
|
+
self._decoder = None
|
|
374
|
+
self._jpeg_encoder = None
|
|
375
|
+
|
|
376
|
+
def __enter__(self) -> Self:
|
|
377
|
+
return self
|
|
378
|
+
|
|
379
|
+
def __exit__(self, *_: object) -> None:
|
|
380
|
+
self.close()
|
|
381
|
+
|
|
382
|
+
def flush(self) -> list[DecompressedFrame]:
|
|
383
|
+
if self._decoder is None:
|
|
384
|
+
return []
|
|
385
|
+
frames = self._decoder.decode(None)
|
|
386
|
+
results: list[DecompressedFrame] = []
|
|
387
|
+
for frame in frames:
|
|
388
|
+
if self._video_format == "compressed":
|
|
389
|
+
encoder = self._ensure_jpeg_encoder(frame.width, frame.height)
|
|
390
|
+
reformatted = frame.reformat(format="yuvj420p")
|
|
391
|
+
reformatted.pts = 0
|
|
392
|
+
packets = encoder.encode(reformatted)
|
|
393
|
+
jpeg_data = b"".join(bytes(p) for p in packets)
|
|
394
|
+
results.append(
|
|
395
|
+
DecompressedFrame(
|
|
396
|
+
data=jpeg_data,
|
|
397
|
+
width=frame.width,
|
|
398
|
+
height=frame.height,
|
|
399
|
+
is_jpeg=True,
|
|
400
|
+
)
|
|
401
|
+
)
|
|
402
|
+
else:
|
|
403
|
+
rgb_frame = frame.reformat(format="rgb24")
|
|
404
|
+
raw_data = rgb_frame.to_ndarray().tobytes()
|
|
405
|
+
results.append(
|
|
406
|
+
DecompressedFrame(
|
|
407
|
+
data=raw_data,
|
|
408
|
+
width=rgb_frame.width,
|
|
409
|
+
height=rgb_frame.height,
|
|
410
|
+
is_jpeg=False,
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
return results
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Video-related ROS schema names and definitions."""
|
|
2
|
+
|
|
3
|
+
COMPRESSED_VIDEO_SCHEMA = "foxglove_msgs/msg/CompressedVideo"
|
|
4
|
+
|
|
5
|
+
# Canonical short schema names, matching the normal form used by CLI exporters.
|
|
6
|
+
COMPRESSED_SCHEMAS = {"sensor_msgs/CompressedImage"}
|
|
7
|
+
RAW_SCHEMAS = {"sensor_msgs/Image"}
|
|
8
|
+
IMAGE_SCHEMAS = COMPRESSED_SCHEMAS | RAW_SCHEMAS
|
|
9
|
+
|
|
10
|
+
FOXGLOVE_COMPRESSED_VIDEO = """builtin_interfaces/Time timestamp
|
|
11
|
+
string frame_id
|
|
12
|
+
uint8[] data
|
|
13
|
+
string format
|
|
14
|
+
|
|
15
|
+
================================================================================
|
|
16
|
+
MSG: builtin_interfaces/Time
|
|
17
|
+
int32 sec
|
|
18
|
+
uint32 nanosec"""
|
|
19
|
+
|
|
20
|
+
COMPRESSED_IMAGE = """\
|
|
21
|
+
std_msgs/Header header
|
|
22
|
+
string format
|
|
23
|
+
uint8[] data
|
|
24
|
+
|
|
25
|
+
================================================================================
|
|
26
|
+
MSG: std_msgs/Header
|
|
27
|
+
builtin_interfaces/Time stamp
|
|
28
|
+
string frame_id
|
|
29
|
+
|
|
30
|
+
================================================================================
|
|
31
|
+
MSG: builtin_interfaces/Time
|
|
32
|
+
int32 sec
|
|
33
|
+
uint32 nanosec"""
|
|
34
|
+
|
|
35
|
+
IMAGE = """\
|
|
36
|
+
std_msgs/Header header
|
|
37
|
+
uint32 height
|
|
38
|
+
uint32 width
|
|
39
|
+
string encoding
|
|
40
|
+
uint8 is_bigendian
|
|
41
|
+
uint32 step
|
|
42
|
+
uint8[] data
|
|
43
|
+
|
|
44
|
+
================================================================================
|
|
45
|
+
MSG: std_msgs/Header
|
|
46
|
+
builtin_interfaces/Time stamp
|
|
47
|
+
string frame_id
|
|
48
|
+
|
|
49
|
+
================================================================================
|
|
50
|
+
MSG: builtin_interfaces/Time
|
|
51
|
+
int32 sec
|
|
52
|
+
uint32 nanosec"""
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: mcap-codec-support
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Reusable MCAP encoder and decoder factories for robotics codecs
|
|
5
|
+
Keywords: mcap,robotics,factories,video,pointcloud
|
|
6
|
+
Author: Marko Bausch
|
|
7
|
+
License: GPL-3.0
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Dist: typing-extensions>=4.15.0
|
|
21
|
+
Requires-Dist: mcap-codec-support[ros2,video,pointcloud,image,draco] ; extra == 'all'
|
|
22
|
+
Requires-Dist: dracopy>=1.7.0 ; extra == 'draco'
|
|
23
|
+
Requires-Dist: mcap-ros2-support-fast ; extra == 'draco'
|
|
24
|
+
Requires-Dist: numpy>=1.24.0 ; extra == 'draco'
|
|
25
|
+
Requires-Dist: pointcloud2 ; extra == 'draco'
|
|
26
|
+
Requires-Dist: mcap-codec-support[video] ; extra == 'image'
|
|
27
|
+
Requires-Dist: imagecodecs>=2024.1.1 ; extra == 'image'
|
|
28
|
+
Requires-Dist: mcap-ros2-support-fast ; extra == 'pointcloud'
|
|
29
|
+
Requires-Dist: pureini ; extra == 'pointcloud'
|
|
30
|
+
Requires-Dist: numpy>=1.24.0 ; extra == 'pointcloud'
|
|
31
|
+
Requires-Dist: pointcloud2 ; extra == 'pointcloud'
|
|
32
|
+
Requires-Dist: mcap-ros2-support-fast ; extra == 'ros2'
|
|
33
|
+
Requires-Dist: av>=12.0.0 ; extra == 'video'
|
|
34
|
+
Requires-Dist: numpy>=1.24.0 ; extra == 'video'
|
|
35
|
+
Requires-Python: >=3.10
|
|
36
|
+
Project-URL: Homepage, https://github.com/mrkbac/robotic-tools
|
|
37
|
+
Project-URL: Issues, https://github.com/mrkbac/robotic-tools/issues
|
|
38
|
+
Project-URL: Repository, https://github.com/mrkbac/robotic-tools
|
|
39
|
+
Provides-Extra: all
|
|
40
|
+
Provides-Extra: draco
|
|
41
|
+
Provides-Extra: image
|
|
42
|
+
Provides-Extra: pointcloud
|
|
43
|
+
Provides-Extra: ros2
|
|
44
|
+
Provides-Extra: video
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
|
|
47
|
+
# mcap-codec-support
|
|
48
|
+
|
|
49
|
+
Reusable MCAP encoder and decoder factories used by `pymcap-cli`.
|
|
50
|
+
|
|
51
|
+
The package is intentionally factory-focused. ROS2 CDR support stays in
|
|
52
|
+
`mcap-ros2-support-fast`; this package composes with it when the relevant
|
|
53
|
+
factories are constructed.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
mcap_codec_support/__init__.py,sha256=CtGwHYyaC1Co5xkoo1CGgwnesTaqDceYMpsZZTnwKKo,51
|
|
2
|
+
mcap_codec_support/_messages.py,sha256=WYEWdOGZwUa-MXS0tNgtwg9MoGJm7o62-YxLdOuaS7Q,1070
|
|
3
|
+
mcap_codec_support/_protocols.py,sha256=kkn2-g6qTf8eM2RFy6eHLjW03HZg9KZsj-cmvtxTQ9c,2506
|
|
4
|
+
mcap_codec_support/_schemas.py,sha256=vd6BsBoqhABlUqrYi6AnQUGtYxP-Tq89bUlAH4rPjQw,633
|
|
5
|
+
mcap_codec_support/pointcloud/__init__.py,sha256=CT2jRZxADInTHMfp6Txdx90eZvF19aZryicUAOeEyPg,1458
|
|
6
|
+
mcap_codec_support/pointcloud/compression.py,sha256=wt4F3fPuAMMy6374lT314JTl20DpDz7L5jPaOsK2e0o,11854
|
|
7
|
+
mcap_codec_support/pointcloud/factories.py,sha256=JO-5DHxdvUW98NLZTJGWUpJvEbquw6_1RdezZ5d54SI,16100
|
|
8
|
+
mcap_codec_support/pointcloud/schemas.py,sha256=w-Jvu6Lh8WdBtF9XRr9gZHAPQzvXvAvLn83nnu4Q2Dk,2759
|
|
9
|
+
mcap_codec_support/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
mcap_codec_support/video/__init__.py,sha256=47IOYwMmrzZESMAxlHbrkisS2UrB9LFr-KeIEH1eA6o,1449
|
|
11
|
+
mcap_codec_support/video/common.py,sha256=ZBD76iy7qiR2W4-OACb4h48QFc0ub85TA2BZca2F5KM,8334
|
|
12
|
+
mcap_codec_support/video/compression.py,sha256=Ke59Zuc0BzEf5nx-WMMjylKTfpYNj1Bu-ekPOCXB1fI,9317
|
|
13
|
+
mcap_codec_support/video/factories.py,sha256=Ei-Q_HX2LA48BpKrsTSg049DuFM276gQkricgiMUXRw,4457
|
|
14
|
+
mcap_codec_support/video/ffmpeg.py,sha256=Elbpvs5DWhgAg--OP1GurNYPBzSHdB7C1j6o3PaQ_LY,31134
|
|
15
|
+
mcap_codec_support/video/file_writer.py,sha256=DvtTc4oXQgJaU-VrUHwl4zNF13lRCNYtiSRcHrbQ2Q0,13643
|
|
16
|
+
mcap_codec_support/video/pyav.py,sha256=3wiDtos8G1YnknB4k4pQ10jyN0SMUFifXEv5ZD3VI0Y,14332
|
|
17
|
+
mcap_codec_support/video/schemas.py,sha256=i_oXYM5JpJ44FzDrVeewg23yGkqM7-37_AHKvx0bXXY,1359
|
|
18
|
+
mcap_codec_support-0.2.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
19
|
+
mcap_codec_support-0.2.0.dist-info/METADATA,sha256=5_B86oYMqD0sXG4Oe1fERs7sBjrvwuhKzF-dBmEbRxY,2271
|
|
20
|
+
mcap_codec_support-0.2.0.dist-info/RECORD,,
|