fix(robodojo): decode JPEG frames with the recorder's channel order - #17
Merged
Conversation
RoboDojo writes its camera buffers by handing an RGB array straight to
cv2.imencode, which expects BGR, so R and B are swapped inside the file. The
reader decoded them with PIL, which returns that stored order, so training saw
channel-swapped frames. Deploy is unaffected: obs_preprocess decodes whatever
the eval client sends, and every in-repo client encodes true RGB, so the model
trained on swapped frames and evaluated on correct ones.
robotwin.py documents and handles exactly this for the RoboTwin corpus.
Confirmed on real data rather than by reading: each corpus ships
preview_video/*.mp4 produced by the recorder itself, and over saturated
pixels the reader now matches those instead of inverting them.
sim corpus, production config |preview - reader| 40..57 -> 3..16
real corpus, no colour jitter |preview - reader| 31.74 -> 4.92
The residual is the JPEG vs H.264 difference; the sim spread is the configured
colour jitter. Per-channel means went from transposed to aligned.
The test fixture encoded with PIL, i.e. the opposite convention to the real
recorder, which is why a wrong decoder passed: it now encodes the way RoboDojo
does, so the existing three-camera channel assertions pin the real contract, and
a new test states the property directly. Reverting the decoder fails 4 tests.
Note for anyone with RoboDojo checkpoints: this changes the training input
distribution, so weights trained before it were fitted on swapped frames.
Closes #13.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wayrise
force-pushed
the
fix/robodojo-jpeg-channel-order
branch
from
September 8, 2026 18:46
275071b to
519ec05
Compare
KraHsu
self-requested a review
September 8, 2026 18:54
KraHsu
approved these changes
Sep 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #13. Thanks @rakhimovv — you called this correctly from the asymmetry alone, without the data to confirm it.
I had RoboDojo trajectories on hand, so I settled the question you left open, then fixed it.
It is the
cv2.imencodecaseYour "if" holds. Each corpus ships
preview_video/*.mp4next todata/, produced by the recorder itself for humans to look at, on a path independent of both candidate decodings. Over saturated pixels (neutral areas are invariant under an R/B swap and just dilute the numbers), the preview matchescv2.imdecodeand inverts PIL's — on both the sim and the real corpora, every episode checked:The sim corpus makes it unarguable by eye: the scene is a rendered wooden desk with yellow number tiles, and PIL's reading makes the wood grain blue and the tiles cyan.
This is not circular — had the preview been made by decoding these JPEGs with cv2 and writing them straight out, it would match PIL instead. It does not, which means the recorder held true RGB, converted correctly for the video, and skipped the conversion for
cv2.imencode. Exactly whatrobotwin.pydescribes.The fix
_decode_jpegnow usescv2.imdecode, withrobotwin.py's rationale carried over. After it:Per-channel means went from transposed to aligned.
Why the tests did not catch it
The fixture encoded with
PIL.Image.save(format="JPEG")— the opposite convention to the recorder — so a wrong decoder round-tripped cleanly. It now encodes the way RoboDojo does, which makes the existing three-camera assertions (headR-dominant,left_wristG,right_wristB) pin the real contract instead of a fictional one, and a new test states the property outright. Reverting the decoder to PIL fails 4 tests.Scope
I walked the whole path rather than assuming, in both directions:
_decode_jpegflips the dataloader's output exactly, in both single-view and multiview mode, so nothing between the decode and the model touches channel order.crop_and_resize,assemble_multiview_layoutandVideoColorJitterare all channel-agnostic.obs_preprocessPIL-decodes the client's bytes and applies the same geometry helpers; feeding it these raw HDF5 bytes reproduces the PIL reading byte for byte. It performs no conversion, so eval colour is whatever the client encodes, and every in-repo client (benchmarks/utils/client.py,mock_genmanip_server.py) encodes true RGB. That is the train/eval mismatch you described.RoboDojo's own eval client lives in XPolicyLab, so I could not verify that half from here; if it also sends true RGB, XPolicyLab/XPolicyLab#116 is this same bug seen downstream and should resolve with this.
Heads-up for anyone holding RoboDojo checkpoints: this changes the training input distribution. Weights trained before it were fitted on channel-swapped frames and will not transfer cleanly.
🤖 Generated with Claude Code