Further unification of camera array functionality

This commit is contained in:
Julian Stirling 2025-08-22 11:00:19 +01:00
parent 5484b51a1e
commit 7cd6b11411
4 changed files with 40 additions and 37 deletions

View file

@ -8,7 +8,6 @@ See repository root for licensing information.
from __future__ import annotations from __future__ import annotations
from typing import Literal, Optional, Tuple, Any from typing import Literal, Optional, Tuple, Any
from types import EllipsisType
import json import json
import io import io
import time import time
@ -263,7 +262,7 @@ class BaseCamera(lt.Thing):
metadata_getter: lt.deps.GetThingStates, metadata_getter: lt.deps.GetThingStates,
logger: lt.deps.InvocationLogger, logger: lt.deps.InvocationLogger,
stream_name: str = "main", stream_name: str = "main",
wait: Optional[float | EllipsisType] = ..., wait: Optional[float] = None,
) -> JPEGBlob: ) -> JPEGBlob:
"""Acquire one image from the camera as a JPEG. """Acquire one image from the camera as a JPEG.
@ -274,20 +273,15 @@ class BaseCamera(lt.Thing):
injected. injected.
:param logger: LabThings InvocationLogger dependency, automatically injected. :param logger: LabThings InvocationLogger dependency, automatically injected.
:param stream_name: A stream name supported by this camera. :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. use the default for the underlying camera.
""" """
fname = datetime.now().strftime("%Y-%m-%d-%H%M%S.jpeg") fname = datetime.now().strftime("%Y-%m-%d-%H%M%S.jpeg")
directory = tempfile.TemporaryDirectory() directory = tempfile.TemporaryDirectory()
jpeg_path = os.path.join(directory.name, fname) jpeg_path = os.path.join(directory.name, fname)
# Using Ellipsis to specify no input specified. As `None` set for wait may img = self.capture_image(stream_name, wait)
# 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)
self._save_capture( self._save_capture(
jpeg_path=jpeg_path, jpeg_path=jpeg_path,
image=img, image=img,

View file

@ -83,7 +83,8 @@ class OpenCVCamera(BaseCamera):
@lt.thing_action @lt.thing_action
def capture_array( def capture_array(
self, self,
resolution: Literal["main", "full"] = "full", stream_name: Literal["main", "full"] = "full",
wait: Optional[float] = None,
) -> NDArray: ) -> NDArray:
"""Acquire one image from the camera and return as an array. """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 It's likely to be highly inefficient - raw and/or uncompressed captures using
binary image formats will be added in due course. 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() ret, frame = self.cap.read()
if not ret: if not ret:
raise RuntimeError( raise RuntimeError(
@ -108,7 +111,7 @@ class OpenCVCamera(BaseCamera):
This function will produce a JPEG image. This function will produce a JPEG image.
""" """
logging.warning( if wait is not None:
f"Simulation camera doesn't respect {stream_name=} or {wait=} arguments." 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()) return Image.fromarray(self.capture_array())

View file

@ -541,12 +541,15 @@ class StreamingPiCamera2(BaseCamera):
:param stream_name: (Optional) The PiCamera2 stream to use, should be one of :param stream_name: (Optional) The PiCamera2 stream to use, should be one of
["main", "lores", "raw", "full"]. Default = "main" ["main", "lores", "raw", "full"]. Default = "main"
:param wait: (Optional, float) Set a timeout in seconds. :param wait: (Optional, float) Set a timeout in seconds. Default = 0.9s,
:rasises TimeoutError: if this time is exceeded during capture. Default = 0.9s,
lower than the 1s timeout for the camera. This ensures that our code times 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"]: if stream_name in ["main", "lores", "raw"]:
with self._streaming_picamera() as cam: with self._streaming_picamera() as cam:
return cam.capture_image(stream_name, wait=wait) 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 It's likely to be highly inefficient - raw and/or uncompressed captures using
binary image formats will be added in due course. binary image formats will be added in due course.
stream_name: (Optional) The PiCamera2 stream to use, should be one of ["main", :param stream_name: (Optional) The PiCamera2 stream to use, should be one of
"lores", "raw", "full"]. Default = "main" ["main", "lores", "raw", "full"]. Default = "main"
wait: (Optional, float) Set a timeout in seconds. :param wait: (Optional, float) Set a timeout in seconds. Default = 0.9s,
A TimeoutError is raised if this time is exceeded during capture. lower than the 1s timeout for the camera. This ensures that our code times
Default = 0.9s, lower than the 1s timeout default in picamera yaml settings 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 # Note that internally the PiCamera creates a PIL image and then converts to
# an image as an array is still a useful feature # numpy with ``np.array(Image.open(io.BytesIO(self.make_buffer(name))))``.
if stream_name == "full": # As such we use capture_image to get an Image from the picamera and return
with self._streaming_picamera(pause_stream=True) as picam2: # as array
capture_config = picam2.create_still_configuration() return np.array(self.capture_image(stream_name, wait))
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)
@lt.thing_property @lt.thing_property
def camera_configuration(self) -> Mapping: def camera_configuration(self) -> Mapping:

View file

@ -277,7 +277,8 @@ class SimulatedCamera(BaseCamera):
@lt.thing_action @lt.thing_action
def capture_array( def capture_array(
self, self,
resolution: Literal["main", "full"] = "full", stream_name: Literal["main", "full"] = "full",
wait: Optional[float] = None,
) -> ArrayModel: ) -> ArrayModel:
"""Acquire one image from the camera and return as an array. """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 It's likely to be highly inefficient - raw and/or uncompressed captures using
binary image formats will be added in due course. 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() return self.generate_frame()
def capture_image( def capture_image(
@ -297,9 +300,9 @@ class SimulatedCamera(BaseCamera):
It is used for capture to memory. It is used for capture to memory.
""" """
logging.warning( if wait is not None:
f"Simulation camera doesn't respect {stream_name=} or {wait=} arguments." 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()) return Image.fromarray(self.generate_frame())
@lt.thing_action @lt.thing_action