Skip to content

Commit 25952e1

Browse files
authored
Merge pull request OpenWAM-Official#17 from OpenWAM-Official/fix/robodojo-jpeg-channel-order
fix(robodojo): decode JPEG frames with the recorder's channel order
2 parents 64f5bd9 + 223a2f9 commit 25952e1

2 files changed

Lines changed: 55 additions & 10 deletions

File tree

‎OpenWAM/openwam/dataloader/robodojo.py‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@
2222

2323
import bisect
2424
import hashlib
25-
import io
2625
import json
2726
import os
2827
import time
2928
from collections.abc import Mapping, Sequence
3029
from pathlib import Path
3130
from typing import Any
3231

32+
import cv2
3333
import h5py
3434
import numpy as np
3535
import torch
@@ -357,14 +357,28 @@ def _jpeg_bytes(value: Any) -> bytes:
357357

358358

359359
def _decode_jpeg(value: Any, *, source: str) -> Image.Image:
360+
"""Decode JPEG bytes from HDF5 to a PIL RGB image.
361+
362+
RoboDojo encodes frames by passing RGB arrays directly to ``cv2.imencode``
363+
(which expects BGR), so R and B channels are swapped inside the JPEG. Using
364+
``cv2.imdecode`` reverses this swap, giving back the original RGB order — no
365+
further conversion needed. This is the same convention ``robotwin.py``
366+
documents for the RoboTwin corpus.
367+
368+
Decoding with PIL instead returns the stored order, i.e. R and B swapped,
369+
which is what this reader did until this was caught: the model trained on
370+
channel-swapped frames while the deploy path (which decodes whatever the
371+
eval client encodes, and every in-repo client encodes true RGB) served
372+
correct ones. Confirmed against the corpora's own ``preview_video/*.mp4``,
373+
which match this decoder and not PIL's, on both the sim and real corpora.
374+
"""
360375
encoded = _jpeg_bytes(value)
361376
if not encoded:
362377
raise ValueError(f"{source}: JPEG entry is empty")
363-
try:
364-
with Image.open(io.BytesIO(encoded)) as image:
365-
return image.convert("RGB").copy()
366-
except Exception as error:
367-
raise ValueError(f"{source}: could not decode JPEG") from error
378+
array = cv2.imdecode(np.frombuffer(encoded, np.uint8), cv2.IMREAD_COLOR)
379+
if array is None:
380+
raise ValueError(f"{source}: could not decode JPEG")
381+
return Image.fromarray(array)
368382

369383

370384
def discover_robodojo_tasks(

‎OpenWAM/tests/test_robodojo_dataloader.py‎

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io
66
from pathlib import Path
77

8+
import cv2
89
import h5py
910
import numpy as np
1011
import pytest
@@ -59,10 +60,18 @@ def write_calibration(root: Path) -> Path:
5960

6061

6162
def encode_jpeg(color: tuple[int, int, int], height: int = 18, width: int = 20) -> bytes:
62-
image = Image.new("RGB", (width, height), color)
63-
buffer = io.BytesIO()
64-
image.save(buffer, format="JPEG", quality=100, subsampling=0)
65-
return buffer.getvalue()
63+
"""Encode ``color`` (RGB) the way the RoboDojo recorder does.
64+
65+
It hands an RGB array straight to ``cv2.imencode``, which expects BGR, so R
66+
and B end up swapped inside the file. Fixtures have to reproduce that or the
67+
reader's ``cv2.imdecode`` — which exists to undo it — looks wrong under test
68+
while being right on real data.
69+
"""
70+
rgb = np.zeros((height, width, 3), dtype=np.uint8)
71+
rgb[:, :] = color
72+
ok, buffer = cv2.imencode(".jpg", rgb, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
73+
assert ok, "cv2.imencode failed"
74+
return buffer.tobytes()
6675

6776

6877
def source_arrays(T: int) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
@@ -856,3 +865,25 @@ def test_registry_constructs_robodojo_and_exports_classes(tmp_path: Path):
856865
assert isinstance(dataset, MultiTaskRoboDojoDataset)
857866
assert isinstance(dataset._sub_datasets[0], RoboDojoDataset)
858867
assert dataset[0]["action"].shape == (2, 80)
868+
869+
870+
def test_decode_jpeg_undoes_the_recorder_channel_swap():
871+
"""The reader must return true RGB, not the stored order.
872+
873+
RoboDojo writes its JPEGs with ``cv2.imencode`` fed an RGB array, so the file
874+
holds R and B swapped. Decoding with PIL returns that stored order and feeds
875+
the model channel-swapped frames, which is what this reader did until it was
876+
caught against the corpora's own preview videos. Pinned here because CI never
877+
sees a real episode.
878+
"""
879+
from openwam.dataloader.robodojo import _decode_jpeg
880+
881+
red = (230, 20, 15)
882+
frame = np.asarray(_decode_jpeg(encode_jpeg(red), source="test"))
883+
884+
assert frame[0, 0, 0] > 200, "red must come back in channel 0"
885+
assert frame[0, 0, 2] < 60, "blue must not have been swapped into channel 0"
886+
887+
# And the PIL reading of the very same bytes is the swap this guards against.
888+
stored = np.asarray(Image.open(io.BytesIO(encode_jpeg(red))).convert("RGB"))
889+
assert stored[0, 0, 2] > 200 and stored[0, 0, 0] < 60

0 commit comments

Comments
 (0)