mcap-codec-support 0.3.0__tar.gz → 0.6.0__tar.gz

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.
Files changed (21) hide show
  1. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/PKG-INFO +1 -1
  2. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/pyproject.toml +1 -1
  3. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/_protocols.py +25 -11
  4. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/pointcloud/compression.py +5 -3
  5. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/pointcloud/factories.py +3 -1
  6. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/video/__init__.py +2 -1
  7. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/video/compression.py +24 -14
  8. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/README.md +0 -0
  9. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/__init__.py +0 -0
  10. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/_messages.py +0 -0
  11. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/_schemas.py +0 -0
  12. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/pointcloud/__init__.py +0 -0
  13. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/pointcloud/_messages.py +0 -0
  14. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/pointcloud/schemas.py +0 -0
  15. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/py.typed +0 -0
  16. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/video/_messages.py +0 -0
  17. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/video/common.py +0 -0
  18. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/video/factories.py +0 -0
  19. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/video/ffmpeg.py +0 -0
  20. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/video/pyav.py +0 -0
  21. {mcap_codec_support-0.3.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/video/schemas.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mcap-codec-support
3
- Version: 0.3.0
3
+ Version: 0.6.0
4
4
  Summary: Reusable MCAP encoder and decoder factories for robotics codecs
5
5
  Keywords: mcap,robotics,factories,video,pointcloud
6
6
  Author: Marko Bausch
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcap-codec-support"
3
- version = "0.3.0"
3
+ version = "0.6.0"
4
4
  description = "Reusable MCAP encoder and decoder factories for robotics codecs"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Protocol
5
+ from typing import TYPE_CHECKING, Protocol, TypeAlias, TypeVar
6
6
 
7
7
  if TYPE_CHECKING:
8
8
  from av import VideoFrame
@@ -10,6 +10,11 @@ if TYPE_CHECKING:
10
10
 
11
11
  from mcap_codec_support.video.common import DecompressedFrame, EncoderConfig
12
12
 
13
+ # The frame representation differs by backend: PyAV works on ``av.VideoFrame``,
14
+ # the ffmpeg-CLI backend on raw ``bytes``. Parameterizing keeps decode→encode
15
+ # paired per backend instead of pretending they share one frame type.
16
+ FrameT = TypeVar("FrameT")
17
+
13
18
 
14
19
  class RawImageMessage(Protocol):
15
20
  """Structural shape of a ROS ``sensor_msgs/Image`` message."""
@@ -17,6 +22,7 @@ class RawImageMessage(Protocol):
17
22
  width: int
18
23
  height: int
19
24
  encoding: str
25
+ step: int
20
26
  data: bytes
21
27
 
22
28
 
@@ -27,12 +33,12 @@ class CompressedImageMsg(Protocol):
27
33
  def data(self) -> bytes | bytearray | memoryview: ...
28
34
 
29
35
 
30
- class VideoEncoderProtocol(Protocol):
31
- """Structural interface shared by VideoEncoder and FFmpegVideoEncoder."""
36
+ class VideoEncoderProtocol(Protocol[FrameT]):
37
+ """Encoder interface; ``FrameT`` is the per-backend frame type."""
32
38
 
33
39
  config: EncoderConfig
34
40
 
35
- def encode(self, frame: VideoFrame) -> bytes | None: ...
41
+ def encode(self, frame: FrameT) -> bytes | None: ...
36
42
 
37
43
  def flush_packets(self) -> list[bytes]: ...
38
44
 
@@ -49,8 +55,13 @@ class VideoDecompressorProtocol(Protocol):
49
55
  ...
50
56
 
51
57
 
52
- class VideoCompressionBackend(Protocol):
53
- """Backend used by roscompress for CompressedVideo output."""
58
+ class VideoCompressionBackend(Protocol[FrameT]):
59
+ """Backend used by roscompress for CompressedVideo output.
60
+
61
+ ``FrameT`` ties ``decode_*`` output to the frame type ``create_encoder``'s
62
+ encoder consumes, so a backend can't decode to one frame type and encode
63
+ another.
64
+ """
54
65
 
55
66
  label: str
56
67
  prefetch_supported: bool
@@ -59,11 +70,9 @@ class VideoCompressionBackend(Protocol):
59
70
 
60
71
  def resolve_encoder(self, codec: str) -> str: ...
61
72
 
62
- def decode_compressed(self, data: bytes) -> tuple[VideoFrame, int, int]: ...
73
+ def decode_compressed(self, data: bytes) -> tuple[FrameT, int, int]: ...
63
74
 
64
- def decode_image(
65
- self, msg: DecodedMessage, schema_name: str
66
- ) -> tuple[VideoFrame, int, int]: ...
75
+ def decode_image(self, msg: DecodedMessage, schema_name: str) -> tuple[FrameT, int, int]: ...
67
76
 
68
77
  def create_encoder(
69
78
  self,
@@ -74,6 +83,11 @@ class VideoCompressionBackend(Protocol):
74
83
  *,
75
84
  input_pix_fmt: str | None = None,
76
85
  scale: tuple[int, int] | None = None,
77
- ) -> VideoEncoderProtocol: ...
86
+ ) -> VideoEncoderProtocol[FrameT]: ...
78
87
 
79
88
  def get_pix_fmt(self, topic: str) -> str | None: ...
89
+
90
+
91
+ # A backend chosen at runtime is either the PyAV (VideoFrame) or ffmpeg-CLI
92
+ # (bytes) flavor; this union is the honest type at that dynamic boundary.
93
+ AnyVideoBackend: TypeAlias = "VideoCompressionBackend[VideoFrame] | VideoCompressionBackend[bytes]"
@@ -97,10 +97,12 @@ class CloudiniPointCloudCompressor:
97
97
  info = _build_encoding_info(
98
98
  msg, self._encoding_opt, self._compression_opt, self._resolution
99
99
  )
100
- if self._cached_info != info:
100
+ encoder = self._cached_encoder
101
+ if self._cached_info != info or encoder is None:
101
102
  self._cached_info = info
102
- self._cached_encoder = self._PointcloudEncoder(info)
103
- return self._cached_encoder.encode(bytes(msg.data)) # type: ignore[union-attr]
103
+ encoder = self._PointcloudEncoder(info)
104
+ self._cached_encoder = encoder
105
+ return encoder.encode(bytes(msg.data))
104
106
 
105
107
 
106
108
  def _compute_position_quantization(
@@ -226,7 +226,9 @@ def _decode_draco_payload(payload: bytes, header: Header) -> Pointcloud2Dict:
226
226
  dtype = np.dtype([(name, values.dtype) for name, values in columns])
227
227
  point_data = np.empty(point_count, dtype=dtype)
228
228
  for name, values in columns:
229
- point_data[name] = values
229
+ # Structured-array field assignment; numpy's stubs type ``point_data`` as a
230
+ # plain float64 array, so they don't model string-key (field) assignment.
231
+ point_data[name] = values # ty: ignore[invalid-assignment]
230
232
 
231
233
  fields: list[PointFieldDict] = [
232
234
  {"name": field.name, "offset": field.offset, "datatype": field.datatype, "count": 1}
@@ -1,6 +1,6 @@
1
1
  """Video MCAP factories, schema constants, and backend helpers."""
2
2
 
3
- from mcap_codec_support._protocols import VideoCompressionBackend
3
+ from mcap_codec_support._protocols import AnyVideoBackend, VideoCompressionBackend
4
4
  from mcap_codec_support.video.common import (
5
5
  EncoderBackend,
6
6
  EncoderConfig,
@@ -33,6 +33,7 @@ __all__ = [
33
33
  "IMAGE",
34
34
  "IMAGE_SCHEMAS",
35
35
  "RAW_SCHEMAS",
36
+ "AnyVideoBackend",
36
37
  "EncoderBackend",
37
38
  "EncoderConfig",
38
39
  "EncoderMode",
@@ -6,6 +6,8 @@ import io
6
6
  from collections import deque
7
7
  from typing import TYPE_CHECKING, Any
8
8
 
9
+ import numpy as np
10
+
9
11
  from mcap_codec_support._schemas import normalize_schema_name
10
12
  from mcap_codec_support.video.common import (
11
13
  DEFAULT_FPS,
@@ -29,13 +31,17 @@ if TYPE_CHECKING:
29
31
  from collections.abc import Iterable, Iterator
30
32
  from concurrent.futures import Future, ThreadPoolExecutor
31
33
 
34
+ import numpy.typing as npt
35
+ from av import VideoFrame
32
36
  from small_mcap import DecodedMessage
33
37
 
34
38
  from mcap_codec_support._protocols import (
39
+ AnyVideoBackend,
35
40
  RawImageMessage,
36
- VideoCompressionBackend,
37
41
  VideoDecompressorProtocol,
38
42
  )
43
+ from mcap_codec_support.video.ffmpeg import FFmpegVideoEncoder
44
+ from mcap_codec_support.video.pyav import VideoEncoder
39
45
 
40
46
 
41
47
  class _PyAVCompressionBackend:
@@ -52,13 +58,13 @@ class _PyAVCompressionBackend:
52
58
 
53
59
  return resolve_encoder(codec)
54
60
 
55
- def decode_compressed(self, data: bytes) -> tuple[Any, int, int]:
61
+ def decode_compressed(self, data: bytes) -> tuple[VideoFrame, int, int]:
56
62
  from mcap_codec_support.video.pyav import decode_compressed_frame # noqa: PLC0415
57
63
 
58
64
  frame = decode_compressed_frame(data)
59
65
  return frame, frame.width, frame.height
60
66
 
61
- def decode_image(self, msg: DecodedMessage, schema_name: str) -> tuple[Any, int, int]:
67
+ def decode_image(self, msg: DecodedMessage, schema_name: str) -> tuple[VideoFrame, int, int]:
62
68
  if schema_name in COMPRESSED_SCHEMAS:
63
69
  return self.decode_compressed(bytes(msg.decoded_message.data))
64
70
 
@@ -75,9 +81,12 @@ class _PyAVCompressionBackend:
75
81
  codec_name: str,
76
82
  quality: int,
77
83
  *,
78
- input_pix_fmt: str | None = None, # noqa: ARG002
79
- scale: tuple[int, int] | None = None, # noqa: ARG002
80
- ) -> Any:
84
+ input_pix_fmt: str | None = None,
85
+ scale: tuple[int, int] | None = None,
86
+ ) -> VideoEncoder:
87
+ # PyAV reformats input frames per-frame inside VideoEncoder.encode, so
88
+ # the protocol's pix-fmt / scale knobs are FFmpeg-CLI-only.
89
+ del input_pix_fmt, scale
81
90
  from mcap_codec_support.video.pyav import VideoEncoder # noqa: PLC0415
82
91
 
83
92
  return VideoEncoder(
@@ -114,13 +123,13 @@ class _FfmpegCliCompressionBackend:
114
123
 
115
124
  return resolve_encoder(codec)
116
125
 
117
- def decode_compressed(self, data: bytes) -> tuple[Any, int, int]:
126
+ def decode_compressed(self, data: bytes) -> tuple[bytes, int, int]:
118
127
  from mcap_codec_support.video.ffmpeg import probe_image_dimensions # noqa: PLC0415
119
128
 
120
129
  width, height = probe_image_dimensions(data)
121
130
  return data, width, height
122
131
 
123
- def decode_image(self, msg: DecodedMessage, schema_name: str) -> tuple[Any, int, int]:
132
+ def decode_image(self, msg: DecodedMessage, schema_name: str) -> tuple[bytes, int, int]:
124
133
  data = bytes(msg.decoded_message.data)
125
134
  topic = msg.channel.topic
126
135
 
@@ -147,7 +156,7 @@ class _FfmpegCliCompressionBackend:
147
156
  *,
148
157
  input_pix_fmt: str | None = None,
149
158
  scale: tuple[int, int] | None = None,
150
- ) -> Any:
159
+ ) -> FFmpegVideoEncoder:
151
160
  from mcap_codec_support.video.ffmpeg import FFmpegVideoEncoder # noqa: PLC0415
152
161
 
153
162
  return FFmpegVideoEncoder(
@@ -164,7 +173,7 @@ class _FfmpegCliCompressionBackend:
164
173
 
165
174
  def create_video_compression_backend(
166
175
  mode: EncoderMode, codec: str, *, do_video: bool
167
- ) -> VideoCompressionBackend:
176
+ ) -> AnyVideoBackend:
168
177
  """Select the roscompress video backend."""
169
178
  if mode is EncoderMode.FFMPEG_CLI:
170
179
  return _FfmpegCliCompressionBackend()
@@ -180,7 +189,7 @@ def create_video_compression_backend(
180
189
 
181
190
  def prefetch_image_decodes(
182
191
  messages: Iterable[DecodedMessage],
183
- backend: VideoCompressionBackend,
192
+ backend: AnyVideoBackend,
184
193
  pool: ThreadPoolExecutor,
185
194
  prefetch: int = 8,
186
195
  ) -> Iterator[tuple[DecodedMessage, Future[Any] | None]]:
@@ -227,11 +236,12 @@ def encode_raw_image_to_jpeg(
227
236
  return buf.getvalue(), target_w, target_h
228
237
 
229
238
 
230
- def decode_compressed_image_to_rgb_array(data: bytes) -> Any:
231
- """Decode JPEG/PNG compressed image bytes to an RGB numpy array."""
239
+ def decode_compressed_image_to_rgb_array(data: bytes) -> npt.NDArray[np.uint8]:
240
+ """Decode JPEG/PNG compressed image bytes to an RGB (uint8) numpy array."""
232
241
  from mcap_codec_support.video.pyav import decode_compressed_frame # noqa: PLC0415
233
242
 
234
- return decode_compressed_frame(data).to_ndarray(format="rgb24")
243
+ rgb = decode_compressed_frame(data).to_ndarray(format="rgb24")
244
+ return np.asarray(rgb, dtype=np.uint8)
235
245
 
236
246
 
237
247
  def create_video_decompressor(