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 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,

View file

@ -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())

View file

@ -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:

View file

@ -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