diff --git a/ofm_config_full.json b/ofm_config_full.json index 4cd0cb47..7ae0469d 100644 --- a/ofm_config_full.json +++ b/ofm_config_full.json @@ -14,8 +14,7 @@ } }, "/background_detect/": "openflexure_microscope_server.things.background_detect:BackgroundDetectThing", - "/z_stack/": "openflexure_microscope_server.things.z_stack:ZStackThing", - "/capturing/": "openflexure_microscope_server.things.z_stack:CapturingThing" + "/capture/": "openflexure_microscope_server.things.capture:CaptureThing" }, "settings_folder": "/var/openflexure/settings/" } diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index 92eabd41..b85d9cb2 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -11,21 +11,29 @@ from contextlib import contextmanager import logging import time from typing import Annotated, Mapping, Optional, Sequence +import os +import shutil +import glob from fastapi import Depends from labthings_fastapi.thing import Thing from labthings_fastapi.dependencies.blocking_portal import BlockingPortal -from labthings_fastapi.decorators import thing_action +from labthings_fastapi.decorators import thing_action, thing_property +from labthings_fastapi.dependencies.metadata import GetThingStates from labthings_fastapi.types.numpy import NDArray +from labthings_fastapi.dependencies.thing import direct_thing_client_dependency +from labthings_fastapi.dependencies.invocation import InvocationLogger + from .camera import RawCameraDependency as Camera from .camera import CameraDependency as WrappedCamera from .stage import StageDependency as Stage +from .capture import CaptureThing import numpy as np from pydantic import BaseModel -### Autofocus utilities +CaptureDep = direct_thing_client_dependency(CaptureThing, "/capture/") class JPEGSharpnessMonitor: @@ -251,3 +259,78 @@ class AutofocusThing(Thing): cutoff = threshold * (peak - base) return current_sharpness >= base + cutoff + + @thing_property + def images_to_capture(self) -> int: + """The number of images to capture and save in a stack + Defaults to 1 unless you need to see either side of focus""" + return self.thing_settings.get("images_to_capture", 1) + + @images_to_capture.setter + def images_to_capture(self, value: int) -> None: + self.thing_settings["images_to_capture"] = value + + @thing_property + def stack_dz(self) -> int: + """Space in steps between images in a z-stack + Suggested is 50 for 60-100x + 100 for 40x + 200 for 20x""" + return self.thing_settings.get("stack_dz", 50) + + @stack_dz.setter + def stack_dz(self, value: int) -> None: + self.thing_settings["stack_dz"] = value + + @thing_action + def run_z_stack( + self, + cam: WrappedCamera, + stage: Stage, + logger: InvocationLogger, + metadata_getter: GetThingStates, + capture: CaptureDep, + images_dir: str, + stack_dir: str, + ) -> None: + """Run a z stack, saving all images to stack_dir and copying the + central image to stack_dir""" + stack_dz = self.stack_dz + images_to_capture = self.images_to_capture + + stack_z_range = stack_dz * (images_to_capture - 1) + stage.move_relative(z=-stack_z_range / 2) + + for capture_count in range(images_to_capture): + jpeg_path = os.path.join( + stack_dir, + f"{capture_count}.jpeg", + ) + capture._capture_and_save( + jpeg_path=jpeg_path, + cam=cam, + logger=logger, + metadata_getter=metadata_getter, + ) + + # If the stack isn't complete yet, move + if capture_count + 1 < images_to_capture: + stage.move_relative(z=stack_dz) + time.sleep(0.3) + + self.copy_central_image(images_dir, stack_dir) + + def copy_central_image( + self, + images_dir: str, + stack_dir: str, + ): + """Gets a list of images in a folder (stack_dir), sorts them, and copies the central image + to images dir.""" + image_list = glob.glob(os.path.join(stack_dir, "*")) + image_list.sort() + central_index = (len(image_list) - 1) // 2 + central_image = image_list[central_index] + xy_location = os.path.basename(stack_dir) + + shutil.copy(central_image, os.path.join(images_dir, f"{xy_location}.jpeg")) diff --git a/src/openflexure_microscope_server/things/z_stack.py b/src/openflexure_microscope_server/things/capture.py similarity index 52% rename from src/openflexure_microscope_server/things/z_stack.py rename to src/openflexure_microscope_server/things/capture.py index e9eacf22..214c05b9 100644 --- a/src/openflexure_microscope_server/things/z_stack.py +++ b/src/openflexure_microscope_server/things/capture.py @@ -1,28 +1,26 @@ -import os import numpy as np from PIL import Image import time import piexif import json -import shutil -import glob from labthings_fastapi.dependencies.metadata import GetThingStates from labthings_fastapi.thing import Thing -from labthings_fastapi.decorators import thing_action, thing_property +from labthings_fastapi.decorators import thing_action from .camera import CameraDependency as CamDep -from .stage import StageDependency as StageDep from labthings_fastapi.dependencies.invocation import ( InvocationLogger, ) -from labthings_fastapi.dependencies.thing import direct_thing_client_dependency class CaptureError(RuntimeError): """An error trying to capture from Picamera""" -class CapturingThing(Thing): +class CaptureThing(Thing): + """A temporary Thing to handle capturing to disk with associated metadata + Will be moved to the camera Thing or dependency in due course""" + @thing_action def _capture_and_save( self, @@ -89,81 +87,3 @@ class CapturingThing(Thing): logger.warning(f"Failed to add metadata to {jpeg_path}") except Exception as e: raise IOError(f"An error occurred while saving {jpeg_path}") from e - - -CapturingDep = direct_thing_client_dependency(CapturingThing, "/capturing/") - - -class ZStackThing(Thing): - @thing_property - def images_to_capture(self) -> int: - """The number of images to capture and save in a stack - Defaults to 1 unless you need to see either side of focus""" - return self.thing_settings.get("images_to_capture", 1) - - @images_to_capture.setter - def images_to_capture(self, value: int) -> None: - self.thing_settings["images_to_capture"] = value - - @thing_property - def stack_dz(self) -> int: - """Space in steps between images in a z-stack - Suggested is 50 for 60-100x - 100 for 40x - 200 for 20x""" - return self.thing_settings.get("stack_dz", 50) - - @stack_dz.setter - def stack_dz(self, value: int) -> None: - self.thing_settings["stack_dz"] = value - - @thing_action - def run_z_stack( - self, - cam: CamDep, - stage: StageDep, - logger: InvocationLogger, - metadata_getter: GetThingStates, - capture: CapturingDep, - images_dir: str, - stack_dir: str, - ) -> None: - stack_dz = self.stack_dz - images_to_capture = self.images_to_capture - - stack_z_range = stack_dz * (images_to_capture - 1) - stage.move_relative(z=-stack_z_range / 2) - - for capture_count in range(images_to_capture): - jpeg_path = os.path.join( - stack_dir, - f"{capture_count}.jpeg", - ) - capture._capture_and_save( - jpeg_path=jpeg_path, - cam=cam, - logger=logger, - metadata_getter=metadata_getter, - ) - - # If the stack isn't complete yet, move - if capture_count + 1 < images_to_capture: - stage.move_relative(z=stack_dz) - time.sleep(0.3) - - self.copy_central_image(images_dir, stack_dir, logger) - - def copy_central_image( - self, - images_dir: str, - stack_dir: str, - ): - """Gets a list of images in a folder (stack_dir), sorts them, and copies the central image - to images dir.""" - image_list = glob.glob(os.path.join(stack_dir, "*")) - image_list.sort() - central_index = (len(image_list) - 1) // 2 - central_image = image_list[central_index] - xy_location = os.path.basename(stack_dir) - - shutil.copy(central_image, os.path.join(images_dir, f"{xy_location}.jpeg")) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index f3592591..940c1d3a 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -33,7 +33,6 @@ from openflexure_microscope_server.utilities import ErrorCapturingThread from openflexure_microscope_server.things.autofocus import AutofocusThing from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper from openflexure_microscope_server.things.background_detect import BackgroundDetectThing -from openflexure_microscope_server.things.z_stack import ZStackThing from openflexure_microscope_server import scan_planners CSMDep = direct_thing_client_dependency(CameraStageMapper, "/camera_stage_mapping/") @@ -160,7 +159,6 @@ class SmartScanThing(Thing): self._background_detect: Optional[BackgroundDep] = None self._ongoing_scan_name: Optional[str] = None self._starting_position: Optional[Mapping[str, int]] = None - self._z_stack: Optional[ZStackDep] = None self._capture_thread: Optional[ErrorCapturingThread] = None self._scan_images_taken: Optional[int] = None # TODO Scan data is a dict during refactoring, should become a dataclass @@ -177,7 +175,6 @@ class SmartScanThing(Thing): metadata_getter: GetThingStates, csm: CSMDep, background_detect: BackgroundDep, - z_stack: ZStackDep, scan_name: str = "", ): """Move the stage to cover an area, taking images that can be tiled together. @@ -201,7 +198,6 @@ class SmartScanThing(Thing): self._csm = csm self._background_detect = background_detect self._capture_thread = None - self._z_stack = z_stack self._scan_images_taken = 0 # Don't set self._scan_data dictionary. This is done at the start of _run_scan @@ -236,7 +232,6 @@ class SmartScanThing(Thing): self._ongoing_scan_name = None self._scan_images_taken = None self._scan_data = None - self._z_stack = None self._scan_lock.release() @_scan_running @@ -596,7 +591,7 @@ class SmartScanThing(Thing): f"{new_pos_xyz[0]}_{new_pos_xyz[1]}", ) os.makedirs(site_folder, exist_ok=True) - self._z_stack.run_z_stack( + self._autofocus.run_z_stack( images_dir=self._ongoing_scan_images_dir, stack_dir=site_folder, )