Dynamic UI for scan workflows
This commit is contained in:
parent
2a3a54b936
commit
21b779a4a5
4 changed files with 157 additions and 66 deletions
|
|
@ -8,7 +8,8 @@
|
||||||
"smart_scan": {
|
"smart_scan": {
|
||||||
"class": "openflexure_microscope_server.things.smart_scan:SmartScanThing",
|
"class": "openflexure_microscope_server.things.smart_scan:SmartScanThing",
|
||||||
"kwargs": {
|
"kwargs": {
|
||||||
"scans_folder": "./openflexure/scans/"
|
"scans_folder": "./openflexure/scans/",
|
||||||
|
"default_workflow": "histo_scan_workflow"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"histo_scan_workflow": "openflexure_microscope_server.things.scan_workflows:HistoScanWorkflow",
|
"histo_scan_workflow": "openflexure_microscope_server.things.scan_workflows:HistoScanWorkflow",
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ from openflexure_microscope_server.things.background_detect import (
|
||||||
)
|
)
|
||||||
from openflexure_microscope_server.things.camera import BaseCamera
|
from openflexure_microscope_server.things.camera import BaseCamera
|
||||||
from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper
|
from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper
|
||||||
|
from openflexure_microscope_server.ui import PropertyControl, property_control_for
|
||||||
|
|
||||||
SettingModelType = TypeVar("SettingModelType", bound=BaseModel)
|
SettingModelType = TypeVar("SettingModelType", bound=BaseModel)
|
||||||
|
|
||||||
|
|
@ -44,6 +45,11 @@ class ScanWorkflow(Generic[SettingModelType], lt.Thing):
|
||||||
scan planning, acquisition routine.
|
scan planning, acquisition routine.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
display_name: str = lt.property(default="Base Workflow", readonly=True)
|
||||||
|
ui_blurb: str = lt.property(
|
||||||
|
default="If you see this message, something is wrong.", readonly=True
|
||||||
|
)
|
||||||
|
|
||||||
_settings_model: type[SettingModelType]
|
_settings_model: type[SettingModelType]
|
||||||
|
|
||||||
# All workflows must have a set class for scan planning
|
# All workflows must have a set class for scan planning
|
||||||
|
|
@ -106,6 +112,13 @@ class ScanWorkflow(Generic[SettingModelType], lt.Thing):
|
||||||
"Each specific ScanWorkflow must implement an acquisition routine"
|
"Each specific ScanWorkflow must implement an acquisition routine"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@lt.property
|
||||||
|
def settings_ui(self) -> list[PropertyControl]:
|
||||||
|
"""A list of PropertyControl objects to create the settings in the scan tab."""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Each scan workflow must implement a settings_ui method."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class HistoScanSettingsModel(BaseModel):
|
class HistoScanSettingsModel(BaseModel):
|
||||||
"""The settings for a scan with the HistoScanWorkflow.
|
"""The settings for a scan with the HistoScanWorkflow.
|
||||||
|
|
@ -129,6 +142,16 @@ class HistoScanWorkflow(ScanWorkflow[HistoScanSettingsModel]):
|
||||||
the centre position, scanning only where it detects sample.
|
the centre position, scanning only where it detects sample.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
display_name: str = lt.property(default="Histo Scan", readonly=True)
|
||||||
|
ui_blurb: str = lt.property(
|
||||||
|
default=(
|
||||||
|
"This scan workflow is optimised for scanning H&E stained biopsies. It"
|
||||||
|
"spirals out from the starting location, scanning only where it detects"
|
||||||
|
"sample. It also works well for many other flat, well-featured samples."
|
||||||
|
),
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
_settings_model = HistoScanSettingsModel
|
_settings_model = HistoScanSettingsModel
|
||||||
_planner_cls: type[ScanPlanner] = SmartSpiral
|
_planner_cls: type[ScanPlanner] = SmartSpiral
|
||||||
# Thing Slots
|
# Thing Slots
|
||||||
|
|
@ -428,3 +451,24 @@ class HistoScanWorkflow(ScanWorkflow[HistoScanSettingsModel]):
|
||||||
focus_height = None
|
focus_height = None
|
||||||
|
|
||||||
return imaged, focus_height
|
return imaged, focus_height
|
||||||
|
|
||||||
|
@lt.property
|
||||||
|
def settings_ui(self) -> list[PropertyControl]:
|
||||||
|
"""A list of PropertyControl objects to create the settings in the scan tab."""
|
||||||
|
return [
|
||||||
|
property_control_for(self, "overlap", label="Image Overlap (0.1-0.7)"),
|
||||||
|
property_control_for(
|
||||||
|
self, "skip_background", label="Detect and Skip Empty Fields "
|
||||||
|
),
|
||||||
|
property_control_for(
|
||||||
|
self, "stack_images_to_save", label="Images in Stack to Save"
|
||||||
|
),
|
||||||
|
property_control_for(
|
||||||
|
self,
|
||||||
|
"stack_min_images_to_test",
|
||||||
|
label="Minimum number of images to test for focus",
|
||||||
|
),
|
||||||
|
property_control_for(self, "stack_dz", label="Stack dz (steps)"),
|
||||||
|
property_control_for(self, "autofocus_dz", label="Autofocus Range (steps)"),
|
||||||
|
property_control_for(self, "max_range", label="Maximum Distance (steps)"),
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
"""The core sample scanning functionality for the OpenFlexure Microscope.
|
"""The core sample scanning functionality for the OpenFlexure Microscope.
|
||||||
|
|
||||||
SmartScan provides sample scanning functionality including automatic background
|
SmartScan provides sample scanning functionality. This functionality can be customised
|
||||||
detection (via the ``CameraThing``) and automatic path planning via
|
by different ``ScanWorkflow`` Things which control the path planning and acquisition
|
||||||
`scan_planners`. It manages the directories of past scans via `scan_directories`.
|
routines. It manages the directories of past scans via `scan_directories`.
|
||||||
It also controls external processes for live stitching composite images, and
|
It also controls external processes for live stitching composite images, and
|
||||||
the creation of the final stitched images.
|
the creation of the final stitched images.
|
||||||
"""
|
"""
|
||||||
|
|
@ -20,6 +20,7 @@ from typing import (
|
||||||
Mapping,
|
Mapping,
|
||||||
Optional,
|
Optional,
|
||||||
ParamSpec,
|
ParamSpec,
|
||||||
|
Self,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -30,10 +31,11 @@ from pydantic import BaseModel, PlainSerializer
|
||||||
import labthings_fastapi as lt
|
import labthings_fastapi as lt
|
||||||
|
|
||||||
from openflexure_microscope_server import scan_directories, stitching
|
from openflexure_microscope_server import scan_directories, stitching
|
||||||
|
from openflexure_microscope_server.utilities import coerce_thing_selector
|
||||||
|
|
||||||
# Things
|
# Things
|
||||||
from .camera import BaseCamera
|
from .camera import BaseCamera
|
||||||
from .scan_workflows import HistoScanWorkflow, ScanWorkflow
|
from .scan_workflows import ScanWorkflow
|
||||||
from .stage import BaseStage
|
from .stage import BaseStage
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
@ -127,10 +129,13 @@ class SmartScanThing(lt.Thing):
|
||||||
|
|
||||||
_cam: BaseCamera = lt.thing_slot()
|
_cam: BaseCamera = lt.thing_slot()
|
||||||
_stage: BaseStage = lt.thing_slot()
|
_stage: BaseStage = lt.thing_slot()
|
||||||
_workflow: HistoScanWorkflow = lt.thing_slot()
|
_all_workflows: Mapping[str, ScanWorkflow] = lt.thing_slot()
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, thing_server_interface: lt.ThingServerInterface, scans_folder: str
|
self,
|
||||||
|
thing_server_interface: lt.ThingServerInterface,
|
||||||
|
scans_folder: str,
|
||||||
|
default_workflow: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialise a SmartScanThing saving to and loading from the input directory.
|
"""Initialise a SmartScanThing saving to and loading from the input directory.
|
||||||
|
|
||||||
|
|
@ -141,6 +146,36 @@ class SmartScanThing(lt.Thing):
|
||||||
super().__init__(thing_server_interface)
|
super().__init__(thing_server_interface)
|
||||||
self._scan_dir_manager = scan_directories.ScanDirectoryManager(scans_folder)
|
self._scan_dir_manager = scan_directories.ScanDirectoryManager(scans_folder)
|
||||||
self._scan_lock = threading.Lock()
|
self._scan_lock = threading.Lock()
|
||||||
|
self._default_workflow = default_workflow
|
||||||
|
self._workflow_name: Optional[str] = None
|
||||||
|
|
||||||
|
def __enter__(self) -> Self:
|
||||||
|
"""Open hardware connection when the Thing context manager is opened."""
|
||||||
|
self._workflow_name = coerce_thing_selector(
|
||||||
|
thing_mapping=self._all_workflows,
|
||||||
|
selected=self.workflow_name,
|
||||||
|
default=self._default_workflow,
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
# Note that the default detector name is set at init. This is over written if
|
||||||
|
# setting is loaded from disk.
|
||||||
|
@lt.setting
|
||||||
|
def workflow_name(self) -> Optional[str]:
|
||||||
|
"""The name of the scan workflow selector."""
|
||||||
|
return self._workflow_name
|
||||||
|
|
||||||
|
@workflow_name.setter
|
||||||
|
def _set_workflow_name(self, name: str) -> None:
|
||||||
|
"""Validate and set workflow_name."""
|
||||||
|
if name not in self._all_workflows:
|
||||||
|
self.logger.warning(f"{name} is not a valid scan workflow name.")
|
||||||
|
self._workflow_name = name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _workflow(self) -> ScanWorkflow:
|
||||||
|
"""The active scan workflow object."""
|
||||||
|
return self._all_workflows[self.workflow_name]
|
||||||
|
|
||||||
_ongoing_scan: Optional[scan_directories.ScanDirectory] = None
|
_ongoing_scan: Optional[scan_directories.ScanDirectory] = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,32 @@
|
||||||
<template>
|
<template>
|
||||||
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
|
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
|
||||||
<div class="control-component uk-padding-small">
|
<div class="control-component uk-padding-small">
|
||||||
<div v-show="!scanning" class="uk-padding-small">
|
<div v-show="!scanning" v-observe-visibility="visibilityChanged" class="uk-padding-small">
|
||||||
<ul uk-accordion="multiple: true">
|
<ul uk-accordion="multiple: true">
|
||||||
<li>
|
<li>
|
||||||
<a class="uk-accordion-title" href="#">Configure</a>
|
<a class="uk-accordion-title" href="#">Scan Settings</a>
|
||||||
|
<div class="uk-accordion-content">
|
||||||
|
<h4 v-if="workflowDisplayName" class="workflow-name">
|
||||||
|
{{ workflowDisplayName }}
|
||||||
|
</h4>
|
||||||
|
<p class="workflow-blurb">{{ workflowBlurb }}</p>
|
||||||
|
<div
|
||||||
|
v-for="(setting, index) in workflowSettings"
|
||||||
|
:key="'detector_setting' + index"
|
||||||
|
class="uk-margin"
|
||||||
|
>
|
||||||
|
<server-specified-property-control :property-data="setting" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="uk-open">
|
||||||
|
<a class="uk-accordion-title" href="#">Stitching Settings</a>
|
||||||
<div class="uk-accordion-content">
|
<div class="uk-accordion-content">
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
<propertyControl
|
<propertyControl
|
||||||
thing-name="smart_scan"
|
thing-name="smart_scan"
|
||||||
property-name="max_range"
|
property-name="stitch_automatically"
|
||||||
label="Maximum Distance (steps)"
|
label="Automatically Stitch Images Together"
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="uk-margin">
|
|
||||||
<propertyControl
|
|
||||||
thing-name="smart_scan"
|
|
||||||
property-name="autofocus_dz"
|
|
||||||
label="Autofocus Range (steps)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="uk-margin">
|
|
||||||
<propertyControl
|
|
||||||
thing-name="autofocus"
|
|
||||||
property-name="stack_dz"
|
|
||||||
label="Stack dz (steps)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="uk-margin">
|
|
||||||
<propertyControl
|
|
||||||
thing-name="autofocus"
|
|
||||||
property-name="stack_images_to_save"
|
|
||||||
label="Images in Stack to Save"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="uk-margin">
|
|
||||||
<propertyControl
|
|
||||||
thing-name="autofocus"
|
|
||||||
property-name="stack_min_images_to_test"
|
|
||||||
label="Minimum number of images to test for focus"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="uk-margin">
|
|
||||||
<propertyControl
|
|
||||||
thing-name="smart_scan"
|
|
||||||
property-name="overlap"
|
|
||||||
label="Image Overlap (0-1)"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
|
|
@ -57,25 +38,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="uk-open">
|
|
||||||
<a class="uk-accordion-title" href="#">Scan Settings</a>
|
|
||||||
<div class="uk-accordion-content">
|
|
||||||
<div class="uk-margin">
|
|
||||||
<propertyControl
|
|
||||||
thing-name="smart_scan"
|
|
||||||
property-name="skip_background"
|
|
||||||
label="Detect and Skip Empty Fields"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="uk-margin">
|
|
||||||
<propertyControl
|
|
||||||
thing-name="smart_scan"
|
|
||||||
property-name="stitch_automatically"
|
|
||||||
label="Automatically Stitch Images Together"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<label class="uk-form-label" for="form-stacked-text">Sample ID</label>
|
<label class="uk-form-label" for="form-stacked-text">Sample ID</label>
|
||||||
<div class="uk-form-controls">
|
<div class="uk-form-controls">
|
||||||
|
|
@ -149,6 +111,7 @@
|
||||||
<script>
|
<script>
|
||||||
import streamDisplay from "./streamContent.vue";
|
import streamDisplay from "./streamContent.vue";
|
||||||
import propertyControl from "../labThingsComponents/propertyControl.vue";
|
import propertyControl from "../labThingsComponents/propertyControl.vue";
|
||||||
|
import ServerSpecifiedPropertyControl from "../labThingsComponents/serverSpecifiedPropertyControl.vue";
|
||||||
import actionLogDisplay from "../labThingsComponents/actionLogDisplay.vue";
|
import actionLogDisplay from "../labThingsComponents/actionLogDisplay.vue";
|
||||||
import actionProgressBar from "../labThingsComponents/actionProgressBar.vue";
|
import actionProgressBar from "../labThingsComponents/actionProgressBar.vue";
|
||||||
import MiniStreamDisplay from "../genericComponents/miniStreamDisplay.vue";
|
import MiniStreamDisplay from "../genericComponents/miniStreamDisplay.vue";
|
||||||
|
|
@ -160,6 +123,7 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
streamDisplay,
|
streamDisplay,
|
||||||
propertyControl,
|
propertyControl,
|
||||||
|
ServerSpecifiedPropertyControl,
|
||||||
actionLogDisplay,
|
actionLogDisplay,
|
||||||
actionProgressBar,
|
actionProgressBar,
|
||||||
MiniStreamDisplay,
|
MiniStreamDisplay,
|
||||||
|
|
@ -177,6 +141,10 @@ export default {
|
||||||
log: [],
|
log: [],
|
||||||
lastStitchedImage: null,
|
lastStitchedImage: null,
|
||||||
scan_name: "",
|
scan_name: "",
|
||||||
|
workflowName: undefined,
|
||||||
|
workflowSettings: [],
|
||||||
|
workflowDisplayName: undefined,
|
||||||
|
workflowBlurb: undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -189,12 +157,49 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
this.readSettings();
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
visibilityChanged(isVisible) {
|
||||||
|
if (isVisible) {
|
||||||
|
this.readSettings();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async readSettings() {
|
||||||
|
this.workflowName = await this.readThingProperty("smart_scan", "workflow_name");
|
||||||
|
if (this.workflowName) {
|
||||||
|
this.ready = await this.readThingProperty(this.workflowName, "ready");
|
||||||
|
this.workflowSettings = await this.readThingProperty(this.workflowName, "settings_ui");
|
||||||
|
console.log(this.workflowSettings);
|
||||||
|
this.workflowDisplayName = await this.readThingProperty(this.workflowName, "display_name");
|
||||||
|
this.workflowBlurb = await this.readThingProperty(this.workflowName, "ui_blurb");
|
||||||
|
}
|
||||||
|
},
|
||||||
onScanError: function (error) {
|
onScanError: function (error) {
|
||||||
this.scanRunning = false;
|
this.scanRunning = false;
|
||||||
this.modalError(error);
|
this.modalError(error);
|
||||||
},
|
},
|
||||||
correlateCurrentScan() {},
|
/**
|
||||||
|
* Transition the UI into "scanning" mode and begin polling for scan updates.
|
||||||
|
*
|
||||||
|
* IMPORTANT:
|
||||||
|
* This method does NOT start a scan on the server.
|
||||||
|
*
|
||||||
|
* The <ActionButton> component is responsible for:
|
||||||
|
* - initiating the scan action on the backend when the user clicks the button
|
||||||
|
* - detecting and resuming an already-running scan when the page loads
|
||||||
|
*
|
||||||
|
* As a result, this method may be invoked in two cases:
|
||||||
|
* 1. Immediately after the user clicks "Start Smart Scan"
|
||||||
|
* 2. Automatically on page load if <ActionButton> detects an ongoing scan
|
||||||
|
*
|
||||||
|
* This function only:
|
||||||
|
* - updates local UI state to reflect that scanning is in progress
|
||||||
|
* - clears any previous preview image
|
||||||
|
* - starts the polling loop that fetches scan progress and preview images
|
||||||
|
*/
|
||||||
startScanning() {
|
startScanning() {
|
||||||
this.lastStitchedImage = null;
|
this.lastStitchedImage = null;
|
||||||
this.scanning = true;
|
this.scanning = true;
|
||||||
|
|
@ -233,4 +238,10 @@ export default {
|
||||||
.control-component {
|
.control-component {
|
||||||
width: 33%;
|
width: 33%;
|
||||||
}
|
}
|
||||||
|
.workflow-name {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.workflow-blurb {
|
||||||
|
color: #a2a2a2;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue