Camera settings exposed in UI defined by server.

This commit is contained in:
Julian Stirling 2025-07-27 16:54:02 +01:00
parent 3651013440
commit e7e3a08210
7 changed files with 159 additions and 80 deletions

View file

@ -20,7 +20,7 @@ import piexif
import labthings_fastapi as lt
from labthings_fastapi.types.numpy import NDArray
from openflexure_microscope_server.ui import ActionButton
from openflexure_microscope_server.ui import ActionButton, PropertyControl
from openflexure_microscope_server.background_detect import (
ColourChannelDetectLUV,
BackgroundDetectAlgorithm,
@ -487,6 +487,11 @@ class BaseCamera(lt.Thing):
"""The calibration actions that appear only in settings panel."""
return []
@lt.thing_property
def manual_camera_settings(self) -> list[PropertyControl]:
"""The camera settings to expose as property controls in the settings panel."""
return []
# Note that the default detector name is set at init. This is over written if
# setting is loaded from disk.
@lt.thing_setting

View file

@ -35,7 +35,12 @@ from picamera2.outputs import Output
import labthings_fastapi as lt
from labthings_fastapi.exceptions import NotConnectedToServerError
from openflexure_microscope_server.ui import ActionButton, action_button_for
from openflexure_microscope_server.ui import (
ActionButton,
PropertyControl,
action_button_for,
property_control_for,
)
from . import picamera_recalibrate_utils as recalibrate_utils
from . import BaseCamera, JPEGBlob, ArrayModel
@ -833,6 +838,33 @@ class StreamingPiCamera2(BaseCamera):
),
]
@lt.thing_property
def manual_camera_settings(self) -> list[PropertyControl]:
"""The camera settings to expose as property controls in the settings panel."""
return [
property_control_for(
self,
"exposure_time",
label="Exposure Time (0-33251)",
read_back=True,
read_back_delay=1000,
),
property_control_for(
self,
"analogue_gain",
label="Analogue Gain",
read_back=True,
read_back_delay=1000,
),
property_control_for(
self,
"colour_gains",
label="Colour Gains",
read_back=True,
read_back_delay=1000,
),
]
@lt.thing_property
def lens_shading_tables(self) -> Optional[LensShading]:
"""The current lens shading (i.e. flat-field correction).

View file

@ -21,7 +21,12 @@ from scipy.ndimage import gaussian_filter
import labthings_fastapi as lt
from openflexure_microscope_server.ui import ActionButton, action_button_for
from openflexure_microscope_server.ui import (
ActionButton,
PropertyControl,
action_button_for,
property_control_for,
)
from . import BaseCamera, JPEGBlob, ArrayModel
from ..stage import BaseStage
@ -166,7 +171,7 @@ class SimulatedCamera(BaseCamera):
)
# Add noise and convert to uint8
image += RNG.normal(scale=2, size=self.shape).astype("int16")
image += RNG.normal(scale=self.noise_level, size=self.shape).astype("int16")
image[image < 0] = 0
image[image > 255] = 255
return image.astype("uint8")
@ -221,6 +226,8 @@ class SimulatedCamera(BaseCamera):
return self._capture_thread.is_alive()
return False
noise_level = lt.ThingProperty(float, 2.0)
def _capture_frames(self):
portal = lt.get_blocking_portal(self)
while self._capture_enabled:
@ -306,3 +313,8 @@ class SimulatedCamera(BaseCamera):
action_button_for(self.load_sample, submit_label="Load Sample"),
action_button_for(self.remove_sample, submit_label="Remove Sample"),
]
@lt.thing_property
def manual_camera_settings(self) -> list[PropertyControl]:
"""The camera settings to expose as property controls in the settings panel."""
return [property_control_for(self, "noise_level", label="Noise Level")]

View file

@ -3,18 +3,6 @@
from pydantic import BaseModel
def action_button_for(action, **kwargs):
"""Create an action button for the specified Thing Action.
:param action: The thing action to create a button for.
:param kwargs: Any attribute of `ActionButton` except for ``thing`` or ``action``.
"""
thing = action.args[0]
thing_path = thing.path.strip("/")
action_name = action.func.__name__
return ActionButton(thing=thing_path, action=action_name, **kwargs)
class ActionButton(BaseModel):
"""The data required for creating an actionButton in Vue.
@ -53,3 +41,54 @@ class ActionButton(BaseModel):
success_message: str = "Success!"
"""The message to show on successful completion."""
def action_button_for(action, **kwargs) -> ActionButton:
"""Create a ActionButton data for the specified Thing Action.
:param action: The thing action to create a button for.
:param kwargs: Any attribute of `ActionButton` except for ``thing`` or ``action``.
"""
thing = action.args[0]
thing_path = thing.path.strip("/")
action_name = action.func.__name__
return ActionButton(thing=thing_path, action=action_name, **kwargs)
class PropertyControl(BaseModel):
"""The data required for creating an actionButton in Vue.
Currently this cannot be used to submitData, or to submitOnEvent.
"""
thing: str
"""The Thing "path" for the Thing instance."""
property_name: str
"""The name of the property (or setting)."""
label: str
"""The label to show in the UI"""
read_back: bool = False
"""Whether or not to read back the property after setting.
This is useful for hardware settings that may be coerced to the closest value.
"""
read_back_delay: int = 1000
"""The delay in ms before reading back the property."""
def property_control_for(thing, property_name, **kwargs) -> PropertyControl:
"""Create an PropertyControl data for the specified Thing Property.
:param thing: The instance of the thing that has the property to be controlled.
:param property_name: The name of the property to create a control for.
:param kwargs: Any attribute of `PropertyControl` except for ``thing`` or
``property_name``. If label is not set here it will be the property name.
"""
thing_path = thing.path.strip("/")
if "label" not in kwargs:
kwargs["label"] = property_name
return PropertyControl(thing=thing_path, property_name=property_name, **kwargs)