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)

View file

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

View file

@ -0,0 +1,31 @@
<template>
<property-control
:thing="propertyData.thing"
:property-name="propertyData.property_name"
:thing-name="propertyData.thing"
:read-back="propertyData.read_back"
:read-back-delay="propertyData.read_back_delay"
/>
</template>
<script>
import PropertyControl from "./propertyControl.vue";
// Export main app
export default {
name: "ServerSpecifiedPropertyControl",
components: {
PropertyControl
},
props: {
propertyData: {
type: Object,
required: true,
}
}
};
</script>
<style lang="less"></style>

View file

@ -4,37 +4,14 @@
<div class="uk-width-large">
<h3>Automatic calibration</h3>
<cameraCalibrationSettings :camera-uri="cameraUri" />
<h3>Manual camera settings</h3>
<form @submit.prevent="applySettingsRequest">
<div class="uk-margin-small-bottom">
<ul uk-accordion="multiple: true">
<li class="uk-open">
<a class="uk-accordion-title" href="#">Pi Camera Settings</a>
<div class="uk-accordion-content">
<PropertyControl
label="Exposure Time (0-33251)"
property-name="exposure_time"
thing-name="camera"
:read-back-delay="1000"
/>
<PropertyControl
label="Analogue Gain"
property-name="analogue_gain"
thing-name="camera"
:read-back-delay="1000"
/>
<PropertyControl
label="Colour Gains"
property-name="colour_gains"
thing-name="camera"
:read-back-delay="1000"
/>
</div>
</li>
</ul>
</div>
</form>
<div class="uk-margin-small-bottom">
<server-specified-property-control
v-for="(setting, index) in manualCameraSettings"
:key="'cam_setting' + index"
:property-data="setting"
/>
</div>
</div>
<div id="mini-stream">
@ -47,7 +24,7 @@
<script>
import cameraCalibrationSettings from "./cameraSettingsComponents/cameraCalibrationSettings.vue";
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
import PropertyControl from "../../labThingsComponents/propertyControl.vue";
import ServerSpecifiedPropertyControl from "../../labThingsComponents/serverSpecifiedPropertyControl.vue";
// Export main app
export default {
@ -56,37 +33,12 @@ export default {
components: {
cameraCalibrationSettings,
miniStreamDisplay,
PropertyControl
ServerSpecifiedPropertyControl
},
data: function() {
data() {
return {
picamera: {
shutter_speed: undefined,
analog_gain: undefined,
digital_gain: undefined,
framerate: undefined,
awb_gains: undefined
},
mjpeg_bitrate: undefined,
stream_resolution: undefined,
jpeg_quality: undefined,
bitrateOptions: [
{ text: "Maximum (unlimited)", value: -1 },
{ text: "High (25Mbps)", value: 25000000 },
{ text: "Normal (17Mbps)", value: 17000000 },
{ text: "Low (5Mbps)", value: 5000000 },
{ text: "Very low (2.5Mbps)", value: 2500000 }
],
resolutionOptions: [
{ text: "Higher (832, 624)", value: [832, 624] },
{ text: "Normal (640, 480)", value: [640, 480] }
],
framerateOptions: [
{ text: "Normal (30fps)", value: 30 },
{ text: "Low (15fps)", value: 15 },
{ text: "Very low (10fps)", value: 10 }
]
manualCameraSettings: [],
};
},
@ -94,6 +46,13 @@ export default {
cameraUri: function() {
return `${this.$store.getters.baseUri}/camera/`;
}
},
async created() {
this.manualCameraSettings = await this.readThingProperty(
"camera",
"manual_camera_settings"
);
}
};
</script>