Skip to content

Possible bug in data augmentation pipeline #859

Description

@wmuron

In src/openpi/models/model.py I noticed a silent bug, which might produce inconsistent color semantics between base and wrist cameras, mainly due to hue component of augmax.ColorJitter transform and different RNG used within the same frame sample. Because of that, VLA has to rely more on spatial relation of objects, rather than on their colors (VLM sees corrupted colors). Please note that this problem doesn't impact wrist-to-wrist colors, because they are processed with the same ColorJitter + RNG op.

I don't suspect it was an intended behavior, because it would probably be documented somewhere. Below please find a minimal script that visualizes the problem. While the potential fix seems to be trivial, it might impact the training dynamics, so please have a deeper look (e.g. do we want heavier color-reliance between base and wrists cameras? Or maybe this "bug" helps to regularize the model?)

Buggy transform (row: original, base, wrist1, wrist2)
Image
Fixed transform (row: original, base, wrist1, wrist2)
Image

"""
Reproduce augmetnation RNG bug: base and wrist cameras get different ColorJitter.

augmax.Chain splits RNG by transform count. Base uses 4 transforms, wrist uses 1,
so ColorJitter gets different sub-RNGs despite same input seed.
"""

import argparse

import augmax
import cv2
import jax
import jax.numpy as jnp
import numpy as np

W, H = 64, 40
N = 10
SEP = 40


def buggy(rng, img, is_wrist, hue):
    t = [] if is_wrist else [
        augmax.RandomCrop(int(W * 0.95), int(H * 0.95)),
        augmax.Resize(W, H),
        augmax.Rotate((-5, 5)),
    ]
    t += [augmax.ColorJitter(brightness=0.3, contrast=0.4, saturation=0.5, hue=hue)]
    return augmax.Chain(*t)(rng, img)


def fixed(rng, img, is_wrist, hue):
    rng_s, rng_c = jax.random.split(rng)
    if not is_wrist:
        img = augmax.Chain(
            augmax.RandomCrop(int(W * 0.95), int(H * 0.95)),
            augmax.Resize(W, H),
            augmax.Rotate((-5, 5)),
        )(rng_s, img)
    return augmax.Chain(augmax.ColorJitter(brightness=0.3, contrast=0.4, saturation=0.5, hue=hue))(rng_c, img)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--hue", type=float, default=0.1)
    args = parser.parse_args()

    # R/G/B stripes
    img = np.zeros((H, W, 3), dtype=np.float32)
    img[:, :W//3, 0] = 0.8
    img[:, W//3:2*W//3, 1] = 0.8
    img[:, 2*W//3:, 2] = 0.8

    cams = ["base", "l_wrist", "r_wrist"]
    u8 = lambda x: (np.array(x).clip(0, 1) * 255).astype(np.uint8)
    vsep = np.full((H, 1, 3), SEP, dtype=np.uint8)
    hsep = lambda w: np.full((1, w, 3), SEP, dtype=np.uint8)

    rows_b, rows_f = [], []
    for seed in range(N):
        rng = jax.random.key(seed * 100)
        rb, rf = [u8(img)], [u8(img)]
        for c in cams:
            w = "wrist" in c
            rb += [vsep, u8(buggy(rng, jnp.array(img), w, args.hue))]
            rf += [vsep, u8(fixed(rng, jnp.array(img), w, args.hue))]
        rows_b.append(np.hstack(rb))
        rows_f.append(np.hstack(rf))

    def stack(rows):
        out = [rows[0]]
        for r in rows[1:]:
            out += [hsep(r.shape[1]), r]
        return np.vstack(out)

    grid_b = cv2.cvtColor(stack(rows_b), cv2.COLOR_RGB2BGR)
    grid_f = cv2.cvtColor(stack(rows_f), cv2.COLOR_RGB2BGR)

    cv2.imwrite("augmax_buggy.png", grid_b)
    cv2.imwrite("augmax_fixed.png", grid_f)
    print("Saved: augmax_buggy.png, augmax_fixed.png")


if __name__ == "__main__":
    main()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions