mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-30 04:59:44 +00:00
fix(find-cameras): enforce sequential lifecycle and add configurable warmup (#3593)
* Connect, test and disconnected camera instances sequentially * Add warmup-s cli argument to lerobot-find-cameras script * Reduce default record time from 6 to 2 seconds in find_cameras * Annotate return value of save_image function * Initialize logging configuration in find_cameras
This commit is contained in:
@@ -28,7 +28,6 @@ lerobot-find-cameras
|
|||||||
# NOTE(Steven): macOS cameras sometimes report different FPS at init time, not an issue here as we don't specify FPS when opening the cameras, but the information displayed might not be truthful.
|
# NOTE(Steven): macOS cameras sometimes report different FPS at init time, not an issue here as we don't specify FPS when opening the cameras, but the information displayed might not be truthful.
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import concurrent.futures
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -133,7 +132,7 @@ def save_image(
|
|||||||
camera_identifier: str | int,
|
camera_identifier: str | int,
|
||||||
images_dir: Path,
|
images_dir: Path,
|
||||||
camera_type: str,
|
camera_type: str,
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Saves a single image to disk using Pillow. Handles color conversion if necessary.
|
Saves a single image to disk using Pillow. Handles color conversion if necessary.
|
||||||
"""
|
"""
|
||||||
@@ -152,7 +151,7 @@ def save_image(
|
|||||||
logger.error(f"Failed to save image for camera {camera_identifier} (type {camera_type}): {e}")
|
logger.error(f"Failed to save image for camera {camera_identifier} (type {camera_type}): {e}")
|
||||||
|
|
||||||
|
|
||||||
def create_camera_instance(cam_meta: dict[str, Any]) -> dict[str, Any] | None:
|
def create_camera_instance(cam_meta: dict[str, Any], *, warmup_s: int = 1) -> dict[str, Any] | None:
|
||||||
"""Create and connect to a camera instance based on metadata."""
|
"""Create and connect to a camera instance based on metadata."""
|
||||||
cam_type = cam_meta.get("type")
|
cam_type = cam_meta.get("type")
|
||||||
cam_id = cam_meta.get("id")
|
cam_id = cam_meta.get("id")
|
||||||
@@ -165,12 +164,14 @@ def create_camera_instance(cam_meta: dict[str, Any]) -> dict[str, Any] | None:
|
|||||||
cv_config = OpenCVCameraConfig(
|
cv_config = OpenCVCameraConfig(
|
||||||
index_or_path=cam_id,
|
index_or_path=cam_id,
|
||||||
color_mode=ColorMode.RGB,
|
color_mode=ColorMode.RGB,
|
||||||
|
warmup_s=warmup_s,
|
||||||
)
|
)
|
||||||
instance = OpenCVCamera(cv_config)
|
instance = OpenCVCamera(cv_config)
|
||||||
elif cam_type == "RealSense":
|
elif cam_type == "RealSense":
|
||||||
rs_config = RealSenseCameraConfig(
|
rs_config = RealSenseCameraConfig(
|
||||||
serial_number_or_name=cam_id,
|
serial_number_or_name=cam_id,
|
||||||
color_mode=ColorMode.RGB,
|
color_mode=ColorMode.RGB,
|
||||||
|
warmup_s=warmup_s,
|
||||||
)
|
)
|
||||||
instance = RealSenseCamera(rs_config)
|
instance = RealSenseCamera(rs_config)
|
||||||
else:
|
else:
|
||||||
@@ -188,9 +189,7 @@ def create_camera_instance(cam_meta: dict[str, Any]) -> dict[str, Any] | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def process_camera_image(
|
def process_camera_image(cam_dict: dict[str, Any], output_dir: Path, current_time: float) -> None:
|
||||||
cam_dict: dict[str, Any], output_dir: Path, current_time: float
|
|
||||||
) -> concurrent.futures.Future | None:
|
|
||||||
"""Capture and process an image from a single camera."""
|
"""Capture and process an image from a single camera."""
|
||||||
cam = cam_dict["instance"]
|
cam = cam_dict["instance"]
|
||||||
meta = cam_dict["meta"]
|
meta = cam_dict["meta"]
|
||||||
@@ -200,7 +199,7 @@ def process_camera_image(
|
|||||||
try:
|
try:
|
||||||
image_data = cam.read()
|
image_data = cam.read()
|
||||||
|
|
||||||
return save_image(
|
save_image(
|
||||||
image_data,
|
image_data,
|
||||||
cam_id_str,
|
cam_id_str,
|
||||||
output_dir,
|
output_dir,
|
||||||
@@ -215,10 +214,9 @@ def process_camera_image(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def cleanup_cameras(cameras_to_use: list[dict[str, Any]]):
|
def cleanup_camera(cam_dict: dict[str, Any]) -> None:
|
||||||
"""Disconnect all cameras."""
|
"""Disconnect all cameras."""
|
||||||
logger.info(f"Disconnecting {len(cameras_to_use)} cameras...")
|
logger.info(f"Disconnecting camera with ID {cam_dict['meta'].get('id')}...")
|
||||||
for cam_dict in cameras_to_use:
|
|
||||||
try:
|
try:
|
||||||
if cam_dict["instance"] and cam_dict["instance"].is_connected:
|
if cam_dict["instance"] and cam_dict["instance"].is_connected:
|
||||||
cam_dict["instance"].disconnect()
|
cam_dict["instance"].disconnect()
|
||||||
@@ -230,6 +228,7 @@ def save_images_from_all_cameras(
|
|||||||
output_dir: Path,
|
output_dir: Path,
|
||||||
record_time_s: float = 2.0,
|
record_time_s: float = 2.0,
|
||||||
camera_type: str | None = None,
|
camera_type: str | None = None,
|
||||||
|
warmup_s: int = 1,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Connects to detected cameras (optionally filtered by type) and saves images from each.
|
Connects to detected cameras (optionally filtered by type) and saves images from each.
|
||||||
@@ -240,6 +239,7 @@ def save_images_from_all_cameras(
|
|||||||
record_time_s: Duration in seconds to record images.
|
record_time_s: Duration in seconds to record images.
|
||||||
camera_type: Optional string to filter cameras ("realsense" or "opencv").
|
camera_type: Optional string to filter cameras ("realsense" or "opencv").
|
||||||
If None, uses all detected cameras.
|
If None, uses all detected cameras.
|
||||||
|
warmup_s: Duration in seconds to warmup camera before recording images.
|
||||||
"""
|
"""
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
logger.info(f"Saving images to {output_dir}")
|
logger.info(f"Saving images to {output_dir}")
|
||||||
@@ -249,39 +249,23 @@ def save_images_from_all_cameras(
|
|||||||
logger.warning("No cameras detected matching the criteria. Cannot save images.")
|
logger.warning("No cameras detected matching the criteria. Cannot save images.")
|
||||||
return
|
return
|
||||||
|
|
||||||
cameras_to_use = []
|
logger.info(
|
||||||
for cam_meta in all_camera_metadata:
|
f"Starting image capture for {record_time_s} seconds from {len(all_camera_metadata)} cameras."
|
||||||
camera_instance = create_camera_instance(cam_meta)
|
)
|
||||||
if camera_instance:
|
|
||||||
cameras_to_use.append(camera_instance)
|
|
||||||
|
|
||||||
if not cameras_to_use:
|
|
||||||
logger.warning("No cameras could be connected. Aborting image save.")
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info(f"Starting image capture for {record_time_s} seconds from {len(cameras_to_use)} cameras.")
|
|
||||||
start_time = time.perf_counter()
|
|
||||||
|
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=len(cameras_to_use) * 2) as executor:
|
|
||||||
try:
|
try:
|
||||||
|
for cam_meta in all_camera_metadata:
|
||||||
|
cam_dict = create_camera_instance(cam_meta, warmup_s=warmup_s)
|
||||||
|
if cam_dict is None:
|
||||||
|
continue
|
||||||
|
start_time = time.perf_counter()
|
||||||
while time.perf_counter() - start_time < record_time_s:
|
while time.perf_counter() - start_time < record_time_s:
|
||||||
futures = []
|
|
||||||
current_capture_time = time.perf_counter()
|
current_capture_time = time.perf_counter()
|
||||||
|
process_camera_image(cam_dict, output_dir, current_capture_time)
|
||||||
for cam_dict in cameras_to_use:
|
cleanup_camera(cam_dict)
|
||||||
future = process_camera_image(cam_dict, output_dir, current_capture_time)
|
|
||||||
if future:
|
|
||||||
futures.append(future)
|
|
||||||
|
|
||||||
if futures:
|
|
||||||
concurrent.futures.wait(futures)
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info("Capture interrupted by user.")
|
logger.info("Capture interrupted by user.")
|
||||||
finally:
|
finally:
|
||||||
print("\nFinalizing image saving...")
|
|
||||||
executor.shutdown(wait=True)
|
|
||||||
cleanup_cameras(cameras_to_use)
|
|
||||||
print(f"Image capture finished. Images saved to {output_dir}")
|
print(f"Image capture finished. Images saved to {output_dir}")
|
||||||
|
|
||||||
|
|
||||||
@@ -291,7 +275,6 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Unified camera utility script for listing cameras and capturing images."
|
description="Unified camera utility script for listing cameras and capturing images."
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"camera_type",
|
"camera_type",
|
||||||
type=str,
|
type=str,
|
||||||
@@ -309,8 +292,14 @@ def main():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--record-time-s",
|
"--record-time-s",
|
||||||
type=float,
|
type=float,
|
||||||
default=6.0,
|
default=2.0,
|
||||||
help="Time duration to attempt capturing frames. Default: 6 seconds.",
|
help="Time duration to attempt capturing frames. Default: 2 seconds.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--warmup-s",
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
help="Time duration to warmup camera before attempting to capture frames. Default: 1 second.",
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
save_images_from_all_cameras(**vars(args))
|
save_images_from_all_cameras(**vars(args))
|
||||||
|
|||||||
Reference in New Issue
Block a user