Get UI working for background detector things

This commit is contained in:
Julian Stirling 2026-01-13 19:05:24 +00:00
parent 44860bf777
commit 3e8874188e
5 changed files with 70 additions and 38 deletions

View file

@ -13,6 +13,8 @@ from pydantic import BaseModel
import labthings_fastapi as lt import labthings_fastapi as lt
from openflexure_microscope_server.ui import PropertyControl, property_control_for
SettingsType = TypeVar("SettingsType", bound=BaseModel) SettingsType = TypeVar("SettingsType", bound=BaseModel)
BackgroundType = TypeVar("BackgroundType", bound=BaseModel) BackgroundType = TypeVar("BackgroundType", bound=BaseModel)
@ -32,6 +34,8 @@ class ChannelBlankError(RuntimeError):
class BackgroundDetectAlgorithm(lt.Thing): class BackgroundDetectAlgorithm(lt.Thing):
"""The base class for defining background detect algorithms.""" """The base class for defining background detect algorithms."""
display_name: str = lt.property(default="Base Detector", readonly=True)
@lt.property @lt.property
def ready(self) -> bool: def ready(self) -> bool:
"""Whether the background detector is ready.""" """Whether the background detector is ready."""
@ -59,6 +63,13 @@ class BackgroundDetectAlgorithm(lt.Thing):
"Each background detect algorithm must implement an set_background method." "Each background detect algorithm must implement an set_background method."
) )
@lt.property
def settings_ui(self) -> list[PropertyControl]:
"""A list of PropertyControl objects to create the settings in the UI."""
raise NotImplementedError(
"Each background detect algorithm must implement an settings_ui method."
)
class ColourChannelDetectLUV(BackgroundDetectAlgorithm): class ColourChannelDetectLUV(BackgroundDetectAlgorithm):
"""Compare images with a known background in LUV colourspace. """Compare images with a known background in LUV colourspace.
@ -68,6 +79,8 @@ class ColourChannelDetectLUV(BackgroundDetectAlgorithm):
intuitive way. intuitive way.
""" """
display_name: str = lt.property(default="Colour Channel (LUV)", readonly=True)
background_means: Optional[list[float]] = lt.setting(default=None, readonly=True) background_means: Optional[list[float]] = lt.setting(default=None, readonly=True)
"""The mean of each channel in the colourspace.""" """The mean of each channel in the colourspace."""
background_stds: Optional[list[float]] = lt.setting(default=None, readonly=True) background_stds: Optional[list[float]] = lt.setting(default=None, readonly=True)
@ -91,10 +104,20 @@ class ColourChannelDetectLUV(BackgroundDetectAlgorithm):
image to be labeled as containing sample. image to be labeled as containing sample.
""" """
@lt.property
def settings_ui(self) -> list[PropertyControl]:
"""A list of PropertyControl objects to create the settings in the UI."""
return [
property_control_for(self, "channel_tolerance", label="Channel Tolerance"),
property_control_for(
self, "min_sample_coverage", label="Sample Coverage Required (%)"
),
]
@lt.property @lt.property
def ready(self) -> bool: def ready(self) -> bool:
"""Whether the background detector is ready.""" """Whether the background detector is ready."""
return not (self.background_means is None or self.background_stds is None) return self.background_means is not None and self.background_stds is not None
def background_mask(self, image: np.ndarray) -> np.ndarray: def background_mask(self, image: np.ndarray) -> np.ndarray:
"""Calculate a binary image, showing whether each pixel is background. """Calculate a binary image, showing whether each pixel is background.
@ -180,6 +203,8 @@ class ChannelDeviationLUV(BackgroundDetectAlgorithm):
to the median standard deviation for a grid of background images. to the median standard deviation for a grid of background images.
""" """
display_name: str = lt.property(default="Channel Deviation (LUV)", readonly=True)
background_stds: Optional[list[float]] = lt.setting(default=None, readonly=True) background_stds: Optional[list[float]] = lt.setting(default=None, readonly=True)
"""The standard deviation of each channel in the colourspace.""" """The standard deviation of each channel in the colourspace."""
@ -203,6 +228,21 @@ class ChannelDeviationLUV(BackgroundDetectAlgorithm):
image to be labeled as containing sample. image to be labeled as containing sample.
""" """
@lt.property
def settings_ui(self) -> list[PropertyControl]:
"""A list of PropertyControl objects to create the settings in the UI."""
return [
property_control_for(self, "channel_tolerance", label="Channel Tolerance"),
property_control_for(
self, "min_sample_coverage", label="Sample Coverage Required (%)"
),
]
@lt.property
def ready(self) -> bool:
"""Whether the background detector is ready."""
return self.background_stds is not None
def get_sample_coverage(self, image: np.ndarray) -> float: def get_sample_coverage(self, image: np.ndarray) -> float:
"""Return the percentage of the input image that is background. """Return the percentage of the input image that is background.

View file

@ -173,6 +173,10 @@ class BaseCamera(lt.Thing):
dictionary in this function. Configuration will be added at a later date. dictionary in this function. Configuration will be added at a later date.
""" """
super().__init__(thing_server_interface) super().__init__(thing_server_interface)
# Default is never updated but is used if the value set from settings is
# incorrect. In the future a better way to set defaults for thing slot mappings
# would be ideal.
self._default_detector = "bg_channel_deviations_luv"
self._detector_name = "bg_channel_deviations_luv" self._detector_name = "bg_channel_deviations_luv"
def __enter__(self) -> Self: def __enter__(self) -> Self:
@ -591,6 +595,9 @@ class BaseCamera(lt.Thing):
@property @property
def active_detector(self) -> BackgroundDetectAlgorithm: def active_detector(self) -> BackgroundDetectAlgorithm:
"""The active background detector instance.""" """The active background detector instance."""
if self.detector_name not in self.background_detectors:
self.logger.warning(f"No detector named {self.detector_name}")
self.detector_name = self._default_detector
return self.background_detectors[self.detector_name] return self.background_detectors[self.detector_name]
@lt.action @lt.action

View file

@ -181,7 +181,7 @@ class SimulatedCamera(BaseCamera):
@lt.property @lt.property
def calibration_required(self) -> bool: def calibration_required(self) -> bool:
"""Whether the camera needs calibrating.""" """Whether the camera needs calibrating."""
return not self.background_detector_status.ready return not self.active_detector.ready
def generate_sprites(self) -> None: def generate_sprites(self) -> None:
"""Generate sprites to populate the image.""" """Generate sprites to populate the image."""

View file

@ -234,7 +234,7 @@ class SmartScanThing(lt.Thing):
self._csm.assert_calibration() self._csm.assert_calibration()
if self.skip_background: if self.skip_background:
if not self._cam.background_detector_status.ready: if not self._cam.active_detector.ready:
raise RuntimeError( raise RuntimeError(
"Background is not set: you need to calibrate background detection." "Background is not set: you need to calibrate background detection."
) )

View file

@ -5,18 +5,13 @@
<li> <li>
<a class="uk-accordion-title" href="#">Configure</a> <a class="uk-accordion-title" href="#">Configure</a>
<div class="uk-accordion-content"> <div class="uk-accordion-content">
<h4 v-if="backgroundDetectorName" class="detector-name"> <h4 v-if="backgroundDetectorDisplayName" class="detector-name">
{{ backgroundDetectorName }} {{ backgroundDetectorDisplayName }}
</h4> </h4>
<input-from-schema <server-specified-property-control
v-if="backgroundDetectorStatus" v-for="(setting, index) in backgroundDetectorSettings"
v-model="backgroundDetectorStatus.settings" :key="'detector_setting' + index"
:data-schema="backgroundDetectorStatus.settings_schema" :property-data="setting"
label=""
:animate="animate"
@requestUpdate="readSettings"
@sendValue="writeSettings"
@animationShown="resetAnimate"
/> />
</div> </div>
</li> </li>
@ -51,33 +46,26 @@
<script> <script>
import ActionButton from "../../labThingsComponents/actionButton.vue"; import ActionButton from "../../labThingsComponents/actionButton.vue";
import InputFromSchema from "../../labThingsComponents/inputFromSchema.vue"; import ServerSpecifiedPropertyControl from "../../labThingsComponents/serverSpecifiedPropertyControl.vue";
export default { export default {
components: { components: {
ActionButton, ActionButton,
InputFromSchema, ServerSpecifiedPropertyControl,
}, },
data() { data() {
return { return {
backgroundDetectorStatus: undefined, ready: false,
backgroundDetectorName: undefined, backgroundDetectorName: undefined,
backgroundDetectorDisplayName: undefined,
backgroundDetectorSettings: [],
animate: false, animate: false,
}; };
}, },
computed: {
ready() {
const status = this.backgroundDetectorStatus;
return status && status.ready === true;
},
},
async created() { async created() {
this.backgroundDetectorStatus = await this.readThingProperty( this.readSettings();
"camera",
"background_detector_status",
);
}, },
methods: { methods: {
@ -95,18 +83,15 @@ export default {
}, },
readSettings: async function () { readSettings: async function () {
this.backgroundDetectorName = await this.readThingProperty("camera", "detector_name"); this.backgroundDetectorName = await this.readThingProperty("camera", "detector_name");
this.backgroundDetectorStatus = await this.readThingProperty( this.ready = await this.readThingProperty(this.backgroundDetectorName, "ready");
"camera", this.backgroundDetectorSettings = await this.readThingProperty(
"background_detector_status", this.backgroundDetectorName,
"settings_ui",
);
this.backgroundDetectorDisplayName = await this.readThingProperty(
this.backgroundDetectorName,
"display_name",
); );
},
writeSettings: async function (requestedValue) {
await this.invokeAction("camera", "update_detector_settings", { data: requestedValue });
this.animate = true;
this.readSettings();
},
resetAnimate: function () {
this.animate = false;
}, },
}, },
}; };