"""A wrapper for the Webots camera device.""" from __future__ import annotations from abc import ABC, abstractmethod from functools import lru_cache from math import tan from sbot_interface.devices.util import WebotsDevice, get_globals, get_robot_device g = get_globals() class BaseCamera(ABC): """Base class for camera devices.""" @abstractmethod def get_image(self) -> bytes: """Get a frame from the camera, encoded as a byte string.""" pass @abstractmethod def get_resolution(self) -> tuple[int, int]: """Get the resolution of the camera in pixels, width x height.""" pass @abstractmethod def get_calibration(self) -> tuple[float, float, float, float]: """Return the intrinsic camera calibration parameters fx, fy, cx, cy.""" pass class NullCamera(BaseCamera): """ Null camera device. Allows the robot to run without a camera device attached. """ def get_image(self) -> bytes: """Get a frame from the camera, encoded as a byte string.""" return b'' def get_resolution(self) -> tuple[int, int]: """Get the resolution of the camera in pixels, width x height.""" return 0, 0 def get_calibration(self) -> tuple[float, float, float, float]: """Return the intrinsic camera calibration parameters fx, fy, cx, cy.""" return 0, 0, 0, 0 # Camera class Camera(BaseCamera): """ A wrapper for the Webots camera device. The camera will sleep for 1 frame time before capturing an image to ensure the image is up to date. :param device_name: The name of the camera device. :param frame_rate: The frame rate of the camera in frames per second. """ def __init__(self, device_name: str, frame_rate: int) -> None: self._device = get_robot_device(g.robot, device_name, WebotsDevice.Camera) # round down to the nearest timestep self.sample_time = int(((1000 / frame_rate) // g.timestep) * g.timestep) def get_image(self) -> bytes: """ Get a frame from the camera, encoded as a byte string. Sleeps for 1 frame time before capturing the image to ensure the image is up to date. NOTE The image data buffer is automatically freed at the end of the timestep, so must not be accessed after any sleep. :return: The image data as a byte string. """ # A frame is only captured every sample_time milliseconds the camera is enabled # so we need to wait for a frame to be captured after enabling the camera. # The image data buffer is automatically freed at the end of the timestep. self._device.enable(self.sample_time) g.sleep(self.sample_time / 1000) image_data_raw = self._device.getImage() # Disable the camera to save computation self._device.disable() # type: ignore[no-untyped-call] return image_data_raw @lru_cache def get_resolution(self) -> tuple[int, int]: """Get the resolution of the camera in pixels, width x height.""" return self._device.getWidth(), self._device.getHeight() @lru_cache def get_calibration(self) -> tuple[float, float, float, float]: """Return the intrinsic camera calibration parameters fx, fy, cx, cy.""" return ( (self._device.getWidth() / 2) / tan(self._device.getFov() / 2), # fx (self._device.getWidth() / 2) / tan(self._device.getFov() / 2), # fy self._device.getWidth() // 2, # cx self._device.getHeight() // 2, # cy )