diff --git a/ofm_config_simulation.json b/ofm_config_simulation.json index 8cc7602f..367088de 100644 --- a/ofm_config_simulation.json +++ b/ofm_config_simulation.json @@ -8,7 +8,8 @@ "smart_scan": { "class": "openflexure_microscope_server.things.smart_scan:SmartScanThing", "kwargs": { - "scans_folder": "./openflexure/scans/" + "scans_folder": "./openflexure/scans/", + "default_workflow": "histo_scan_workflow" } }, "histo_scan_workflow": "openflexure_microscope_server.things.scan_workflows:HistoScanWorkflow", diff --git a/src/openflexure_microscope_server/things/scan_workflows.py b/src/openflexure_microscope_server/things/scan_workflows.py index 39548663..58563457 100644 --- a/src/openflexure_microscope_server/things/scan_workflows.py +++ b/src/openflexure_microscope_server/things/scan_workflows.py @@ -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_stage_mapping import CameraStageMapper +from openflexure_microscope_server.ui import PropertyControl, property_control_for SettingModelType = TypeVar("SettingModelType", bound=BaseModel) @@ -44,6 +45,11 @@ class ScanWorkflow(Generic[SettingModelType], lt.Thing): 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] # 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" ) + @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): """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. """ + 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 _planner_cls: type[ScanPlanner] = SmartSpiral # Thing Slots @@ -428,3 +451,24 @@ class HistoScanWorkflow(ScanWorkflow[HistoScanSettingsModel]): focus_height = None 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)"), + ] diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 9768ef29..2a44474a 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -1,8 +1,8 @@ """The core sample scanning functionality for the OpenFlexure Microscope. -SmartScan provides sample scanning functionality including automatic background -detection (via the ``CameraThing``) and automatic path planning via -`scan_planners`. It manages the directories of past scans via `scan_directories`. +SmartScan provides sample scanning functionality. This functionality can be customised +by different ``ScanWorkflow`` Things which control the path planning and acquisition +routines. It manages the directories of past scans via `scan_directories`. It also controls external processes for live stitching composite images, and the creation of the final stitched images. """ @@ -20,6 +20,7 @@ from typing import ( Mapping, Optional, ParamSpec, + Self, TypeVar, ) @@ -30,10 +31,11 @@ from pydantic import BaseModel, PlainSerializer import labthings_fastapi as lt from openflexure_microscope_server import scan_directories, stitching +from openflexure_microscope_server.utilities import coerce_thing_selector # Things from .camera import BaseCamera -from .scan_workflows import HistoScanWorkflow, ScanWorkflow +from .scan_workflows import ScanWorkflow from .stage import BaseStage T = TypeVar("T") @@ -127,10 +129,13 @@ class SmartScanThing(lt.Thing): _cam: BaseCamera = lt.thing_slot() _stage: BaseStage = lt.thing_slot() - _workflow: HistoScanWorkflow = lt.thing_slot() + _all_workflows: Mapping[str, ScanWorkflow] = lt.thing_slot() 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: """Initialise a SmartScanThing saving to and loading from the input directory. @@ -141,6 +146,36 @@ class SmartScanThing(lt.Thing): super().__init__(thing_server_interface) self._scan_dir_manager = scan_directories.ScanDirectoryManager(scans_folder) 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 diff --git a/webapp/src/components/tabContentComponents/slideScanContent.vue b/webapp/src/components/tabContentComponents/slideScanContent.vue index 0fdf6e1f..0290a07f 100644 --- a/webapp/src/components/tabContentComponents/slideScanContent.vue +++ b/webapp/src/components/tabContentComponents/slideScanContent.vue @@ -1,51 +1,32 @@