mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-30 13:09:40 +00:00
88d47e8313
Salvage of huggingface/lerobot#3918 by @nathon-lee — rebased to main. lerobot-dataset-viz only logged known scalars; custom float/bool fields now appear in the Rerun blueprint and frame stream when shape is scalar. refactor(viz): add new non-default colums viz Co-authored-by: nathon-lee <leejianwoo@gmail.com>
193 lines
6.7 KiB
Python
193 lines
6.7 KiB
Python
#!/usr/bin/env python
|
|
|
|
# Copyright 2024 The HuggingFace Inc. team. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
import sys
|
|
from types import SimpleNamespace
|
|
|
|
import numpy as np
|
|
import pytest
|
|
import torch
|
|
|
|
pytest.importorskip("datasets", reason="datasets is required (install lerobot[dataset])")
|
|
|
|
from lerobot.scripts.lerobot_dataset_viz import visualize_dataset
|
|
from lerobot.utils import import_utils
|
|
from lerobot.utils.constants import ACTION, DONE, OBS_STATE, REWARD, SUCCESS, TRUNCATED
|
|
from lerobot.utils.dataset_visualization_utils import (
|
|
get_extra_scalar_keys,
|
|
is_scalar_feature,
|
|
is_scalar_like,
|
|
scalar_to_float,
|
|
)
|
|
|
|
|
|
class DummyMeta:
|
|
camera_keys = ["observation.images.front"]
|
|
|
|
|
|
class DummyFeatureDataset:
|
|
meta = DummyMeta()
|
|
features = {
|
|
"index": {"dtype": "int64", "shape": [1]},
|
|
"timestamp": {"dtype": "float32", "shape": [1]},
|
|
"episode_index": {"dtype": "int64", "shape": [1]},
|
|
"frame_index": {"dtype": "int64", "shape": [1]},
|
|
"task_index": {"dtype": "int64", "shape": [1]},
|
|
ACTION: {"dtype": "float32", "shape": [6]},
|
|
OBS_STATE: {"dtype": "float32", "shape": [6]},
|
|
DONE: {"dtype": "bool", "shape": [1]},
|
|
REWARD: {"dtype": "float32", "shape": [1]},
|
|
SUCCESS: {"dtype": "bool", "shape": [1]},
|
|
TRUNCATED: {"dtype": "bool", "shape": [1]},
|
|
"observation.images.front": {"dtype": "video", "shape": [3, 480, 640]},
|
|
"q_target": {"dtype": "float32", "shape": [1]},
|
|
"quality": {"dtype": "double", "shape": [1]},
|
|
"intervention": {"dtype": "bool", "shape": []},
|
|
"embedding": {"dtype": "float32", "shape": [32]},
|
|
"comment": {"dtype": "string", "shape": [1]},
|
|
}
|
|
|
|
|
|
class DummyVizDataset:
|
|
repo_id = "dummy/custom-scalars"
|
|
depth_output_unit = "m"
|
|
meta = SimpleNamespace(camera_keys=[], depth_keys=[], stats=None)
|
|
features = {
|
|
"index": {"dtype": "int64", "shape": [1]},
|
|
"timestamp": {"dtype": "float32", "shape": [1]},
|
|
ACTION: {"dtype": "float32", "shape": [2], "names": ["x", "y"]},
|
|
"q_target": {"dtype": "float32", "shape": [1]},
|
|
"intervention": {"dtype": "bool", "shape": [1]},
|
|
"embedding": {"dtype": "float32", "shape": [2]},
|
|
}
|
|
|
|
def __len__(self):
|
|
return 2
|
|
|
|
def __getitem__(self, index):
|
|
return {
|
|
"index": torch.tensor(index),
|
|
"timestamp": torch.tensor(index / 10),
|
|
ACTION: torch.tensor([index, index + 1], dtype=torch.float32),
|
|
"q_target": torch.tensor([index + 0.5]),
|
|
"intervention": torch.tensor([index == 0]),
|
|
"embedding": torch.tensor([index, index + 1], dtype=torch.float32),
|
|
}
|
|
|
|
|
|
@pytest.mark.skip("TODO: add dummy videos")
|
|
def test_visualize_local_dataset(tmp_path, lerobot_dataset_factory):
|
|
root = tmp_path / "dataset"
|
|
output_dir = tmp_path / "outputs"
|
|
dataset = lerobot_dataset_factory(root=root)
|
|
rrd_path = visualize_dataset(
|
|
dataset,
|
|
episode_index=0,
|
|
batch_size=32,
|
|
save=True,
|
|
output_dir=output_dir,
|
|
)
|
|
assert rrd_path.exists()
|
|
|
|
|
|
def test_get_extra_scalar_keys_skips_known_metadata_and_non_scalars():
|
|
dataset = DummyFeatureDataset()
|
|
|
|
assert get_extra_scalar_keys(dataset) == [TRUNCATED, "q_target", "quality", "intervention"]
|
|
assert get_extra_scalar_keys(dataset, additional_known_keys=(TRUNCATED,)) == [
|
|
"q_target",
|
|
"quality",
|
|
"intervention",
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("feature", "expected"),
|
|
[
|
|
({"dtype": "float32", "shape": (1,)}, True),
|
|
({"dtype": "double", "shape": [1]}, True),
|
|
({"dtype": "bool", "shape": []}, True),
|
|
({"dtype": "int64", "shape": 1}, True),
|
|
({"dtype": "float32", "shape": (3,)}, False),
|
|
({"dtype": "complex64", "shape": [1]}, False),
|
|
({"dtype": "datetime64[ns]", "shape": [1]}, False),
|
|
({"dtype": "string", "shape": [1]}, False),
|
|
({"shape": [1]}, False),
|
|
],
|
|
)
|
|
def test_is_scalar_feature(feature, expected):
|
|
assert is_scalar_feature(feature) is expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("value", "expected"),
|
|
[
|
|
(torch.tensor(1.5), True),
|
|
(torch.tensor([1.5]), True),
|
|
(torch.tensor([1.5, 2.5]), False),
|
|
(np.array(2.0), True),
|
|
(np.array([2.0]), True),
|
|
(np.array([2.0, 3.0]), False),
|
|
(True, True),
|
|
("not numeric", False),
|
|
],
|
|
)
|
|
def test_is_scalar_like(value, expected):
|
|
assert is_scalar_like(value) is expected
|
|
|
|
|
|
def test_scalar_to_float():
|
|
assert scalar_to_float(torch.tensor(3.0)) == 3.0
|
|
assert scalar_to_float(np.array([4.0])) == 4.0
|
|
|
|
|
|
def test_visualize_dataset_logs_extra_scalars(monkeypatch):
|
|
logged = []
|
|
initialized = []
|
|
|
|
dummy_rrb = SimpleNamespace(
|
|
Spatial2DView=lambda origin=None, name=None: SimpleNamespace(
|
|
kind="spatial", origin=origin, name=name
|
|
),
|
|
TimeSeriesView=lambda origin=None, name=None, overrides=None: SimpleNamespace(
|
|
kind="time_series", origin=origin, name=name, overrides=overrides
|
|
),
|
|
Grid=lambda *views: SimpleNamespace(views=views),
|
|
Blueprint=lambda root: SimpleNamespace(root=root),
|
|
)
|
|
dummy_rr = SimpleNamespace(
|
|
SeriesLines=lambda names=None: SimpleNamespace(names=names),
|
|
Scalars=lambda value: SimpleNamespace(value=value),
|
|
init=lambda *args, **kwargs: initialized.append((args, kwargs)),
|
|
log=lambda key, entity: logged.append((key, entity)),
|
|
set_time=lambda *args, **kwargs: None,
|
|
blueprint=dummy_rrb,
|
|
)
|
|
monkeypatch.setitem(sys.modules, "rerun", dummy_rr)
|
|
monkeypatch.setitem(sys.modules, "rerun.blueprint", dummy_rrb)
|
|
monkeypatch.setattr(import_utils, "require_package", lambda *args, **kwargs: None)
|
|
|
|
visualize_dataset(DummyVizDataset(), episode_index=0, batch_size=2)
|
|
|
|
logged_keys = [key for key, _ in logged]
|
|
assert logged_keys.count("q_target") == 2
|
|
assert logged_keys.count("intervention") == 2
|
|
assert "embedding" not in logged_keys
|
|
|
|
blueprint = initialized[0][1]["default_blueprint"]
|
|
time_series_origins = {view.origin for view in blueprint.root.views if view.kind == "time_series"}
|
|
assert {"q_target", "intervention"} <= time_series_origins
|
|
assert "embedding" not in time_series_origins
|