mcap-codec-support 0.5.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.5.0 → mcap_codec_support-0.6.0}/PKG-INFO +1 -1
  2. {mcap_codec_support-0.5.0 → mcap_codec_support-0.6.0}/pyproject.toml +1 -1
  3. {mcap_codec_support-0.5.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/_protocols.py +24 -11
  4. {mcap_codec_support-0.5.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/pointcloud/compression.py +5 -3
  5. {mcap_codec_support-0.5.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/pointcloud/factories.py +3 -1
  6. {mcap_codec_support-0.5.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/video/__init__.py +2 -1
  7. {mcap_codec_support-0.5.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/video/compression.py +19 -12
  8. {mcap_codec_support-0.5.0 → mcap_codec_support-0.6.0}/README.md +0 -0
  9. {mcap_codec_support-0.5.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/__init__.py +0 -0
  10. {mcap_codec_support-0.5.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/_messages.py +0 -0
  11. {mcap_codec_support-0.5.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/_schemas.py +0 -0
  12. {mcap_codec_support-0.5.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/pointcloud/__init__.py +0 -0
  13. {mcap_codec_support-0.5.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/pointcloud/_messages.py +0 -0
  14. {mcap_codec_support-0.5.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/pointcloud/schemas.py +0 -0
  15. {mcap_codec_support-0.5.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/py.typed +0 -0
  16. {mcap_codec_support-0.5.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/video/_messages.py +0 -0
  17. {mcap_codec_support-0.5.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/video/common.py +0 -0
  18. {mcap_codec_support-0.5.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/video/factories.py +0 -0
  19. {mcap_codec_support-0.5.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/video/ffmpeg.py +0 -0
  20. {mcap_codec_support-0.5.0 → mcap_codec_support-0.6.0}/src/mcap_codec_support/video/pyav.py +0 -0
  21. {mcap_codec_support-0.5.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.5.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.5.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."""
@@ -28,12 +33,12 @@ class CompressedImageMsg(Protocol):
28
33
  def data(self) -> bytes | bytearray | memoryview: ...
29
34
 
30
35
 
31
- class VideoEncoderProtocol(Protocol):
32
- """Structural interface shared by VideoEncoder and FFmpegVideoEncoder."""
36
+ class VideoEncoderProtocol(Protocol[FrameT]):
37
+ """Encoder interface; ``FrameT`` is the per-backend frame type."""
33
38
 
34
39
  config: EncoderConfig
35
40
 
36
- def encode(self, frame: VideoFrame) -> bytes | None: ...
41
+ def encode(self, frame: FrameT) -> bytes | None: ...
37
42
 
38
43
  def flush_packets(self) -> list[bytes]: ...
39
44
 
@@ -50,8 +55,13 @@ class VideoDecompressorProtocol(Protocol):
50
55
  ...
51
56
 
52
57
 
53
- class VideoCompressionBackend(Protocol):
54
- """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
+ """
55
65
 
56
66
  label: str
57
67
  prefetch_supported: bool
@@ -60,11 +70,9 @@ class VideoCompressionBackend(Protocol):
60
70
 
61
71
  def resolve_encoder(self, codec: str) -> str: ...
62
72
 
63
- def decode_compressed(self, data: bytes) -> tuple[VideoFrame, int, int]: ...
73
+ def decode_compressed(self, data: bytes) -> tuple[FrameT, int, int]: ...
64
74
 
65
- def decode_image(
66
- self, msg: DecodedMessage, schema_name: str
67
- ) -> tuple[VideoFrame, int, int]: ...
75
+ def decode_image(self, msg: DecodedMessage, schema_name: str) -> tuple[FrameT, int, int]: ...
68
76
 
69
77
  def create_encoder(
70
78
  self,
@@ -75,6 +83,11 @@ class VideoCompressionBackend(Protocol):
75
83
  *,
76
84
  input_pix_fmt: str | None = None,
77
85
  scale: tuple[int, int] | None = None,
78
- ) -> VideoEncoderProtocol: ...
86
+ ) -> VideoEncoderProtocol[FrameT]: ...
79
87
 
80
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
 
@@ -77,7 +83,7 @@ class _PyAVCompressionBackend:
77
83
  *,
78
84
  input_pix_fmt: str | None = None,
79
85
  scale: tuple[int, int] | None = None,
80
- ) -> Any:
86
+ ) -> VideoEncoder:
81
87
  # PyAV reformats input frames per-frame inside VideoEncoder.encode, so
82
88
  # the protocol's pix-fmt / scale knobs are FFmpeg-CLI-only.
83
89
  del input_pix_fmt, scale
@@ -117,13 +123,13 @@ class _FfmpegCliCompressionBackend:
117
123
 
118
124
  return resolve_encoder(codec)
119
125
 
120
- def decode_compressed(self, data: bytes) -> tuple[Any, int, int]:
126
+ def decode_compressed(self, data: bytes) -> tuple[bytes, int, int]:
121
127
  from mcap_codec_support.video.ffmpeg import probe_image_dimensions # noqa: PLC0415
122
128
 
123
129
  width, height = probe_image_dimensions(data)
124
130
  return data, width, height
125
131
 
126
- 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]:
127
133
  data = bytes(msg.decoded_message.data)
128
134
  topic = msg.channel.topic
129
135
 
@@ -150,7 +156,7 @@ class _FfmpegCliCompressionBackend:
150
156
  *,
151
157
  input_pix_fmt: str | None = None,
152
158
  scale: tuple[int, int] | None = None,
153
- ) -> Any:
159
+ ) -> FFmpegVideoEncoder:
154
160
  from mcap_codec_support.video.ffmpeg import FFmpegVideoEncoder # noqa: PLC0415
155
161
 
156
162
  return FFmpegVideoEncoder(
@@ -167,7 +173,7 @@ class _FfmpegCliCompressionBackend:
167
173
 
168
174
  def create_video_compression_backend(
169
175
  mode: EncoderMode, codec: str, *, do_video: bool
170
- ) -> VideoCompressionBackend:
176
+ ) -> AnyVideoBackend:
171
177
  """Select the roscompress video backend."""
172
178
  if mode is EncoderMode.FFMPEG_CLI:
173
179
  return _FfmpegCliCompressionBackend()
@@ -183,7 +189,7 @@ def create_video_compression_backend(
183
189
 
184
190
  def prefetch_image_decodes(
185
191
  messages: Iterable[DecodedMessage],
186
- backend: VideoCompressionBackend,
192
+ backend: AnyVideoBackend,
187
193
  pool: ThreadPoolExecutor,
188
194
  prefetch: int = 8,
189
195
  ) -> Iterator[tuple[DecodedMessage, Future[Any] | None]]:
@@ -230,11 +236,12 @@ def encode_raw_image_to_jpeg(
230
236
  return buf.getvalue(), target_w, target_h
231
237
 
232
238
 
233
- def decode_compressed_image_to_rgb_array(data: bytes) -> Any:
234
- """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."""
235
241
  from mcap_codec_support.video.pyav import decode_compressed_frame # noqa: PLC0415
236
242
 
237
- 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)
238
245
 
239
246
 
240
247
  def create_video_decompressor(