From e7e3a08210f26c266951fec1c87b065dc716c5d9 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Sun, 27 Jul 2025 16:54:02 +0100 Subject: [PATCH] Camera settings exposed in UI defined by server. --- .../things/camera/__init__.py | 7 +- .../things/camera/picamera.py | 34 +++++++- .../things/camera/simulation.py | 16 +++- src/openflexure_microscope_server/ui.py | 63 ++++++++++++--- .../labThingsComponents/propertyControl.vue | 11 +-- .../serverSpecifiedPropertyControl.vue | 31 ++++++++ .../settingsComponents/cameraSettings.vue | 77 +++++-------------- 7 files changed, 159 insertions(+), 80 deletions(-) create mode 100644 webapp/src/components/labThingsComponents/serverSpecifiedPropertyControl.vue diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index 465ab97f..fcccaae8 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -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 diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index 349ad4bd..1f67741f 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -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). diff --git a/src/openflexure_microscope_server/things/camera/simulation.py b/src/openflexure_microscope_server/things/camera/simulation.py index 4aa790e4..5903891b 100644 --- a/src/openflexure_microscope_server/things/camera/simulation.py +++ b/src/openflexure_microscope_server/things/camera/simulation.py @@ -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")] diff --git a/src/openflexure_microscope_server/ui.py b/src/openflexure_microscope_server/ui.py index 7fad40d3..be2a5c12 100644 --- a/src/openflexure_microscope_server/ui.py +++ b/src/openflexure_microscope_server/ui.py @@ -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) diff --git a/webapp/src/components/labThingsComponents/propertyControl.vue b/webapp/src/components/labThingsComponents/propertyControl.vue index 27f9ab55..e3ce0cbb 100644 --- a/webapp/src/components/labThingsComponents/propertyControl.vue +++ b/webapp/src/components/labThingsComponents/propertyControl.vue @@ -98,9 +98,14 @@ export default { type: String, required: true }, + readBack: { + type: Boolean, + required: false, + default: false + }, readBackDelay: { type: Number, - default: undefined, + default: 1000, required: false } }, @@ -124,9 +129,6 @@ export default { return 1; } }, - readBack: function() { - return this.readBackDelay !== undefined; - }, propertyDescription: function() { try { return this.thingDescription(this.thingName).properties[ @@ -202,7 +204,6 @@ export default { writeProperty: async function() { try { let requestedValue = this.value; - console.log("writing", requestedValue); await this.writeThingProperty( this.thingName, this.propertyName, diff --git a/webapp/src/components/labThingsComponents/serverSpecifiedPropertyControl.vue b/webapp/src/components/labThingsComponents/serverSpecifiedPropertyControl.vue new file mode 100644 index 00000000..a0f81ab5 --- /dev/null +++ b/webapp/src/components/labThingsComponents/serverSpecifiedPropertyControl.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/webapp/src/components/tabContentComponents/settingsComponents/cameraSettings.vue b/webapp/src/components/tabContentComponents/settingsComponents/cameraSettings.vue index ddcef921..7d56cb8c 100644 --- a/webapp/src/components/tabContentComponents/settingsComponents/cameraSettings.vue +++ b/webapp/src/components/tabContentComponents/settingsComponents/cameraSettings.vue @@ -4,37 +4,14 @@

Automatic calibration

-

Manual camera settings

-
-
- -
-
+
+ +
@@ -47,7 +24,7 @@