From e2f27bf71bb30bd96c954b0b2b342f2ee0bf10c3 Mon Sep 17 00:00:00 2001 From: Anthony Chan Date: Tue, 7 Apr 2026 06:50:18 -0700 Subject: [PATCH 1/4] Fix lerobot_train script without interpolation (#3281) --- src/lerobot/scripts/lerobot_record.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lerobot/scripts/lerobot_record.py b/src/lerobot/scripts/lerobot_record.py index bea2cc1c1..c58f8f103 100644 --- a/src/lerobot/scripts/lerobot_record.py +++ b/src/lerobot/scripts/lerobot_record.py @@ -421,6 +421,7 @@ def record_loop( act_processed_policy: RobotAction = make_robot_action(action_values, dataset.features) # Applies a pipeline to the action, default is IdentityProcessor robot_action_to_send = robot_action_processor((act_processed_policy, obs)) + action_values = robot_action_to_send elif policy is None and isinstance(teleop, Teleoperator): act = teleop.get_action() From 7c032f19fc29c7082c9862382d5abfd14621054e Mon Sep 17 00:00:00 2001 From: Francesco Capuano <74058581+fracapuano@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:59:11 +0200 Subject: [PATCH 2/4] feat(dataset): registering torchvision transforms (#3153) * add: a flexible transformation registry * fix: image transforms can be set both at init and after * add: tests * fix: take in review * feat(datasets): add image transform setters * fix: pre-commit * fix: CI --------- Signed-off-by: Francesco Capuano <74058581+fracapuano@users.noreply.github.com> --- src/lerobot/datasets/lerobot_dataset.py | 23 ++++++++-- src/lerobot/datasets/multi_dataset.py | 14 +++++- tests/datasets/test_datasets.py | 58 +++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 5 deletions(-) diff --git a/src/lerobot/datasets/lerobot_dataset.py b/src/lerobot/datasets/lerobot_dataset.py index f719222fd..1725046f2 100644 --- a/src/lerobot/datasets/lerobot_dataset.py +++ b/src/lerobot/datasets/lerobot_dataset.py @@ -151,9 +151,11 @@ class LeRobotDataset(torch.utils.data.Dataset): ``$HF_LEROBOT_HOME/hub``. episodes (list[int] | None, optional): If specified, this will only load episodes specified by their episode_index in this list. Defaults to None. - image_transforms (Callable | None, optional): You can pass standard v2 image transforms from - torchvision.transforms.v2 here which will be applied to visual modalities (whether they come - from videos or images). Defaults to None. + image_transforms (Callable | None, optional): + Transform applied to visual modalities inside `__getitem__` after image decoding / tensor + conversion. This works for both image-backed and video-backed observations and can later be + updated with `set_image_transforms()` or cleared with `clear_image_transforms()`. + Defaults to None. delta_timestamps (dict[list[float]] | None, optional): _description_. Defaults to None. tolerance_s (float, optional): Tolerance in seconds used to ensure data timestamps are actually in sync with the fps value. It is used at the init of the dataset to make sure that each @@ -192,7 +194,8 @@ class LeRobotDataset(torch.utils.data.Dataset): super().__init__() self.repo_id = repo_id self._requested_root = Path(root) if root else None - self.image_transforms = image_transforms + self.reader = None + self.set_image_transforms(image_transforms) self.delta_timestamps = delta_timestamps self.episodes = episodes self.tolerance_s = tolerance_s @@ -475,6 +478,18 @@ class LeRobotDataset(torch.utils.data.Dataset): f"}})" ) + def set_image_transforms(self, image_transforms: Callable | None) -> None: + """Replace the transform applied to visual observations.""" + if image_transforms is not None and not callable(image_transforms): + raise TypeError("image_transforms must be callable or None.") + self.image_transforms = image_transforms + if self.reader is not None: + self.reader._image_transforms = image_transforms + + def clear_image_transforms(self) -> None: + """Remove the transform applied to visual observations.""" + self.set_image_transforms(None) + # ── Hub methods (stay on facade) ────────────────────────────────── def push_to_hub( diff --git a/src/lerobot/datasets/multi_dataset.py b/src/lerobot/datasets/multi_dataset.py index d16c5bb07..092443077 100644 --- a/src/lerobot/datasets/multi_dataset.py +++ b/src/lerobot/datasets/multi_dataset.py @@ -89,12 +89,24 @@ class MultiLeRobotDataset(torch.utils.data.Dataset): ) self.disabled_features.update(extra_keys) - self.image_transforms = image_transforms self.delta_timestamps = delta_timestamps # TODO(rcadene, aliberts): We should not perform this aggregation for datasets # with multiple robots of different ranges. Instead we should have one normalization # per robot. self.stats = aggregate_stats([dataset.meta.stats for dataset in self._datasets]) + self.set_image_transforms(image_transforms) + + def set_image_transforms(self, image_transforms: Callable | None) -> None: + """Replace the transform for this dataset and its children.""" + if image_transforms is not None and not callable(image_transforms): + raise TypeError("image_transforms must be callable or None.") + self.image_transforms = image_transforms + for dataset in getattr(self, "_datasets", []): + dataset.set_image_transforms(self.image_transforms) + + def clear_image_transforms(self) -> None: + """Remove the transform from this dataset and its children.""" + self.set_image_transforms(None) @property def repo_id_to_index(self): diff --git a/tests/datasets/test_datasets.py b/tests/datasets/test_datasets.py index b2518149f..d4e9e88b8 100644 --- a/tests/datasets/test_datasets.py +++ b/tests/datasets/test_datasets.py @@ -24,6 +24,7 @@ import torch from huggingface_hub import HfApi from PIL import Image from safetensors.torch import load_file +from torchvision.transforms import v2 import lerobot from lerobot.configs.default import DatasetConfig @@ -34,6 +35,7 @@ from lerobot.datasets.image_writer import image_array_to_pil_image from lerobot.datasets.io_utils import hf_transform_to_torch from lerobot.datasets.lerobot_dataset import LeRobotDataset from lerobot.datasets.multi_dataset import MultiLeRobotDataset +from lerobot.datasets.transforms import ImageTransforms, ImageTransformsConfig from lerobot.datasets.utils import ( DEFAULT_CHUNK_SIZE, DEFAULT_DATA_FILE_SIZE_IN_MB, @@ -355,6 +357,62 @@ def test_add_frame_image_pil(image_dataset): assert dataset[0]["image"].shape == torch.Size(DUMMY_CHW) +def test_set_image_transforms_applies_transparently(image_dataset): + dataset = image_dataset + dataset.add_frame({"image": np.random.rand(*DUMMY_CHW), "task": "Dummy task"}) + dataset.save_episode() + dataset.finalize() + + dataset.set_image_transforms(v2.Resize((224, 224))) + assert dataset[0]["image"].shape == torch.Size((3, 224, 224)) + + dataset.set_image_transforms(v2.Resize((128, 128))) + assert dataset[0]["image"].shape == torch.Size((3, 128, 128)) + + dataset.clear_image_transforms() + assert dataset[0]["image"].shape == torch.Size(DUMMY_CHW) + + +def test_set_image_transforms_supports_lerobot_image_transforms(image_dataset): + dataset = image_dataset + dataset.add_frame({"image": np.random.rand(*DUMMY_CHW), "task": "Dummy task"}) + dataset.save_episode() + dataset.finalize() + + image_transforms = ImageTransforms(ImageTransformsConfig(enable=False)) + dataset.set_image_transforms(image_transforms) + + assert dataset.image_transforms is image_transforms + assert dataset[0]["image"].shape == torch.Size(DUMMY_CHW) + + +def test_set_image_transforms_supports_loaded_dataset(tmp_path, lerobot_dataset_factory): + dataset = lerobot_dataset_factory(root=tmp_path / "test", use_videos=False) + dataset.set_image_transforms(v2.Compose([v2.Resize((224, 224)), v2.Resize((112, 112))])) + + camera_key = dataset.meta.camera_keys[0] + assert dataset[0][camera_key].shape == torch.Size((3, 112, 112)) + + +def test_multilerobot_dataset_set_image_transforms_propagates(tmp_path, lerobot_dataset_factory): + root = tmp_path / "multi" + repo_ids = ["lerobot/test_multi_a", "lerobot/test_multi_b"] + + for repo_id in repo_ids: + lerobot_dataset_factory(root=root / repo_id, repo_id=repo_id, use_videos=False) + + dataset = MultiLeRobotDataset(repo_ids, root=root, download_videos=False) + dataset.set_image_transforms(v2.Resize((96, 96))) + + camera_key = dataset.camera_keys[0] + assert dataset[0][camera_key].shape == torch.Size((3, 96, 96)) + assert all(child.image_transforms is dataset.image_transforms for child in dataset._datasets) + + dataset.clear_image_transforms() + assert dataset.image_transforms is None + assert all(child.image_transforms is None for child in dataset._datasets) + + def test_image_array_to_pil_image_wrong_range_float_0_255(): image = np.random.rand(*DUMMY_HWC) * 255 with pytest.raises(ValueError): From 1396b9fab7aecddd10006c33c47a487ffdcb54b4 Mon Sep 17 00:00:00 2001 From: Pauline Bailly-Masson <155966238+paulinebm@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:11:14 +0200 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=94=92=20Pin=20GitHub=20Actions=20to?= =?UTF-8?q?=20commit=20SHAs=20(#3265)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔒 pin quality.yml actions to commit SHAs * 🔒 pin fast_tests.yml actions to commit SHAs * 🔒 pin full_tests.yml actions to commit SHAs * 🔒 pin documentation.yml actions to commit SHAs * 🔒 pin documentation-upload-pr.yml actions to commit SHAs * 🔒 pin release.yml actions to commit SHAs * 🔒 pin security.yml actions to commit SHAs --------- Co-authored-by: Steven Palma --- .github/workflows/documentation-upload-pr.yml | 2 +- .github/workflows/documentation.yml | 4 ++-- .github/workflows/fast_tests.yml | 4 ++-- .github/workflows/full_tests.yml | 12 ++++++------ .github/workflows/quality.yml | 6 +++--- .github/workflows/release.yml | 12 ++++++------ .github/workflows/security.yml | 4 ++-- 7 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/documentation-upload-pr.yml b/.github/workflows/documentation-upload-pr.yml index 6ee2a5caa..315abec1f 100644 --- a/.github/workflows/documentation-upload-pr.yml +++ b/.github/workflows/documentation-upload-pr.yml @@ -33,7 +33,7 @@ jobs: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' && github.repository == 'huggingface/lerobot' - uses: huggingface/doc-builder/.github/workflows/upload_pr_documentation.yml@main + uses: huggingface/doc-builder/.github/workflows/upload_pr_documentation.yml@90b4ee2c10b81b5c1a6367c4e6fc9e2fb510a7e3 # main with: package_name: lerobot secrets: diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index c7926c542..6efa1273e 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -55,7 +55,7 @@ jobs: github.repository == 'huggingface/lerobot' permissions: contents: read - uses: huggingface/doc-builder/.github/workflows/build_main_documentation.yml@main + uses: huggingface/doc-builder/.github/workflows/build_main_documentation.yml@90b4ee2c10b81b5c1a6367c4e6fc9e2fb510a7e3 # main with: commit_sha: ${{ github.sha }} package: lerobot @@ -78,7 +78,7 @@ jobs: permissions: contents: read pull-requests: write - uses: huggingface/doc-builder/.github/workflows/build_pr_documentation.yml@main + uses: huggingface/doc-builder/.github/workflows/build_pr_documentation.yml@90b4ee2c10b81b5c1a6367c4e6fc9e2fb510a7e3 # main with: commit_sha: ${{ github.event.pull_request.head.sha }} pr_number: ${{ github.event.number }} diff --git a/.github/workflows/fast_tests.yml b/.github/workflows/fast_tests.yml index d0e73071f..d78bdd21b 100644 --- a/.github/workflows/fast_tests.yml +++ b/.github/workflows/fast_tests.yml @@ -65,7 +65,7 @@ jobs: HF_LEROBOT_HOME: /mnt/cache/.cache/huggingface/lerobot HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false lfs: true @@ -83,7 +83,7 @@ jobs: libusb-1.0-0-dev speech-dispatcher libgeos-dev portaudio19-dev - name: Setup uv and Python - uses: astral-sh/setup-uv@v6 # zizmor: ignore[unpinned-uses] + uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 with: enable-cache: true version: ${{ env.UV_VERSION }} diff --git a/.github/workflows/full_tests.yml b/.github/workflows/full_tests.yml index c04815279..c672689d8 100644 --- a/.github/workflows/full_tests.yml +++ b/.github/workflows/full_tests.yml @@ -63,7 +63,7 @@ jobs: HF_LEROBOT_HOME: /mnt/cache/.cache/huggingface/lerobot HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: lfs: true persist-credentials: false @@ -80,7 +80,7 @@ jobs: speech-dispatcher libgeos-dev portaudio19-dev - name: Setup uv and Python - uses: astral-sh/setup-uv@v6 # zizmor: ignore[unpinned-uses] + uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 with: enable-cache: true version: ${{ env.UV_VERSION }} @@ -137,21 +137,21 @@ jobs: sudo apt-get update sudo apt-get install git-lfs git lfs install - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: lfs: true persist-credentials: false - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 # zizmor: ignore[unpinned-uses] + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 with: cache-binary: false - name: Login to Docker Hub - uses: docker/login-action@v3 # zizmor: ignore[unpinned-uses] + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }} password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }} - name: Build and push Docker image - uses: docker/build-push-action@v6 # zizmor: ignore[unpinned-uses] + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . file: ./docker/Dockerfile.internal diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index a84e9c17e..a7c49076d 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -43,16 +43,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: '3.12' - name: Run pre-commit hooks - uses: pre-commit/action@v3.0.1 # zizmor: ignore[unpinned-uses] + uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 with: extra_args: --all-files --show-diff-on-failure --color=always diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7bd2be6c..aad52cf07 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,12 +38,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: '3.12' @@ -104,7 +104,7 @@ jobs: - name: Publish to TestPyPI for pre-releases # True for tags like 'v0.2.0-rc1' if: startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-') - uses: pypa/gh-action-pypi-publish@v1.13.0 # zizmor: ignore[unpinned-uses, use-trusted-publishing] + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: repository-url: https://test.pypi.org/legacy/ verbose: true @@ -112,7 +112,7 @@ jobs: - name: Publish to PyPI if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-') - uses: pypa/gh-action-pypi-publish@v1.13.0 # zizmor: ignore[unpinned-uses, use-trusted-publishing] + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: verbose: true print-hash: true @@ -127,7 +127,7 @@ jobs: env: MUJOCO_GL: egl steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: lfs: true persist-credentials: false @@ -137,7 +137,7 @@ jobs: git curl libglib2.0-0 libegl1-mesa-dev ffmpeg libusb-1.0-0-dev \ speech-dispatcher libgeos-dev portaudio19-dev - name: Setup uv and Python - uses: astral-sh/setup-uv@v6 # zizmor: ignore[unpinned-uses] + uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 with: enable-cache: true # zizmor: ignore[cache-poisoning] version: ${{ env.UV_VERSION }} diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 50c0c1fc3..8e2af59ca 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -43,12 +43,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v6 # zizmor: ignore[unpinned-uses] + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false - name: Secret Scanning - uses: trufflesecurity/trufflehog@v3.90.0 # zizmor: ignore[unpinned-uses] + uses: trufflesecurity/trufflehog@eafb8c5f6a06175141c27f17bcc17941853d0047 # v3.90.0 with: extra_args: --only-verified From 4eecbad32b0380c64bd5574bbf74e56e5255bdea Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Tue, 7 Apr 2026 17:17:33 +0200 Subject: [PATCH 4/4] chore(dependencies): Bump lerobot to 0.5.2 (#3307) * chore(dependencies): Bump lerobot to 0.5.2 * chore(dependecies): upgrade uv.lock --- pyproject.toml | 2 +- uv.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c299199c7..79409a200 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ discord = "https://discord.gg/s3KuuzsPFb" [project] name = "lerobot" -version = "0.5.1" +version = "0.5.2" description = "🤗 LeRobot: State-of-the-art Machine Learning for Real-World Robotics in Pytorch" dynamic = ["readme"] license = { text = "Apache-2.0" } diff --git a/uv.lock b/uv.lock index a4f8adc69..4a8f37bc1 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.14' and platform_machine != 's390x' and sys_platform == 'linux'", @@ -1828,7 +1828,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.9.0" +version = "1.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -1841,9 +1841,9 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/bb/62c7aa86f63a05e2f9b96642fdef9b94526a23979820b09f5455deff4983/huggingface_hub-1.9.0.tar.gz", hash = "sha256:0ea5be7a56135c91797cae6ad726e38eaeb6eb4b77cefff5c9d38ba0ecf874f7", size = 750326, upload-time = "2026-04-03T08:35:55.888Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/40/68d9b286b125d9318ae95c8f8b206e8672e7244b0eea61ebb4a88037638c/huggingface_hub-1.9.1.tar.gz", hash = "sha256:442af372207cc24dcb089caf507fcd7dbc1217c11d6059a06f6b90afe64e8bd2", size = 750355, upload-time = "2026-04-07T13:47:59.167Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/37/0d15d16150e1829f3e90962c99f28257f6de9e526a680b4c6f5acdb54fd2/huggingface_hub-1.9.0-py3-none-any.whl", hash = "sha256:2999328c058d39fd19ab748dd09bd4da2fbaa4f4c1ddea823eab103051e14a1f", size = 637355, upload-time = "2026-04-03T08:35:53.897Z" }, + { url = "https://files.pythonhosted.org/packages/3d/af/10a89c54937dccf6c10792770f362d96dd67aedfde108e6e1fd7a0836789/huggingface_hub-1.9.1-py3-none-any.whl", hash = "sha256:8dae771b969b318203727a6c6c5209d25e661f6f0dd010fc09cc4a12cf81c657", size = 637356, upload-time = "2026-04-07T13:47:57.239Z" }, ] [[package]] @@ -2184,7 +2184,7 @@ wheels = [ [[package]] name = "lerobot" -version = "0.5.1" +version = "0.5.2" source = { editable = "." } dependencies = [ { name = "accelerate" },