A start to implementing ScanWorkflow

This commit is contained in:
Julian Stirling 2026-01-14 13:35:23 +00:00
parent 5762fc5947
commit 5ce74cad8a
2 changed files with 60 additions and 8 deletions

View file

@ -0,0 +1,50 @@
from typing import Mapping, Optional
import labthings_fastapi as lt
from openflexure_microscope_server.scan_planners import ScanPlanner, SmartSpiral
from openflexure_microscope_server.things.autofocus import AutofocusThing
from openflexure_microscope_server.things.background_detect import (
BackgroundDetectAlgorithm,
ChannelDeviationLUV,
)
class ScanWorkflow(lt.Thing):
"""A base class for all Scanworkflows.
Scan workflows set the behaviour of a scan, inclduing the background detection,
scan planning, aquisition routine.
"""
_detector: Optional[BackgroundDetectAlgorithm | Mapping[str, BackgroundDetectAlgorithm]] = lt.thing_slot()
_planner_cls: type[ScanPlanner]
@lt.property
def ready(self) -> bool:
"""Whether this scanworkflow is ready to start."""
raise NotImplementedError(
"Each specific ScanWorkflow must implement a ready property."
)
def pre_scan_routine(self) -> None:
raise NotImplementedError(
"Each specific ScanWorkflow must implement a pre-scan routine."
)
class HistoScanWorkflow(ScanWorkflow):
_detector: ChannelDeviationLUV = lt.thing_slot()
_planner_cls: type[ScanPlanner] = SmartSpiral
_autofocus: AutofocusThing = lt.thing_slot()
autofocus_dz: int = lt.setting(default=1000)
"""The z distance to perform an autofocus in steps."""
@lt.property
def ready(self) -> bool:
"""Whether this scanworkflow is ready to start."""
return self._detector.ready
def pre_scan_routine(self) -> None:
self._autofocus.looping_autofocus(dz=self.autofocus_dz, start="centre")

View file

@ -35,6 +35,7 @@ from openflexure_microscope_server import scan_directories, scan_planners, stitc
from .autofocus import AutofocusThing, StackParams
from .camera import BaseCamera
from .camera_stage_mapping import CameraStageMapper, CSMUncalibratedError
from .scan_workflows import HistoScanWorkflow, ScanWorkflow
from .stage import BaseStage
T = TypeVar("T")
@ -103,6 +104,7 @@ class SmartScanThing(lt.Thing):
_cam: BaseCamera = lt.thing_slot()
_csm: CameraStageMapper = lt.thing_slot()
_stage: BaseStage = lt.thing_slot()
_workflow: HistoScanWorkflow = lt.thing_slot()
def __init__(
self, thing_server_interface: lt.ThingServerInterface, scans_folder: str
@ -195,7 +197,6 @@ class SmartScanThing(lt.Thing):
self._check_background_and_csm_set()
self._ongoing_scan = self._scan_dir_manager.new_scan_dir(scan_name)
self._latest_scan_name = self.ongoing_scan.name
self._autofocus.looping_autofocus(dz=self.autofocus_dz, start="centre")
self._run_scan()
except Exception as e:
# If _scan_data is set then scan started
@ -388,8 +389,11 @@ class SmartScanThing(lt.Thing):
starting x,y,z position.
"""
try:
# probably make workflow a context manager with a lock?
workflow = self._workflow
self._cam.start_streaming(main_resolution=(3280, 2464))
self._scan_data = self._collect_scan_data()
workflow.pre_scan_routine(self._scan_data)
self.ongoing_scan.save_scan_data(self._scan_data)
images_dir = self.ongoing_scan.images_dir
if images_dir is None:
@ -398,7 +402,7 @@ class SmartScanThing(lt.Thing):
)
self._stack_params = self._autofocus.create_stack_params(
images_dir=images_dir,
autofocus_dz=self.autofocus_dz,
autofocus_dz=self.scan_data.autofocus_dz,
save_resolution=self.scan_data.save_resolution,
)
self._preview_stitcher = stitching.PreviewStitcher(
@ -408,7 +412,7 @@ class SmartScanThing(lt.Thing):
)
# This is the main loop of the scan!
self._main_scan_loop()
self._main_scan_loop(workflow)
self._save_final_scan_data(scan_result="success")
except lt.exceptions.InvocationCancelledError:
@ -437,7 +441,7 @@ class SmartScanThing(lt.Thing):
self._perform_final_stitch()
@_scan_running
def _main_scan_loop(self) -> None:
def _main_scan_loop(self, workflow: ScanWorkflow) -> None:
"""Run the main loop of the scan.
This loop runs during a scan, until no more scan x,y positions
@ -580,10 +584,8 @@ class SmartScanThing(lt.Thing):
skip_background: bool = lt.setting(default=True)
"""Whether to detect and skip empty fields of view.
This uses the settings from the ``BackgroundDetectThing``."""
autofocus_dz: int = lt.setting(default=1000)
"""The z distance to perform an autofocus in steps."""
This uses the settings from the ``BackgroundDetectThing``.
"""
overlap: float = lt.setting(default=0.45)
"""The fraction (0-1) that adjacent images should overlap in x or y."""