Stacking in autofocus with temp capture Thing
This commit is contained in:
parent
dee11deec1
commit
9f39f61d03
4 changed files with 92 additions and 95 deletions
|
|
@ -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/"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue