Compare commits

...

54 Commits

Author SHA1 Message Date
Remi Cadene
1be6fc3a7d linux only 2024-07-29 18:25:40 +02:00
Remi Cadene
1af22d685b opencv.py 2024-07-29 18:06:02 +02:00
Remi Cadene
b0f4f4f9a1 Add compatibility with camera index as port 2024-07-19 11:32:31 +02:00
Remi Cadene
1664dac300 fix 2024-07-18 18:48:41 +02:00
Remi Cadene
895182b272 Merge remote-tracking branch 'origin/main' into user/rcadene/2024_07_16_control_robot_v2 2024-07-17 23:04:14 +02:00
Remi Cadene
2c7089e9f9 robot disconnect, fix cv2.waitkey 2024-07-17 23:01:20 +02:00
Remi Cadene
3e2c43296c Display cameras and add reset env in run_policy (WIP) 2024-07-17 10:37:23 +02:00
Remi Cadene
6fc1d0dfc1 Update transforms (increase sharpness, reduce saturation, reduce hue) 2024-07-16 18:04:55 +02:00
Remi Cadene
694a5dbca8 fix color 2024-07-15 17:36:05 +02:00
Remi
61d9d74308 Merge branch 'main' into user/rcadene/2024_06_22_control_robot 2024-07-15 14:35:02 +02:00
Remi Cadene
87e39da641 shutil.rmtree(tmp_imgs_dir) 2024-07-15 14:20:35 +02:00
Simon Alibert
03d778e672 Fix docker build 2024-07-15 11:06:08 +02:00
Remi Cadene
14bc60270f back 2024-07-14 17:31:12 +02:00
Remi Cadene
0407b0dab2 DEBUG 2024-07-14 17:25:27 +02:00
Remi Cadene
8b4a1c7a7f fix 2024-07-14 17:12:57 +02:00
Remi Cadene
d878ec27d5 is_headless 2024-07-14 14:52:15 +00:00
Remi Cadene
42325f5990 Try adding python3-dev in lerobot-gpu-dev/Dockerfile 2024-07-14 16:45:09 +02:00
Remi Cadene
d2a17d2c74 Add push to hub, Camera 0 and 1, pynput optional 2024-07-14 16:35:22 +02:00
Remi Cadene
3fc351074c task: null 2024-07-13 16:46:25 +02:00
Remi Cadene
3e80ec65d2 Add env/koch_real.yaml 2024-07-13 16:38:24 +02:00
Remi Cadene
8be46f3760 Add env/koch_real.yaml 2024-07-13 16:37:44 +02:00
Remi Cadene
30c7a9e6fc nit + num-image-writers 8 2024-07-12 20:00:41 +02:00
Remi Cadene
d525e1b0f8 Add keyboard interaction, Add tqdm, Optimize stuff, Fix, Add resuming 2024-07-12 19:56:28 +02:00
Remi Cadene
7a659dbd6b Add reset-time-s, Add keyboard early exit, Add comments 2024-07-12 12:58:56 +02:00
Remi Cadene
1993d29296 Remove int32 convertion function, Set PID 1500, 0, 400 for elbow 2024-07-12 11:24:29 +02:00
Remi Cadene
e615176845 nit 2024-07-11 20:03:56 +02:00
Remi Cadene
3918e118c3 Add assert_same_address 2024-07-11 18:38:23 +02:00
Remi Cadene
17bc32c1e3 nit 2024-07-11 17:16:24 +02:00
Remi Cadene
73692e7459 Make data recording smooth and small fix 2024-07-11 17:14:23 +02:00
Remi Cadene
e6aa8cc641 more ports 2024-07-11 15:27:03 +02:00
Remi Cadene
5494faf096 fix unit tests (minimal) 2024-07-11 00:24:58 +02:00
Remi Cadene
01f8cede0b Add koch to CI 2024-07-10 23:05:36 +02:00
Remi Cadene
5d092b1be2 nit 2024-07-10 20:01:29 +02:00
Remi Cadene
7560372404 removed to radian representations 2024-07-10 19:56:51 +02:00
Remi Cadene
1bc13f39bf rename color to color_mode 2024-07-10 19:54:23 +02:00
Remi Cadene
95de4b7454 Skip tests if alexander koch robot is not available 2024-07-10 19:41:57 +02:00
Remi Cadene
c2388c59be uncomment shutil.rmtree(tmp_imgs_dir) 2024-07-10 19:07:15 +02:00
Remi Cadene
f7a7ed9aa9 Update opencv and dynamixel (find_port) 2024-07-10 18:47:59 +02:00
Remi Cadene
7411cf2a7a Improve opencv 2024-07-10 17:43:33 +02:00
Remi Cadene
2bebdf78a0 Nit + Remove benchmark opencv + Clean + Add docstring 2024-07-10 17:35:18 +02:00
Remi Cadene
fb06417a83 remove comment read/write single value 2024-07-10 14:13:22 +02:00
Remi Cadene
dd7bce7563 format 2024-07-10 14:11:53 +02:00
Remi Cadene
68a561570c fix unit test 2024-07-10 14:05:58 +02:00
Remi Cadene
52e760a88e Almost done 2024-07-10 00:07:40 +02:00
Remi Cadene
798373e7bf All tests passing except test_control_robot.py 2024-07-09 22:57:36 +02:00
Remi Cadene
a0432f1608 Add unit tests (WIP) 2024-07-09 22:57:36 +02:00
Remi Cadene
3ba05d53b9 Remove async in dynamixel 2024-07-09 22:57:36 +02:00
Remi Cadene
72b2e342ae revert threads to thread, results to color_image in OpenCVCamera 2024-07-09 22:57:36 +02:00
Remi Cadene
d83d34d9b3 Add log_control_info 2024-07-09 22:57:36 +02:00
Remi Cadene
3ff789c181 Add async_read and async_write (it doesnt work) 2024-07-09 22:57:36 +02:00
Remi Cadene
6e77a399a2 Add dynamixel-sdk as extra 2024-07-09 22:57:35 +02:00
Remi Cadene
8a7aa50e97 Style 2024-07-09 22:57:12 +02:00
Remi Cadene
47aac0dff7 rebase lock 2024-07-09 22:57:11 +02:00
Remi Cadene
858d49fc04 Add robot_devices and control_robot script 2024-07-09 22:56:46 +02:00
6 changed files with 199 additions and 53 deletions

View File

@@ -50,6 +50,24 @@ def find_camera_indices(raise_when_empty=False, max_index_search_range=MAX_OPENC
return camera_ids
def find_camera_ports(raise_when_empty=False):
camera_ports = []
for port in Path("/dev").glob("video*"):
port = str(port)
camera = cv2.VideoCapture(port)
is_open = camera.isOpened()
camera.release()
if is_open:
print(f"Camera found at port {port}")
camera_ports.append(port)
if raise_when_empty and len(camera_ports) == 0:
raise OSError(
"Not a single camera was detected. Try re-plugging, or re-installing `opencv2`, or your camera driver, or make sure your camera is compatible with opencv2."
)
return camera_ports
def save_image(img_array, camera_index, frame_index, images_dir):
img = Image.fromarray(img_array)
@@ -59,11 +77,16 @@ def save_image(img_array, camera_index, frame_index, images_dir):
def save_images_from_cameras(
images_dir: Path, camera_ids=None, fps=None, width=None, height=None, record_time_s=2
images_dir: Path, camera_ids: list[int] | list[str] | None=None, fps=None, width=None, height=None, record_time_s=2
):
if camera_ids is None:
print("Finding available camera indices")
camera_ids = find_camera_indices()
print("Finding available camera indices through '/dev/video*' ports")
camera_ids = find_camera_ports()
if len(camera_ids) == 0:
print(f"Finding available camera indices through scanning all indices from 0 to {MAX_OPENCV_INDEX}")
camera_ids = find_camera_indices()
print("Connecting cameras")
cameras = []
@@ -76,7 +99,7 @@ def save_images_from_cameras(
cameras.append(camera)
images_dir = Path(
images_dir,
images_dir
)
if images_dir.exists():
shutil.rmtree(
@@ -99,7 +122,7 @@ def save_images_from_cameras(
executor.submit(
save_image,
image,
camera.camera_index,
camera.camera_index_to_int(),
frame_index,
images_dir,
)
@@ -182,7 +205,7 @@ class OpenCVCamera:
```
"""
def __init__(self, camera_index: int, config: OpenCVCameraConfig | None = None, **kwargs):
def __init__(self, camera_index: int | str, config: OpenCVCameraConfig | None = None, **kwargs):
if config is None:
config = OpenCVCameraConfig()
# Overwrite config arguments using kwargs
@@ -194,17 +217,17 @@ class OpenCVCamera:
self.height = config.height
self.color_mode = config.color_mode
if not isinstance(self.camera_index, int):
raise ValueError(
f"Camera index must be provided as an int, but {self.camera_index} was given instead."
)
self.camera = None
self.is_connected = False
self.thread = None
self.stop_event = None
self.color_image = None
self.logs = {}
def camera_index_to_int(self):
if isinstance(self.camera_index, int):
return self.camera_index
return int(self.camera_index.replace("/dev/video", ""))
def connect(self):
if self.is_connected:
@@ -221,7 +244,9 @@ class OpenCVCamera:
# valid cameras.
if not is_camera_open:
# Verify that the provided `camera_index` is valid before printing the traceback
available_cam_ids = find_camera_indices()
available_cam_ids = find_camera_ports()
if len(available_cam_ids) == 0:
available_cam_ids = find_camera_indices()
if self.camera_index not in available_cam_ids:
raise ValueError(
f"`camera_index` is expected to be one of these available cameras {available_cam_ids}, but {self.camera_index} is provided instead."
@@ -365,7 +390,7 @@ if __name__ == "__main__":
)
parser.add_argument(
"--camera-ids",
type=int,
type=str,
nargs="*",
default=None,
help="List of camera indices used to instantiate the `OpenCVCamera`. If not provided, find and use all available camera indices.",

View File

@@ -2,6 +2,7 @@ from pathlib import Path
from typing import Protocol
import cv2
import einops
import numpy as np
@@ -39,6 +40,16 @@ def save_depth_image(depth, path, write_shape=False):
cv2.imwrite(str(path), depth_image)
def convert_torch_image_to_cv2(tensor, rgb_to_bgr=True):
assert tensor.ndim == 3
c, h, w = tensor.shape
assert c < h and c < w
color_image = einops.rearrange(tensor, "c h w -> h w c").numpy()
if rgb_to_bgr:
color_image = cv2.cvtColor(color_image, cv2.COLOR_RGB2BGR)
return color_image
# Defines a camera type
class Camera(Protocol):
def connect(self): ...

View File

@@ -9,7 +9,7 @@ def make_robot(name):
robot = KochRobot(
leader_arms={
"main": DynamixelMotorsBus(
port="/dev/tty.usbmodem575E0031751",
port="/dev/ttyACM1",
motors={
# name: (index, model)
"shoulder_pan": (1, "xl330-m077"),
@@ -23,7 +23,7 @@ def make_robot(name):
},
follower_arms={
"main": DynamixelMotorsBus(
port="/dev/tty.usbmodem575E0032081",
port="/dev/ttyACM0",
motors={
# name: (index, model)
"shoulder_pan": (1, "xl430-w250"),
@@ -36,8 +36,8 @@ def make_robot(name):
),
},
cameras={
"laptop": OpenCVCamera(0, fps=30, width=640, height=480),
"phone": OpenCVCamera(1, fps=30, width=640, height=480),
"laptop": OpenCVCamera("/dev/video2", fps=30, width=640, height=480),
"phone": OpenCVCamera("/dev/video6", fps=30, width=640, height=480),
},
)
else:

View File

@@ -72,13 +72,13 @@ training:
min_max: [0.8, 1.2]
saturation:
weight: 1
min_max: [0.5, 1.5]
min_max: [0.7, 1.3]
hue:
weight: 1
min_max: [-0.05, 0.05]
min_max: [-0.02, 0.02]
sharpness:
weight: 1
min_max: [0.8, 1.2]
min_max: [0.1, 1.9]
eval:
n_episodes: 1

View File

@@ -88,8 +88,10 @@ import platform
import shutil
import time
from contextlib import nullcontext
from functools import cache
from pathlib import Path
import cv2
import torch
import tqdm
from huggingface_hub import create_branch
@@ -105,6 +107,7 @@ from lerobot.common.datasets.push_dataset_to_hub.utils import concatenate_episod
from lerobot.common.datasets.utils import calculate_episode_data_index
from lerobot.common.datasets.video_utils import encode_video_frames
from lerobot.common.policies.factory import make_policy
from lerobot.common.robot_devices.cameras.utils import convert_torch_image_to_cv2
from lerobot.common.robot_devices.robots.factory import make_robot
from lerobot.common.robot_devices.robots.utils import Robot
from lerobot.common.utils.utils import get_safe_torch_device, init_hydra_config, init_logging, set_global_seed
@@ -179,7 +182,8 @@ def log_control_info(robot, dt_s, episode_index=None, frame_index=None, fps=None
logging.info(info_str)
def get_is_headless():
@cache
def is_headless():
if platform.system() == "Linux":
display = os.environ.get("DISPLAY")
if display is None or display == "":
@@ -255,32 +259,10 @@ def record_dataset(
else:
episode_index = 0
is_headless = get_is_headless()
# Execute a few seconds without recording data, to give times
# to the robot devices to connect and start synchronizing.
timestamp = 0
start_time = time.perf_counter()
is_warmup_print = False
while timestamp < warmup_time_s:
if not is_warmup_print:
logging.info("Warming up (no data recording)")
os.system('say "Warmup" &')
is_warmup_print = True
now = time.perf_counter()
observation, action = robot.teleop_step(record_data=True)
if not is_headless:
image_keys = [key for key in observation if "image" in key]
dt_s = time.perf_counter() - now
busy_wait(1 / fps - dt_s)
dt_s = time.perf_counter() - now
log_control_info(robot, dt_s, fps=fps)
timestamp = time.perf_counter() - start_time
if is_headless():
logging.info(
"Headless environment detected. Display cameras on screen and keyboard inputs will not be available."
)
# Allow to exit early while recording an episode or resetting the environment,
# by tapping the right arrow key '->'. This might require a sudo permission
@@ -290,9 +272,7 @@ def record_dataset(
stop_recording = False
# Only import pynput if not in a headless environment
if is_headless:
logging.info("Headless environment detected. Keyboard input will not be available.")
else:
if not is_headless():
from pynput import keyboard
def on_press(key):
@@ -315,6 +295,35 @@ def record_dataset(
listener = keyboard.Listener(on_press=on_press)
listener.start()
# Execute a few seconds without recording data, to give times
# to the robot devices to connect and start synchronizing.
timestamp = 0
start_time = time.perf_counter()
is_warmup_print = False
while timestamp < warmup_time_s:
if not is_warmup_print:
logging.info("Warming up (no data recording)")
os.system('say "Warmup" &')
is_warmup_print = True
now = time.perf_counter()
observation, action = robot.teleop_step(record_data=True)
if not is_headless():
image_keys = [key for key in observation if "image" in key]
for key in image_keys:
cv2.imshow(key, cv2.cvtColor(observation[key].numpy(), cv2.COLOR_RGB2BGR))
cv2.waitKey(1)
dt_s = time.perf_counter() - now
busy_wait(1 / fps - dt_s)
dt_s = time.perf_counter() - now
log_control_info(robot, dt_s, fps=fps)
timestamp = time.perf_counter() - start_time
# Save images using threads to reach high fps (30 and more)
# Using `with` to exist smoothly if an execption is raised.
# Using only 4 worker threads to avoid blocking the main thread.
@@ -342,6 +351,12 @@ def record_dataset(
)
]
if not is_headless():
image_keys = [key for key in observation if "image" in key]
for key in image_keys:
cv2.imshow(key, cv2.cvtColor(observation[key].numpy(), cv2.COLOR_RGB2BGR))
cv2.waitKey(1)
for key in not_image_keys:
if key not in ep_dict:
ep_dict[key] = []
@@ -434,7 +449,7 @@ def record_dataset(
if is_last_episode:
logging.info("Done recording")
os.system('say "Done recording"')
if not is_headless:
if not is_headless():
listener.stop()
logging.info("Waiting for threads writing the images on disk to terminate...")
@@ -444,6 +459,10 @@ def record_dataset(
pass
break
robot.disconnect()
if not is_headless():
cv2.destroyAllWindows()
num_episodes = episode_index
logging.info("Encoding videos")
@@ -493,6 +512,7 @@ def record_dataset(
stats = compute_stats(lerobot_dataset)
lerobot_dataset.stats = stats
else:
stats = {}
logging.info("Skipping computation of the dataset statistrics")
hf_dataset = hf_dataset.with_format(None) # to remove transforms that cant be saved
@@ -544,7 +564,14 @@ def replay_episode(robot: Robot, episode: int, fps: int | None = None, root="dat
log_control_info(robot, dt_s, fps=fps)
def run_policy(robot: Robot, policy: torch.nn.Module, hydra_cfg: DictConfig, run_time_s: float | None = None):
def run_policy(
robot: Robot,
policy: torch.nn.Module,
hydra_cfg: DictConfig,
warmup_time_s: float = 4,
run_time_s: float | None = None,
reset_time_s: float = 15,
):
# TODO(rcadene): Add option to record eval dataset and logs
# Check device is available
@@ -562,12 +589,76 @@ def run_policy(robot: Robot, policy: torch.nn.Module, hydra_cfg: DictConfig, run
if not robot.is_connected:
robot.connect()
if is_headless():
logging.info(
"Headless environment detected. Display cameras on screen and keyboard inputs will not be available."
)
# Allow to reset environment or exit early
# by tapping the right arrow key '->'. This might require a sudo permission
# to allow your terminal to monitor keyboard events.
reset_environment = False
exit_reset = False
# Only import pynput if not in a headless environment
if not is_headless():
from pynput import keyboard
def on_press(key):
nonlocal reset_environment, exit_reset
try:
if key == keyboard.Key.right and not reset_environment:
print("Right arrow key pressed. Suspend robot control to reset environment...")
reset_environment = True
elif key == keyboard.Key.right and reset_environment:
print("Right arrow key pressed. Enable robot control and exit reset environment...")
exit_reset = True
except Exception as e:
print(f"Error handling key press: {e}")
listener = keyboard.Listener(on_press=on_press)
listener.start()
# Execute a few seconds without recording data, to give times
# to the robot devices to connect and start synchronizing.
timestamp = 0
start_time = time.perf_counter()
is_warmup_print = False
while timestamp < warmup_time_s:
if not is_warmup_print:
logging.info("Warming up (no data recording)")
os.system('say "Warmup" &')
is_warmup_print = True
now = time.perf_counter()
observation = robot.capture_observation()
if not is_headless():
image_keys = [key for key in observation if "image" in key]
for key in image_keys:
cv2.imshow(key, convert_torch_image_to_cv2(observation[key]))
cv2.waitKey(1)
dt_s = time.perf_counter() - now
busy_wait(1 / fps - dt_s)
dt_s = time.perf_counter() - now
log_control_info(robot, dt_s, fps=fps)
timestamp = time.perf_counter() - start_time
start_time = time.perf_counter()
while True:
now = time.perf_counter()
observation = robot.capture_observation()
if not is_headless():
image_keys = [key for key in observation if "image" in key]
for key in image_keys:
cv2.imshow(key, convert_torch_image_to_cv2(observation[key]))
cv2.waitKey(1)
with (
torch.inference_mode(),
torch.autocast(device_type=device.type)
@@ -598,6 +689,25 @@ def run_policy(robot: Robot, policy: torch.nn.Module, hydra_cfg: DictConfig, run
if run_time_s is not None and time.perf_counter() - start_time > run_time_s:
break
if reset_environment:
# Start resetting env while the executor are finishing
logging.info("Reset the environment")
os.system('say "Reset the environment" &')
# Wait if necessary
timestamp = 0
start_time = time.perf_counter()
with tqdm.tqdm(total=reset_time_s, desc="Waiting") as pbar:
while timestamp < reset_time_s:
time.sleep(1)
timestamp = time.perf_counter() - start_time
pbar.update(1)
if exit_reset:
exit_reset = False
break
reset_environment = False
if __name__ == "__main__":
parser = argparse.ArgumentParser()

View File

@@ -172,4 +172,4 @@ def visualize_transforms_cli(cfg):
if __name__ == "__main__":
visualize_transforms()
visualize_transforms_cli()