Refactor camera streaming into modes
This commit is contained in:
parent
2c54e38c28
commit
aff860a086
6 changed files with 225 additions and 179 deletions
|
|
@ -13,6 +13,7 @@ import json
|
|||
import os
|
||||
import tempfile
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from types import TracebackType
|
||||
from typing import Annotated, Any, Literal, Mapping, Optional, Self, Tuple
|
||||
|
|
@ -165,7 +166,16 @@ class CameraMemoryBuffer:
|
|||
del self._storage[key]
|
||||
|
||||
|
||||
class BaseCamera(OFMThing):
|
||||
class StreamingMode(BaseModel):
|
||||
"""Description of streaming modes for the camera.
|
||||
|
||||
Cameras can sub class this to record camera specific information about the mode.
|
||||
"""
|
||||
|
||||
description: str
|
||||
|
||||
|
||||
class BaseCamera(OFMThing, ABC):
|
||||
"""The base class for all cameras. All cameras must directly inherit from this class.
|
||||
|
||||
The connection to the camera hardware should be added to the ``__enter__`` method not
|
||||
|
|
@ -196,6 +206,12 @@ class BaseCamera(OFMThing):
|
|||
self._default_background_detector = "bg_channel_deviations_luv"
|
||||
self._background_detector_name: Optional[str] = None
|
||||
|
||||
if "default" and "full_resolution" not in self.streaming_modes:
|
||||
raise KeyError(
|
||||
f"Camera {type(self).__name__} doesn't define both a 'default' and a "
|
||||
"'full_resolution' streaming mode."
|
||||
)
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
"""Open hardware connection when the Thing context manager is opened."""
|
||||
super().__enter__()
|
||||
|
|
@ -236,21 +252,51 @@ class BaseCamera(OFMThing):
|
|||
"""
|
||||
return False
|
||||
|
||||
@lt.property
|
||||
def streaming_modes(self) -> Mapping[str, StreamingMode]:
|
||||
"""Modes the camera can stream in."""
|
||||
return {
|
||||
"default": StreamingMode(
|
||||
description=(
|
||||
"The standard mode that balances capture resolution and stream "
|
||||
"size."
|
||||
)
|
||||
),
|
||||
"full_resolution": StreamingMode(
|
||||
description=(
|
||||
"Streaming the camera in full resolution. For this camera, this is "
|
||||
"identical to the default mode."
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
streaming_mode: str = lt.property(default="default", readonly=True)
|
||||
|
||||
@lt.action
|
||||
def start_streaming(
|
||||
self, main_resolution: tuple[int, int] = (800, 800), buffer_count: int = 1
|
||||
) -> None:
|
||||
"""Start (or stop and restart) the camera.
|
||||
def change_streaming_mode(self, mode: str = "default") -> None:
|
||||
"""Change the mode the camera is streaming in.
|
||||
|
||||
:param main_resolution: the resolution to use for the main stream.
|
||||
:param buffer_count: number of images in the stream buffer.
|
||||
:param mode: Must be a key from ``streaming_modes``
|
||||
|
||||
Note that the default values for both parameters should be set appropriately
|
||||
for the specific camera when defining a new Camera Thing.
|
||||
When creating a subclass. Subclass the method ``_start_streaming`` rather than
|
||||
this method.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"CameraThings must define their own start_streaming method"
|
||||
)
|
||||
if not self.stream_active:
|
||||
raise RuntimeError(
|
||||
"Cannot change streaming mode before streaming is started."
|
||||
)
|
||||
if mode not in self.streaming_modes:
|
||||
self.logger.warning(
|
||||
f"Streaming mode {mode}, is not known. Expecting a mode from "
|
||||
f"{self.streaming_modes.keys()}. Streaming in default mode."
|
||||
)
|
||||
mode = "default"
|
||||
if mode != self.streaming_mode:
|
||||
self._start_streaming(mode=mode)
|
||||
|
||||
@abstractmethod
|
||||
def _start_streaming(self, mode: str = "default") -> None:
|
||||
"""Start (or stop and restart) the camera."""
|
||||
|
||||
def kill_mjpeg_streams(self) -> None:
|
||||
"""Kill the streams now as the server is shutting down.
|
||||
|
|
@ -373,20 +419,17 @@ class BaseCamera(OFMThing):
|
|||
|
||||
return datafile_path
|
||||
|
||||
@abstractmethod
|
||||
@lt.property
|
||||
def stream_active(self) -> bool:
|
||||
"""Whether the MJPEG stream is active."""
|
||||
raise NotImplementedError(
|
||||
"CameraThings must define their own stream_active method"
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
@lt.action
|
||||
def discard_frames(self) -> None:
|
||||
"""Discard frames so that the next frame captured is fresh."""
|
||||
raise NotImplementedError(
|
||||
"CameraThings must define their own discard_frames method"
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
@lt.action
|
||||
def capture_array(
|
||||
self,
|
||||
|
|
@ -394,9 +437,6 @@ class BaseCamera(OFMThing):
|
|||
wait: Optional[float] = 5,
|
||||
) -> NDArray:
|
||||
"""Acquire one image from the camera and return as an array."""
|
||||
raise NotImplementedError(
|
||||
"CameraThings must define their own capture_array method"
|
||||
)
|
||||
|
||||
downsampled_array_factor: int = lt.property(default=2, ge=1)
|
||||
"""The downsampling factor when calling capture_downsampled_array."""
|
||||
|
|
@ -535,15 +575,13 @@ class BaseCamera(OFMThing):
|
|||
)
|
||||
return self._thing_server_interface.call_async_task(stream.next_frame_size)
|
||||
|
||||
@abstractmethod
|
||||
def capture_image(
|
||||
self,
|
||||
stream_name: Literal["main", "lores", "full"],
|
||||
wait: Optional[float] = None,
|
||||
) -> Image.Image:
|
||||
"""Capture a PIL image from stream stream_name with timeout wait."""
|
||||
raise NotImplementedError(
|
||||
"CameraThings must define their own capture_image method"
|
||||
)
|
||||
|
||||
@lt.action
|
||||
def capture_and_save(
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class OpenCVCamera(BaseCamera):
|
|||
self.logger.warning(f"{self.camera_name} not found.")
|
||||
self._camera_name = next(iter(self.cameras))
|
||||
|
||||
self._start_stream()
|
||||
self._start_streaming()
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
|
|
@ -66,7 +66,7 @@ class OpenCVCamera(BaseCamera):
|
|||
|
||||
Before releasing the camera the capture thread is closed.
|
||||
"""
|
||||
self._stop_stream()
|
||||
self._stop_streaming()
|
||||
super().__exit__(exc_type, exc_value, traceback)
|
||||
|
||||
_camera_name = ""
|
||||
|
|
@ -89,12 +89,15 @@ class OpenCVCamera(BaseCamera):
|
|||
self._camera_name = value
|
||||
if value not in self.cameras:
|
||||
raise ValueError(f"{value} is not a valid camera name.")
|
||||
self._start_stream()
|
||||
self._start_streaming()
|
||||
|
||||
def _start_stream(self) -> None:
|
||||
def _start_streaming(self, mode: str = "default") -> None:
|
||||
"""Start the camera stream or restart if running."""
|
||||
if mode not in self.streaming_modes:
|
||||
raise ValueError(f"Unknown mode {mode}")
|
||||
self.streaming_mode = mode
|
||||
if self.stream_active:
|
||||
self._stop_stream()
|
||||
self._stop_streaming()
|
||||
self.cap = cv2.VideoCapture(
|
||||
self.cameras[self.camera_name], opencv_utils.BACKEND
|
||||
)
|
||||
|
|
@ -102,7 +105,7 @@ class OpenCVCamera(BaseCamera):
|
|||
self._capture_thread = Thread(target=self._capture_frames)
|
||||
self._capture_thread.start()
|
||||
|
||||
def _stop_stream(self) -> None:
|
||||
def _stop_streaming(self) -> None:
|
||||
"""Stop the camera stream."""
|
||||
if self.stream_active:
|
||||
self._capture_enabled = False
|
||||
|
|
|
|||
|
|
@ -22,12 +22,12 @@ import logging
|
|||
import os
|
||||
import tempfile
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import contextmanager
|
||||
from threading import RLock
|
||||
from types import TracebackType
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Annotated,
|
||||
Any,
|
||||
Iterator,
|
||||
Literal,
|
||||
|
|
@ -41,7 +41,6 @@ from picamera2 import Picamera2
|
|||
from picamera2.encoders import MJPEGEncoder
|
||||
from picamera2.outputs import Output
|
||||
from PIL import Image
|
||||
from pydantic import BaseModel, BeforeValidator
|
||||
|
||||
import labthings_fastapi as lt
|
||||
from labthings_fastapi.exceptions import ServerNotRunningError
|
||||
|
|
@ -55,7 +54,7 @@ from openflexure_microscope_server.ui import (
|
|||
property_control_for,
|
||||
)
|
||||
|
||||
from . import BaseCamera
|
||||
from . import BaseCamera, StreamingMode
|
||||
from . import picamera_recalibrate_utils as recalibrate_utils
|
||||
from . import picamera_tuning_file_utils as tf_utils
|
||||
|
||||
|
|
@ -64,11 +63,6 @@ if TYPE_CHECKING:
|
|||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORTED_CAMS_SENSOR_INFO = {
|
||||
"picamera_v2": recalibrate_utils.IMX219_SENSOR_INFO,
|
||||
"picamera_hq": recalibrate_utils.IMX477_SENSOR_INFO,
|
||||
}
|
||||
|
||||
|
||||
class PicameraModelError(RuntimeError):
|
||||
"""There is a problem Picamera sensor model set by the configuration."""
|
||||
|
|
@ -101,38 +95,28 @@ class PicameraStreamOutput(Output):
|
|||
self.stream.add_frame(frame)
|
||||
|
||||
|
||||
class SensorMode(BaseModel):
|
||||
"""A Pydantic model holding all the information about a specific sensor mode.
|
||||
class PiCamera2StreamingMode(StreamingMode):
|
||||
"""Streaming mode configuration for the PiCamera2."""
|
||||
|
||||
This data is as reported by the PiCamera2 module.
|
||||
"""
|
||||
|
||||
unpacked: str
|
||||
main_resolution: tuple[int, int]
|
||||
lores_resolution: tuple[int, int]
|
||||
sensor_mode_resolution: tuple[int, int]
|
||||
bit_depth: int
|
||||
size: tuple[int, int]
|
||||
fps: float
|
||||
crop_limits: tuple[int, int, int, int]
|
||||
exposure_limits: tuple[Optional[int], Optional[int], Optional[int]]
|
||||
format: Annotated[str, BeforeValidator(repr)]
|
||||
use_lores_as_preview: bool
|
||||
buffer_count: int = 6
|
||||
scaler_crop: Optional[tuple[int, int, int, int]] = None
|
||||
|
||||
@property
|
||||
def sensor_mode_dict(self) -> dict[str, Any]:
|
||||
"""The dictionary expected by PiCamera2 for setting sensor mode."""
|
||||
return {"output_size": self.sensor_mode_resolution, "bit_depth": self.bit_depth}
|
||||
|
||||
|
||||
class SensorModeSelector(BaseModel):
|
||||
"""A Pydantic model holding the two values needed to select a PiCamera Sensor mode.
|
||||
|
||||
These values are the output size and the bit depth.
|
||||
|
||||
This is a Pydantic model so that it can sent by FastAPI
|
||||
"""
|
||||
|
||||
output_size: tuple[int, int]
|
||||
bit_depth: int
|
||||
|
||||
|
||||
class StreamingPiCamera2(BaseCamera):
|
||||
class StreamingPiCamera2(BaseCamera, ABC):
|
||||
"""A Thing that provides and interface to the Raspberry Pi Camera.
|
||||
|
||||
Currently the Thing only supports the PiCamera v2 board. This needs
|
||||
generalisation.
|
||||
This is an abstract base class for all picamera models. Use a subclass specific
|
||||
to the camera model.
|
||||
"""
|
||||
|
||||
_focus_fom: int
|
||||
|
|
@ -140,31 +124,34 @@ class StreamingPiCamera2(BaseCamera):
|
|||
tuning: dict = lt.setting(default_factory=dict, readonly=True)
|
||||
"""The Raspberry PiCamera Tuning File JSON."""
|
||||
|
||||
# Subclasses should defined both of these.
|
||||
_camera_board: str
|
||||
_sensor_info: recalibrate_utils.SensorInfo
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
thing_server_interface: lt.ThingServerInterface,
|
||||
camera_num: int = 0,
|
||||
camera_board: str = "picamera_v2",
|
||||
) -> None:
|
||||
"""Initialise the camera with the given camera number.
|
||||
|
||||
This makes no connection to the camera (except to get the default tuning file).
|
||||
|
||||
:param thing_server_interface: The interface between this Thing and the server.
|
||||
:param camera_num: The number of the camera. This should generally be left as 0
|
||||
as most Raspberry Pi boards only support 1 camera.
|
||||
:param camera_board: The camera board used. Supported options are "picamera_v2"
|
||||
and "picamera_hq".
|
||||
"""
|
||||
super().__init__(thing_server_interface)
|
||||
self._setting_save_in_progress = False
|
||||
self._camera_num = camera_num
|
||||
self._camera_board = camera_board
|
||||
if camera_board not in SUPPORTED_CAMS_SENSOR_INFO:
|
||||
raise PicameraModelError(
|
||||
f"The camera_board {camera_board} is not supported. Supported boards "
|
||||
f"are {SUPPORTED_CAMS_SENSOR_INFO.keys()}."
|
||||
|
||||
# Check the subclass defined the _camera_board and _sensor_info
|
||||
if not hasattr(self, "_camera_board") or not hasattr(self, "_sensor_info"):
|
||||
raise AttributeError(
|
||||
f"{type(self).__name__} must define both both _camera_board and "
|
||||
"_sensor_info attributes"
|
||||
)
|
||||
self._sensor_info = SUPPORTED_CAMS_SENSOR_INFO[camera_board]
|
||||
|
||||
self._picamera_lock = RLock()
|
||||
self._picamera = None
|
||||
|
||||
|
|
@ -188,9 +175,6 @@ class StreamingPiCamera2(BaseCamera):
|
|||
# trigger a ServerNotRunningError
|
||||
self._colour_gains = tf_utils.get_colour_gains_from_lst(self.tuning)
|
||||
|
||||
stream_resolution: tuple[int, int] = lt.property(default=(820, 616))
|
||||
"""Resolution to use for the MJPEG stream."""
|
||||
|
||||
mjpeg_bitrate: Optional[int] = lt.property(default=100000000)
|
||||
"""Bitrate for MJPEG stream (None for default)."""
|
||||
|
||||
|
|
@ -302,41 +286,6 @@ class StreamingPiCamera2(BaseCamera):
|
|||
"Sharpness": 1,
|
||||
}
|
||||
|
||||
_sensor_modes: Optional[list[SensorMode]] = None
|
||||
|
||||
@lt.property
|
||||
def sensor_modes(self) -> list[SensorMode]:
|
||||
"""All the available modes the current sensor supports."""
|
||||
if not self._sensor_modes:
|
||||
with self._streaming_picamera() as cam:
|
||||
self._sensor_modes = cam.sensor_modes
|
||||
return self._sensor_modes
|
||||
|
||||
_sensor_mode: Optional[dict] = None
|
||||
|
||||
@lt.property
|
||||
def sensor_mode(self) -> Optional[SensorModeSelector]:
|
||||
"""The intended sensor mode of the camera."""
|
||||
if self._sensor_mode is None:
|
||||
return None
|
||||
return SensorModeSelector(**self._sensor_mode)
|
||||
|
||||
@sensor_mode.setter
|
||||
def _set_sensor_mode(self, new_mode: Optional[SensorModeSelector | dict]) -> None:
|
||||
"""Change the sensor mode used."""
|
||||
if new_mode is None:
|
||||
self._sensor_mode = None
|
||||
elif isinstance(new_mode, SensorModeSelector):
|
||||
self._sensor_mode = new_mode.model_dump()
|
||||
elif isinstance(new_mode, dict):
|
||||
self._sensor_mode = new_mode
|
||||
|
||||
# By pausing the stream on when accessing, streaming_picamera
|
||||
# self._sensor_mode will be read and set when the stream restarts
|
||||
# after the context manager closes.
|
||||
with self._streaming_picamera(pause_stream=True):
|
||||
pass
|
||||
|
||||
@lt.property
|
||||
def sensor_resolution(self) -> Optional[tuple[int, int]]:
|
||||
"""The native resolution of the camera's sensor."""
|
||||
|
|
@ -406,13 +355,11 @@ class StreamingPiCamera2(BaseCamera):
|
|||
"""Start streaming when the Thing context manager is opened.
|
||||
|
||||
This opens the picamera connection, initialises the camera, sets the
|
||||
sensor_modes property, and then starts the streams.
|
||||
property, and then starts the streams.
|
||||
"""
|
||||
super().__enter__()
|
||||
self._initialise_picamera(check_sensor_model=True)
|
||||
# Sensor modes is a cached property read it once after initialising the camera
|
||||
_modes = self.sensor_modes
|
||||
self.start_streaming()
|
||||
self._start_streaming()
|
||||
return self
|
||||
|
||||
@property
|
||||
|
|
@ -437,12 +384,12 @@ class StreamingPiCamera2(BaseCamera):
|
|||
already_streaming = self.stream_active
|
||||
with self._picamera_lock:
|
||||
if pause_stream and already_streaming:
|
||||
self.stop_streaming(stop_web_stream=False)
|
||||
self._stop_streaming(stop_web_stream=False)
|
||||
try:
|
||||
yield self._picamera
|
||||
finally:
|
||||
if pause_stream and already_streaming:
|
||||
self.start_streaming()
|
||||
self._start_streaming()
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
|
|
@ -451,59 +398,54 @@ class StreamingPiCamera2(BaseCamera):
|
|||
traceback: Optional[TracebackType],
|
||||
) -> None:
|
||||
"""Close the picamera connection when the Thing context manager is closed."""
|
||||
self.stop_streaming()
|
||||
self._stop_streaming()
|
||||
with self._streaming_picamera() as cam:
|
||||
cam.close()
|
||||
del self._picamera
|
||||
super().__exit__(exc_type, exc_value, traceback)
|
||||
|
||||
@lt.action
|
||||
def start_streaming(
|
||||
self, main_resolution: tuple[int, int] = (820, 616), buffer_count: int = 6
|
||||
) -> None:
|
||||
@abstractmethod
|
||||
@lt.property
|
||||
def streaming_modes(self) -> Mapping[str, PiCamera2StreamingMode]:
|
||||
"""Modes the camera can stream in."""
|
||||
|
||||
def _start_streaming(self, mode: str = "default") -> None:
|
||||
"""Start the MJPEG stream. This is where persistent controls are sent to camera.
|
||||
|
||||
Sets the camera resolutions based on input parameters, and sets the low-res
|
||||
resolution to (320, 240). Note: (320, 240) is a standard from the Pi Camera
|
||||
manual.
|
||||
Sets the camera stream resoltuons based on input mode
|
||||
|
||||
Create two streams:
|
||||
|
||||
* ``lores_mjpeg_stream`` for autofocus at low-res resolution
|
||||
* ``mjpeg_stream`` for preview. This is the ``main_resolution`` if this is less
|
||||
than (1280, 960), or the low-res resolution if above. This allows for
|
||||
high resolution capture without streaming high resolution video.
|
||||
|
||||
main_resolution: the resolution for the main configuration. Defaults to
|
||||
(820, 616), 1/4 sensor size.
|
||||
buffer_count: the number of frames to hold in the buffer. Higher uses more memory,
|
||||
lower may cause dropped frames. Value must be between 1 and 8, Defaults to 6.
|
||||
* ``lores_mjpeg_stream`` for autofocus at ``lores_resolution``
|
||||
* ``mjpeg_stream`` for preview. This is at the ``main_resolution`` unless
|
||||
``use_lores_as_preview`` is True, in which case it is a copy of
|
||||
``lores_mjpeg_stream``
|
||||
"""
|
||||
if mode not in self.streaming_modes:
|
||||
raise ValueError(f"Unknown mode {mode}")
|
||||
self.streaming_mode = mode
|
||||
|
||||
mode_info = self.streaming_modes[mode]
|
||||
controls = self._get_persistent_controls()
|
||||
# Buffer count can't be negative, zero, or too high.
|
||||
if buffer_count < 1 or buffer_count > 8:
|
||||
# 8 is slightly arbitrary. 6 is the PiCamera default for video
|
||||
# and the documentation only says that setting values higher gives
|
||||
# diminishing returns, and that the true maximum is hardware dependent
|
||||
raise ValueError(
|
||||
f"Can't set a buffer count of {buffer_count}. "
|
||||
"Buffer count must be an integer from 1-8"
|
||||
)
|
||||
|
||||
if mode_info.scaler_crop is not None:
|
||||
controls["ScalerCrop"] = mode_info.scaler_crop
|
||||
|
||||
with self._streaming_picamera() as picam:
|
||||
try:
|
||||
if picam.started:
|
||||
picam.stop()
|
||||
picam.stop_encoder() # make sure there are no other encoders going
|
||||
stream_config = picam.create_video_configuration(
|
||||
main={"size": main_resolution},
|
||||
lores={"size": (320, 240), "format": "YUV420"},
|
||||
sensor=self._sensor_mode,
|
||||
main={"size": mode_info.main_resolution},
|
||||
lores={"size": mode_info.lores_resolution, "format": "YUV420"},
|
||||
sensor=mode_info.sensor_mode_dict,
|
||||
controls=controls,
|
||||
)
|
||||
stream_config["buffer_count"] = buffer_count
|
||||
stream_config["buffer_count"] = mode_info.buffer_count
|
||||
picam.configure(stream_config)
|
||||
LOGGER.info("Starting picamera MJPEG stream...")
|
||||
stream_name = "lores" if main_resolution[0] > 1280 else "main"
|
||||
stream_name = "lores" if mode_info.use_lores_as_preview else "main"
|
||||
picam.start_recording(
|
||||
MJPEGEncoder(self.mjpeg_bitrate),
|
||||
PicameraStreamOutput(self.mjpeg_stream),
|
||||
|
|
@ -515,16 +457,12 @@ class StreamingPiCamera2(BaseCamera):
|
|||
name="lores",
|
||||
)
|
||||
except Exception as e:
|
||||
LOGGER.exception("Error while starting preview: {e}")
|
||||
LOGGER.exception(e)
|
||||
LOGGER.error(f"Error while starting preview: {e}.")
|
||||
else:
|
||||
self.stream_active = True
|
||||
LOGGER.debug(
|
||||
"Started MJPEG stream at %s on port %s", self.stream_resolution, 1
|
||||
)
|
||||
LOGGER.debug("Started MJPEG stream in %s mode.", mode)
|
||||
|
||||
@lt.action
|
||||
def stop_streaming(self, stop_web_stream: bool = True) -> None:
|
||||
def _stop_streaming(self, stop_web_stream: bool = True) -> None:
|
||||
"""Stop the MJPEG stream."""
|
||||
with self._streaming_picamera() as picam:
|
||||
try:
|
||||
|
|
@ -549,14 +487,17 @@ class StreamingPiCamera2(BaseCamera):
|
|||
cam.capture_metadata()
|
||||
|
||||
@contextmanager
|
||||
def _switch_to_still_capture_mode(self) -> Iterator[Picamera2]:
|
||||
def _switch_to_single_capture_mode(self) -> Iterator[Picamera2]:
|
||||
"""Get the picamera lock, pause stream and switch into still capture config.
|
||||
|
||||
Restarts stream when complete.
|
||||
"""
|
||||
mode_info = self.streaming_modes["full_resolution"]
|
||||
with self._streaming_picamera(pause_stream=True) as cam:
|
||||
LOGGER.debug("Reconfiguring camera for full resolution capture")
|
||||
cam.configure(cam.create_still_configuration(sensor=self._sensor_mode))
|
||||
cam.configure(
|
||||
cam.create_still_configuration(sensor=mode_info.sensor_mode_dict)
|
||||
)
|
||||
cam.start()
|
||||
time.sleep(self._sensor_info.short_pause)
|
||||
yield cam
|
||||
|
|
@ -595,7 +536,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
with self._streaming_picamera() as cam:
|
||||
return cam.capture_image(stream_name, wait=wait)
|
||||
elif stream_name == "full":
|
||||
with self._switch_to_still_capture_mode() as cam:
|
||||
with self._switch_to_single_capture_mode() as cam:
|
||||
return cam.capture_image(name="main", wait=wait)
|
||||
else:
|
||||
raise ValueError(f'Unknown stream name "{stream_name}"')
|
||||
|
|
@ -625,7 +566,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
# Raw cannot used capture_image.
|
||||
if wait is None:
|
||||
wait = 0.9
|
||||
with self._switch_to_still_capture_mode() as cam:
|
||||
with self._switch_to_single_capture_mode() as cam:
|
||||
return cam.capture_array(name="raw", 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))))``.
|
||||
|
|
@ -968,3 +909,77 @@ class StreamingPiCamera2(BaseCamera):
|
|||
"gamma_correction": self.gamma_correction,
|
||||
}
|
||||
return state
|
||||
|
||||
|
||||
class PiCameraV2(StreamingPiCamera2):
|
||||
"""A Thing that provides and interface to the Raspberry Pi Camera V2."""
|
||||
|
||||
_camera_board = "picamera_v2"
|
||||
_sensor_info = recalibrate_utils.IMX219_SENSOR_INFO
|
||||
|
||||
@lt.property
|
||||
def streaming_modes(self) -> Mapping[str, PiCamera2StreamingMode]:
|
||||
"""Modes the camera can stream in."""
|
||||
return {
|
||||
"default": PiCamera2StreamingMode(
|
||||
description=(
|
||||
"The standard mode that balances capture resolution and stream "
|
||||
"size."
|
||||
),
|
||||
main_resolution=(820, 616),
|
||||
lores_resolution=(320, 240),
|
||||
sensor_mode_resolution=(3280, 2464),
|
||||
bit_depth=10,
|
||||
use_lores_as_preview=False,
|
||||
),
|
||||
"full_resolution": PiCamera2StreamingMode(
|
||||
description=(
|
||||
"Streaming the camera in 8MP full resolution. The preview stream "
|
||||
"sent to the UI will be the low resolution (lores) stream. "
|
||||
"This allows better image capture at expense of preview quality."
|
||||
),
|
||||
main_resolution=(3280, 2464),
|
||||
lores_resolution=(320, 240),
|
||||
sensor_mode_resolution=(3280, 2464),
|
||||
bit_depth=10,
|
||||
use_lores_as_preview=True,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class PiCameraHQ(StreamingPiCamera2):
|
||||
"""A Thing that provides and interface to the Raspberry Pi Camera HQ."""
|
||||
|
||||
_camera_board = "picamera_hq"
|
||||
_sensor_info = recalibrate_utils.IMX477_SENSOR_INFO
|
||||
|
||||
@lt.property
|
||||
def streaming_modes(self) -> Mapping[str, PiCamera2StreamingMode]:
|
||||
"""Modes the camera can stream in."""
|
||||
return {
|
||||
"default": PiCamera2StreamingMode(
|
||||
description=(
|
||||
"The standard mode that balances capture resolution and stream "
|
||||
"size."
|
||||
),
|
||||
main_resolution=(700, 700),
|
||||
lores_resolution=(350, 350),
|
||||
sensor_mode_resolution=(4056, 3040),
|
||||
bit_depth=12,
|
||||
use_lores_as_preview=False,
|
||||
scaler_crop=(628, 120, 2800, 2800),
|
||||
),
|
||||
"full_resolution": PiCamera2StreamingMode(
|
||||
description=(
|
||||
"Streaming the camera in 12MP full resolution. The preview stream "
|
||||
"sent to the UI will be the low resolution (lores) stream. "
|
||||
"This allows better image capture at expense of preview quality."
|
||||
),
|
||||
main_resolution=(2800, 2800),
|
||||
lores_resolution=(350, 350),
|
||||
sensor_mode_resolution=(4056, 3040),
|
||||
bit_depth=12,
|
||||
use_lores_as_preview=True,
|
||||
scaler_crop=(628, 120, 2800, 2800),
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -413,7 +413,7 @@ class SimulatedCamera(BaseCamera):
|
|||
"""Start the capture thread when the Thing context manager is opened."""
|
||||
super().__enter__()
|
||||
self.generate_canvas()
|
||||
self.start_streaming()
|
||||
self._start_streaming()
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
|
|
@ -428,13 +428,10 @@ class SimulatedCamera(BaseCamera):
|
|||
self._capture_thread.join()
|
||||
super().__exit__(exc_type, exc_value, traceback)
|
||||
|
||||
@lt.action
|
||||
def start_streaming(
|
||||
self, main_resolution: tuple[int, int] = (820, 616), buffer_count: int = 1
|
||||
) -> None:
|
||||
def _start_streaming(self, mode: str = "default") -> None:
|
||||
"""Start the live stream.
|
||||
|
||||
The start_streaming method is used a camera ``Thing`` to begin streaming
|
||||
The _start_streaming method is used a camera ``Thing`` to begin streaming
|
||||
images or to adjust the stream resolution if streaming is already active.
|
||||
|
||||
The simulation camera does not currently support the resolution argument.
|
||||
|
|
@ -442,13 +439,11 @@ class SimulatedCamera(BaseCamera):
|
|||
If called while already streaming, the warning will be emitted and no other
|
||||
action will be taken.
|
||||
|
||||
:param main_resolution: Currently ignored, this argument exists to ensure consistent API across camera Things.
|
||||
:param buffer_count: Currently ignored, this argument exists to ensure consistent API across camera Things.
|
||||
:param mode: The name of the streaming mode to use.
|
||||
"""
|
||||
LOGGER.warning(
|
||||
f"Simulation camera doesn't respect {main_resolution=} or {buffer_count=} "
|
||||
"arguments."
|
||||
)
|
||||
if mode not in self.streaming_modes:
|
||||
raise ValueError(f"Unknown mode {mode}")
|
||||
self.streaming_mode = mode
|
||||
if not self.stream_active:
|
||||
self._capture_enabled = True
|
||||
self._capture_thread = Thread(target=self._capture_frames)
|
||||
|
|
|
|||
|
|
@ -401,7 +401,7 @@ class SmartScanThing(OFMThing):
|
|||
)
|
||||
|
||||
self.ongoing_scan.save_scan_data(self._scan_data)
|
||||
self._cam.start_streaming(main_resolution=(3280, 2464))
|
||||
self._cam.change_streaming_mode(mode="full_resolution")
|
||||
workflow.pre_scan_routine(self._scan_data.workflow_settings)
|
||||
|
||||
# If stitching settings are None then this type of scan can't be stitched
|
||||
|
|
@ -436,7 +436,7 @@ class SmartScanThing(OFMThing):
|
|||
# if it ended due to an exception.
|
||||
|
||||
# Start streaming in the default resolution again as soon as possible
|
||||
self._cam.start_streaming()
|
||||
self._cam.change_streaming_mode(mode="default")
|
||||
|
||||
# This is what happens if the scan completes successfully or the
|
||||
# user cancels it.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue