diff --git a/tests/cameras/mock_pyrealsense2.py b/tests/cameras/mock_pyrealsense2.py deleted file mode 100644 index c477eb062..000000000 --- a/tests/cameras/mock_pyrealsense2.py +++ /dev/null @@ -1,148 +0,0 @@ -# Copyright 2024 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import enum - -import numpy as np - - -class stream(enum.Enum): # noqa: N801 - color = 0 - depth = 1 - - -class format(enum.Enum): # noqa: N801 - rgb8 = 0 - z16 = 1 - - -class config: # noqa: N801 - def enable_device(self, device_id: str): - self.device_enabled = device_id - - def enable_stream(self, stream_type: stream, width=None, height=None, color_format=None, fps=None): - self.stream_type = stream_type - # Overwrite default values when possible - self.width = 848 if width is None else width - self.height = 480 if height is None else height - self.color_format = format.rgb8 if color_format is None else color_format - self.fps = 30 if fps is None else fps - - -class RSColorProfile: - def __init__(self, config): - self.config = config - - def fps(self): - return self.config.fps - - def width(self): - return self.config.width - - def height(self): - return self.config.height - - -class RSColorStream: - def __init__(self, config): - self.config = config - - def as_video_stream_profile(self): - return RSColorProfile(self.config) - - -class RSProfile: - def __init__(self, config): - self.config = config - - def get_stream(self, color_format): - del color_format # unused - return RSColorStream(self.config) - - -class pipeline: # noqa: N801 - def __init__(self): - self.started = False - self.config = None - - def start(self, config): - self.started = True - self.config = config - return RSProfile(self.config) - - def stop(self): - if not self.started: - raise RuntimeError("You need to start the camera before stop.") - self.started = False - self.config = None - - def wait_for_frames(self, timeout_ms=50000): - del timeout_ms # unused - return RSFrames(self.config) - - -class RSFrames: - def __init__(self, config): - self.config = config - - def get_color_frame(self): - return RSColorFrame(self.config) - - def get_depth_frame(self): - return RSDepthFrame(self.config) - - -class RSColorFrame: - def __init__(self, config): - self.config = config - - def get_data(self): - data = np.ones((self.config.height, self.config.width, 3), dtype=np.uint8) - # Create a difference between rgb and bgr - data[:, :, 0] = 2 - return data - - -class RSDepthFrame: - def __init__(self, config): - self.config = config - - def get_data(self): - return np.ones((self.config.height, self.config.width), dtype=np.uint16) - - -class RSDevice: - def __init__(self): - pass - - def get_info(self, camera_info) -> str: - del camera_info # unused - # return fake serial number - return "123456789" - - -class context: # noqa: N801 - def __init__(self): - pass - - def query_devices(self): - return [RSDevice()] - - -class camera_info: # noqa: N801 - # fake name - name = "Intel RealSense D435I" - - def __init__(self, serial_number): - del serial_number - pass diff --git a/tests/cameras/test_cameras.py b/tests/cameras/test_cameras.py deleted file mode 100644 index 6dbc716c9..000000000 --- a/tests/cameras/test_cameras.py +++ /dev/null @@ -1,252 +0,0 @@ -# Copyright 2024 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Tests for physical cameras and their mocked versions. -If the physical camera is not connected to the computer, or not working, -the test will be skipped. - -Example of running a specific test: -```bash -pytest -sx tests/test_cameras.py::test_camera -``` - -Example of running test on a real camera connected to the computer: -```bash -pytest -sx 'tests/test_cameras.py::test_camera[opencv-False]' -pytest -sx 'tests/test_cameras.py::test_camera[intelrealsense-False]' -``` - -Example of running test on a mocked version of the camera: -```bash -pytest -sx 'tests/test_cameras.py::test_camera[opencv-True]' -pytest -sx 'tests/test_cameras.py::test_camera[intelrealsense-True]' -``` -""" - -import numpy as np -import pytest - -from lerobot.common.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError -from tests.utils import TEST_CAMERA_TYPES, make_camera, require_camera - -# Maximum absolute difference between two consecutive images recorded by a camera. -# This value differs with respect to the camera. -MAX_PIXEL_DIFFERENCE = 25 - - -def compute_max_pixel_difference(first_image, second_image): - return np.abs(first_image.astype(float) - second_image.astype(float)).max() - - -@pytest.mark.parametrize("camera_type, mock", TEST_CAMERA_TYPES) -@require_camera -def test_camera(request, camera_type, mock): - """Test assumes that `camera.read()` returns the same image when called multiple times in a row. - So the environment should not change (you shouldnt be in front of the camera) and the camera should not be moving. - - Warning: The tests worked for a macbookpro camera, but I am getting assertion error (`np.allclose(color_image, async_color_image)`) - for my iphone camera and my LG monitor camera. - """ - # TODO(rcadene): measure fps in nightly? - # TODO(rcadene): test logs - - if camera_type == "opencv" and not mock: - pytest.skip("TODO(rcadene): fix test for opencv physical camera") - - camera_kwargs = {"camera_type": camera_type, "mock": mock} - - # Test instantiating - camera = make_camera(**camera_kwargs) - - # Test reading, async reading, disconnecting before connecting raises an error - with pytest.raises(DeviceNotConnectedError): - camera.read() - with pytest.raises(DeviceNotConnectedError): - camera.async_read() - with pytest.raises(DeviceNotConnectedError): - camera.disconnect() - - # Test deleting the object without connecting first - del camera - - # Test connecting - camera = make_camera(**camera_kwargs) - camera.connect() - assert camera.is_connected - assert camera.fps is not None - assert camera.capture_width is not None - assert camera.capture_height is not None - - # Test connecting twice raises an error - with pytest.raises(DeviceAlreadyConnectedError): - camera.connect() - - # Test reading from the camera - color_image = camera.read() - assert isinstance(color_image, np.ndarray) - assert color_image.ndim == 3 - h, w, c = color_image.shape - assert c == 3 - assert w > h - - # Test read and async_read outputs similar images - # ...warming up as the first frames can be black - for _ in range(30): - camera.read() - color_image = camera.read() - async_color_image = camera.async_read() - error_msg = ( - "max_pixel_difference between read() and async_read()", - compute_max_pixel_difference(color_image, async_color_image), - ) - # TODO(rcadene): properly set `rtol` - np.testing.assert_allclose( - color_image, async_color_image, rtol=1e-5, atol=MAX_PIXEL_DIFFERENCE, err_msg=error_msg - ) - - # Test disconnecting - camera.disconnect() - assert camera.camera is None - assert camera.thread is None - - # Test disconnecting with `__del__` - camera = make_camera(**camera_kwargs) - camera.connect() - del camera - - # Test acquiring a bgr image - camera = make_camera(**camera_kwargs, color_mode="bgr") - camera.connect() - assert camera.color_mode == "bgr" - bgr_color_image = camera.read() - np.testing.assert_allclose( - color_image, bgr_color_image[:, :, [2, 1, 0]], rtol=1e-5, atol=MAX_PIXEL_DIFFERENCE, err_msg=error_msg - ) - del camera - - # Test acquiring a rotated image - camera = make_camera(**camera_kwargs) - camera.connect() - ori_color_image = camera.read() - del camera - - for rotation in [None, 90, 180, -90]: - camera = make_camera(**camera_kwargs, rotation=rotation) - camera.connect() - - if mock: - import tests.cameras.mock_cv2 as cv2 - else: - import cv2 - - if rotation is None: - manual_rot_img = ori_color_image - assert camera.rotation is None - elif rotation == 90: - manual_rot_img = np.rot90(color_image, k=1) - assert camera.rotation == cv2.ROTATE_90_CLOCKWISE - elif rotation == 180: - manual_rot_img = np.rot90(color_image, k=2) - assert camera.rotation == cv2.ROTATE_180 - elif rotation == -90: - manual_rot_img = np.rot90(color_image, k=3) - assert camera.rotation == cv2.ROTATE_90_COUNTERCLOCKWISE - - rot_color_image = camera.read() - - np.testing.assert_allclose( - rot_color_image, manual_rot_img, rtol=1e-5, atol=MAX_PIXEL_DIFFERENCE, err_msg=error_msg - ) - del camera - - # TODO(rcadene): Add a test for a camera that doesnt support fps=60 and raises an OSError - # TODO(rcadene): Add a test for a camera that supports fps=60 - - # Test width and height can be set - camera = make_camera(**camera_kwargs, fps=30, width=1280, height=720) - camera.connect() - assert camera.fps == 30 - assert camera.width == 1280 - assert camera.height == 720 - color_image = camera.read() - h, w, c = color_image.shape - assert h == 720 - assert w == 1280 - assert c == 3 - del camera - - # Test not supported width and height raise an error - camera = make_camera(**camera_kwargs, fps=30, width=0, height=0) - with pytest.raises(OSError): - camera.connect() - del camera - - -@pytest.mark.parametrize("camera_type, mock", TEST_CAMERA_TYPES) -@require_camera -def test_save_images_from_cameras(tmp_path, request, camera_type, mock): - # TODO(rcadene): refactor - if camera_type == "opencv": - from lerobot.common.cameras.opencv.camera_opencv import save_images_from_cameras - elif camera_type == "intelrealsense": - from lerobot.common.cameras.intel.camera_realsense import save_images_from_cameras - - # Small `record_time_s` to speedup unit tests - save_images_from_cameras(tmp_path, record_time_s=0.02, mock=mock) - - -@pytest.mark.parametrize("camera_type, mock", TEST_CAMERA_TYPES) -@require_camera -def test_camera_rotation(request, camera_type, mock): - config_kwargs = {"camera_type": camera_type, "mock": mock, "width": 640, "height": 480, "fps": 30} - - # No rotation. - camera = make_camera(**config_kwargs, rotation=None) - camera.connect() - assert camera.capture_width == 640 - assert camera.capture_height == 480 - assert camera.width == 640 - assert camera.height == 480 - no_rot_img = camera.read() - h, w, c = no_rot_img.shape - assert h == 480 and w == 640 and c == 3 - camera.disconnect() - - # Rotation = 90 (clockwise). - camera = make_camera(**config_kwargs, rotation=90) - camera.connect() - # With a 90° rotation, we expect the metadata dimensions to be swapped. - assert camera.capture_width == 640 - assert camera.capture_height == 480 - assert camera.width == 480 - assert camera.height == 640 - import cv2 - - assert camera.rotation == cv2.ROTATE_90_CLOCKWISE - rot_img = camera.read() - h, w, c = rot_img.shape - assert h == 640 and w == 480 and c == 3 - camera.disconnect() - - # Rotation = 180. - camera = make_camera(**config_kwargs, rotation=None) - camera.connect() - assert camera.capture_width == 640 - assert camera.capture_height == 480 - assert camera.width == 640 - assert camera.height == 480 - no_rot_img = camera.read() - h, w, c = no_rot_img.shape - assert h == 480 and w == 640 and c == 3 - camera.disconnect() diff --git a/tests/cameras/test_opencv.py b/tests/cameras/test_opencv.py index 6d94c322c..b137e5c59 100644 --- a/tests/cameras/test_opencv.py +++ b/tests/cameras/test_opencv.py @@ -14,6 +14,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Example of running a specific test: +# ```bash +# pytest tests/cameras/test_opencv.py::test_connect +# ``` + import numpy as np import pytest diff --git a/tests/cameras/test_realsense.py b/tests/cameras/test_realsense.py index 757d4c33c..a4e2afa1d 100644 --- a/tests/cameras/test_realsense.py +++ b/tests/cameras/test_realsense.py @@ -14,6 +14,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Example of running a specific test: +# ```bash +# pytest tests/cameras/test_opencv.py::test_connect +# ``` + import os from unittest.mock import patch