This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new da17cf0c71 [python] Decode palette image colors for training tensors 
(#10096)
da17cf0c71 is described below

commit da17cf0c719918ff671e6a8ca125657dbe124456
Author: chaoyang <[email protected]>
AuthorDate: Thu Sep 24 14:38:07 2026 +0800

    [python] Decode palette image colors for training tensors (#10096)
---
 .../pypaimon/multimodal/window_transforms.py       | 10 ++++--
 .../pypaimon/tests/window_transforms_test.py       | 41 ++++++++++++++++++++++
 2 files changed, 48 insertions(+), 3 deletions(-)

diff --git a/paimon-python/pypaimon/multimodal/window_transforms.py 
b/paimon-python/pypaimon/multimodal/window_transforms.py
index 3385b8525d..e8af830a3d 100644
--- a/paimon-python/pypaimon/multimodal/window_transforms.py
+++ b/paimon-python/pypaimon/multimodal/window_transforms.py
@@ -38,8 +38,9 @@ def to_tensor(values, dtype=None):
 def images_to_tensor(values, return_uint8=False):
     """Decode a non-empty sequence of image bytes to a TCHW tensor.
 
-    Apply EXIF orientation and preserve grayscale as one channel. Eight-bit
-    pixels become float32 in [0, 1], or stay uint8 with ``return_uint8=True``.
+    Apply EXIF orientation, expand palettes to RGB/RGBA, and preserve grayscale
+    as one channel. Eight-bit pixels become float32 in [0, 1], or stay uint8
+    with ``return_uint8=True``.
     Higher-bit-depth pixels always become float32 in their original units.
     All decoded frames must have the same shape.
     """
@@ -70,7 +71,10 @@ def _decode_image(payload):
             "or Pillow.") from error
 
     with Image.open(io.BytesIO(payload)) as image:
-        array = np.array(ImageOps.exif_transpose(image), copy=True)
+        image = ImageOps.exif_transpose(image)
+        if image.mode == "P":
+            image = image.convert("RGBA" if "transparency" in image.info else 
"RGB")
+        array = np.array(image, copy=True)
     if array.ndim == 2:
         array = array[:, :, None]
     return array
diff --git a/paimon-python/pypaimon/tests/window_transforms_test.py 
b/paimon-python/pypaimon/tests/window_transforms_test.py
index 45df0766bb..7a74f38954 100644
--- a/paimon-python/pypaimon/tests/window_transforms_test.py
+++ b/paimon-python/pypaimon/tests/window_transforms_test.py
@@ -58,6 +58,47 @@ class WindowTransformsTest(unittest.TestCase):
         gray = _png(np.array([[0, 255]], dtype=np.uint8))
         self.assertEqual([[[[0., 1.]]]], images_to_tensor([gray]).tolist())
 
+    def test_palette_colors_and_transparency_are_expanded(self):
+        from pypaimon.multimodal.lerobot.dataset import _image_tensor
+
+        for transparency, alpha in (
+                (None, None), (0, [0, 255]), (bytes([255, 128]), [255, 128])):
+            with self.subTest(transparency=transparency):
+                image = Image.new("P", (2, 1))
+                image.putpalette([255, 0, 0, 0, 255, 0] + [0] * 762)
+                image.putdata([0, 1])
+                buffer = io.BytesIO()
+                options = {} if transparency is None else {"transparency": 
transparency}
+                image.save(buffer, format="PNG", **options)
+                payload = buffer.getvalue()
+                channels = [[[255, 0]], [[0, 255]], [[0, 0]]]
+                if alpha is not None:
+                    channels.append([alpha])
+                expected = torch.tensor([channels], dtype=torch.uint8)
+
+                torch.testing.assert_close(
+                    images_to_tensor([payload], return_uint8=True), expected)
+                torch.testing.assert_close(
+                    images_to_tensor([payload]), expected.float() / 255)
+                feature = {"dtype": "image", "shape": [1, 2, len(channels)]}
+                torch.testing.assert_close(
+                    _image_tensor(payload, feature, return_uint8=True), 
expected[0])
+
+    def test_palette_frames_stack_with_rgb_frames_after_exif_orientation(self):
+        image = Image.new("P", (2, 1))
+        image.putpalette([255, 0, 0, 0, 255, 0] + [0] * 762)
+        image.putdata([0, 1])
+        buffer = io.BytesIO()
+        exif = Image.Exif()
+        exif[274] = 6
+        image.save(buffer, format="PNG", exif=exif)
+        rgb = _png(np.array([[[255, 0, 0]], [[0, 255, 0]]], dtype=np.uint8))
+
+        result = images_to_tensor([buffer.getvalue(), rgb], return_uint8=True)
+
+        self.assertEqual((2, 3, 2, 1), tuple(result.shape))
+        torch.testing.assert_close(result[0], result[1])
+
     def test_high_bit_depth_keeps_native_units(self):
         payload = _png(np.array([[0, 1024, 65535]], dtype=np.uint16))
         for return_uint8 in (False, True):

Reply via email to