chore(cameras): address unresolved conversations

This commit is contained in:
Steven Palma
2025-05-21 15:41:52 +02:00
parent 95f3e53eba
commit 47d5008407
7 changed files with 32 additions and 26 deletions

View File

@@ -36,6 +36,7 @@ class Camera(abc.ABC):
fps (int | None): Configured frames per second fps (int | None): Configured frames per second
width (int | None): Frame width in pixels width (int | None): Frame width in pixels
height (int | None): Frame height in pixels height (int | None): Frame height in pixels
warmup_time (int | None): Time reading frames before returning from connect (in seconds)
Example: Example:
class MyCamera(Camera): class MyCamera(Camera):
@@ -55,6 +56,7 @@ class Camera(abc.ABC):
self.fps: int | None = config.fps self.fps: int | None = config.fps
self.width: int | None = config.width self.width: int | None = config.width
self.height: int | None = config.height self.height: int | None = config.height
self.warmup_time: int | None = config.warmup_time
@property @property
@abc.abstractmethod @abc.abstractmethod

View File

@@ -21,12 +21,12 @@ from enum import Enum
import draccus import draccus
class ColorMode(Enum): class ColorMode(str, Enum):
RGB = "rgb" RGB = "rgb"
BGR = "bgr" BGR = "bgr"
class Cv2Rotation(Enum): class Cv2Rotation(int, Enum):
NO_ROTATION = 0 NO_ROTATION = 0
ROTATE_90 = 90 ROTATE_90 = 90
ROTATE_180 = 180 ROTATE_180 = 180
@@ -38,6 +38,7 @@ class CameraConfig(draccus.ChoiceRegistry, abc.ABC):
fps: int | None = None fps: int | None = None
width: int | None = None width: int | None = None
height: int | None = None height: int | None = None
warmup_time: int | None = None
@property @property
def type(self) -> str: def type(self) -> str:

View File

@@ -113,6 +113,7 @@ class OpenCVCamera(Camera):
self.fps = config.fps self.fps = config.fps
self.color_mode = config.color_mode self.color_mode = config.color_mode
self.warmup_time = config.warmup_time
self.videocapture: cv2.VideoCapture | None = None self.videocapture: cv2.VideoCapture | None = None
@@ -121,7 +122,7 @@ class OpenCVCamera(Camera):
self.frame_queue: queue.Queue = queue.Queue(maxsize=1) self.frame_queue: queue.Queue = queue.Queue(maxsize=1)
self.rotation: int | None = get_cv2_rotation(config.rotation) self.rotation: int | None = get_cv2_rotation(config.rotation)
self.backend: int = get_cv2_backend() # NOTE(Steven): If we specify backend the opencv open fails self.backend: int = get_cv2_backend()
if self.height and self.width: if self.height and self.width:
if self.rotation in [cv2.ROTATE_90_CLOCKWISE, cv2.ROTATE_90_COUNTERCLOCKWISE]: if self.rotation in [cv2.ROTATE_90_CLOCKWISE, cv2.ROTATE_90_COUNTERCLOCKWISE]:
@@ -157,7 +158,7 @@ class OpenCVCamera(Camera):
# blocking in multi-threaded applications, especially during data collection. # blocking in multi-threaded applications, especially during data collection.
cv2.setNumThreads(1) cv2.setNumThreads(1)
self.videocapture = cv2.VideoCapture(self.index_or_path) self.videocapture = cv2.VideoCapture(self.index_or_path, self.backend)
if not self.videocapture.isOpened(): if not self.videocapture.isOpened():
self.videocapture.release() self.videocapture.release()
@@ -170,8 +171,15 @@ class OpenCVCamera(Camera):
self._configure_capture_settings() self._configure_capture_settings()
if warmup: if warmup:
logger.debug(f"Reading a warm-up frame for {self.index_or_path}...") if self.warmup_time is None:
self.read() # NOTE(Steven): For now we just read one frame, we could also loop for X frames/secs raise ValueError(
f"Warmup time is not set for {self}. Please set a warmup time in the configuration."
)
logger.debug(f"Reading a warm-up frames for {self} for {self.warmup_time} seconds...")
start_time = time.time()
while time.time() - start_time < self.warmup_time:
self.read()
time.sleep(0.1)
logger.debug(f"Camera {self.index_or_path} connected and configured successfully.") logger.debug(f"Camera {self.index_or_path} connected and configured successfully.")

View File

@@ -18,7 +18,6 @@ Provides the RealSenseCamera class for capturing frames from Intel RealSense cam
import contextlib import contextlib
import logging import logging
import math
import queue import queue
import time import time
from threading import Event, Thread from threading import Event, Thread
@@ -129,6 +128,7 @@ class RealSenseCamera(Camera):
self.fps = config.fps self.fps = config.fps
self.color_mode = config.color_mode self.color_mode = config.color_mode
self.use_depth = config.use_depth self.use_depth = config.use_depth
self.warmup_time = config.warmup_time
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
@@ -186,8 +186,15 @@ class RealSenseCamera(Camera):
self._validate_capture_settings() self._validate_capture_settings()
if warmup: if warmup:
logger.debug(f"Reading a warm-up frame for {self}...") if self.warmup_time is None:
self.read() # NOTE(Steven): For now we just read one frame, we could also loop for X frames/secs raise ValueError(
f"Warmup time is not set for {self}. Please set a warmup time in the configuration."
)
logger.debug(f"Reading a warm-up frames for {self} for {self.warmup_time} seconds...")
start_time = time.time()
while time.time() - start_time < self.warmup_time:
self.read()
time.sleep(0.1)
logger.info(f"{self} connected.") logger.info(f"{self} connected.")
@@ -314,8 +321,6 @@ class RealSenseCamera(Camera):
if self.fps is None: if self.fps is None:
self.fps = stream.fps() self.fps = stream.fps()
else:
self._validate_fps(stream)
if self.width is None or self.height is None: if self.width is None or self.height is None:
actual_width = int(round(stream.width())) actual_width = int(round(stream.width()))
@@ -333,20 +338,8 @@ class RealSenseCamera(Camera):
if self.use_depth: if self.use_depth:
stream = self.rs_profile.get_stream(rs.stream.depth).as_video_stream_profile() stream = self.rs_profile.get_stream(rs.stream.depth).as_video_stream_profile()
self._validate_fps(stream)
self._validate_width_and_height(stream) self._validate_width_and_height(stream)
def _validate_fps(self, stream: rs.video_stream_profile) -> None:
"""Validates and sets the internal FPS based on actual stream FPS."""
actual_fps = stream.fps()
# Use math.isclose for robust float comparison
if not math.isclose(self.fps, actual_fps, rel_tol=1e-3):
raise RuntimeError(
f"Failed to set requested FPS {self.fps} for {self}. Actual value reported: {actual_fps}."
)
logger.debug(f"FPS set to {actual_fps} for {self}.")
def _validate_width_and_height(self, stream) -> None: def _validate_width_and_height(self, stream) -> None:
"""Validates and sets the internal capture width and height based on actual stream width.""" """Validates and sets the internal capture width and height based on actual stream width."""
actual_width = int(round(stream.width())) actual_width = int(round(stream.width()))

View File

@@ -62,7 +62,7 @@ def get_cv2_backend() -> int:
import cv2 import cv2
available_cv2_backends = { available_cv2_backends = {
"Linux": cv2.CAP_DSHOW, "Linux": cv2.CAP_V4L2,
"Windows": cv2.CAP_AVFOUNDATION, "Windows": cv2.CAP_AVFOUNDATION,
"Darwin": cv2.CAP_ANY, "Darwin": cv2.CAP_ANY,
} }

View File

@@ -145,7 +145,9 @@ def test_async_read_timeout():
try: try:
with pytest.raises(TimeoutError): with pytest.raises(TimeoutError):
camera.async_read(timeout_ms=0) camera.async_read(
timeout_ms=0
) # NOTE(Steven): This is flaky as sdometimes we actually get a frame
finally: finally:
if camera.is_connected: if camera.is_connected:
camera.disconnect() camera.disconnect()

View File

@@ -110,7 +110,7 @@ def test_read_depth():
camera = RealSenseCamera(config) camera = RealSenseCamera(config)
camera.connect(warmup=False) camera.connect(warmup=False)
img = camera.read_depth(timeout_ms=500) # NOTE(Steven): Reading depth takes longer img = camera.read_depth(timeout_ms=1000) # NOTE(Steven): Reading depth takes longer
assert isinstance(img, np.ndarray) assert isinstance(img, np.ndarray)