From 18e49ba50ce6c36ec1bbefdfd202ecf81cc91733 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Wed, 24 Sep 2025 16:03:26 +0100 Subject: [PATCH] Test stack params at start of scan main loop --- .../things/autofocus.py | 64 +++++++++++++------ .../things/smart_scan.py | 14 ++-- 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index 88ad91eb..63cc41e6 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -10,7 +10,7 @@ See repository root for licensing information. from contextlib import contextmanager import logging import time -from typing import Annotated, Mapping, Optional, Sequence, Literal +from typing import Annotated, Mapping, Optional, Sequence, Literal, Type import os from dataclasses import dataclass @@ -44,6 +44,7 @@ class StackParams: autofocus_dz: int, images_dir: str, save_resolution: tuple[int, int], + logger: lt.deps.InvocationLogger, ) -> None: """Initialise the parameters. All arguments are keyword only. @@ -58,7 +59,18 @@ class StackParams: :param autofocus_dz: The number of steps in a full autofocus (when required) :param images_dir: The directory to save images to disk :param save_resolution: The resolution to save the captures to disk with + :param logger: A logger passed from the calling Thing """ + if min_images_to_test < 3: + logger.warning( + "Can't test for focus with fewer than 3 images. Running scan with test images = 3" + ) + min_images_to_test = 3 + elif min_images_to_test > 9: + logger.warning( + "Testing with more than 9 images is likely to focus on the cover slip, or strike the sample. Running scan with test images = 9" + ) + min_images_to_test = 9 if min_images_to_test < images_to_save: raise ValueError("Can't save more images than the minimum number tested") if min_images_to_test % 2 == 0 or min_images_to_test <= 0: @@ -467,7 +479,7 @@ class AutofocusThing(lt.Thing): enough more images may be captured. After new images are captured the number sets the number of images used for checking if focus is central. - Defaults to 9 which balances reliability and speed/ + Defaults to 9 which balances reliability and speed. """ stack_dz = lt.ThingSetting(initial_value=50, model=int) @@ -480,15 +492,40 @@ class AutofocusThing(lt.Thing): * 200 for 20x """ + @lt.thing_action + def set_stack_params( + self, + images_dir: str, + autofocus_dz: int, + save_resolution: tuple[int, int], + logger: lt.deps.InvocationLogger, + ) -> Type[StackParams]: + """Set up the parameters used for all stacks in a scan. + + :param images_dir: the folder to save all images + :param autofocus_dz: the range to autofocus over if a stack fails + :param save_resolution: The resolution to save the captures to disk with + :param logger: A logger passed from the calling Thing + + :returns: A StackParams object with the required parameters. + """ + return StackParams( + stack_dz=self.stack_dz, + images_to_save=self.stack_images_to_save, + min_images_to_test=self.stack_min_images_to_test, + autofocus_dz=autofocus_dz, + images_dir=images_dir, + save_resolution=save_resolution, + logger=logger, + ) + @lt.thing_action def run_smart_stack( self, cam: CameraClient, stage: Stage, sharpness_monitor: SharpnessMonitorDep, - images_dir: str, - autofocus_dz: int, - save_resolution: tuple[int, int], + stack_parameters: Type[StackParams], ) -> tuple[bool, int]: """Run a smart stack. @@ -503,27 +540,14 @@ class AutofocusThing(lt.Thing): :param stage: Stage Dependency supplied by LabThings dependency injection :param sharpness_monitor: Sharpness Monitor Dependency (for focus detection) supplied by LabThings dependency injection - :param images_dir: the folder to save all images - :param autofocus_dz: the range to autofocus over if a stack fails - :param save_resolution: The resolution the images should be saved at, the - images will be resampled if this doesn't match the camera's capture - resolution + :param stack_parameters: A StackParams object containing the required + parameters to run a stack. :returns: A tuple containing: * A boolean, True if stack was successfully * The z position of the sharpest image """ - # Set the variables to prevent changes from the GUI or other windows - stack_parameters = StackParams( - stack_dz=self.stack_dz, - images_to_save=self.stack_images_to_save, - min_images_to_test=self.stack_min_images_to_test, - autofocus_dz=autofocus_dz, - images_dir=images_dir, - save_resolution=save_resolution, - ) - tries = 0 # Loop until a stack is successful while tries < stack_parameters.max_attempts: diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 1483278b..593237b2 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -26,7 +26,7 @@ from openflexure_microscope_server import scan_planners from openflexure_microscope_server import stitching # Things -from .autofocus import AutofocusThing +from .autofocus import AutofocusThing, StackParams from .camera_stage_mapping import CameraStageMapper from .camera import CameraDependency as CameraClient from .stage import StageDependency as StageDep @@ -121,6 +121,7 @@ class SmartScanThing(lt.Thing): self._stage: Optional[StageDep] = None self._cam: Optional[CameraClient] = None self._csm: Optional[CSMDep] = None + self._stack_params: Optional[StackParams] = None self._ongoing_scan: Optional[scan_directories.ScanDirectory] = None self._scan_data: Optional[scan_directories.ScanData] = None @@ -189,6 +190,7 @@ class SmartScanThing(lt.Thing): self._scan_lock.release() # Ensure any PreviewStitcher created cannot be reused. self._preview_stitcher = None + self._stack_params = None # Remove any scan folders containing zero images. self.purge_empty_scans(logger=logger) @@ -363,6 +365,12 @@ class SmartScanThing(lt.Thing): self._cam.start_streaming(main_resolution=(3280, 2464)) self._scan_data = self._collect_scan_data() self._ongoing_scan.save_scan_data(self._scan_data) + self._stack_params = self._autofocus.set_stack_params( + images_dir=self._ongoing_scan.images_dir, + autofocus_dz=self.autofocus_dz, + save_resolution=self._scan_data.save_resolution, + logger=self._scan_logger, + ) self._preview_stitcher = stitching.PreviewStitcher( self._ongoing_scan.images_dir, overlap=self._scan_data.overlap, @@ -451,9 +459,7 @@ class SmartScanThing(lt.Thing): continue focused, focused_height = self._autofocus.run_smart_stack( - images_dir=self._ongoing_scan.images_dir, - autofocus_dz=self._scan_data.autofocus_dz, - save_resolution=self._scan_data.save_resolution, + stack_parameters=self._stack_params ) current_pos_xyz = (new_pos_xyz[0], new_pos_xyz[1], focused_height)