Refactor and simplify how picamera settings work
This commit is contained in:
parent
df8a9d0e93
commit
35d6e45892
2 changed files with 162 additions and 206 deletions
|
|
@ -16,7 +16,7 @@ https://datasheets.raspberrypi.com/camera/raspberry-pi-camera-guide.pdf
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Annotated, Any, Iterator, Literal, Mapping, Optional
|
from typing import Annotated, Iterator, Literal, Mapping, Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -28,35 +28,18 @@ from threading import RLock
|
||||||
|
|
||||||
from pydantic import BaseModel, BeforeValidator
|
from pydantic import BaseModel, BeforeValidator
|
||||||
import piexif
|
import piexif
|
||||||
import picamera2
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from picamera2 import Picamera2
|
from picamera2 import Picamera2
|
||||||
from picamera2.encoders import MJPEGEncoder
|
from picamera2.encoders import MJPEGEncoder
|
||||||
from picamera2.outputs import Output
|
from picamera2.outputs import Output
|
||||||
|
|
||||||
import labthings_fastapi as lt
|
import labthings_fastapi as lt
|
||||||
|
from labthings_fastapi.exceptions import NotConnectedToServerError
|
||||||
|
|
||||||
from . import picamera_recalibrate_utils as recalibrate_utils
|
from . import picamera_recalibrate_utils as recalibrate_utils
|
||||||
from . import BaseCamera, JPEGBlob, ArrayModel
|
from . import BaseCamera, JPEGBlob, ArrayModel
|
||||||
|
|
||||||
|
|
||||||
class PicameraControl(lt.ThingProperty):
|
|
||||||
def __init__(
|
|
||||||
self, control_name: str, model: type = float, description: Optional[str] = None
|
|
||||||
):
|
|
||||||
"""A property descriptor controlling a picamera control"""
|
|
||||||
super().__init__(model, observable=False, description=description)
|
|
||||||
self.control_name = control_name
|
|
||||||
|
|
||||||
def _getter(self, obj: StreamingPiCamera2):
|
|
||||||
with obj.picamera() as cam:
|
|
||||||
return cam.capture_metadata()[self.control_name]
|
|
||||||
|
|
||||||
def _setter(self, obj: StreamingPiCamera2, value: Any):
|
|
||||||
with obj.picamera() as cam:
|
|
||||||
cam.set_controls({self.control_name: value})
|
|
||||||
|
|
||||||
|
|
||||||
class PicameraStreamOutput(Output):
|
class PicameraStreamOutput(Output):
|
||||||
"""An Output class that sends frames to a stream"""
|
"""An Output class that sends frames to a stream"""
|
||||||
|
|
||||||
|
|
@ -126,84 +109,23 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
"""A Thing that represents an OpenCV camera"""
|
"""A Thing that represents an OpenCV camera"""
|
||||||
|
|
||||||
def __init__(self, camera_num: int = 0):
|
def __init__(self, camera_num: int = 0):
|
||||||
|
self._setting_save_in_progress = False
|
||||||
self.camera_num = camera_num
|
self.camera_num = camera_num
|
||||||
self.camera_configs: dict[str, dict] = {}
|
self.camera_configs: dict[str, dict] = {}
|
||||||
# Note persistent controls will be updated with settings, in __enter__.
|
self._picamera_lock = None
|
||||||
self.persistent_controls = {
|
self._picamera = None
|
||||||
"AeEnable": False,
|
logging.info("Starting & reconfiguring camera to populate sensor_modes.")
|
||||||
"AnalogueGain": 1.0,
|
with Picamera2(camera_num=self.camera_num) as cam:
|
||||||
"AwbEnable": False,
|
self.default_tuning = recalibrate_utils.load_default_tuning(cam)
|
||||||
"Brightness": 0,
|
logging.info("Done reading sensor modes & default tuning.")
|
||||||
"ColourGains": (1, 1),
|
# Set tuning to default tuning. This will be overwritten when the Thing is
|
||||||
"Contrast": 1,
|
# connects to the server if tuning is saved to disk.
|
||||||
"ExposureTime": 0,
|
|
||||||
"Saturation": 1,
|
|
||||||
"Sharpness": 1,
|
|
||||||
}
|
|
||||||
self.persistent_control_tolerances = {
|
|
||||||
"ExposureTime": 30,
|
|
||||||
}
|
|
||||||
|
|
||||||
def update_persistent_controls(self, discard_frames: int = 1):
|
|
||||||
"""Update the persistent controls dict from the camera
|
|
||||||
|
|
||||||
Query the camera and update the value of `persistent_controls` to
|
|
||||||
match the current state of the camera.
|
|
||||||
|
|
||||||
There is a work-around here, that will suppress small updates. There
|
|
||||||
appears to be a bug in the camera code that causes a slight drift in
|
|
||||||
`ExposureTime` each time the camera is reinitialised: this can
|
|
||||||
add up over time, particularly if the camera is reconfigured many
|
|
||||||
times. To get around this, we look in `self.persistent_control_tolerances`
|
|
||||||
and only update `self.persistent_controls` if the change is greater than
|
|
||||||
this tolerance.
|
|
||||||
"""
|
|
||||||
with self._streaming_picamera() as cam:
|
|
||||||
for i in range(discard_frames):
|
|
||||||
# Discard frames, so data is fresh
|
|
||||||
cam.capture_metadata()
|
|
||||||
for key, value in cam.capture_metadata().items():
|
|
||||||
if key not in self.persistent_controls:
|
|
||||||
# Skip keys that are not persistent controls
|
|
||||||
continue
|
|
||||||
if self._control_value_within_tolerance(key, value):
|
|
||||||
logging.debug(
|
|
||||||
"Ignoring a small change in persistent control %s from %s to "
|
|
||||||
"%s while updating persistent controls.",
|
|
||||||
key,
|
|
||||||
self.persistent_controls[key],
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.persistent_controls[key] = value
|
|
||||||
self.thing_settings.update(self.persistent_controls)
|
|
||||||
|
|
||||||
def _control_value_within_tolerance(self, key: str, value: float) -> bool:
|
|
||||||
"""Return True if the updated value for a persistent control is within
|
|
||||||
tolerance of current value. This allows ignoring small changes
|
|
||||||
"""
|
|
||||||
if key not in self.persistent_control_tolerances:
|
|
||||||
# Not tolerance range set, so it is not within tolerance
|
|
||||||
return False
|
|
||||||
|
|
||||||
difference = np.abs(self.persistent_controls[key] - value)
|
|
||||||
return difference < self.persistent_control_tolerances[key]
|
|
||||||
|
|
||||||
def settings_to_persistent_controls(self):
|
|
||||||
"""Update the persistent controls dict from the settings dict
|
|
||||||
|
|
||||||
NB this must be called **after** self.thing_settings is initialised,
|
|
||||||
i.e. during or after `__enter__`.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
pc = self.thing_settings["persistent_controls"]
|
self.tuning = self.default_tuning
|
||||||
except KeyError:
|
except NotConnectedToServerError:
|
||||||
return # If there are no saved settings, use defaults
|
# This will throw an error after setting as we are not connected to
|
||||||
for k in self.persistent_controls:
|
# a server. But we know this, so we ignore the error.
|
||||||
try:
|
pass
|
||||||
self.persistent_controls[k] = pc[k]
|
|
||||||
except KeyError:
|
|
||||||
pass # If controls are missing, leave at default
|
|
||||||
|
|
||||||
stream_resolution = lt.ThingProperty(
|
stream_resolution = lt.ThingProperty(
|
||||||
tuple[int, int],
|
tuple[int, int],
|
||||||
|
|
@ -225,27 +147,100 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
readonly=True,
|
readonly=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
analogue_gain = PicameraControl("AnalogueGain", float)
|
def save_settings(self):
|
||||||
colour_gains = PicameraControl("ColourGains", tuple[float, float])
|
"""Override save_settings to ensure that camera properties don't recurse.
|
||||||
|
|
||||||
exposure_time = PicameraControl(
|
This method is run by any Thing when a ThingSetting is saved. However, the
|
||||||
"ExposureTime", int, description="The exposure time in microseconds"
|
method reads the thing_setting. As reading the thing setting talks to the
|
||||||
)
|
camera and calls save_settings if the value is not as expected, this could
|
||||||
|
cause recursion. Aslo this means that saving one setting causes all others
|
||||||
|
to be read each time.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._setting_save_in_progress = True
|
||||||
|
super().save_settings()
|
||||||
|
finally:
|
||||||
|
self._setting_save_in_progress = False
|
||||||
|
|
||||||
|
## Persistent controls! These are settings
|
||||||
|
|
||||||
|
_analogue_gain: float = 1.0
|
||||||
|
|
||||||
|
@lt.thing_setting
|
||||||
|
def analogue_gain(self) -> float:
|
||||||
|
if not self._setting_save_in_progress and self.streaming:
|
||||||
|
with self._streaming_picamera() as cam:
|
||||||
|
cam_value = cam.capture_metadata()["AnalogueGain"]
|
||||||
|
if cam_value != self._analogue_gain:
|
||||||
|
self._analogue_gain = cam_value
|
||||||
|
self.save_settings()
|
||||||
|
return self._analogue_gain
|
||||||
|
|
||||||
|
@analogue_gain.setter
|
||||||
|
def analogue_gain(self, value: float):
|
||||||
|
self._analogue_gain = value
|
||||||
|
if self.streaming:
|
||||||
|
with self._streaming_picamera() as cam:
|
||||||
|
cam.set_controls({"AnalogueGain": value})
|
||||||
|
|
||||||
|
_colour_gains: tuple[float, float] = (1.0, 1.0)
|
||||||
|
|
||||||
|
@lt.thing_setting
|
||||||
|
def colour_gains(self) -> tuple[float, float]:
|
||||||
|
if not self._setting_save_in_progress and self.streaming:
|
||||||
|
with self._streaming_picamera() as cam:
|
||||||
|
cam_value = cam.capture_metadata()["ColourGains"]
|
||||||
|
if cam_value != self._colour_gains:
|
||||||
|
self._colour_gains = cam_value
|
||||||
|
self.save_settings()
|
||||||
|
return self._colour_gains
|
||||||
|
|
||||||
|
@colour_gains.setter
|
||||||
|
def colour_gains(self, value: tuple[float, float]):
|
||||||
|
self._colour_gains = value
|
||||||
|
if self.streaming:
|
||||||
|
with self._streaming_picamera() as cam:
|
||||||
|
cam.set_controls({"ColourGains": value})
|
||||||
|
|
||||||
|
_exposure_time: int = 0
|
||||||
|
|
||||||
|
@lt.thing_setting
|
||||||
|
def exposure_time(self) -> int:
|
||||||
|
if not self._setting_save_in_progress and self.streaming:
|
||||||
|
with self._streaming_picamera() as cam:
|
||||||
|
cam_value = cam.capture_metadata()["ExposureTime"]
|
||||||
|
if cam_value != self._exposure_time:
|
||||||
|
self._exposure_time = cam_value
|
||||||
|
self.save_settings()
|
||||||
|
return self._exposure_time
|
||||||
|
|
||||||
@exposure_time.setter
|
@exposure_time.setter
|
||||||
def exposure_time(self, value: int):
|
def exposure_time(self, value: int):
|
||||||
"""
|
_exposure_time = value
|
||||||
Custom setter for the above exposure_time PicameraControl.
|
if self.streaming:
|
||||||
|
with self._streaming_picamera() as cam:
|
||||||
|
# Note: This set a value 1 higher than requested as picamera2 always
|
||||||
|
# sets a lower value than requested, even if the requested is allowed
|
||||||
|
cam.set_controls({"ExposureTime": value + 1})
|
||||||
|
|
||||||
This is overriding the standard setter for a PicameraControl, as
|
def _get_persistent_controls(self) -> dict:
|
||||||
the value needs to be adjusted before setting to behave as expected.
|
discard_frames: int = 1
|
||||||
|
if self.streaming:
|
||||||
See comment within the function for more detail.
|
with self._streaming_picamera() as cam:
|
||||||
"""
|
# Discard frames, so data is fresh
|
||||||
with self._streaming_picamera() as cam:
|
for i in range(discard_frames):
|
||||||
# Note: This set a value 1 higher than requested as picamera2 always sets
|
cam.capture_metadata()
|
||||||
# a lower value than requested, even if the requested is allowed
|
return {
|
||||||
cam.set_controls({"ExposureTime": value + 1})
|
"AeEnable": False,
|
||||||
|
"AnalogueGain": self.analogue_gain,
|
||||||
|
"AwbEnable": False,
|
||||||
|
"Brightness": 0,
|
||||||
|
"ColourGains": self.colour_gains,
|
||||||
|
"Contrast": 1,
|
||||||
|
"ExposureTime": self.exposure_time,
|
||||||
|
"Saturation": 1,
|
||||||
|
"Sharpness": 1,
|
||||||
|
}
|
||||||
|
|
||||||
_sensor_modes = None
|
_sensor_modes = None
|
||||||
|
|
||||||
|
|
@ -259,7 +254,7 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
|
|
||||||
_sensor_mode: Optional[dict] = None
|
_sensor_mode: Optional[dict] = None
|
||||||
|
|
||||||
@lt.thing_setting
|
@lt.thing_property
|
||||||
def sensor_mode(self) -> Optional[SensorModeSelector]:
|
def sensor_mode(self) -> Optional[SensorModeSelector]:
|
||||||
"""The intended sensor mode of the camera"""
|
"""The intended sensor mode of the camera"""
|
||||||
if self._sensor_mode is None:
|
if self._sensor_mode is None:
|
||||||
|
|
@ -277,74 +272,64 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
elif isinstance(new_mode, dict):
|
elif isinstance(new_mode, dict):
|
||||||
self._sensor_mode = new_mode
|
self._sensor_mode = new_mode
|
||||||
|
|
||||||
if isinstance(new_mode, SensorModeSelector):
|
# By pausing the stream on when accessing, streaming_picamera
|
||||||
new_mode = new_mode.model_dump()
|
# self._sensor_mode will be read and set when the stream restarts
|
||||||
|
# after the context manager closes.
|
||||||
with self._streaming_picamera(pause_stream=True):
|
with self._streaming_picamera(pause_stream=True):
|
||||||
self.thing_settings["sensor_mode"] = new_mode
|
pass
|
||||||
|
|
||||||
@lt.thing_setting
|
@lt.thing_property
|
||||||
def sensor_resolution(self) -> tuple[int, int]:
|
def sensor_resolution(self) -> Optional[tuple[int, int]]:
|
||||||
"""The native resolution of the camera's sensor"""
|
"""The native resolution of the camera's sensor"""
|
||||||
with self._streaming_picamera() as cam:
|
with self._streaming_picamera() as cam:
|
||||||
return cam.sensor_resolution
|
return cam.sensor_resolution
|
||||||
|
|
||||||
tuning = lt.ThingSetting(Optional[dict], None, readonly=True)
|
tuning = lt.ThingSetting(Optional[dict], None, readonly=True)
|
||||||
|
|
||||||
def initialise_tuning(self):
|
def _initialise_picamera(self):
|
||||||
"""Read the tuning from the settings, or load default tuning
|
"""Acquire the picamera device and store it as `self._picamera`.
|
||||||
|
|
||||||
NB this relies on `self.thing_settings` and `self.default_tuning`
|
This duplicates logic in `Picamera2.__init__` to provide a tuning file that
|
||||||
so will fail if it's run before those are populated in `__enter__`.
|
will be read when the camera system initialises.
|
||||||
"""
|
"""
|
||||||
if "tuning" in self.thing_settings:
|
if self._picamera_lock is not None:
|
||||||
self.tuning = self.thing_settings["tuning"].dict
|
|
||||||
else:
|
|
||||||
logging.info("Did not find tuning in settings, reading from camera...")
|
|
||||||
self.tuning = self.default_tuning
|
|
||||||
|
|
||||||
def initialise_picamera(self):
|
|
||||||
"""Acquire the picamera device and store it as `self._picamera`"""
|
|
||||||
if hasattr(self, "_picamera_lock"):
|
|
||||||
# Don't close the camera if it's in use
|
# Don't close the camera if it's in use
|
||||||
self._picamera_lock.acquire()
|
self._picamera_lock.acquire()
|
||||||
with tempfile.NamedTemporaryFile("w") as tuning_file:
|
with tempfile.NamedTemporaryFile("w") as tuning_file:
|
||||||
# This duplicates logic in `Picamera2.__init__` to provide a tuning file
|
|
||||||
# that will be read when the camera system initialises.
|
|
||||||
# This is a necessary work-around until `picamera2` better supports
|
|
||||||
# reinitialisation of the camera with new tuning.
|
|
||||||
json.dump(self.tuning, tuning_file)
|
json.dump(self.tuning, tuning_file)
|
||||||
tuning_file.flush() # but leave it open as closing it will delete it
|
tuning_file.flush() # but leave it open as closing it will delete it
|
||||||
os.environ["LIBCAMERA_RPI_TUNING_FILE"] = tuning_file.name
|
os.environ["LIBCAMERA_RPI_TUNING_FILE"] = tuning_file.name
|
||||||
# NB even though we've put the tuning file in the environment, we will
|
|
||||||
# need to specify the filename in the `Picamera2` initialiser as otherwise
|
if self._picamera is not None:
|
||||||
# it will be overwritten with None.
|
logging.info("Closing picamera object for reinitialisation")
|
||||||
if hasattr(self, "_picamera") and self._picamera:
|
|
||||||
print("Closing picamera object for reinitialisation")
|
|
||||||
logging.info(
|
logging.info(
|
||||||
"Camera object already exists, closing for reinitialisation"
|
"Camera object already exists, closing for reinitialisation"
|
||||||
)
|
)
|
||||||
self._picamera.close()
|
self._picamera.close()
|
||||||
print("Picamera closed, deleting picamera")
|
logging.info("Picamera closed, deleting picamera")
|
||||||
del self._picamera
|
del self._picamera
|
||||||
recalibrate_utils.recreate_camera_manager()
|
recalibrate_utils.recreate_camera_manager()
|
||||||
print("Creating new Picamera2 object")
|
|
||||||
self._picamera = picamera2.Picamera2(
|
logging.info("Creating new Picamera2 object")
|
||||||
|
# Specify tuning file otherwise it will be overwritten with None.
|
||||||
|
self._picamera = Picamera2(
|
||||||
camera_num=self.camera_num,
|
camera_num=self.camera_num,
|
||||||
tuning=self.tuning,
|
tuning=self.tuning,
|
||||||
)
|
)
|
||||||
self._picamera_lock = RLock()
|
self._picamera_lock = RLock()
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.populate_default_tuning()
|
self._initialise_picamera()
|
||||||
self.initialise_tuning()
|
|
||||||
self.initialise_picamera()
|
|
||||||
# populate sensor modes by reading the property
|
# populate sensor modes by reading the property
|
||||||
self.sensor_modes
|
self.sensor_modes
|
||||||
self.settings_to_persistent_controls()
|
|
||||||
self.settings_to_properties()
|
|
||||||
self.start_streaming()
|
self.start_streaming()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def streaming(self) -> bool:
|
||||||
|
"""True if the camera is streaming"""
|
||||||
|
return self._picamera is not None and self._picamera.started
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def _streaming_picamera(self, pause_stream=False) -> Iterator[Picamera2]:
|
def _streaming_picamera(self, pause_stream=False) -> Iterator[Picamera2]:
|
||||||
"""Lock access to picamera and return the underlying `Picamera2` instance.
|
"""Lock access to picamera and return the underlying `Picamera2` instance.
|
||||||
|
|
@ -356,14 +341,12 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
* Updated persistent controls
|
* Updated persistent controls
|
||||||
* Stop the MJPEG Stream
|
* Stop the MJPEG Stream
|
||||||
* Yield the `Picamera2` instance for function calling the context manager to
|
* Yield the `Picamera2` instance for function calling the context manager to
|
||||||
make changes. If seetings are changed run `update_persistent_controls()` before
|
make changes.
|
||||||
closing the context manager.
|
|
||||||
* On closing of the context manager the stream will restart.
|
* On closing of the context manager the stream will restart.
|
||||||
"""
|
"""
|
||||||
already_streaming = self.stream_active
|
already_streaming = self.stream_active
|
||||||
with self._picamera_lock:
|
with self._picamera_lock:
|
||||||
if pause_stream and already_streaming:
|
if pause_stream and already_streaming:
|
||||||
self.update_persistent_controls()
|
|
||||||
self.stop_streaming(stop_web_stream=False)
|
self.stop_streaming(stop_web_stream=False)
|
||||||
try:
|
try:
|
||||||
yield self._picamera
|
yield self._picamera
|
||||||
|
|
@ -371,24 +354,7 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
if pause_stream and already_streaming:
|
if pause_stream and already_streaming:
|
||||||
self.start_streaming()
|
self.start_streaming()
|
||||||
|
|
||||||
def populate_default_tuning(self):
|
|
||||||
"""Sensor modes are enumerated and stored, once, on start-up (`__enter__`).
|
|
||||||
|
|
||||||
This opens and closes the camera - must be run before the camera is
|
|
||||||
initialised.
|
|
||||||
"""
|
|
||||||
logging.info("Starting & reconfiguring camera to populate sensor_modes.")
|
|
||||||
with Picamera2(camera_num=self.camera_num) as cam:
|
|
||||||
self.default_tuning = recalibrate_utils.load_default_tuning(cam)
|
|
||||||
logging.info("Done reading sensor modes & default tuning.")
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, traceback):
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
# Allow key controls to persist across restarts
|
|
||||||
self.update_persistent_controls()
|
|
||||||
self.thing_settings["persistent_controls"] = self.persistent_controls
|
|
||||||
self.thing_settings["tuning"] = self.tuning
|
|
||||||
self.properties_to_settings()
|
|
||||||
self.thing_settings.write_to_file()
|
|
||||||
# Shut down the camera
|
# Shut down the camera
|
||||||
self.stop_streaming()
|
self.stop_streaming()
|
||||||
with self._streaming_picamera() as cam:
|
with self._streaming_picamera() as cam:
|
||||||
|
|
@ -400,7 +366,7 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
self, main_resolution: tuple[int, int] = (820, 616), buffer_count: int = 6
|
self, main_resolution: tuple[int, int] = (820, 616), buffer_count: int = 6
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Start the MJPEG stream
|
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
|
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
|
resolution to (320, 240). Note: (320, 240) is a standard from the Pi Camera
|
||||||
|
|
@ -417,7 +383,7 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
buffer_count: the number of frames to hold in the buffer. Higher uses more memory,
|
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.
|
lower may cause dropped frames. Value must be between 1 and 8, Defaults to 6.
|
||||||
"""
|
"""
|
||||||
|
controls = self._get_persistent_controls()
|
||||||
# Buffer count can't be negative, zero, or too high.
|
# Buffer count can't be negative, zero, or too high.
|
||||||
if buffer_count < 1 or buffer_count > 8:
|
if buffer_count < 1 or buffer_count > 8:
|
||||||
# 8 is slightly arbitrary. 6 is the PiCamera default for video
|
# 8 is slightly arbitrary. 6 is the PiCamera default for video
|
||||||
|
|
@ -435,8 +401,8 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
stream_config = picam.create_video_configuration(
|
stream_config = picam.create_video_configuration(
|
||||||
main={"size": main_resolution},
|
main={"size": main_resolution},
|
||||||
lores={"size": (320, 240), "format": "YUV420"},
|
lores={"size": (320, 240), "format": "YUV420"},
|
||||||
sensor=self.thing_settings.get("sensor_mode", None),
|
sensor=self._sensor_mode,
|
||||||
controls=self.persistent_controls,
|
controls=controls,
|
||||||
)
|
)
|
||||||
stream_config["buffer_count"] = buffer_count
|
stream_config["buffer_count"] = buffer_count
|
||||||
picam.configure(stream_config)
|
picam.configure(stream_config)
|
||||||
|
|
@ -634,7 +600,6 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
target_white_level=target_white_level,
|
target_white_level=target_white_level,
|
||||||
percentile=percentile,
|
percentile=percentile,
|
||||||
)
|
)
|
||||||
self.update_persistent_controls()
|
|
||||||
|
|
||||||
@lt.thing_action
|
@lt.thing_action
|
||||||
def calibrate_white_balance(
|
def calibrate_white_balance(
|
||||||
|
|
@ -669,7 +634,6 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
recalibrate_utils.adjust_white_balance_from_raw(
|
recalibrate_utils.adjust_white_balance_from_raw(
|
||||||
cam, percentile=99, method=method
|
cam, percentile=99, method=method
|
||||||
)
|
)
|
||||||
self.update_persistent_controls()
|
|
||||||
|
|
||||||
@lt.thing_action
|
@lt.thing_action
|
||||||
def calibrate_lens_shading(self) -> None:
|
def calibrate_lens_shading(self) -> None:
|
||||||
|
|
@ -687,22 +651,31 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
# (Cb).
|
# (Cb).
|
||||||
L, Cr, Cb = recalibrate_utils.lst_from_camera(cam) # noqa: N806
|
L, Cr, Cb = recalibrate_utils.lst_from_camera(cam) # noqa: N806
|
||||||
recalibrate_utils.set_static_lst(self.tuning, L, Cr, Cb)
|
recalibrate_utils.set_static_lst(self.tuning, L, Cr, Cb)
|
||||||
self.initialise_picamera()
|
self._initialise_picamera()
|
||||||
|
|
||||||
@lt.thing_property
|
@lt.thing_property
|
||||||
def colour_correction_matrix(
|
def colour_correction_matrix(
|
||||||
self,
|
self,
|
||||||
) -> tuple[float, float, float, float, float, float, float, float, float]:
|
) -> tuple[float, float, float, float, float, float, float, float, float]:
|
||||||
"""An alias for `colour_correction_matrix` to fit the micromanager API"""
|
"""The `colour_correction_matrix` from the tuning file.
|
||||||
return self.thing_settings.get(
|
|
||||||
"colour_correction_matrix",
|
This is broken out into its own property for convenience and compatibility with
|
||||||
tuple(recalibrate_utils.get_static_ccm(self.tuning)[0]["ccm"]),
|
the micromanager API
|
||||||
)
|
|
||||||
|
Ir is a 9 value tuple used to specify the 3x3 matrix that the GPU pipeline uses
|
||||||
|
to convert from the camera R,G,B vector to the standard R,G,B.
|
||||||
|
|
||||||
|
See page Raspberry Pi Camera Algorithm and Tuning Guide, page 45.
|
||||||
|
"""
|
||||||
|
return tuple(recalibrate_utils.get_static_ccm(self.tuning)[0]["ccm"])
|
||||||
|
|
||||||
@colour_correction_matrix.setter # type: ignore
|
@colour_correction_matrix.setter # type: ignore
|
||||||
def colour_correction_matrix(self, value) -> None:
|
def colour_correction_matrix(self, value) -> None:
|
||||||
self.thing_settings["colour_correction_matrix"] = value
|
recalibrate_utils.set_static_ccm(self.tuning, value)
|
||||||
self.calibrate_colour_correction(value)
|
|
||||||
|
if self._picamera is not None:
|
||||||
|
with self._streaming_picamera(pause_stream=True):
|
||||||
|
self._initialise_picamera()
|
||||||
|
|
||||||
@lt.thing_action
|
@lt.thing_action
|
||||||
def reset_ccm(self):
|
def reset_ccm(self):
|
||||||
|
|
@ -712,7 +685,7 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
These values are from the Raspberry Pi Camera Algorithm and Tuning Guide, page
|
These values are from the Raspberry Pi Camera Algorithm and Tuning Guide, page
|
||||||
45.
|
45.
|
||||||
"""
|
"""
|
||||||
# This is flattened 3x3 matrix. See `calibrate_colour_correction`
|
# This is flattened 3x3 matrix. See `colour_correction_matrix`
|
||||||
col_corr_matrix = [
|
col_corr_matrix = [
|
||||||
1.80439,
|
1.80439,
|
||||||
-0.73699,
|
-0.73699,
|
||||||
|
|
@ -726,25 +699,6 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
]
|
]
|
||||||
self.colour_correction_matrix = col_corr_matrix
|
self.colour_correction_matrix = col_corr_matrix
|
||||||
|
|
||||||
@lt.thing_action
|
|
||||||
def calibrate_colour_correction(
|
|
||||||
self,
|
|
||||||
col_corr_matrix: tuple[
|
|
||||||
float, float, float, float, float, float, float, float, float
|
|
||||||
],
|
|
||||||
) -> None:
|
|
||||||
"""Overwrite the colour correction matrix in camera tuning
|
|
||||||
|
|
||||||
col_corr_matrix: This is a 9 value tuple used to specify the 3x3
|
|
||||||
matrix that the GPU pipeline uses to convert from the camera R,G,B vector
|
|
||||||
to the standard R,G,B.
|
|
||||||
|
|
||||||
See page Raspberry Pi Camera Algorithm and Tuning Guide, page 45.
|
|
||||||
"""
|
|
||||||
with self._streaming_picamera(pause_stream=True):
|
|
||||||
recalibrate_utils.set_static_ccm(self.tuning, col_corr_matrix)
|
|
||||||
self.initialise_picamera()
|
|
||||||
|
|
||||||
@lt.thing_action
|
@lt.thing_action
|
||||||
def set_static_green_equalisation(self, offset: int = 65535) -> None:
|
def set_static_green_equalisation(self, offset: int = 65535) -> None:
|
||||||
"""Set the green equalisation to a static value.
|
"""Set the green equalisation to a static value.
|
||||||
|
|
@ -758,7 +712,7 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
"""
|
"""
|
||||||
with self._streaming_picamera(pause_stream=True):
|
with self._streaming_picamera(pause_stream=True):
|
||||||
recalibrate_utils.set_static_geq(self.tuning, offset)
|
recalibrate_utils.set_static_geq(self.tuning, offset)
|
||||||
self.initialise_picamera()
|
self._initialise_picamera()
|
||||||
|
|
||||||
@lt.thing_action
|
@lt.thing_action
|
||||||
def full_auto_calibrate(self) -> None:
|
def full_auto_calibrate(self) -> None:
|
||||||
|
|
@ -795,7 +749,7 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
recalibrate_utils.set_static_lst(
|
recalibrate_utils.set_static_lst(
|
||||||
self.tuning, flat_array, flat_array, flat_array
|
self.tuning, flat_array, flat_array, flat_array
|
||||||
)
|
)
|
||||||
self.initialise_picamera()
|
self._initialise_picamera()
|
||||||
|
|
||||||
@lt.thing_property
|
@lt.thing_property
|
||||||
def lens_shading_tables(self) -> Optional[LensShading]:
|
def lens_shading_tables(self) -> Optional[LensShading]:
|
||||||
|
|
@ -841,7 +795,7 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
cr=lst.Cr,
|
cr=lst.Cr,
|
||||||
cb=lst.Cb,
|
cb=lst.Cb,
|
||||||
)
|
)
|
||||||
self.initialise_picamera()
|
self._initialise_picamera()
|
||||||
|
|
||||||
def correct_colour_gains_for_lens_shading(
|
def correct_colour_gains_for_lens_shading(
|
||||||
self, colour_gains: tuple[float, float]
|
self, colour_gains: tuple[float, float]
|
||||||
|
|
@ -887,7 +841,7 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
luminance = alsc["luminance_lut"]
|
luminance = alsc["luminance_lut"]
|
||||||
flat = np.ones((12, 16))
|
flat = np.ones((12, 16))
|
||||||
recalibrate_utils.set_static_lst(self.tuning, luminance, flat, flat)
|
recalibrate_utils.set_static_lst(self.tuning, luminance, flat, flat)
|
||||||
self.initialise_picamera()
|
self._initialise_picamera()
|
||||||
|
|
||||||
@lt.thing_action
|
@lt.thing_action
|
||||||
def reset_lens_shading(self) -> None:
|
def reset_lens_shading(self) -> None:
|
||||||
|
|
@ -898,7 +852,7 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
"""
|
"""
|
||||||
with self._streaming_picamera(pause_stream=True):
|
with self._streaming_picamera(pause_stream=True):
|
||||||
recalibrate_utils.copy_alsc_section(self.default_tuning, self.tuning)
|
recalibrate_utils.copy_alsc_section(self.default_tuning, self.tuning)
|
||||||
self.initialise_picamera()
|
self._initialise_picamera()
|
||||||
|
|
||||||
@lt.thing_property
|
@lt.thing_property
|
||||||
def lens_shading_is_static(self) -> bool:
|
def lens_shading_is_static(self) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -256,7 +256,9 @@ def test_outer_scan_wo_sample_skip():
|
||||||
"""Test setup and teardown of the scan."""
|
"""Test setup and teardown of the scan."""
|
||||||
|
|
||||||
def _set_skip_background(mock_ss_thing):
|
def _set_skip_background(mock_ss_thing):
|
||||||
mock_ss_thing.thing_settings["skip_background"] = False
|
# As the Thing is not connected to a server we set the setting via the internal
|
||||||
|
# __dict__ to avoid triggering property emits that require a server
|
||||||
|
mock_ss_thing.__dict__["skip_background"] = False
|
||||||
|
|
||||||
mock_ss_thing, exec_info = _run_only_outer_scan(_set_skip_background)
|
mock_ss_thing, exec_info = _run_only_outer_scan(_set_skip_background)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue