Stacking in autofocus with temp capture Thing

This commit is contained in:
jaknapper 2025-04-23 19:16:23 +01:00
parent dee11deec1
commit 9f39f61d03
4 changed files with 92 additions and 95 deletions

View file

@ -14,8 +14,7 @@
} }
}, },
"/background_detect/": "openflexure_microscope_server.things.background_detect:BackgroundDetectThing", "/background_detect/": "openflexure_microscope_server.things.background_detect:BackgroundDetectThing",
"/z_stack/": "openflexure_microscope_server.things.z_stack:ZStackThing", "/capture/": "openflexure_microscope_server.things.capture:CaptureThing"
"/capturing/": "openflexure_microscope_server.things.z_stack:CapturingThing"
}, },
"settings_folder": "/var/openflexure/settings/" "settings_folder": "/var/openflexure/settings/"
} }

View file

@ -11,21 +11,29 @@ from contextlib import contextmanager
import logging import logging
import time import time
from typing import Annotated, Mapping, Optional, Sequence from typing import Annotated, Mapping, Optional, Sequence
import os
import shutil
import glob
from fastapi import Depends from fastapi import Depends
from labthings_fastapi.thing import Thing from labthings_fastapi.thing import Thing
from labthings_fastapi.dependencies.blocking_portal import BlockingPortal 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.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 RawCameraDependency as Camera
from .camera import CameraDependency as WrappedCamera from .camera import CameraDependency as WrappedCamera
from .stage import StageDependency as Stage from .stage import StageDependency as Stage
from .capture import CaptureThing
import numpy as np import numpy as np
from pydantic import BaseModel from pydantic import BaseModel
### Autofocus utilities CaptureDep = direct_thing_client_dependency(CaptureThing, "/capture/")
class JPEGSharpnessMonitor: class JPEGSharpnessMonitor:
@ -251,3 +259,78 @@ class AutofocusThing(Thing):
cutoff = threshold * (peak - base) cutoff = threshold * (peak - base)
return current_sharpness >= base + cutoff 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"))

View file

@ -1,28 +1,26 @@
import os
import numpy as np import numpy as np
from PIL import Image from PIL import Image
import time import time
import piexif import piexif
import json import json
import shutil
import glob
from labthings_fastapi.dependencies.metadata import GetThingStates from labthings_fastapi.dependencies.metadata import GetThingStates
from labthings_fastapi.thing import Thing 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 .camera import CameraDependency as CamDep
from .stage import StageDependency as StageDep
from labthings_fastapi.dependencies.invocation import ( from labthings_fastapi.dependencies.invocation import (
InvocationLogger, InvocationLogger,
) )
from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
class CaptureError(RuntimeError): class CaptureError(RuntimeError):
"""An error trying to capture from Picamera""" """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 @thing_action
def _capture_and_save( def _capture_and_save(
self, self,
@ -89,81 +87,3 @@ class CapturingThing(Thing):
logger.warning(f"Failed to add metadata to {jpeg_path}") logger.warning(f"Failed to add metadata to {jpeg_path}")
except Exception as e: except Exception as e:
raise IOError(f"An error occurred while saving {jpeg_path}") from 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"))

View file

@ -33,7 +33,6 @@ from openflexure_microscope_server.utilities import ErrorCapturingThread
from openflexure_microscope_server.things.autofocus import AutofocusThing from openflexure_microscope_server.things.autofocus import AutofocusThing
from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper
from openflexure_microscope_server.things.background_detect import BackgroundDetectThing 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 from openflexure_microscope_server import scan_planners
CSMDep = direct_thing_client_dependency(CameraStageMapper, "/camera_stage_mapping/") CSMDep = direct_thing_client_dependency(CameraStageMapper, "/camera_stage_mapping/")
@ -160,7 +159,6 @@ class SmartScanThing(Thing):
self._background_detect: Optional[BackgroundDep] = None self._background_detect: Optional[BackgroundDep] = None
self._ongoing_scan_name: Optional[str] = None self._ongoing_scan_name: Optional[str] = None
self._starting_position: Optional[Mapping[str, int]] = None self._starting_position: Optional[Mapping[str, int]] = None
self._z_stack: Optional[ZStackDep] = None
self._capture_thread: Optional[ErrorCapturingThread] = None self._capture_thread: Optional[ErrorCapturingThread] = None
self._scan_images_taken: Optional[int] = None self._scan_images_taken: Optional[int] = None
# TODO Scan data is a dict during refactoring, should become a dataclass # TODO Scan data is a dict during refactoring, should become a dataclass
@ -177,7 +175,6 @@ class SmartScanThing(Thing):
metadata_getter: GetThingStates, metadata_getter: GetThingStates,
csm: CSMDep, csm: CSMDep,
background_detect: BackgroundDep, background_detect: BackgroundDep,
z_stack: ZStackDep,
scan_name: str = "", scan_name: str = "",
): ):
"""Move the stage to cover an area, taking images that can be tiled together. """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._csm = csm
self._background_detect = background_detect self._background_detect = background_detect
self._capture_thread = None self._capture_thread = None
self._z_stack = z_stack
self._scan_images_taken = 0 self._scan_images_taken = 0
# Don't set self._scan_data dictionary. This is done at the start of _run_scan # 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._ongoing_scan_name = None
self._scan_images_taken = None self._scan_images_taken = None
self._scan_data = None self._scan_data = None
self._z_stack = None
self._scan_lock.release() self._scan_lock.release()
@_scan_running @_scan_running
@ -596,7 +591,7 @@ class SmartScanThing(Thing):
f"{new_pos_xyz[0]}_{new_pos_xyz[1]}", f"{new_pos_xyz[0]}_{new_pos_xyz[1]}",
) )
os.makedirs(site_folder, exist_ok=True) 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, images_dir=self._ongoing_scan_images_dir,
stack_dir=site_folder, stack_dir=site_folder,
) )