diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index 6a83737b..efe9003d 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -8,7 +8,6 @@ See repository root for licensing information. from __future__ import annotations from typing import Literal, Optional, Tuple, Any -from types import EllipsisType import json import io import time @@ -263,7 +262,7 @@ class BaseCamera(lt.Thing): metadata_getter: lt.deps.GetThingStates, logger: lt.deps.InvocationLogger, stream_name: str = "main", - wait: Optional[float | EllipsisType] = ..., + wait: Optional[float] = None, ) -> JPEGBlob: """Acquire one image from the camera as a JPEG. @@ -274,20 +273,15 @@ class BaseCamera(lt.Thing): injected. :param logger: LabThings InvocationLogger dependency, automatically injected. :param stream_name: A stream name supported by this camera. - :param wait: (Optional, float) Set a timeout in seconds. If not set it will + :param wait: (Optional, float) Set a timeout in seconds. If None it will use the default for the underlying camera. """ fname = datetime.now().strftime("%Y-%m-%d-%H%M%S.jpeg") directory = tempfile.TemporaryDirectory() jpeg_path = os.path.join(directory.name, fname) - # Using Ellipsis to specify no input specified. As `None` set for wait may - # have a meaning as it does for the Picamera. If wait is Ellipsis then - # do not specify. - if wait is Ellipsis: - img = self.capture_image(stream_name) - else: - img = self.capture_image(stream_name, wait) + img = self.capture_image(stream_name, wait) + self._save_capture( jpeg_path=jpeg_path, image=img, diff --git a/src/openflexure_microscope_server/things/camera/opencv.py b/src/openflexure_microscope_server/things/camera/opencv.py index b5352983..7c42d478 100644 --- a/src/openflexure_microscope_server/things/camera/opencv.py +++ b/src/openflexure_microscope_server/things/camera/opencv.py @@ -83,7 +83,8 @@ class OpenCVCamera(BaseCamera): @lt.thing_action def capture_array( self, - resolution: Literal["main", "full"] = "full", + stream_name: Literal["main", "full"] = "full", + wait: Optional[float] = None, ) -> NDArray: """Acquire one image from the camera and return as an array. @@ -91,7 +92,9 @@ class OpenCVCamera(BaseCamera): It's likely to be highly inefficient - raw and/or uncompressed captures using binary image formats will be added in due course. """ - logging.warning(f"OpenCV camera doesn't respect {resolution} setting") + if wait is not None: + logging.warning("OpenCV camera has no wait option. Use None.") + logging.warning(f"OpenCV camera doesn't respect {stream_name=}") ret, frame = self.cap.read() if not ret: raise RuntimeError( @@ -108,7 +111,7 @@ class OpenCVCamera(BaseCamera): This function will produce a JPEG image. """ - logging.warning( - f"Simulation camera doesn't respect {stream_name=} or {wait=} arguments." - ) + if wait is not None: + logging.warning("OpenCV camera has no wait option. Use None.") + logging.warning(f"OpenCV camera doesn't respect {stream_name=}") return Image.fromarray(self.capture_array()) diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index 18ef52c9..60ecd582 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -541,12 +541,15 @@ class StreamingPiCamera2(BaseCamera): :param stream_name: (Optional) The PiCamera2 stream to use, should be one of ["main", "lores", "raw", "full"]. Default = "main" - :param wait: (Optional, float) Set a timeout in seconds. - - :rasises TimeoutError: if this time is exceeded during capture. Default = 0.9s, + :param wait: (Optional, float) Set a timeout in seconds. Default = 0.9s, lower than the 1s timeout for the camera. This ensures that our code times - out and returns before the camera times out. + out and returns before the camera times out. If None is set the default + value of 0.9 will be used to prevent the possibility of the camera locking. + + :raises TimeoutError: if this time is exceeded during capture. """ + if wait is None: + wait = 0.9 if stream_name in ["main", "lores", "raw"]: with self._streaming_picamera() as cam: return cam.capture_image(stream_name, wait=wait) @@ -572,20 +575,20 @@ class StreamingPiCamera2(BaseCamera): It's likely to be highly inefficient - raw and/or uncompressed captures using binary image formats will be added in due course. - stream_name: (Optional) The PiCamera2 stream to use, should be one of ["main", - "lores", "raw", "full"]. Default = "main" - wait: (Optional, float) Set a timeout in seconds. - A TimeoutError is raised if this time is exceeded during capture. - Default = 0.9s, lower than the 1s timeout default in picamera yaml settings + :param stream_name: (Optional) The PiCamera2 stream to use, should be one of + ["main", "lores", "raw", "full"]. Default = "main" + :param wait: (Optional, float) Set a timeout in seconds. Default = 0.9s, + lower than the 1s timeout for the camera. This ensures that our code times + out and returns before the camera times out. If None is set the default + value of 0.9 will be used to prevent the possibility of the camera locking. + + :raises TimeoutError: if this time is exceeded during capture. """ - # This was slower than capture_image for our use case, but directly returning - # an image as an array is still a useful feature - if stream_name == "full": - with self._streaming_picamera(pause_stream=True) as picam2: - capture_config = picam2.create_still_configuration() - return picam2.switch_mode_and_capture_array(capture_config, wait=wait) - with self._streaming_picamera() as cam: - return cam.capture_array(stream_name, wait=wait) + # Note that internally the PiCamera creates a PIL image and then converts to + # numpy with ``np.array(Image.open(io.BytesIO(self.make_buffer(name))))``. + # As such we use capture_image to get an Image from the picamera and return + # as array + return np.array(self.capture_image(stream_name, wait)) @lt.thing_property def camera_configuration(self) -> Mapping: diff --git a/src/openflexure_microscope_server/things/camera/simulation.py b/src/openflexure_microscope_server/things/camera/simulation.py index 177eacc5..7c7a856b 100644 --- a/src/openflexure_microscope_server/things/camera/simulation.py +++ b/src/openflexure_microscope_server/things/camera/simulation.py @@ -277,7 +277,8 @@ class SimulatedCamera(BaseCamera): @lt.thing_action def capture_array( self, - resolution: Literal["main", "full"] = "full", + stream_name: Literal["main", "full"] = "full", + wait: Optional[float] = None, ) -> ArrayModel: """Acquire one image from the camera and return as an array. @@ -285,7 +286,9 @@ class SimulatedCamera(BaseCamera): It's likely to be highly inefficient - raw and/or uncompressed captures using binary image formats will be added in due course. """ - logging.warning(f"Simulation camera doesn't respect {resolution=} setting") + if wait is not None: + logging.warning("Simulation camera has no wait option. Use None.") + logging.warning(f"Simulation camera camera doesn't respect {stream_name=}") return self.generate_frame() def capture_image( @@ -297,9 +300,9 @@ class SimulatedCamera(BaseCamera): It is used for capture to memory. """ - logging.warning( - f"Simulation camera doesn't respect {stream_name=} or {wait=} arguments." - ) + if wait is not None: + logging.warning("Simulation camera has no wait option. Use None.") + logging.warning(f"Simulation camera camera doesn't respect {stream_name=}") return Image.fromarray(self.generate_frame()) @lt.thing_action