Compare commits
4 Commits
01cfe33d16
...
f778a04f05
Author | SHA1 | Date |
---|---|---|
|
f778a04f05 | |
|
4041f57943 | |
|
ca88603c4b | |
|
4b35bb22e1 |
|
@ -98,14 +98,14 @@ conda create -y -n lerobot python=3.10
|
||||||
conda activate lerobot
|
conda activate lerobot
|
||||||
```
|
```
|
||||||
|
|
||||||
When using `miniconda`, if you don't have `ffmpeg` in your environment:
|
When using `miniconda`, install `ffmpeg` in your environment:
|
||||||
```bash
|
```bash
|
||||||
conda install ffmpeg
|
conda install ffmpeg -c conda-forge
|
||||||
```
|
```
|
||||||
|
|
||||||
Install 🤗 LeRobot:
|
Install 🤗 LeRobot:
|
||||||
```bash
|
```bash
|
||||||
pip install --no-binary=av -e .
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
> **NOTE:** If you encounter build errors, you may need to install additional dependencies (`cmake`, `build-essential`, and `ffmpeg libs`). On Linux, run:
|
> **NOTE:** If you encounter build errors, you may need to install additional dependencies (`cmake`, `build-essential`, and `ffmpeg libs`). On Linux, run:
|
||||||
|
@ -118,7 +118,7 @@ For simulations, 🤗 LeRobot comes with gymnasium environments that can be inst
|
||||||
|
|
||||||
For instance, to install 🤗 LeRobot with aloha and pusht, use:
|
For instance, to install 🤗 LeRobot with aloha and pusht, use:
|
||||||
```bash
|
```bash
|
||||||
pip install --no-binary=av -e ".[aloha, pusht]"
|
pip install -e ".[aloha, pusht]"
|
||||||
```
|
```
|
||||||
|
|
||||||
To use [Weights and Biases](https://docs.wandb.ai/quickstart) for experiment tracking, log in with
|
To use [Weights and Biases](https://docs.wandb.ai/quickstart) for experiment tracking, log in with
|
||||||
|
|
|
@ -17,12 +17,21 @@
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
import os
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
|
import rerun as rr
|
||||||
|
|
||||||
|
# see https://rerun.io/docs/howto/visualization/limit-ram
|
||||||
|
RERUN_MEMORY_LIMIT = os.getenv("LEROBOT_RERUN_MEMORY_LIMIT", "5%")
|
||||||
|
|
||||||
|
|
||||||
def display_and_save_video_stream(output_dir: Path, fps: int, width: int, height: int):
|
def display_and_save_video_stream(output_dir: Path, fps: int, width: int, height: int, duration: int):
|
||||||
|
rr.init("lerobot_capture_camera_feed")
|
||||||
|
rr.spawn(memory_limit=RERUN_MEMORY_LIMIT)
|
||||||
|
|
||||||
now = dt.datetime.now()
|
now = dt.datetime.now()
|
||||||
capture_dir = output_dir / f"{now:%Y-%m-%d}" / f"{now:%H-%M-%S}"
|
capture_dir = output_dir / f"{now:%Y-%m-%d}" / f"{now:%H-%M-%S}"
|
||||||
if not capture_dir.exists():
|
if not capture_dir.exists():
|
||||||
|
@ -39,24 +48,21 @@ def display_and_save_video_stream(output_dir: Path, fps: int, width: int, height
|
||||||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
|
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
|
||||||
|
|
||||||
frame_index = 0
|
frame_index = 0
|
||||||
while True:
|
start_time = time.time()
|
||||||
|
while time.time() - start_time < duration:
|
||||||
ret, frame = cap.read()
|
ret, frame = cap.read()
|
||||||
|
|
||||||
if not ret:
|
if not ret:
|
||||||
print("Error: Could not read frame.")
|
print("Error: Could not read frame.")
|
||||||
break
|
break
|
||||||
|
rr.log("video/stream", rr.Image(frame.numpy()), static=True)
|
||||||
cv2.imshow("Video Stream", frame)
|
|
||||||
cv2.imwrite(str(capture_dir / f"frame_{frame_index:06d}.png"), frame)
|
cv2.imwrite(str(capture_dir / f"frame_{frame_index:06d}.png"), frame)
|
||||||
frame_index += 1
|
frame_index += 1
|
||||||
|
|
||||||
# Break the loop on 'q' key press
|
# Release the capture
|
||||||
if cv2.waitKey(1) & 0xFF == ord("q"):
|
|
||||||
break
|
|
||||||
|
|
||||||
# Release the capture and destroy all windows
|
|
||||||
cap.release()
|
cap.release()
|
||||||
cv2.destroyAllWindows()
|
|
||||||
|
# TODO(Steven): Add a graceful shutdown via a close() method for the Viewer context, though not currently supported in the Rerun API.
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -86,5 +92,11 @@ if __name__ == "__main__":
|
||||||
default=720,
|
default=720,
|
||||||
help="Height of the captured images.",
|
help="Height of the captured images.",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--duration",
|
||||||
|
type=int,
|
||||||
|
default=20,
|
||||||
|
help="Duration in seconds for which the video stream should be captured.",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
display_and_save_video_stream(**vars(args))
|
display_and_save_video_stream(**vars(args))
|
||||||
|
|
|
@ -57,9 +57,15 @@ conda activate lerobot
|
||||||
git clone https://github.com/huggingface/lerobot.git ~/lerobot
|
git clone https://github.com/huggingface/lerobot.git ~/lerobot
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 5. Install LeRobot with dependencies for the feetech motors:
|
#### 5. Install ffmpeg in your environment:
|
||||||
|
When using `miniconda`, install `ffmpeg` in your environment:
|
||||||
```bash
|
```bash
|
||||||
cd ~/lerobot && pip install --no-binary=av -e ".[feetech]"
|
conda install ffmpeg -c conda-forge
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. Install LeRobot with dependencies for the feetech motors:
|
||||||
|
```bash
|
||||||
|
cd ~/lerobot && pip install -e ".[feetech]"
|
||||||
```
|
```
|
||||||
|
|
||||||
Great :hugs:! You are now done installing LeRobot and we can begin assembling the SO100 arms :robot:.
|
Great :hugs:! You are now done installing LeRobot and we can begin assembling the SO100 arms :robot:.
|
||||||
|
@ -491,6 +497,9 @@ python lerobot/scripts/control_robot.py \
|
||||||
|
|
||||||
#### a. Teleop with displaying cameras
|
#### a. Teleop with displaying cameras
|
||||||
Follow [this guide to setup your cameras](https://github.com/huggingface/lerobot/blob/main/examples/7_get_started_with_real_robot.md#c-add-your-cameras-with-opencvcamera). Then you will be able to display the cameras on your computer while you are teleoperating by running the following code. This is useful to prepare your setup before recording your first dataset.
|
Follow [this guide to setup your cameras](https://github.com/huggingface/lerobot/blob/main/examples/7_get_started_with_real_robot.md#c-add-your-cameras-with-opencvcamera). Then you will be able to display the cameras on your computer while you are teleoperating by running the following code. This is useful to prepare your setup before recording your first dataset.
|
||||||
|
|
||||||
|
> **NOTE:** To visualize the data, enable `--control.display_data=true`. This streams the data using `rerun`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python lerobot/scripts/control_robot.py \
|
python lerobot/scripts/control_robot.py \
|
||||||
--robot.type=so100 \
|
--robot.type=so100 \
|
||||||
|
|
|
@ -67,9 +67,15 @@ conda activate lerobot
|
||||||
git clone https://github.com/huggingface/lerobot.git ~/lerobot
|
git clone https://github.com/huggingface/lerobot.git ~/lerobot
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 5. Install LeRobot with dependencies for the feetech motors:
|
#### 5. Install ffmpeg in your environment:
|
||||||
|
When using `miniconda`, install `ffmpeg` in your environment:
|
||||||
```bash
|
```bash
|
||||||
cd ~/lerobot && pip install --no-binary=av -e ".[feetech]"
|
conda install ffmpeg -c conda-forge
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. Install LeRobot with dependencies for the feetech motors:
|
||||||
|
```bash
|
||||||
|
cd ~/lerobot && pip install -e ".[feetech]"
|
||||||
```
|
```
|
||||||
|
|
||||||
## C. Install LeRobot on laptop
|
## C. Install LeRobot on laptop
|
||||||
|
@ -108,9 +114,15 @@ conda activate lerobot
|
||||||
git clone https://github.com/huggingface/lerobot.git ~/lerobot
|
git clone https://github.com/huggingface/lerobot.git ~/lerobot
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 5. Install LeRobot with dependencies for the feetech motors:
|
#### 5. Install ffmpeg in your environment:
|
||||||
|
When using `miniconda`, install `ffmpeg` in your environment:
|
||||||
```bash
|
```bash
|
||||||
cd ~/lerobot && pip install --no-binary=av -e ".[feetech]"
|
conda install ffmpeg -c conda-forge
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. Install LeRobot with dependencies for the feetech motors:
|
||||||
|
```bash
|
||||||
|
cd ~/lerobot && pip install -e ".[feetech]"
|
||||||
```
|
```
|
||||||
|
|
||||||
Great :hugs:! You are now done installing LeRobot and we can begin assembling the SO100 arms and Mobile base :robot:.
|
Great :hugs:! You are now done installing LeRobot and we can begin assembling the SO100 arms and Mobile base :robot:.
|
||||||
|
@ -412,6 +424,8 @@ python lerobot/scripts/control_robot.py \
|
||||||
--control.fps=30
|
--control.fps=30
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **NOTE:** To visualize the data, enable `--control.display_data=true`. This streams the data using `rerun`. For the `--control.type=remote_robot` you will also need to set `--control.viewer_ip` and `--control.viewer_port`
|
||||||
|
|
||||||
You should see on your laptop something like this: ```[INFO] Connected to remote robot at tcp://172.17.133.91:5555 and video stream at tcp://172.17.133.91:5556.``` Now you can move the leader arm and use the keyboard (w,a,s,d) to drive forward, left, backwards, right. And use (z,x) to turn left or turn right. You can use (r,f) to increase and decrease the speed of the mobile robot. There are three speed modes, see the table below:
|
You should see on your laptop something like this: ```[INFO] Connected to remote robot at tcp://172.17.133.91:5555 and video stream at tcp://172.17.133.91:5556.``` Now you can move the leader arm and use the keyboard (w,a,s,d) to drive forward, left, backwards, right. And use (z,x) to turn left or turn right. You can use (r,f) to increase and decrease the speed of the mobile robot. There are three speed modes, see the table below:
|
||||||
| Speed Mode | Linear Speed (m/s) | Rotation Speed (deg/s) |
|
| Speed Mode | Linear Speed (m/s) | Rotation Speed (deg/s) |
|
||||||
| ---------- | ------------------ | ---------------------- |
|
| ---------- | ------------------ | ---------------------- |
|
||||||
|
|
|
@ -31,9 +31,15 @@ conda create -y -n lerobot python=3.10 && conda activate lerobot
|
||||||
git clone https://github.com/huggingface/lerobot.git ~/lerobot
|
git clone https://github.com/huggingface/lerobot.git ~/lerobot
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Install LeRobot with dependencies for the feetech motors:
|
5. Install ffmpeg in your environment:
|
||||||
|
When using `miniconda`, install `ffmpeg` in your environment:
|
||||||
```bash
|
```bash
|
||||||
cd ~/lerobot && pip install --no-binary=av -e ".[feetech]"
|
conda install ffmpeg -c conda-forge
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Install LeRobot with dependencies for the feetech motors:
|
||||||
|
```bash
|
||||||
|
cd ~/lerobot && pip install -e ".[feetech]"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configure the motors
|
## Configure the motors
|
||||||
|
@ -212,6 +218,9 @@ python lerobot/scripts/control_robot.py \
|
||||||
|
|
||||||
**Teleop with displaying cameras**
|
**Teleop with displaying cameras**
|
||||||
Follow [this guide to setup your cameras](https://github.com/huggingface/lerobot/blob/main/examples/7_get_started_with_real_robot.md#c-add-your-cameras-with-opencvcamera). Then you will be able to display the cameras on your computer while you are teleoperating by running the following code. This is useful to prepare your setup before recording your first dataset.
|
Follow [this guide to setup your cameras](https://github.com/huggingface/lerobot/blob/main/examples/7_get_started_with_real_robot.md#c-add-your-cameras-with-opencvcamera). Then you will be able to display the cameras on your computer while you are teleoperating by running the following code. This is useful to prepare your setup before recording your first dataset.
|
||||||
|
|
||||||
|
> **NOTE:** To visualize the data, enable `--control.display_data=true`. This streams the data using `rerun`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python lerobot/scripts/control_robot.py \
|
python lerobot/scripts/control_robot.py \
|
||||||
--robot.type=moss \
|
--robot.type=moss \
|
||||||
|
|
|
@ -18,7 +18,7 @@ training outputs directory. In the latter case, you might want to run examples/3
|
||||||
|
|
||||||
It requires the installation of the 'gym_pusht' simulation environment. Install it by running:
|
It requires the installation of the 'gym_pusht' simulation environment. Install it by running:
|
||||||
```bash
|
```bash
|
||||||
pip install --no-binary=av -e ".[pusht]"`
|
pip install -e ".[pusht]"`
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ First, install the additional dependencies required for robots built with dynami
|
||||||
|
|
||||||
Using `pip`:
|
Using `pip`:
|
||||||
```bash
|
```bash
|
||||||
pip install --no-binary=av -e ".[dynamixel]"
|
pip install -e ".[dynamixel]"
|
||||||
```
|
```
|
||||||
|
|
||||||
Using `poetry`:
|
Using `poetry`:
|
||||||
|
@ -55,6 +55,9 @@ Finally, connect both arms to your computer via USB. Note that the USB doesn't p
|
||||||
Now you are ready to configure your motors for the first time, as detailed in the sections below. In the upcoming sections, you'll learn about our classes and functions by running some python code in an interactive session, or by copy-pasting it in a python file.
|
Now you are ready to configure your motors for the first time, as detailed in the sections below. In the upcoming sections, you'll learn about our classes and functions by running some python code in an interactive session, or by copy-pasting it in a python file.
|
||||||
|
|
||||||
If you have already configured your motors the first time, you can streamline the process by directly running the teleoperate script (which is detailed further in the tutorial):
|
If you have already configured your motors the first time, you can streamline the process by directly running the teleoperate script (which is detailed further in the tutorial):
|
||||||
|
|
||||||
|
> **NOTE:** To visualize the data, enable `--control.display_data=true`. This streams the data using `rerun`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python lerobot/scripts/control_robot.py \
|
python lerobot/scripts/control_robot.py \
|
||||||
--robot.type=koch \
|
--robot.type=koch \
|
||||||
|
@ -828,10 +831,10 @@ It contains:
|
||||||
|
|
||||||
Troubleshooting:
|
Troubleshooting:
|
||||||
- On Linux, if you encounter any issue during video encoding with `ffmpeg: unknown encoder libsvtav1`, you can:
|
- On Linux, if you encounter any issue during video encoding with `ffmpeg: unknown encoder libsvtav1`, you can:
|
||||||
- install with conda-forge by running `conda install -c conda-forge ffmpeg` (it should be compiled with `libsvtav1`),
|
- install with conda-forge by running `conda install -c conda-forge ffmpeg` (it should be compiled with `libsvtav1`),
|
||||||
- or, install [Homebrew](https://brew.sh) and run `brew install ffmpeg` (it should be compiled with `libsvtav1`),
|
> **NOTE:** This usually installs `ffmpeg 7.X` for your platform (check the version installed with `ffmpeg -encoders | grep libsvtav1`). If it isn't `ffmpeg 7.X` or lacks `libsvtav1` support, you can explicitly install `ffmpeg 7.X` using: `conda install ffmpeg=7.1.1 -c conda-forge`
|
||||||
- or, install [ffmpeg build dependencies](https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu#GettheDependencies) and [compile ffmpeg from source with libsvtav1](https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu#libsvtav1),
|
- or, install [ffmpeg build dependencies](https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu#GettheDependencies) and [compile ffmpeg from source with libsvtav1](https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu#libsvtav1),
|
||||||
- and, make sure you use the corresponding ffmpeg binary to your install with `which ffmpeg`.
|
- and, make sure you use the corresponding ffmpeg binary to your install with `which ffmpeg`.
|
||||||
- On Linux, if the left and right arrow keys and escape key don't have any effect during data recording, make sure you've set the `$DISPLAY` environment variable. See [pynput limitations](https://pynput.readthedocs.io/en/latest/limitations.html#linux).
|
- On Linux, if the left and right arrow keys and escape key don't have any effect during data recording, make sure you've set the `$DISPLAY` environment variable. See [pynput limitations](https://pynput.readthedocs.io/en/latest/limitations.html#linux).
|
||||||
|
|
||||||
At the end of data recording, your dataset will be uploaded on your Hugging Face page (e.g. https://huggingface.co/datasets/cadene/koch_test) that you can obtain by running:
|
At the end of data recording, your dataset will be uploaded on your Hugging Face page (e.g. https://huggingface.co/datasets/cadene/koch_test) that you can obtain by running:
|
||||||
|
|
|
@ -43,14 +43,19 @@ conda create -y -n lerobot python=3.10 && conda activate lerobot
|
||||||
git clone https://github.com/huggingface/lerobot.git ~/lerobot
|
git clone https://github.com/huggingface/lerobot.git ~/lerobot
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Install LeRobot with stretch dependencies:
|
6. When using `miniconda`, install `ffmpeg` in your environment:
|
||||||
```bash
|
```bash
|
||||||
cd ~/lerobot && pip install --no-binary=av -e ".[stretch]"
|
conda install ffmpeg -c conda-forge
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Install LeRobot with stretch dependencies:
|
||||||
|
```bash
|
||||||
|
cd ~/lerobot && pip install -e ".[stretch]"
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** If you get this message, you can ignore it: `ERROR: pip's dependency resolver does not currently take into account all the packages that are installed.`
|
> **Note:** If you get this message, you can ignore it: `ERROR: pip's dependency resolver does not currently take into account all the packages that are installed.`
|
||||||
|
|
||||||
7. Run a [system check](https://docs.hello-robot.com/0.3/getting_started/stretch_hardware_overview/#system-check) to make sure your robot is ready:
|
8. Run a [system check](https://docs.hello-robot.com/0.3/getting_started/stretch_hardware_overview/#system-check) to make sure your robot is ready:
|
||||||
```bash
|
```bash
|
||||||
stretch_system_check.py
|
stretch_system_check.py
|
||||||
```
|
```
|
||||||
|
@ -97,6 +102,8 @@ This is equivalent to running `stretch_robot_home.py`
|
||||||
Before trying teleoperation, you need activate the gamepad controller by pressing the middle button. For more info, see Stretch's [doc](https://docs.hello-robot.com/0.3/getting_started/hello_robot/#gamepad-teleoperation).
|
Before trying teleoperation, you need activate the gamepad controller by pressing the middle button. For more info, see Stretch's [doc](https://docs.hello-robot.com/0.3/getting_started/hello_robot/#gamepad-teleoperation).
|
||||||
|
|
||||||
Now try out teleoperation (see above documentation to learn about the gamepad controls):
|
Now try out teleoperation (see above documentation to learn about the gamepad controls):
|
||||||
|
|
||||||
|
> **NOTE:** To visualize the data, enable `--control.display_data=true`. This streams the data using `rerun`.
|
||||||
```bash
|
```bash
|
||||||
python lerobot/scripts/control_robot.py \
|
python lerobot/scripts/control_robot.py \
|
||||||
--robot.type=stretch \
|
--robot.type=stretch \
|
||||||
|
|
|
@ -30,9 +30,14 @@ conda create -y -n lerobot python=3.10 && conda activate lerobot
|
||||||
git clone https://github.com/huggingface/lerobot.git ~/lerobot
|
git clone https://github.com/huggingface/lerobot.git ~/lerobot
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Install LeRobot with dependencies for the Aloha motors (dynamixel) and cameras (intelrealsense):
|
5. When using `miniconda`, install `ffmpeg` in your environment:
|
||||||
```bash
|
```bash
|
||||||
cd ~/lerobot && pip install --no-binary=av -e ".[dynamixel, intelrealsense]"
|
conda install ffmpeg -c conda-forge
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Install LeRobot with dependencies for the Aloha motors (dynamixel) and cameras (intelrealsense):
|
||||||
|
```bash
|
||||||
|
cd ~/lerobot && pip install -e ".[dynamixel, intelrealsense]"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Teleoperate
|
## Teleoperate
|
||||||
|
@ -43,6 +48,9 @@ Teleoperation consists in manually operating the leader arms to move the followe
|
||||||
2. Our code assumes that your robot has been assembled following Trossen Robotics instructions. This allows us to skip calibration, as we use the pre-defined calibration files in `.cache/calibration/aloha_default`. If you replace a motor, make sure you follow the exact instructions from Trossen Robotics.
|
2. Our code assumes that your robot has been assembled following Trossen Robotics instructions. This allows us to skip calibration, as we use the pre-defined calibration files in `.cache/calibration/aloha_default`. If you replace a motor, make sure you follow the exact instructions from Trossen Robotics.
|
||||||
|
|
||||||
By running the following code, you can start your first **SAFE** teleoperation:
|
By running the following code, you can start your first **SAFE** teleoperation:
|
||||||
|
|
||||||
|
> **NOTE:** To visualize the data, enable `--control.display_data=true`. This streams the data using `rerun`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python lerobot/scripts/control_robot.py \
|
python lerobot/scripts/control_robot.py \
|
||||||
--robot.type=aloha \
|
--robot.type=aloha \
|
||||||
|
|
|
@ -24,7 +24,7 @@ Designed by Physical Intelligence. Ported from Jax by Hugging Face.
|
||||||
|
|
||||||
Install pi0 extra dependencies:
|
Install pi0 extra dependencies:
|
||||||
```bash
|
```bash
|
||||||
pip install --no-binary=av -e ".[pi0]"
|
pip install -e ".[pi0]"
|
||||||
```
|
```
|
||||||
|
|
||||||
Example of finetuning the pi0 pretrained model (`pi0_base` in `openpi`):
|
Example of finetuning the pi0 pretrained model (`pi0_base` in `openpi`):
|
||||||
|
|
|
@ -41,7 +41,7 @@ class TeleoperateControlConfig(ControlConfig):
|
||||||
fps: int | None = None
|
fps: int | None = None
|
||||||
teleop_time_s: float | None = None
|
teleop_time_s: float | None = None
|
||||||
# Display all cameras on screen
|
# Display all cameras on screen
|
||||||
display_cameras: bool = True
|
display_data: bool = False
|
||||||
|
|
||||||
|
|
||||||
@ControlConfig.register_subclass("record")
|
@ControlConfig.register_subclass("record")
|
||||||
|
@ -82,7 +82,7 @@ class RecordControlConfig(ControlConfig):
|
||||||
# Not enough threads might cause low camera fps.
|
# Not enough threads might cause low camera fps.
|
||||||
num_image_writer_threads_per_camera: int = 4
|
num_image_writer_threads_per_camera: int = 4
|
||||||
# Display all cameras on screen
|
# Display all cameras on screen
|
||||||
display_cameras: bool = True
|
display_data: bool = False
|
||||||
# Use vocal synthesis to read events.
|
# Use vocal synthesis to read events.
|
||||||
play_sounds: bool = True
|
play_sounds: bool = True
|
||||||
# Resume recording on an existing dataset.
|
# Resume recording on an existing dataset.
|
||||||
|
@ -116,6 +116,11 @@ class ReplayControlConfig(ControlConfig):
|
||||||
@dataclass
|
@dataclass
|
||||||
class RemoteRobotConfig(ControlConfig):
|
class RemoteRobotConfig(ControlConfig):
|
||||||
log_interval: int = 100
|
log_interval: int = 100
|
||||||
|
# Display all cameras on screen
|
||||||
|
display_data: bool = False
|
||||||
|
# Rerun configuration for remote robot (https://ref.rerun.io/docs/python/0.22.1/common/initialization_functions/#rerun.connect_tcp)
|
||||||
|
viewer_ip: str | None = None
|
||||||
|
viewer_port: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
|
@ -24,7 +24,7 @@ from contextlib import nullcontext
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from functools import cache
|
from functools import cache
|
||||||
|
|
||||||
import cv2
|
import rerun as rr
|
||||||
import torch
|
import torch
|
||||||
from deepdiff import DeepDiff
|
from deepdiff import DeepDiff
|
||||||
from termcolor import colored
|
from termcolor import colored
|
||||||
|
@ -174,13 +174,13 @@ def warmup_record(
|
||||||
events,
|
events,
|
||||||
enable_teleoperation,
|
enable_teleoperation,
|
||||||
warmup_time_s,
|
warmup_time_s,
|
||||||
display_cameras,
|
display_data,
|
||||||
fps,
|
fps,
|
||||||
):
|
):
|
||||||
control_loop(
|
control_loop(
|
||||||
robot=robot,
|
robot=robot,
|
||||||
control_time_s=warmup_time_s,
|
control_time_s=warmup_time_s,
|
||||||
display_cameras=display_cameras,
|
display_data=display_data,
|
||||||
events=events,
|
events=events,
|
||||||
fps=fps,
|
fps=fps,
|
||||||
teleoperate=enable_teleoperation,
|
teleoperate=enable_teleoperation,
|
||||||
|
@ -192,7 +192,7 @@ def record_episode(
|
||||||
dataset,
|
dataset,
|
||||||
events,
|
events,
|
||||||
episode_time_s,
|
episode_time_s,
|
||||||
display_cameras,
|
display_data,
|
||||||
policy,
|
policy,
|
||||||
fps,
|
fps,
|
||||||
single_task,
|
single_task,
|
||||||
|
@ -200,7 +200,7 @@ def record_episode(
|
||||||
control_loop(
|
control_loop(
|
||||||
robot=robot,
|
robot=robot,
|
||||||
control_time_s=episode_time_s,
|
control_time_s=episode_time_s,
|
||||||
display_cameras=display_cameras,
|
display_data=display_data,
|
||||||
dataset=dataset,
|
dataset=dataset,
|
||||||
events=events,
|
events=events,
|
||||||
policy=policy,
|
policy=policy,
|
||||||
|
@ -215,7 +215,7 @@ def control_loop(
|
||||||
robot,
|
robot,
|
||||||
control_time_s=None,
|
control_time_s=None,
|
||||||
teleoperate=False,
|
teleoperate=False,
|
||||||
display_cameras=False,
|
display_data=False,
|
||||||
dataset: LeRobotDataset | None = None,
|
dataset: LeRobotDataset | None = None,
|
||||||
events=None,
|
events=None,
|
||||||
policy: PreTrainedPolicy = None,
|
policy: PreTrainedPolicy = None,
|
||||||
|
@ -264,11 +264,15 @@ def control_loop(
|
||||||
frame = {**observation, **action, "task": single_task}
|
frame = {**observation, **action, "task": single_task}
|
||||||
dataset.add_frame(frame)
|
dataset.add_frame(frame)
|
||||||
|
|
||||||
if display_cameras and not is_headless():
|
# TODO(Steven): This should be more general (for RemoteRobot instead of checking the name, but anyways it will change soon)
|
||||||
|
if (display_data and not is_headless()) or (display_data and robot.robot_type.startswith("lekiwi")):
|
||||||
|
for k, v in action.items():
|
||||||
|
for i, vv in enumerate(v):
|
||||||
|
rr.log(f"sent_{k}_{i}", rr.Scalar(vv.numpy()))
|
||||||
|
|
||||||
image_keys = [key for key in observation if "image" in key]
|
image_keys = [key for key in observation if "image" in key]
|
||||||
for key in image_keys:
|
for key in image_keys:
|
||||||
cv2.imshow(key, cv2.cvtColor(observation[key].numpy(), cv2.COLOR_RGB2BGR))
|
rr.log(key, rr.Image(observation[key].numpy()), static=True)
|
||||||
cv2.waitKey(1)
|
|
||||||
|
|
||||||
if fps is not None:
|
if fps is not None:
|
||||||
dt_s = time.perf_counter() - start_loop_t
|
dt_s = time.perf_counter() - start_loop_t
|
||||||
|
@ -297,15 +301,11 @@ def reset_environment(robot, events, reset_time_s, fps):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def stop_recording(robot, listener, display_cameras):
|
def stop_recording(robot, listener, display_data):
|
||||||
robot.disconnect()
|
robot.disconnect()
|
||||||
|
|
||||||
if not is_headless():
|
if not is_headless() and listener is not None:
|
||||||
if listener is not None:
|
listener.stop()
|
||||||
listener.stop()
|
|
||||||
|
|
||||||
if display_cameras:
|
|
||||||
cv2.destroyAllWindows()
|
|
||||||
|
|
||||||
|
|
||||||
def sanity_check_dataset_name(repo_id, policy_cfg):
|
def sanity_check_dataset_name(repo_id, policy_cfg):
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# List of allowed schemes and hosts for external requests
|
||||||
|
ALLOWED_SCHEMES = {"http", "https"}
|
||||||
|
ALLOWED_HOSTS = {
|
||||||
|
"localhost",
|
||||||
|
"127.0.0.1",
|
||||||
|
# Add other trusted hosts here as needed
|
||||||
|
}
|
|
@ -135,15 +135,19 @@ python lerobot/scripts/control_robot.py \
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
|
||||||
|
import rerun as rr
|
||||||
|
|
||||||
# from safetensors.torch import load_file, save_file
|
# from safetensors.torch import load_file, save_file
|
||||||
from lerobot.common.datasets.lerobot_dataset import LeRobotDataset
|
from lerobot.common.datasets.lerobot_dataset import LeRobotDataset
|
||||||
from lerobot.common.policies.factory import make_policy
|
from lerobot.common.policies.factory import make_policy
|
||||||
from lerobot.common.robot_devices.control_configs import (
|
from lerobot.common.robot_devices.control_configs import (
|
||||||
CalibrateControlConfig,
|
CalibrateControlConfig,
|
||||||
|
ControlConfig,
|
||||||
ControlPipelineConfig,
|
ControlPipelineConfig,
|
||||||
RecordControlConfig,
|
RecordControlConfig,
|
||||||
RemoteRobotConfig,
|
RemoteRobotConfig,
|
||||||
|
@ -153,6 +157,7 @@ from lerobot.common.robot_devices.control_configs import (
|
||||||
from lerobot.common.robot_devices.control_utils import (
|
from lerobot.common.robot_devices.control_utils import (
|
||||||
control_loop,
|
control_loop,
|
||||||
init_keyboard_listener,
|
init_keyboard_listener,
|
||||||
|
is_headless,
|
||||||
log_control_info,
|
log_control_info,
|
||||||
record_episode,
|
record_episode,
|
||||||
reset_environment,
|
reset_environment,
|
||||||
|
@ -232,7 +237,7 @@ def teleoperate(robot: Robot, cfg: TeleoperateControlConfig):
|
||||||
control_time_s=cfg.teleop_time_s,
|
control_time_s=cfg.teleop_time_s,
|
||||||
fps=cfg.fps,
|
fps=cfg.fps,
|
||||||
teleoperate=True,
|
teleoperate=True,
|
||||||
display_cameras=cfg.display_cameras,
|
display_data=cfg.display_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -280,7 +285,7 @@ def record(
|
||||||
# 3. place the cameras windows on screen
|
# 3. place the cameras windows on screen
|
||||||
enable_teleoperation = policy is None
|
enable_teleoperation = policy is None
|
||||||
log_say("Warmup record", cfg.play_sounds)
|
log_say("Warmup record", cfg.play_sounds)
|
||||||
warmup_record(robot, events, enable_teleoperation, cfg.warmup_time_s, cfg.display_cameras, cfg.fps)
|
warmup_record(robot, events, enable_teleoperation, cfg.warmup_time_s, cfg.display_data, cfg.fps)
|
||||||
|
|
||||||
if has_method(robot, "teleop_safety_stop"):
|
if has_method(robot, "teleop_safety_stop"):
|
||||||
robot.teleop_safety_stop()
|
robot.teleop_safety_stop()
|
||||||
|
@ -296,7 +301,7 @@ def record(
|
||||||
dataset=dataset,
|
dataset=dataset,
|
||||||
events=events,
|
events=events,
|
||||||
episode_time_s=cfg.episode_time_s,
|
episode_time_s=cfg.episode_time_s,
|
||||||
display_cameras=cfg.display_cameras,
|
display_data=cfg.display_data,
|
||||||
policy=policy,
|
policy=policy,
|
||||||
fps=cfg.fps,
|
fps=cfg.fps,
|
||||||
single_task=cfg.single_task,
|
single_task=cfg.single_task,
|
||||||
|
@ -326,7 +331,7 @@ def record(
|
||||||
break
|
break
|
||||||
|
|
||||||
log_say("Stop recording", cfg.play_sounds, blocking=True)
|
log_say("Stop recording", cfg.play_sounds, blocking=True)
|
||||||
stop_recording(robot, listener, cfg.display_cameras)
|
stop_recording(robot, listener, cfg.display_data)
|
||||||
|
|
||||||
if cfg.push_to_hub:
|
if cfg.push_to_hub:
|
||||||
dataset.push_to_hub(tags=cfg.tags, private=cfg.private)
|
dataset.push_to_hub(tags=cfg.tags, private=cfg.private)
|
||||||
|
@ -363,6 +368,40 @@ def replay(
|
||||||
log_control_info(robot, dt_s, fps=cfg.fps)
|
log_control_info(robot, dt_s, fps=cfg.fps)
|
||||||
|
|
||||||
|
|
||||||
|
def _init_rerun(control_config: ControlConfig, session_name: str = "lerobot_control_loop") -> None:
|
||||||
|
"""Initializes the Rerun SDK for visualizing the control loop.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
control_config: Configuration determining data display and robot type.
|
||||||
|
session_name: Rerun session name. Defaults to "lerobot_control_loop".
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If viewer IP is missing for non-remote configurations with display enabled.
|
||||||
|
"""
|
||||||
|
if (control_config.display_data and not is_headless()) or (
|
||||||
|
control_config.display_data and isinstance(control_config, RemoteRobotConfig)
|
||||||
|
):
|
||||||
|
# Configure Rerun flush batch size default to 8KB if not set
|
||||||
|
batch_size = os.getenv("RERUN_FLUSH_NUM_BYTES", "8000")
|
||||||
|
os.environ["RERUN_FLUSH_NUM_BYTES"] = batch_size
|
||||||
|
|
||||||
|
# Initialize Rerun based on configuration
|
||||||
|
rr.init(session_name)
|
||||||
|
if isinstance(control_config, RemoteRobotConfig):
|
||||||
|
viewer_ip = control_config.viewer_ip
|
||||||
|
viewer_port = control_config.viewer_port
|
||||||
|
if not viewer_ip or not viewer_port:
|
||||||
|
raise ValueError(
|
||||||
|
"Viewer IP & Port are required for remote config. Set via config file/CLI or disable control_config.display_data."
|
||||||
|
)
|
||||||
|
logging.info(f"Connecting to viewer at {viewer_ip}:{viewer_port}")
|
||||||
|
rr.connect_tcp(f"{viewer_ip}:{viewer_port}")
|
||||||
|
else:
|
||||||
|
# Get memory limit for rerun viewer parameters
|
||||||
|
memory_limit = os.getenv("LEROBOT_RERUN_MEMORY_LIMIT", "10%")
|
||||||
|
rr.spawn(memory_limit=memory_limit)
|
||||||
|
|
||||||
|
|
||||||
@parser.wrap()
|
@parser.wrap()
|
||||||
def control_robot(cfg: ControlPipelineConfig):
|
def control_robot(cfg: ControlPipelineConfig):
|
||||||
init_logging()
|
init_logging()
|
||||||
|
@ -370,17 +409,22 @@ def control_robot(cfg: ControlPipelineConfig):
|
||||||
|
|
||||||
robot = make_robot_from_config(cfg.robot)
|
robot = make_robot_from_config(cfg.robot)
|
||||||
|
|
||||||
|
# TODO(Steven): Blueprint for fixed window size
|
||||||
|
|
||||||
if isinstance(cfg.control, CalibrateControlConfig):
|
if isinstance(cfg.control, CalibrateControlConfig):
|
||||||
calibrate(robot, cfg.control)
|
calibrate(robot, cfg.control)
|
||||||
elif isinstance(cfg.control, TeleoperateControlConfig):
|
elif isinstance(cfg.control, TeleoperateControlConfig):
|
||||||
|
_init_rerun(control_config=cfg.control, session_name="lerobot_control_loop_teleop")
|
||||||
teleoperate(robot, cfg.control)
|
teleoperate(robot, cfg.control)
|
||||||
elif isinstance(cfg.control, RecordControlConfig):
|
elif isinstance(cfg.control, RecordControlConfig):
|
||||||
|
_init_rerun(control_config=cfg.control, session_name="lerobot_control_loop_record")
|
||||||
record(robot, cfg.control)
|
record(robot, cfg.control)
|
||||||
elif isinstance(cfg.control, ReplayControlConfig):
|
elif isinstance(cfg.control, ReplayControlConfig):
|
||||||
replay(robot, cfg.control)
|
replay(robot, cfg.control)
|
||||||
elif isinstance(cfg.control, RemoteRobotConfig):
|
elif isinstance(cfg.control, RemoteRobotConfig):
|
||||||
from lerobot.common.robot_devices.robots.lekiwi_remote import run_lekiwi
|
from lerobot.common.robot_devices.robots.lekiwi_remote import run_lekiwi
|
||||||
|
|
||||||
|
_init_rerun(control_config=cfg.control, session_name="lerobot_control_loop_remote")
|
||||||
run_lekiwi(cfg.robot)
|
run_lekiwi(cfg.robot)
|
||||||
|
|
||||||
if robot.is_connected:
|
if robot.is_connected:
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
|
# Copyright 2023 Hugging Face Inc.
|
||||||
# Copyright 2024 The HuggingFace Inc. team. All rights reserved.
|
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
@ -13,466 +12,393 @@
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
""" Visualize data of **all** frames of any episode of a dataset of type LeRobotDataset.
|
|
||||||
|
|
||||||
Note: The last frame of the episode doesnt always correspond to a final state.
|
|
||||||
That's because our datasets are composed of transition from state to state up to
|
|
||||||
the antepenultimate state associated to the ultimate action to arrive in the final state.
|
|
||||||
However, there might not be a transition from a final state to another state.
|
|
||||||
|
|
||||||
Note: This script aims to visualize the data used to train the neural networks.
|
|
||||||
~What you see is what you get~. When visualizing image modality, it is often expected to observe
|
|
||||||
lossly compression artifacts since these images have been decoded from compressed mp4 videos to
|
|
||||||
save disk space. The compression factor applied has been tuned to not affect success rate.
|
|
||||||
|
|
||||||
Example of usage:
|
|
||||||
|
|
||||||
- Visualize data stored on a local machine:
|
|
||||||
```bash
|
|
||||||
local$ python lerobot/scripts/visualize_dataset_html.py \
|
|
||||||
--repo-id lerobot/pusht
|
|
||||||
|
|
||||||
local$ open http://localhost:9090
|
|
||||||
```
|
|
||||||
|
|
||||||
- Visualize data stored on a distant machine with a local viewer:
|
|
||||||
```bash
|
|
||||||
distant$ python lerobot/scripts/visualize_dataset_html.py \
|
|
||||||
--repo-id lerobot/pusht
|
|
||||||
|
|
||||||
local$ ssh -L 9090:localhost:9090 distant # create a ssh tunnel
|
|
||||||
local$ open http://localhost:9090
|
|
||||||
```
|
|
||||||
|
|
||||||
- Select episodes to visualize:
|
|
||||||
```bash
|
|
||||||
python lerobot/scripts/visualize_dataset_html.py \
|
|
||||||
--repo-id lerobot/pusht \
|
|
||||||
--episodes 7 3 5 1 4
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import csv
|
import base64
|
||||||
import json
|
import os
|
||||||
import logging
|
import sys
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from io import StringIO
|
import urllib.parse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Tuple, Union
|
||||||
|
|
||||||
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
|
||||||
import requests
|
import requests
|
||||||
from flask import Flask, redirect, render_template, request, url_for
|
from allowed_hosts import ALLOWED_HOSTS, ALLOWED_SCHEMES
|
||||||
|
from flask import Flask, jsonify, request
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
from lerobot import available_datasets
|
from lerobot.data.dataset import Dataset
|
||||||
from lerobot.common.datasets.lerobot_dataset import LeRobotDataset
|
from lerobot.data.episode import Episode
|
||||||
from lerobot.common.datasets.utils import IterableNamespace
|
from lerobot.data.frame import Frame
|
||||||
from lerobot.common.utils.utils import init_logging
|
from lerobot.data.utils import get_dataset_path
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app)
|
||||||
|
|
||||||
|
|
||||||
def run_server(
|
def validate_url(url):
|
||||||
dataset: LeRobotDataset | IterableNamespace | None,
|
"""Validate URL against allowed schemes and hosts."""
|
||||||
episodes: list[int] | None,
|
parsed_url = urllib.parse.urlparse(url)
|
||||||
host: str,
|
|
||||||
port: str,
|
|
||||||
static_folder: Path,
|
|
||||||
template_folder: Path,
|
|
||||||
):
|
|
||||||
app = Flask(__name__, static_folder=static_folder.resolve(), template_folder=template_folder.resolve())
|
|
||||||
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0 # specifying not to cache
|
|
||||||
|
|
||||||
@app.route("/")
|
# Check if scheme is allowed
|
||||||
def hommepage(dataset=dataset):
|
if parsed_url.scheme not in ALLOWED_SCHEMES:
|
||||||
if dataset:
|
return False
|
||||||
dataset_namespace, dataset_name = dataset.repo_id.split("/")
|
|
||||||
return redirect(
|
|
||||||
url_for(
|
|
||||||
"show_episode",
|
|
||||||
dataset_namespace=dataset_namespace,
|
|
||||||
dataset_name=dataset_name,
|
|
||||||
episode_id=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
dataset_param, episode_param = None, None
|
# Check if host is allowed
|
||||||
all_params = request.args
|
if parsed_url.netloc not in ALLOWED_HOSTS:
|
||||||
if "dataset" in all_params:
|
return False
|
||||||
dataset_param = all_params["dataset"]
|
|
||||||
if "episode" in all_params:
|
|
||||||
episode_param = int(all_params["episode"])
|
|
||||||
|
|
||||||
if dataset_param:
|
return True
|
||||||
dataset_namespace, dataset_name = dataset_param.split("/")
|
|
||||||
return redirect(
|
|
||||||
url_for(
|
|
||||||
"show_episode",
|
|
||||||
dataset_namespace=dataset_namespace,
|
|
||||||
dataset_name=dataset_name,
|
|
||||||
episode_id=episode_param if episode_param is not None else 0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
featured_datasets = [
|
|
||||||
"lerobot/aloha_static_cups_open",
|
|
||||||
"lerobot/columbia_cairlab_pusht_real",
|
|
||||||
"lerobot/taco_play",
|
|
||||||
]
|
|
||||||
return render_template(
|
|
||||||
"visualize_dataset_homepage.html",
|
|
||||||
featured_datasets=featured_datasets,
|
|
||||||
lerobot_datasets=available_datasets,
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.route("/<string:dataset_namespace>/<string:dataset_name>")
|
def get_episode_data(dataset_path: Path, episode_id: str) -> Tuple[Episode, List[Frame]]:
|
||||||
def show_first_episode(dataset_namespace, dataset_name):
|
dataset = Dataset(dataset_path)
|
||||||
first_episode_id = 0
|
episode = dataset.get_episode(episode_id)
|
||||||
return redirect(
|
frames = episode.get_frames()
|
||||||
url_for(
|
return episode, frames
|
||||||
"show_episode",
|
|
||||||
dataset_namespace=dataset_namespace,
|
|
||||||
dataset_name=dataset_name,
|
|
||||||
episode_id=first_episode_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.route("/<string:dataset_namespace>/<string:dataset_name>/episode_<int:episode_id>")
|
|
||||||
def show_episode(dataset_namespace, dataset_name, episode_id, dataset=dataset, episodes=episodes):
|
|
||||||
repo_id = f"{dataset_namespace}/{dataset_name}"
|
|
||||||
try:
|
|
||||||
if dataset is None:
|
|
||||||
dataset = get_dataset_info(repo_id)
|
|
||||||
except FileNotFoundError:
|
|
||||||
return (
|
|
||||||
"Make sure to convert your LeRobotDataset to v2 & above. See how to convert your dataset at https://github.com/huggingface/lerobot/pull/461",
|
|
||||||
400,
|
|
||||||
)
|
|
||||||
dataset_version = (
|
|
||||||
str(dataset.meta._version) if isinstance(dataset, LeRobotDataset) else dataset.codebase_version
|
|
||||||
)
|
|
||||||
match = re.search(r"v(\d+)\.", dataset_version)
|
|
||||||
if match:
|
|
||||||
major_version = int(match.group(1))
|
|
||||||
if major_version < 2:
|
|
||||||
return "Make sure to convert your LeRobotDataset to v2 & above."
|
|
||||||
|
|
||||||
episode_data_csv_str, columns, ignored_columns = get_episode_data(dataset, episode_id)
|
def get_episode_metadata(episode: Episode, frames: List[Frame]) -> Dict:
|
||||||
dataset_info = {
|
metadata = {
|
||||||
"repo_id": f"{dataset_namespace}/{dataset_name}",
|
"episode_id": episode.episode_id,
|
||||||
"num_samples": dataset.num_frames
|
"num_frames": len(frames),
|
||||||
if isinstance(dataset, LeRobotDataset)
|
"actions": [],
|
||||||
else dataset.total_frames,
|
}
|
||||||
"num_episodes": dataset.num_episodes
|
|
||||||
if isinstance(dataset, LeRobotDataset)
|
for frame in frames:
|
||||||
else dataset.total_episodes,
|
if frame.action is not None:
|
||||||
"fps": dataset.fps,
|
metadata["actions"].append(
|
||||||
}
|
|
||||||
if isinstance(dataset, LeRobotDataset):
|
|
||||||
video_paths = [
|
|
||||||
dataset.meta.get_video_file_path(episode_id, key) for key in dataset.meta.video_keys
|
|
||||||
]
|
|
||||||
videos_info = [
|
|
||||||
{"url": url_for("static", filename=video_path), "filename": video_path.parent.name}
|
|
||||||
for video_path in video_paths
|
|
||||||
]
|
|
||||||
tasks = dataset.meta.episodes[episode_id]["tasks"]
|
|
||||||
else:
|
|
||||||
video_keys = [key for key, ft in dataset.features.items() if ft["dtype"] == "video"]
|
|
||||||
videos_info = [
|
|
||||||
{
|
{
|
||||||
"url": f"https://huggingface.co/datasets/{repo_id}/resolve/main/"
|
"frame_id": frame.frame_id,
|
||||||
+ dataset.video_path.format(
|
"action_type": frame.action.action_type,
|
||||||
episode_chunk=int(episode_id) // dataset.chunks_size,
|
|
||||||
video_key=video_key,
|
|
||||||
episode_index=episode_id,
|
|
||||||
),
|
|
||||||
"filename": video_key,
|
|
||||||
}
|
}
|
||||||
for video_key in video_keys
|
|
||||||
]
|
|
||||||
|
|
||||||
response = requests.get(
|
|
||||||
f"https://huggingface.co/datasets/{repo_id}/resolve/main/meta/episodes.jsonl", timeout=5
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
# Split into lines and parse each line as JSON
|
|
||||||
tasks_jsonl = [json.loads(line) for line in response.text.splitlines() if line.strip()]
|
|
||||||
|
|
||||||
filtered_tasks_jsonl = [row for row in tasks_jsonl if row["episode_index"] == episode_id]
|
|
||||||
tasks = filtered_tasks_jsonl[0]["tasks"]
|
|
||||||
|
|
||||||
videos_info[0]["language_instruction"] = tasks
|
|
||||||
|
|
||||||
if episodes is None:
|
|
||||||
episodes = list(
|
|
||||||
range(dataset.num_episodes if isinstance(dataset, LeRobotDataset) else dataset.total_episodes)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return render_template(
|
return metadata
|
||||||
"visualize_dataset_template.html",
|
|
||||||
episode_id=episode_id,
|
|
||||||
episodes=episodes,
|
def encode_image(image_path: Union[str, Path]) -> str:
|
||||||
dataset_info=dataset_info,
|
with open(image_path, "rb") as image_file:
|
||||||
videos_info=videos_info,
|
encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
|
||||||
episode_data_csv_str=episode_data_csv_str,
|
return encoded_string
|
||||||
columns=columns,
|
|
||||||
ignored_columns=ignored_columns,
|
|
||||||
|
def get_frame_data(frame: Frame) -> Dict:
|
||||||
|
frame_data = {"frame_id": frame.frame_id}
|
||||||
|
|
||||||
|
# Add RGB image
|
||||||
|
if frame.rgb_path is not None:
|
||||||
|
frame_data["rgb"] = encode_image(frame.rgb_path)
|
||||||
|
|
||||||
|
# Add depth image
|
||||||
|
if frame.depth_path is not None:
|
||||||
|
# Convert depth image to color map for visualization
|
||||||
|
depth_image = cv2.imread(str(frame.depth_path), cv2.IMREAD_ANYDEPTH)
|
||||||
|
if depth_image is not None:
|
||||||
|
# Normalize depth image to 0-255
|
||||||
|
depth_image_normalized = cv2.normalize(depth_image, None, 0, 255, cv2.NORM_MINMAX)
|
||||||
|
depth_image_normalized = depth_image_normalized.astype(np.uint8)
|
||||||
|
# Apply color map
|
||||||
|
depth_image_colormap = cv2.applyColorMap(depth_image_normalized, cv2.COLORMAP_JET)
|
||||||
|
# Save to temporary file
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_file:
|
||||||
|
cv2.imwrite(temp_file.name, depth_image_colormap)
|
||||||
|
frame_data["depth"] = encode_image(temp_file.name)
|
||||||
|
# Remove temporary file
|
||||||
|
os.unlink(temp_file.name)
|
||||||
|
|
||||||
|
# Add action
|
||||||
|
if frame.action is not None:
|
||||||
|
frame_data["action"] = {
|
||||||
|
"action_type": frame.action.action_type,
|
||||||
|
"action_args": frame.action.action_args,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add state
|
||||||
|
if frame.state is not None:
|
||||||
|
frame_data["state"] = frame.state
|
||||||
|
|
||||||
|
return frame_data
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
html_content = """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Dataset Viewer</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.episode-selector {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.frame-viewer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.frame-container {
|
||||||
|
margin-right: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.frame-image {
|
||||||
|
max-width: 400px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
.frame-info {
|
||||||
|
margin-top: 10px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
.navigation {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
button:disabled {
|
||||||
|
background-color: #cccccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.frame-counter {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Dataset Viewer</h1>
|
||||||
|
|
||||||
|
<div class="episode-selector">
|
||||||
|
<label for="episode-id">Episode ID:</label>
|
||||||
|
<input type="text" id="episode-id" placeholder="Enter episode ID">
|
||||||
|
<button onclick="loadEpisode()">Load Episode</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="frame-counter">
|
||||||
|
Frame: <span id="current-frame">0</span> / <span id="total-frames">0</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="frame-viewer">
|
||||||
|
<div class="frame-container">
|
||||||
|
<h3>RGB Image</h3>
|
||||||
|
<img id="rgb-image" class="frame-image" src="" alt="RGB Image">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="frame-container">
|
||||||
|
<h3>Depth Image</h3>
|
||||||
|
<img id="depth-image" class="frame-image" src="" alt="Depth Image">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="frame-info" id="frame-info">
|
||||||
|
<h3>Frame Information</h3>
|
||||||
|
<pre id="frame-data"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navigation">
|
||||||
|
<button id="prev-button" onclick="prevFrame()" disabled>Previous Frame</button>
|
||||||
|
<button id="next-button" onclick="nextFrame()" disabled>Next Frame</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentEpisode = null;
|
||||||
|
let currentFrameIndex = 0;
|
||||||
|
let frames = [];
|
||||||
|
|
||||||
|
function loadEpisode() {
|
||||||
|
const episodeId = document.getElementById('episode-id').value;
|
||||||
|
if (!episodeId) {
|
||||||
|
alert('Please enter an episode ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/api/episode/${episodeId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
currentEpisode = data;
|
||||||
|
document.getElementById('total-frames').textContent = data.num_frames;
|
||||||
|
currentFrameIndex = 0;
|
||||||
|
loadFrame(0);
|
||||||
|
document.getElementById('prev-button').disabled = true;
|
||||||
|
document.getElementById('next-button').disabled = data.num_frames <= 1;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading episode:', error);
|
||||||
|
alert('Error loading episode. Please check the episode ID and try again.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFrame(frameIndex) {
|
||||||
|
if (!currentEpisode) return;
|
||||||
|
|
||||||
|
fetch(`/api/episode/${currentEpisode.episode_id}/frame/${frameIndex}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// Update RGB image
|
||||||
|
if (data.rgb) {
|
||||||
|
document.getElementById('rgb-image').src = `data:image/jpeg;base64,${data.rgb}`;
|
||||||
|
} else {
|
||||||
|
document.getElementById('rgb-image').src = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update depth image
|
||||||
|
if (data.depth) {
|
||||||
|
document.getElementById('depth-image').src = `data:image/jpeg;base64,${data.depth}`;
|
||||||
|
} else {
|
||||||
|
document.getElementById('depth-image').src = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update frame info
|
||||||
|
const frameInfo = {
|
||||||
|
frame_id: data.frame_id,
|
||||||
|
action: data.action,
|
||||||
|
state: data.state
|
||||||
|
};
|
||||||
|
document.getElementById('frame-data').textContent = JSON.stringify(frameInfo, null, 2);
|
||||||
|
|
||||||
|
// Update current frame counter
|
||||||
|
document.getElementById('current-frame').textContent = frameIndex + 1;
|
||||||
|
|
||||||
|
// Update navigation buttons
|
||||||
|
document.getElementById('prev-button').disabled = frameIndex === 0;
|
||||||
|
document.getElementById('next-button').disabled = frameIndex >= currentEpisode.num_frames - 1;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading frame:', error);
|
||||||
|
alert('Error loading frame data.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevFrame() {
|
||||||
|
if (currentFrameIndex > 0) {
|
||||||
|
currentFrameIndex--;
|
||||||
|
loadFrame(currentFrameIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextFrame() {
|
||||||
|
if (currentEpisode && currentFrameIndex < currentEpisode.num_frames - 1) {
|
||||||
|
currentFrameIndex++;
|
||||||
|
loadFrame(currentFrameIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
return html_content
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/episode/<episode_id>")
|
||||||
|
def get_episode(episode_id):
|
||||||
|
dataset_path = request.args.get("dataset_path", None)
|
||||||
|
if dataset_path is None:
|
||||||
|
return jsonify({"error": "dataset_path parameter is required"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
episode, frames = get_episode_data(Path(dataset_path), episode_id)
|
||||||
|
metadata = get_episode_metadata(episode, frames)
|
||||||
|
return jsonify(metadata)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/episode/<episode_id>/frame/<int:frame_index>")
|
||||||
|
def get_frame(episode_id, frame_index):
|
||||||
|
dataset_path = request.args.get("dataset_path", None)
|
||||||
|
if dataset_path is None:
|
||||||
|
return jsonify({"error": "dataset_path parameter is required"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
episode, frames = get_episode_data(Path(dataset_path), episode_id)
|
||||||
|
if frame_index < 0 or frame_index >= len(frames):
|
||||||
|
return jsonify({"error": f"Frame index {frame_index} out of range"}), 400
|
||||||
|
|
||||||
|
frame_data = get_frame_data(frames[frame_index])
|
||||||
|
return jsonify(frame_data)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/proxy")
|
||||||
|
def proxy():
|
||||||
|
url = request.args.get("url", None)
|
||||||
|
if url is None:
|
||||||
|
return jsonify({"error": "url parameter is required"}), 400
|
||||||
|
|
||||||
|
# Validate URL against allowed schemes and hosts
|
||||||
|
if not validate_url(url):
|
||||||
|
return jsonify({"error": "URL is not allowed"}), 403
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Make the request but don't forward headers from the original request
|
||||||
|
# to prevent header injection
|
||||||
|
response = requests.get(url, timeout=5)
|
||||||
|
|
||||||
|
# Don't return the actual response to the user, just a success message
|
||||||
|
# This prevents SSRF attacks where the response might contain sensitive information
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Request completed successfully",
|
||||||
|
"status_code": response.status_code,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
app.run(host=host, port=port)
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
def get_ep_csv_fname(episode_id: int):
|
|
||||||
ep_csv_fname = f"episode_{episode_id}.csv"
|
|
||||||
return ep_csv_fname
|
|
||||||
|
|
||||||
|
|
||||||
def get_episode_data(dataset: LeRobotDataset | IterableNamespace, episode_index):
|
|
||||||
"""Get a csv str containing timeseries data of an episode (e.g. state and action).
|
|
||||||
This file will be loaded by Dygraph javascript to plot data in real time."""
|
|
||||||
columns = []
|
|
||||||
|
|
||||||
selected_columns = [col for col, ft in dataset.features.items() if ft["dtype"] in ["float32", "int32"]]
|
|
||||||
selected_columns.remove("timestamp")
|
|
||||||
|
|
||||||
ignored_columns = []
|
|
||||||
for column_name in selected_columns:
|
|
||||||
shape = dataset.features[column_name]["shape"]
|
|
||||||
shape_dim = len(shape)
|
|
||||||
if shape_dim > 1:
|
|
||||||
selected_columns.remove(column_name)
|
|
||||||
ignored_columns.append(column_name)
|
|
||||||
|
|
||||||
# init header of csv with state and action names
|
|
||||||
header = ["timestamp"]
|
|
||||||
|
|
||||||
for column_name in selected_columns:
|
|
||||||
dim_state = (
|
|
||||||
dataset.meta.shapes[column_name][0]
|
|
||||||
if isinstance(dataset, LeRobotDataset)
|
|
||||||
else dataset.features[column_name].shape[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
if "names" in dataset.features[column_name] and dataset.features[column_name]["names"]:
|
|
||||||
column_names = dataset.features[column_name]["names"]
|
|
||||||
while not isinstance(column_names, list):
|
|
||||||
column_names = list(column_names.values())[0]
|
|
||||||
else:
|
|
||||||
column_names = [f"{column_name}_{i}" for i in range(dim_state)]
|
|
||||||
columns.append({"key": column_name, "value": column_names})
|
|
||||||
|
|
||||||
header += column_names
|
|
||||||
|
|
||||||
selected_columns.insert(0, "timestamp")
|
|
||||||
|
|
||||||
if isinstance(dataset, LeRobotDataset):
|
|
||||||
from_idx = dataset.episode_data_index["from"][episode_index]
|
|
||||||
to_idx = dataset.episode_data_index["to"][episode_index]
|
|
||||||
data = (
|
|
||||||
dataset.hf_dataset.select(range(from_idx, to_idx))
|
|
||||||
.select_columns(selected_columns)
|
|
||||||
.with_format("pandas")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
repo_id = dataset.repo_id
|
|
||||||
|
|
||||||
url = f"https://huggingface.co/datasets/{repo_id}/resolve/main/" + dataset.data_path.format(
|
|
||||||
episode_chunk=int(episode_index) // dataset.chunks_size, episode_index=episode_index
|
|
||||||
)
|
|
||||||
df = pd.read_parquet(url)
|
|
||||||
data = df[selected_columns] # Select specific columns
|
|
||||||
|
|
||||||
rows = np.hstack(
|
|
||||||
(
|
|
||||||
np.expand_dims(data["timestamp"], axis=1),
|
|
||||||
*[np.vstack(data[col]) for col in selected_columns[1:]],
|
|
||||||
)
|
|
||||||
).tolist()
|
|
||||||
|
|
||||||
# Convert data to CSV string
|
|
||||||
csv_buffer = StringIO()
|
|
||||||
csv_writer = csv.writer(csv_buffer)
|
|
||||||
# Write header
|
|
||||||
csv_writer.writerow(header)
|
|
||||||
# Write data rows
|
|
||||||
csv_writer.writerows(rows)
|
|
||||||
csv_string = csv_buffer.getvalue()
|
|
||||||
|
|
||||||
return csv_string, columns, ignored_columns
|
|
||||||
|
|
||||||
|
|
||||||
def get_episode_video_paths(dataset: LeRobotDataset, ep_index: int) -> list[str]:
|
|
||||||
# get first frame of episode (hack to get video_path of the episode)
|
|
||||||
first_frame_idx = dataset.episode_data_index["from"][ep_index].item()
|
|
||||||
return [
|
|
||||||
dataset.hf_dataset.select_columns(key)[first_frame_idx][key]["path"]
|
|
||||||
for key in dataset.meta.video_keys
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def get_episode_language_instruction(dataset: LeRobotDataset, ep_index: int) -> list[str]:
|
|
||||||
# check if the dataset has language instructions
|
|
||||||
if "language_instruction" not in dataset.features:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# get first frame index
|
|
||||||
first_frame_idx = dataset.episode_data_index["from"][ep_index].item()
|
|
||||||
|
|
||||||
language_instruction = dataset.hf_dataset[first_frame_idx]["language_instruction"]
|
|
||||||
# TODO (michel-aractingi) hack to get the sentence, some strings in openx are badly stored
|
|
||||||
# with the tf.tensor appearing in the string
|
|
||||||
return language_instruction.removeprefix("tf.Tensor(b'").removesuffix("', shape=(), dtype=string)")
|
|
||||||
|
|
||||||
|
|
||||||
def get_dataset_info(repo_id: str) -> IterableNamespace:
|
|
||||||
response = requests.get(
|
|
||||||
f"https://huggingface.co/datasets/{repo_id}/resolve/main/meta/info.json", timeout=5
|
|
||||||
)
|
|
||||||
response.raise_for_status() # Raises an HTTPError for bad responses
|
|
||||||
dataset_info = response.json()
|
|
||||||
dataset_info["repo_id"] = repo_id
|
|
||||||
return IterableNamespace(dataset_info)
|
|
||||||
|
|
||||||
|
|
||||||
def visualize_dataset_html(
|
|
||||||
dataset: LeRobotDataset | None,
|
|
||||||
episodes: list[int] | None = None,
|
|
||||||
output_dir: Path | None = None,
|
|
||||||
serve: bool = True,
|
|
||||||
host: str = "127.0.0.1",
|
|
||||||
port: int = 9090,
|
|
||||||
force_override: bool = False,
|
|
||||||
) -> Path | None:
|
|
||||||
init_logging()
|
|
||||||
|
|
||||||
template_dir = Path(__file__).resolve().parent.parent / "templates"
|
|
||||||
|
|
||||||
if output_dir is None:
|
|
||||||
# Create a temporary directory that will be automatically cleaned up
|
|
||||||
output_dir = tempfile.mkdtemp(prefix="lerobot_visualize_dataset_")
|
|
||||||
|
|
||||||
output_dir = Path(output_dir)
|
|
||||||
if output_dir.exists():
|
|
||||||
if force_override:
|
|
||||||
shutil.rmtree(output_dir)
|
|
||||||
else:
|
|
||||||
logging.info(f"Output directory already exists. Loading from it: '{output_dir}'")
|
|
||||||
|
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
static_dir = output_dir / "static"
|
|
||||||
static_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
if dataset is None:
|
|
||||||
if serve:
|
|
||||||
run_server(
|
|
||||||
dataset=None,
|
|
||||||
episodes=None,
|
|
||||||
host=host,
|
|
||||||
port=port,
|
|
||||||
static_folder=static_dir,
|
|
||||||
template_folder=template_dir,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Create a simlink from the dataset video folder containing mp4 files to the output directory
|
|
||||||
# so that the http server can get access to the mp4 files.
|
|
||||||
if isinstance(dataset, LeRobotDataset):
|
|
||||||
ln_videos_dir = static_dir / "videos"
|
|
||||||
if not ln_videos_dir.exists():
|
|
||||||
ln_videos_dir.symlink_to((dataset.root / "videos").resolve())
|
|
||||||
|
|
||||||
if serve:
|
|
||||||
run_server(dataset, episodes, host, port, static_dir, template_dir)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser(description="Visualize dataset in HTML")
|
||||||
|
parser.add_argument("--dataset-name", type=str, help="Name of the dataset")
|
||||||
parser.add_argument(
|
parser.add_argument("--dataset-path", type=str, help="Path to the dataset")
|
||||||
"--repo-id",
|
parser.add_argument("--host", type=str, default="localhost", help="Host to run the server on")
|
||||||
type=str,
|
parser.add_argument("--port", type=int, default=8000, help="Port to run the server on")
|
||||||
default=None,
|
|
||||||
help="Name of hugging face repositery containing a LeRobotDataset dataset (e.g. `lerobot/pusht` for https://huggingface.co/datasets/lerobot/pusht).",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--root",
|
|
||||||
type=Path,
|
|
||||||
default=None,
|
|
||||||
help="Root directory for a dataset stored locally (e.g. `--root data`). By default, the dataset will be loaded from hugging face cache folder, or downloaded from the hub if available.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--load-from-hf-hub",
|
|
||||||
type=int,
|
|
||||||
default=0,
|
|
||||||
help="Load videos and parquet files from HF Hub rather than local system.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--episodes",
|
|
||||||
type=int,
|
|
||||||
nargs="*",
|
|
||||||
default=None,
|
|
||||||
help="Episode indices to visualize (e.g. `0 1 5 6` to load episodes of index 0, 1, 5 and 6). By default loads all episodes.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--output-dir",
|
|
||||||
type=Path,
|
|
||||||
default=None,
|
|
||||||
help="Directory path to write html files and kickoff a web server. By default write them to 'outputs/visualize_dataset/REPO_ID'.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--serve",
|
|
||||||
type=int,
|
|
||||||
default=1,
|
|
||||||
help="Launch web server.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--host",
|
|
||||||
type=str,
|
|
||||||
default="127.0.0.1",
|
|
||||||
help="Web host used by the http server.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--port",
|
|
||||||
type=int,
|
|
||||||
default=9090,
|
|
||||||
help="Web port used by the http server.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--force-override",
|
|
||||||
type=int,
|
|
||||||
default=0,
|
|
||||||
help="Delete the output directory if it exists already.",
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"--tolerance-s",
|
|
||||||
type=float,
|
|
||||||
default=1e-4,
|
|
||||||
help=(
|
|
||||||
"Tolerance in seconds used to ensure data timestamps respect the dataset fps value"
|
|
||||||
"This is argument passed to the constructor of LeRobotDataset and maps to its tolerance_s constructor argument"
|
|
||||||
"If not given, defaults to 1e-4."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
kwargs = vars(args)
|
|
||||||
repo_id = kwargs.pop("repo_id")
|
|
||||||
load_from_hf_hub = kwargs.pop("load_from_hf_hub")
|
|
||||||
root = kwargs.pop("root")
|
|
||||||
tolerance_s = kwargs.pop("tolerance_s")
|
|
||||||
|
|
||||||
dataset = None
|
if args.dataset_name is not None:
|
||||||
if repo_id:
|
dataset_path = get_dataset_path(args.dataset_name)
|
||||||
dataset = (
|
elif args.dataset_path is not None:
|
||||||
LeRobotDataset(repo_id, root=root, tolerance_s=tolerance_s)
|
dataset_path = Path(args.dataset_path)
|
||||||
if not load_from_hf_hub
|
else:
|
||||||
else get_dataset_info(repo_id)
|
print("Either --dataset-name or --dataset-path must be provided")
|
||||||
)
|
sys.exit(1)
|
||||||
|
|
||||||
visualize_dataset_html(dataset, **vars(args))
|
app.config["dataset_path"] = dataset_path
|
||||||
|
app.run(host=args.host, port=args.port, debug=True)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -60,9 +60,9 @@ dependencies = [
|
||||||
"jsonlines>=4.0.0",
|
"jsonlines>=4.0.0",
|
||||||
"numba>=0.59.0",
|
"numba>=0.59.0",
|
||||||
"omegaconf>=2.3.0",
|
"omegaconf>=2.3.0",
|
||||||
"opencv-python>=4.9.0",
|
"opencv-python-headless>=4.9.0",
|
||||||
"packaging>=24.2",
|
"packaging>=24.2",
|
||||||
"av>=12.0.5,<13.0.0",
|
"av>=12.0.5",
|
||||||
"pymunk>=6.6.0",
|
"pymunk>=6.6.0",
|
||||||
"pynput>=1.7.7",
|
"pynput>=1.7.7",
|
||||||
"pyzmq>=26.2.1",
|
"pyzmq>=26.2.1",
|
||||||
|
|
|
@ -172,8 +172,7 @@ def test_record_and_replay_and_policy(tmp_path, request, robot_type, mock):
|
||||||
push_to_hub=False,
|
push_to_hub=False,
|
||||||
# TODO(rcadene, aliberts): test video=True
|
# TODO(rcadene, aliberts): test video=True
|
||||||
video=False,
|
video=False,
|
||||||
# TODO(rcadene): display cameras through cv2 sometimes crashes on mac
|
display_data=False,
|
||||||
display_cameras=False,
|
|
||||||
play_sounds=False,
|
play_sounds=False,
|
||||||
)
|
)
|
||||||
dataset = record(robot, rec_cfg)
|
dataset = record(robot, rec_cfg)
|
||||||
|
@ -226,7 +225,7 @@ def test_record_and_replay_and_policy(tmp_path, request, robot_type, mock):
|
||||||
num_episodes=2,
|
num_episodes=2,
|
||||||
push_to_hub=False,
|
push_to_hub=False,
|
||||||
video=False,
|
video=False,
|
||||||
display_cameras=False,
|
display_data=False,
|
||||||
play_sounds=False,
|
play_sounds=False,
|
||||||
num_image_writer_processes=num_image_writer_processes,
|
num_image_writer_processes=num_image_writer_processes,
|
||||||
)
|
)
|
||||||
|
@ -273,7 +272,7 @@ def test_resume_record(tmp_path, request, robot_type, mock):
|
||||||
episode_time_s=1,
|
episode_time_s=1,
|
||||||
push_to_hub=False,
|
push_to_hub=False,
|
||||||
video=False,
|
video=False,
|
||||||
display_cameras=False,
|
display_data=False,
|
||||||
play_sounds=False,
|
play_sounds=False,
|
||||||
num_episodes=1,
|
num_episodes=1,
|
||||||
)
|
)
|
||||||
|
@ -330,7 +329,7 @@ def test_record_with_event_rerecord_episode(tmp_path, request, robot_type, mock)
|
||||||
num_episodes=1,
|
num_episodes=1,
|
||||||
push_to_hub=False,
|
push_to_hub=False,
|
||||||
video=False,
|
video=False,
|
||||||
display_cameras=False,
|
display_data=False,
|
||||||
play_sounds=False,
|
play_sounds=False,
|
||||||
)
|
)
|
||||||
dataset = record(robot, rec_cfg)
|
dataset = record(robot, rec_cfg)
|
||||||
|
@ -380,7 +379,7 @@ def test_record_with_event_exit_early(tmp_path, request, robot_type, mock):
|
||||||
num_episodes=1,
|
num_episodes=1,
|
||||||
push_to_hub=False,
|
push_to_hub=False,
|
||||||
video=False,
|
video=False,
|
||||||
display_cameras=False,
|
display_data=False,
|
||||||
play_sounds=False,
|
play_sounds=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -433,7 +432,7 @@ def test_record_with_event_stop_recording(tmp_path, request, robot_type, mock, n
|
||||||
num_episodes=2,
|
num_episodes=2,
|
||||||
push_to_hub=False,
|
push_to_hub=False,
|
||||||
video=False,
|
video=False,
|
||||||
display_cameras=False,
|
display_data=False,
|
||||||
play_sounds=False,
|
play_sounds=False,
|
||||||
num_image_writer_processes=num_image_writer_processes,
|
num_image_writer_processes=num_image_writer_processes,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue