diff --git a/dimos/memory/codecs/jpeg.py b/dimos/memory/codecs/jpeg.py index 8f1bf8625b..6e717adcec 100644 --- a/dimos/memory/codecs/jpeg.py +++ b/dimos/memory/codecs/jpeg.py @@ -14,7 +14,9 @@ from __future__ import annotations -from dimos.msgs.sensor_msgs.Image import Image +from dimos.msgs.sensor_msgs.Image import Image, ImageFormat + +DEPTH_FORMATS = (ImageFormat.DEPTH, ImageFormat.DEPTH16) class JpegCodec: @@ -22,6 +24,12 @@ class JpegCodec: Uses ``Image.lcm_jpeg_encode/decode`` which preserves ``ts``, ``frame_id``, and all LCM header fields. Pixel data is lossy-compressed via TurboJPEG. + + Depth frames are stored uncompressed instead. JPEG is 8-bit colour: a float32 + metre map cannot be encoded at all, and a uint16 one would be rescaled to 8 + bits and then lossy-compressed, quietly destroying the metric values a cloud + is unprojected from. The LCM envelope carries its own encoding, so both kinds + decode through the same path. """ def __init__(self, quality: int = 50) -> None: @@ -33,7 +41,9 @@ def payload_type(self) -> type[Image]: return Image def encode(self, value: Image) -> bytes: + if value.format in DEPTH_FORMATS: + return value.lcm_encode() return value.lcm_jpeg_encode(quality=self._quality) def decode(self, data: bytes) -> Image: - return Image.lcm_jpeg_decode(data) + return Image.lcm_decode(data) diff --git a/dimos/memory/codecs/test_codecs.py b/dimos/memory/codecs/test_codecs.py index 6c647bf110..b9e42f65fe 100644 --- a/dimos/memory/codecs/test_codecs.py +++ b/dimos/memory/codecs/test_codecs.py @@ -199,6 +199,31 @@ def test_different_values_produce_different_bytes(self, case: Case) -> None: assert len(set(encodings)) > 1, "All values encoded to identical bytes" +class TestJpegCodecDepth: + """Depth shares the Image type with colour, so one codec has to carry both.""" + + @pytest.mark.parametrize( + ("dtype", "fmt"), + [("float32", ImageFormat.DEPTH), ("uint16", ImageFormat.DEPTH16)], + ) + def test_depth_survives_the_roundtrip_exactly(self, dtype: str, fmt: ImageFormat) -> None: + """Lossy depth is not degraded depth, it is wrong geometry. + + A JPEG'd metre map unprojects to a cloud in the wrong place, and the + float32 case does not even encode — it raises out of the recorder. + """ + import numpy as np + + metres = np.linspace(0.5, 6.0, 48 * 64).reshape(48, 64).astype(dtype) + depth = Image(data=metres, format=fmt, frame_id="head_optical", ts=1.0) + + decoded = JpegCodec().decode(JpegCodec().encode(depth)) + + assert decoded.format == fmt + assert decoded.frame_id == "head_optical" + np.testing.assert_array_equal(decoded.data, metres) + + class TestCodecFor: """codec_for() auto-selects the right codec."""