Apply suggestions from code review camera_realsense.py
Co-authored-by: Simon Alibert <75076266+aliberts@users.noreply.github.com>
This commit is contained in:
@@ -68,25 +68,23 @@ class RealSenseCamera(Camera):
|
|||||||
|
|
||||||
Example:
|
Example:
|
||||||
```python
|
```python
|
||||||
from lerobot.common.cameras.intel.camera_realsense import RealSenseCamera
|
from lerobot.common.cameras.intel import RealSenseCamera, RealSenseCameraConfig
|
||||||
from lerobot.common.cameras.intel.configuration_realsense import RealSenseCameraConfig
|
from lerobot.common.cameras import ColorMode
|
||||||
from lerobot.common.cameras.configs import ColorMode
|
|
||||||
|
|
||||||
# Basic usage with serial number
|
# Basic usage with serial number
|
||||||
config = RealSenseCameraConfig(serial_number="1234567890") # Replace with actual SN
|
config = RealSenseCameraConfig(serial_number="1234567890") # Replace with actual SN
|
||||||
camera = RealSenseCamera(config)
|
camera = RealSenseCamera(config)
|
||||||
try:
|
camera.connect()
|
||||||
camera.connect()
|
|
||||||
print(f"Connected to {camera}")
|
# Read 1 frame synchronously
|
||||||
color_image = camera.read() # Synchronous read (color only)
|
color_image = camera.read()
|
||||||
print(f"Read frame shape: {color_image.shape}")
|
print(color_image.shape)
|
||||||
async_image = camera.async_read() # Asynchronous read
|
|
||||||
print(f"Async read frame shape: {async_image.shape}")
|
# Read 1 frame asynchronously
|
||||||
except Exception as e:
|
async_image = camera.async_read()
|
||||||
print(f"An error occurred: {e}")
|
|
||||||
finally:
|
# When done, properly disconnect the camera using
|
||||||
camera.disconnect()
|
camera.disconnect()
|
||||||
print(f"Disconnected from {camera}")
|
|
||||||
|
|
||||||
# Example with depth capture and custom settings
|
# Example with depth capture and custom settings
|
||||||
custom_config = RealSenseCameraConfig(
|
custom_config = RealSenseCameraConfig(
|
||||||
@@ -132,10 +130,10 @@ class RealSenseCamera(Camera):
|
|||||||
else:
|
else:
|
||||||
raise ValueError("RealSenseCameraConfig must provide either 'serial_number' or 'name'.")
|
raise ValueError("RealSenseCameraConfig must provide either 'serial_number' or 'name'.")
|
||||||
|
|
||||||
self.fps: int | None = config.fps
|
self.fps = config.fps
|
||||||
self.channels: int = config.channels
|
self.channels = config.channels
|
||||||
self.color_mode: ColorMode = config.color_mode
|
self.color_mode = config.color_mode
|
||||||
self.use_depth: bool = config.use_depth
|
self.use_depth = config.use_depth
|
||||||
|
|
||||||
self.rs_pipeline: rs.pipeline | None = None
|
self.rs_pipeline: rs.pipeline | None = None
|
||||||
self.rs_profile: rs.pipeline_profile | None = None
|
self.rs_profile: rs.pipeline_profile | None = None
|
||||||
@@ -144,7 +142,6 @@ class RealSenseCamera(Camera):
|
|||||||
self.stop_event: Event | None = None
|
self.stop_event: Event | None = None
|
||||||
self.frame_queue: queue.Queue = queue.Queue(maxsize=1)
|
self.frame_queue: queue.Queue = queue.Queue(maxsize=1)
|
||||||
|
|
||||||
self.logs: dict = {} # For timestamping or other metadata
|
|
||||||
|
|
||||||
self.rotation: int | None = get_cv2_rotation(config.rotation)
|
self.rotation: int | None = get_cv2_rotation(config.rotation)
|
||||||
|
|
||||||
@@ -155,7 +152,6 @@ class RealSenseCamera(Camera):
|
|||||||
self.prerotated_width, self.prerotated_height = self.width, self.height
|
self.prerotated_width, self.prerotated_height = self.width, self.height
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Returns a string representation of the camera instance."""
|
|
||||||
return f"{self.__class__.__name__}({self.serial_number})"
|
return f"{self.__class__.__name__}({self.serial_number})"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -164,34 +160,23 @@ class RealSenseCamera(Camera):
|
|||||||
return self.rs_pipeline is not None and self.rs_profile is not None
|
return self.rs_pipeline is not None and self.rs_profile is not None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_cameras(raise_when_empty: bool = True) -> List[Dict[str, Any]]:
|
def find_cameras() -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Detects available Intel RealSense cameras connected to the system.
|
Detects available Intel RealSense cameras connected to the system.
|
||||||
|
|
||||||
Args:
|
|
||||||
raise_when_empty (bool): If True, raises an OSError if no cameras are found.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[Dict[str, Any]]: A list of dictionaries,
|
List[Dict[str, Any]]: A list of dictionaries,
|
||||||
where each dictionary contains 'type', 'id' (serial number), 'name',
|
where each dictionary contains 'type', 'id' (serial number), 'name',
|
||||||
firmware version, USB type, and other available specs, and the default profile properties (width, height, fps, format).
|
firmware version, USB type, and other available specs, and the default profile properties (width, height, fps, format).
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
OSError: If `raise_when_empty` is True and no cameras are detected,
|
OSError: If pyrealsense2 is not installed.
|
||||||
or if pyrealsense2 is not installed.
|
|
||||||
ImportError: If pyrealsense2 is not installed.
|
ImportError: If pyrealsense2 is not installed.
|
||||||
"""
|
"""
|
||||||
found_cameras_info = []
|
found_cameras_info = []
|
||||||
context = rs.context()
|
context = rs.context()
|
||||||
devices = context.query_devices()
|
devices = context.query_devices()
|
||||||
|
|
||||||
if not devices:
|
|
||||||
logger.warning("No RealSense devices detected.")
|
|
||||||
if raise_when_empty:
|
|
||||||
raise OSError(
|
|
||||||
"No RealSense devices detected. Ensure cameras are connected, "
|
|
||||||
"library (`pyrealsense2`) is installed, and firmware is up-to-date."
|
|
||||||
)
|
|
||||||
|
|
||||||
for device in devices:
|
for device in devices:
|
||||||
camera_info = {
|
camera_info = {
|
||||||
@@ -250,7 +235,7 @@ class RealSenseCamera(Camera):
|
|||||||
logger.info(f"Found serial number '{serial_number}' for camera name '{name}'.")
|
logger.info(f"Found serial number '{serial_number}' for camera name '{name}'.")
|
||||||
return serial_number
|
return serial_number
|
||||||
|
|
||||||
def _configure_realsense_settings(self) -> rs.config:
|
def _make_rs_pipeline_config(self) -> rs.config:
|
||||||
"""Creates and configures the RealSense pipeline configuration object."""
|
"""Creates and configures the RealSense pipeline configuration object."""
|
||||||
rs_config = rs.config()
|
rs_config = rs.config()
|
||||||
rs.config.enable_device(rs_config, self.serial_number)
|
rs.config.enable_device(rs_config, self.serial_number)
|
||||||
@@ -304,7 +289,7 @@ class RealSenseCamera(Camera):
|
|||||||
self.rs_profile.get_stream(rs.stream.depth).as_video_stream_profile()
|
self.rs_profile.get_stream(rs.stream.depth).as_video_stream_profile()
|
||||||
)
|
)
|
||||||
|
|
||||||
def connect(self, do_warmup_read: bool = True):
|
def connect(self, warmup: bool = True):
|
||||||
"""
|
"""
|
||||||
Connects to the RealSense camera specified in the configuration.
|
Connects to the RealSense camera specified in the configuration.
|
||||||
|
|
||||||
@@ -314,14 +299,12 @@ class RealSenseCamera(Camera):
|
|||||||
Raises:
|
Raises:
|
||||||
DeviceAlreadyConnectedError: If the camera is already connected.
|
DeviceAlreadyConnectedError: If the camera is already connected.
|
||||||
ValueError: If the configuration is invalid (e.g., missing serial/name, name not unique).
|
ValueError: If the configuration is invalid (e.g., missing serial/name, name not unique).
|
||||||
ConnectionError: If the camera is found but fails to start the pipeline.
|
ConnectionError: If the camera is found but fails to start the pipeline or no RealSense devices are detected at all.
|
||||||
RuntimeError: If the pipeline starts but fails to apply requested settings.
|
RuntimeError: If the pipeline starts but fails to apply requested settings.
|
||||||
OSError: If no RealSense devices are detected at all.
|
|
||||||
"""
|
"""
|
||||||
if self.is_connected:
|
if self.is_connected:
|
||||||
raise DeviceAlreadyConnectedError(f"{self} is already connected.")
|
raise DeviceAlreadyConnectedError(f"{self} is already connected.")
|
||||||
|
|
||||||
logger.debug(f"Attempting to connect to camera {self.serial_number}...")
|
|
||||||
self.rs_pipeline = rs.pipeline()
|
self.rs_pipeline = rs.pipeline()
|
||||||
rs_config = self._configure_realsense_settings()
|
rs_config = self._configure_realsense_settings()
|
||||||
|
|
||||||
@@ -332,34 +315,29 @@ class RealSenseCamera(Camera):
|
|||||||
self.rs_profile = None
|
self.rs_profile = None
|
||||||
self.rs_pipeline = None
|
self.rs_pipeline = None
|
||||||
raise ConnectionError(
|
raise ConnectionError(
|
||||||
f"Failed to open RealSense camera {self.serial_number}. Error: {e}. "
|
f"Failed to open {self} camera. "
|
||||||
f"Run 'python -m find_cameras list-cameras' for details."
|
f"Run 'python -m find_cameras list-cameras' for details."
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
logger.debug(f"Validating stream configuration for {self.serial_number}...")
|
logger.debug(f"Validating stream configuration for {self}...")
|
||||||
self._validate_capture_settings()
|
self._validate_capture_settings()
|
||||||
|
|
||||||
if do_warmup_read:
|
if warmup:
|
||||||
logger.debug(f"Reading a warm-up frame for {self.serial_number}...")
|
logger.debug(f"Reading a warm-up frame for {self}...")
|
||||||
self.read() # NOTE(Steven): For now we just read one frame, we could also loop for X frames/secs
|
self.read() # NOTE(Steven): For now we just read one frame, we could also loop for X frames/secs
|
||||||
|
|
||||||
logger.info(f"Camera {self.serial_number} connected and configured successfully.")
|
logger.info(f"{self} connected.")
|
||||||
|
|
||||||
def _validate_fps(self, stream) -> None:
|
def _validate_fps(self, stream: rs.video_stream_profile) -> None:
|
||||||
"""Validates and sets the internal FPS based on actual stream FPS."""
|
"""Validates and sets the internal FPS based on actual stream FPS."""
|
||||||
actual_fps = stream.fps()
|
actual_fps = stream.fps()
|
||||||
|
|
||||||
if self.fps is None:
|
if self.fps is None:
|
||||||
self.fps = actual_fps
|
self.fps = actual_fps
|
||||||
logger.info(f"FPS not specified, using camera default: {self.fps} FPS.")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Use math.isclose for robust float comparison
|
# Use math.isclose for robust float comparison
|
||||||
if not math.isclose(self.fps, actual_fps, rel_tol=1e-3):
|
if not math.isclose(self.fps, actual_fps, rel_tol=1e-3):
|
||||||
logger.warning(
|
|
||||||
f"Requested FPS {self.fps} for {self}, but camera reported {actual_fps}. "
|
|
||||||
"This might be due to camera limitations."
|
|
||||||
)
|
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Failed to set requested FPS {self.fps} for {self}. Actual value reported: {actual_fps}."
|
f"Failed to set requested FPS {self.fps} for {self}. Actual value reported: {actual_fps}."
|
||||||
)
|
)
|
||||||
@@ -399,7 +377,7 @@ class RealSenseCamera(Camera):
|
|||||||
)
|
)
|
||||||
logger.debug(f"Capture height set to {actual_height} for {self}.")
|
logger.debug(f"Capture height set to {actual_height} for {self}.")
|
||||||
|
|
||||||
def read_depth(self, timeout_ms: int = 5000) -> np.ndarray:
|
def read_depth(self, timeout_ms: int = 100) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
Reads a single frame (depth) synchronously from the camera.
|
Reads a single frame (depth) synchronously from the camera.
|
||||||
|
|
||||||
@@ -407,7 +385,7 @@ class RealSenseCamera(Camera):
|
|||||||
from the camera hardware via the RealSense pipeline.
|
from the camera hardware via the RealSense pipeline.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
timeout_ms (int): Maximum time in milliseconds to wait for a frame. Defaults to 5000ms.
|
timeout_ms (int): Maximum time in milliseconds to wait for a frame. Defaults to 100ms.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
np.ndarray: The depth map as a NumPy array (height, width)
|
np.ndarray: The depth map as a NumPy array (height, width)
|
||||||
@@ -428,13 +406,11 @@ class RealSenseCamera(Camera):
|
|||||||
|
|
||||||
start_time = time.perf_counter()
|
start_time = time.perf_counter()
|
||||||
|
|
||||||
ret, frame = self.rs_pipeline.try_wait_for_frames(
|
ret, frame = self.rs_pipeline.try_wait_for_frames(timeout_ms=timeout_ms)
|
||||||
timeout_ms=timeout_ms
|
|
||||||
) # NOTE(Steven): This read has a timeout
|
|
||||||
|
|
||||||
if not ret or frame is None:
|
if not ret or frame is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Failed to capture frame from {self}. '.read_depth()' returned status={ret} and frame is None."
|
f"{self} failed to capture frame. Returned status='{ret}'."
|
||||||
)
|
)
|
||||||
|
|
||||||
depth_frame = frame.get_depth_frame()
|
depth_frame = frame.get_depth_frame()
|
||||||
@@ -448,7 +424,7 @@ class RealSenseCamera(Camera):
|
|||||||
self.logs["timestamp_utc"] = capture_timestamp_utc()
|
self.logs["timestamp_utc"] = capture_timestamp_utc()
|
||||||
return depth_map_processed
|
return depth_map_processed
|
||||||
|
|
||||||
def read(self, color_mode: ColorMode | None = None, timeout_ms: int = 5000) -> np.ndarray:
|
def read(self, color_mode: ColorMode | None = None, timeout_ms: int = 100) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
Reads a single frame (color) synchronously from the camera.
|
Reads a single frame (color) synchronously from the camera.
|
||||||
|
|
||||||
@@ -456,7 +432,7 @@ class RealSenseCamera(Camera):
|
|||||||
from the camera hardware via the RealSense pipeline.
|
from the camera hardware via the RealSense pipeline.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
timeout_ms (int): Maximum time in milliseconds to wait for a frame. Defaults to 5000ms.
|
timeout_ms (int): Maximum time in milliseconds to wait for a frame. Defaults to 100ms.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
np.ndarray: The captured color frame as a NumPy array
|
np.ndarray: The captured color frame as a NumPy array
|
||||||
@@ -569,7 +545,7 @@ class RealSenseCamera(Camera):
|
|||||||
|
|
||||||
logger.debug(f"Stopping read loop thread for {self}.")
|
logger.debug(f"Stopping read loop thread for {self}.")
|
||||||
|
|
||||||
def _ensure_read_thread_running(self):
|
def _start_read_thread(self) -> None:
|
||||||
"""Starts or restarts the background read thread if it's not running."""
|
"""Starts or restarts the background read thread if it's not running."""
|
||||||
if self.thread is not None and self.thread.is_alive():
|
if self.thread is not None and self.thread.is_alive():
|
||||||
self.thread.join(timeout=0.1)
|
self.thread.join(timeout=0.1)
|
||||||
@@ -578,14 +554,14 @@ class RealSenseCamera(Camera):
|
|||||||
|
|
||||||
self.stop_event = Event()
|
self.stop_event = Event()
|
||||||
self.thread = Thread(
|
self.thread = Thread(
|
||||||
target=self._read_loop, args=(), name=f"RealSenseReadLoop-{self}-{self.serial_number}"
|
target=self._read_loop, args=(), name=f"{self}_read_loop"
|
||||||
)
|
)
|
||||||
self.thread.daemon = True
|
self.thread.daemon = True
|
||||||
self.thread.start()
|
self.thread.start()
|
||||||
logger.debug(f"Read thread started for {self}.")
|
logger.debug(f"Read thread started for {self}.")
|
||||||
|
|
||||||
# NOTE(Steven): Missing implementation for depth for now
|
# NOTE(Steven): Missing implementation for depth for now
|
||||||
def async_read(self, timeout_ms: float = 2000) -> np.ndarray:
|
def async_read(self, timeout_ms: float = 100) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
Reads the latest available frame data (color or color+depth) asynchronously.
|
Reads the latest available frame data (color or color+depth) asynchronously.
|
||||||
|
|
||||||
@@ -596,7 +572,7 @@ class RealSenseCamera(Camera):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
timeout_ms (float): Maximum time in milliseconds to wait for a frame
|
timeout_ms (float): Maximum time in milliseconds to wait for a frame
|
||||||
to become available in the queue. Defaults to 2000ms (2 seconds).
|
to become available in the queue. Defaults to 100ms (0.1 seconds).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
np.ndarray:
|
np.ndarray:
|
||||||
@@ -663,8 +639,6 @@ class RealSenseCamera(Camera):
|
|||||||
f"Attempted to disconnect {self}, but it appears already disconnected."
|
f"Attempted to disconnect {self}, but it appears already disconnected."
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(f"Disconnecting from camera {self.serial_number}...")
|
|
||||||
|
|
||||||
if self.thread is not None:
|
if self.thread is not None:
|
||||||
self._shutdown_read_thread()
|
self._shutdown_read_thread()
|
||||||
|
|
||||||
@@ -674,4 +648,4 @@ class RealSenseCamera(Camera):
|
|||||||
self.rs_pipeline = None
|
self.rs_pipeline = None
|
||||||
self.rs_profile = None
|
self.rs_profile = None
|
||||||
|
|
||||||
logger.info(f"Camera {self.serial_number} disconnected successfully.")
|
logger.info(f"{self} disconnected.")
|
||||||
|
|||||||
Reference in New Issue
Block a user