Remove deps from autofocus
This commit is contained in:
parent
4a327cc901
commit
189e82e8dc
3 changed files with 75 additions and 131 deletions
|
|
@ -10,20 +10,18 @@ See repository root for licensing information.
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from contextlib import contextmanager
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Annotated, Literal, Mapping, Optional, Sequence
|
from types import TracebackType
|
||||||
|
from typing import Literal, Mapping, Optional, Self, Sequence
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from fastapi import Depends
|
|
||||||
from pydantic import BaseModel, computed_field, field_validator, model_validator
|
from pydantic import BaseModel, computed_field, field_validator, model_validator
|
||||||
|
|
||||||
import labthings_fastapi as lt
|
import labthings_fastapi as lt
|
||||||
from labthings_fastapi.types.numpy import NDArray
|
from labthings_fastapi.types.numpy import NDArray
|
||||||
|
|
||||||
from .camera import CameraDependency as CameraClient
|
from .camera import BaseCamera
|
||||||
from .camera import RawCameraDependency as RawCamera
|
from .stage import BaseStage
|
||||||
from .stage import StageDependency as Stage
|
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
MIN_TEST_IMAGE_COUNT = 3
|
MIN_TEST_IMAGE_COUNT = 3
|
||||||
|
|
@ -224,20 +222,16 @@ class JPEGSharpnessMonitor:
|
||||||
SharpnessMonitorDep as an argument is called.
|
SharpnessMonitorDep as an argument is called.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, stage: BaseStage, camera: BaseCamera) -> None:
|
||||||
self, stage: Stage, camera: RawCamera, portal: lt.deps.BlockingPortal
|
|
||||||
) -> None:
|
|
||||||
"""Initialise a new JPEGSharpnessMonitor. The args are injected automatically.
|
"""Initialise a new JPEGSharpnessMonitor. The args are injected automatically.
|
||||||
|
|
||||||
:param stage: A direct_thing_client dependency for the the microscope stage.
|
:param stage: A direct_thing_client dependency for the the microscope stage.
|
||||||
:param camera: A raw_thing_client depeendency for the camera. This is a raw
|
:param camera: A raw_thing_client depeendency for the camera. This is a raw
|
||||||
dependency as the underlying class needs to be
|
dependency as the underlying class needs to be
|
||||||
:param portal: The asyncio blocking portal for asynchronous task scheduling.
|
|
||||||
"""
|
"""
|
||||||
self.camera = camera
|
self.camera = camera
|
||||||
self.stage = stage
|
self.stage = stage
|
||||||
self.portal = portal
|
LOGGER.debug(f"Created sharpness monitor with {stage}, {camera}")
|
||||||
LOGGER.debug(f"Created sharpness monitor with {stage}, {camera}, {portal}")
|
|
||||||
self.stage_positions: list[Mapping[str, int]] = []
|
self.stage_positions: list[Mapping[str, int]] = []
|
||||||
self.stage_times: list[float] = []
|
self.stage_times: list[float] = []
|
||||||
self.jpeg_times: list[float] = []
|
self.jpeg_times: list[float] = []
|
||||||
|
|
@ -254,14 +248,23 @@ class JPEGSharpnessMonitor:
|
||||||
if not self.running:
|
if not self.running:
|
||||||
break
|
break
|
||||||
|
|
||||||
@contextmanager
|
def __enter__(self) -> Self:
|
||||||
def run(self) -> None:
|
"""Start context manager, during which sharpness from the camera is monitored."""
|
||||||
"""Context manager, during which we will monitor sharpness from the camera."""
|
# Use the cameras _thing_server_interface to get access to the server event loop
|
||||||
self.portal.start_task_soon(self.monitor_sharpness)
|
# and start a task.
|
||||||
try:
|
self.camera._thing_server_interface.start_async_task_soon(
|
||||||
yield
|
self.monitor_sharpness
|
||||||
finally:
|
)
|
||||||
self.running = False
|
return self
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
_exc_type: type[BaseException],
|
||||||
|
_exc_value: Optional[BaseException],
|
||||||
|
_traceback: Optional[TracebackType],
|
||||||
|
) -> None:
|
||||||
|
"""Clean up after context manager is closed."""
|
||||||
|
self.running = False
|
||||||
|
|
||||||
def focus_rel(self, dz: int, block_cancellation: int = False) -> tuple[int, int]:
|
def focus_rel(self, dz: int, block_cancellation: int = False) -> tuple[int, int]:
|
||||||
"""Move the stage by dz, monitoring the position over time.
|
"""Move the stage by dz, monitoring the position over time.
|
||||||
|
|
@ -339,9 +342,6 @@ class JPEGSharpnessMonitor:
|
||||||
return SharpnessDataArrays(**data)
|
return SharpnessDataArrays(**data)
|
||||||
|
|
||||||
|
|
||||||
SharpnessMonitorDep = Annotated[JPEGSharpnessMonitor, Depends()]
|
|
||||||
|
|
||||||
|
|
||||||
class AutofocusThing(lt.Thing):
|
class AutofocusThing(lt.Thing):
|
||||||
"""The Thing concerned with combinations of z axis movements and the camera.
|
"""The Thing concerned with combinations of z axis movements and the camera.
|
||||||
|
|
||||||
|
|
@ -350,10 +350,12 @@ class AutofocusThing(lt.Thing):
|
||||||
field of view to assess focus (autofocus and testing the success of a z-stack)
|
field of view to assess focus (autofocus and testing the success of a z-stack)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_cam: BaseCamera = lt.thing_slot()
|
||||||
|
_stage: BaseStage = lt.thing_slot()
|
||||||
|
|
||||||
@lt.action
|
@lt.action
|
||||||
def fast_autofocus(
|
def fast_autofocus(
|
||||||
self,
|
self,
|
||||||
sharpness_monitor: SharpnessMonitorDep,
|
|
||||||
dz: int = 2000,
|
dz: int = 2000,
|
||||||
start: Literal["centre", "base"] = "centre",
|
start: Literal["centre", "base"] = "centre",
|
||||||
) -> SharpnessDataArrays:
|
) -> SharpnessDataArrays:
|
||||||
|
|
@ -363,7 +365,7 @@ class AutofocusThing(lt.Thing):
|
||||||
the position where the image was sharpest. We'll then move back down, and
|
the position where the image was sharpest. We'll then move back down, and
|
||||||
finally up to the sharpest point.
|
finally up to the sharpest point.
|
||||||
"""
|
"""
|
||||||
with sharpness_monitor.run():
|
with JPEGSharpnessMonitor(self._stage, self._cam) as sharpness_monitor:
|
||||||
# Move to (-dz / 2)
|
# Move to (-dz / 2)
|
||||||
if start == "centre":
|
if start == "centre":
|
||||||
sharpness_monitor.focus_rel(-dz // 2)
|
sharpness_monitor.focus_rel(-dz // 2)
|
||||||
|
|
@ -384,7 +386,6 @@ class AutofocusThing(lt.Thing):
|
||||||
@lt.action
|
@lt.action
|
||||||
def z_move_and_measure_sharpness(
|
def z_move_and_measure_sharpness(
|
||||||
self,
|
self,
|
||||||
sharpness_monitor: SharpnessMonitorDep,
|
|
||||||
dz: Sequence[int],
|
dz: Sequence[int],
|
||||||
wait: float = 0,
|
wait: float = 0,
|
||||||
) -> SharpnessDataArrays:
|
) -> SharpnessDataArrays:
|
||||||
|
|
@ -400,7 +401,7 @@ class AutofocusThing(lt.Thing):
|
||||||
If ``wait`` is specified, we will wait for that many seconds
|
If ``wait`` is specified, we will wait for that many seconds
|
||||||
between moves.
|
between moves.
|
||||||
"""
|
"""
|
||||||
with sharpness_monitor.run():
|
with JPEGSharpnessMonitor(self._stage, self._cam) as sharpness_monitor:
|
||||||
for move_index, current_dz in enumerate(dz):
|
for move_index, current_dz in enumerate(dz):
|
||||||
if move_index > 0 and wait > 0:
|
if move_index > 0 and wait > 0:
|
||||||
time.sleep(wait)
|
time.sleep(wait)
|
||||||
|
|
@ -410,8 +411,6 @@ class AutofocusThing(lt.Thing):
|
||||||
@lt.action
|
@lt.action
|
||||||
def looping_autofocus(
|
def looping_autofocus(
|
||||||
self,
|
self,
|
||||||
stage: Stage,
|
|
||||||
sharpness_monitor: SharpnessMonitorDep,
|
|
||||||
dz: int = 2000,
|
dz: int = 2000,
|
||||||
start: Literal["centre", "base"] = "centre",
|
start: Literal["centre", "base"] = "centre",
|
||||||
) -> tuple[list[float], list[float]]:
|
) -> tuple[list[float], list[float]]:
|
||||||
|
|
@ -425,12 +424,12 @@ class AutofocusThing(lt.Thing):
|
||||||
attempt = 0
|
attempt = 0
|
||||||
backlash = 200
|
backlash = 200
|
||||||
|
|
||||||
with sharpness_monitor.run():
|
with JPEGSharpnessMonitor(self._stage, self._cam) as sharpness_monitor:
|
||||||
while attempt < 10:
|
while attempt < 10:
|
||||||
attempt += 1
|
attempt += 1
|
||||||
if start == "centre":
|
if start == "centre":
|
||||||
stage.move_relative(x=0, y=0, z=-(backlash + dz / 2))
|
self._stage.move_relative(x=0, y=0, z=-(backlash + dz / 2))
|
||||||
stage.move_relative(x=0, y=0, z=backlash)
|
self._stage.move_relative(x=0, y=0, z=backlash)
|
||||||
|
|
||||||
# Always start centrally for future runs
|
# Always start centrally for future runs
|
||||||
start = "centre"
|
start = "centre"
|
||||||
|
|
@ -445,8 +444,8 @@ class AutofocusThing(lt.Thing):
|
||||||
target_max = np.max(heights) - dz / 5
|
target_max = np.max(heights) - dz / 5
|
||||||
|
|
||||||
# move to the peak
|
# move to the peak
|
||||||
stage.move_absolute(z=peak_height - backlash)
|
self._stage.move_absolute(z=peak_height - backlash)
|
||||||
stage.move_absolute(z=peak_height)
|
self._stage.move_absolute(z=peak_height)
|
||||||
|
|
||||||
if target_min < peak_height < target_max:
|
if target_min < peak_height < target_max:
|
||||||
# If it is within the target range then return
|
# If it is within the target range then return
|
||||||
|
|
@ -557,9 +556,6 @@ class AutofocusThing(lt.Thing):
|
||||||
@lt.action
|
@lt.action
|
||||||
def run_smart_stack(
|
def run_smart_stack(
|
||||||
self,
|
self,
|
||||||
cam: CameraClient,
|
|
||||||
stage: Stage,
|
|
||||||
sharpness_monitor: SharpnessMonitorDep,
|
|
||||||
stack_parameters: StackParams,
|
stack_parameters: StackParams,
|
||||||
save_on_failure: bool = False,
|
save_on_failure: bool = False,
|
||||||
check_turning_points: bool = True,
|
check_turning_points: bool = True,
|
||||||
|
|
@ -572,11 +568,6 @@ class AutofocusThing(lt.Thing):
|
||||||
The sharpest image, and optionally images around the sharpest, will be saved
|
The sharpest image, and optionally images around the sharpest, will be saved
|
||||||
to the images_dir with their coordinates in the filename.
|
to the images_dir with their coordinates in the filename.
|
||||||
|
|
||||||
|
|
||||||
:param cam: Camera Dependency supplied by LabThings dependency injection
|
|
||||||
:param stage: Stage Dependency supplied by LabThings dependency injection
|
|
||||||
:param sharpness_monitor: Sharpness Monitor Dependency (for focus detection)
|
|
||||||
supplied by LabThings dependency injection
|
|
||||||
:param stack_parameters: A StackParams object containing the required
|
:param stack_parameters: A StackParams object containing the required
|
||||||
parameters to run a stack.
|
parameters to run a stack.
|
||||||
:param save_on_failure: Whether to save an image even if no focus was found.
|
:param save_on_failure: Whether to save an image even if no focus was found.
|
||||||
|
|
@ -595,8 +586,6 @@ class AutofocusThing(lt.Thing):
|
||||||
success, captures, sharpest_id = self.z_stack(
|
success, captures, sharpest_id = self.z_stack(
|
||||||
stack_parameters=stack_parameters,
|
stack_parameters=stack_parameters,
|
||||||
check_turning_points=check_turning_points,
|
check_turning_points=check_turning_points,
|
||||||
cam=cam,
|
|
||||||
stage=stage,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
|
|
@ -609,12 +598,7 @@ class AutofocusThing(lt.Thing):
|
||||||
initial_z_pos = captures[0].position["z"]
|
initial_z_pos = captures[0].position["z"]
|
||||||
# If a stack is not successful, move to the start and autofocus
|
# If a stack is not successful, move to the start and autofocus
|
||||||
try:
|
try:
|
||||||
self.reset_stack(
|
self.reset_stack(initial_z_pos, stack_parameters.autofocus_dz)
|
||||||
initial_z_pos,
|
|
||||||
stack_parameters.autofocus_dz,
|
|
||||||
stage,
|
|
||||||
sharpness_monitor,
|
|
||||||
)
|
|
||||||
except NoFocusFoundError:
|
except NoFocusFoundError:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -626,7 +610,6 @@ class AutofocusThing(lt.Thing):
|
||||||
sharpest_id=sharpest_id,
|
sharpest_id=sharpest_id,
|
||||||
captures=captures,
|
captures=captures,
|
||||||
stack_parameters=stack_parameters,
|
stack_parameters=stack_parameters,
|
||||||
cam=cam,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return success, _get_capture_by_id(captures, sharpest_id).position["z"]
|
return success, _get_capture_by_id(captures, sharpest_id).position["z"]
|
||||||
|
|
@ -635,8 +618,6 @@ class AutofocusThing(lt.Thing):
|
||||||
self,
|
self,
|
||||||
initial_z_pos: list[int],
|
initial_z_pos: list[int],
|
||||||
autofocus_dz: int,
|
autofocus_dz: int,
|
||||||
stage: Stage,
|
|
||||||
sharpness_monitor: SharpnessMonitorDep,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Return to the initial z position and run a looping autofocus.
|
"""Return to the initial z position and run a looping autofocus.
|
||||||
|
|
||||||
|
|
@ -646,19 +627,14 @@ class AutofocusThing(lt.Thing):
|
||||||
``stage`` and ``sharpness_monitor`` are Thing dependencies passed through
|
``stage`` and ``sharpness_monitor`` are Thing dependencies passed through
|
||||||
from the calling action.
|
from the calling action.
|
||||||
"""
|
"""
|
||||||
stage.move_absolute(z=initial_z_pos)
|
self._stage.move_absolute(z=initial_z_pos)
|
||||||
self.looping_autofocus(
|
self.looping_autofocus(dz=autofocus_dz)
|
||||||
stage=stage,
|
|
||||||
sharpness_monitor=sharpness_monitor,
|
|
||||||
dz=autofocus_dz,
|
|
||||||
)
|
|
||||||
|
|
||||||
def save_stack(
|
def save_stack(
|
||||||
self,
|
self,
|
||||||
sharpest_id: int,
|
sharpest_id: int,
|
||||||
captures: list[list],
|
captures: list[list],
|
||||||
stack_parameters: StackParams,
|
stack_parameters: StackParams,
|
||||||
cam: CameraClient,
|
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Save the required captures to disk.
|
"""Save the required captures to disk.
|
||||||
|
|
||||||
|
|
@ -669,28 +645,24 @@ class AutofocusThing(lt.Thing):
|
||||||
:param captures: a list of captures, including file name, image data and
|
:param captures: a list of captures, including file name, image data and
|
||||||
metadata
|
metadata
|
||||||
:param stack_parameters: a StackParams object holding stack parameters
|
:param stack_parameters: a StackParams object holding stack parameters
|
||||||
:param cam: is a Thing dependency passed through from the calling action
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
sharpest_index = _get_capture_index_by_id(captures, sharpest_id)
|
sharpest_index = _get_capture_index_by_id(captures, sharpest_id)
|
||||||
slice_to_save = stack_parameters.slice_to_save(sharpest_index)
|
slice_to_save = stack_parameters.slice_to_save(sharpest_index)
|
||||||
|
|
||||||
# Loop through the range, saving each capture to disk
|
# Loop through the range, saving each capture to disk
|
||||||
for capture in captures[slice_to_save]:
|
for capture in captures[slice_to_save]:
|
||||||
cam.save_from_memory(
|
self._cam.save_from_memory(
|
||||||
jpeg_path=os.path.join(stack_parameters.images_dir, capture.filename),
|
jpeg_path=os.path.join(stack_parameters.images_dir, capture.filename),
|
||||||
save_resolution=stack_parameters.save_resolution,
|
save_resolution=stack_parameters.save_resolution,
|
||||||
buffer_id=capture.buffer_id,
|
buffer_id=capture.buffer_id,
|
||||||
)
|
)
|
||||||
cam.clear_buffers()
|
self._cam.clear_buffers()
|
||||||
return sharpest_index
|
return sharpest_index
|
||||||
|
|
||||||
def z_stack(
|
def z_stack(
|
||||||
self,
|
self,
|
||||||
stack_parameters: StackParams,
|
stack_parameters: StackParams,
|
||||||
check_turning_points: bool,
|
check_turning_points: bool,
|
||||||
cam: CameraClient,
|
|
||||||
stage: Stage,
|
|
||||||
) -> tuple[bool, list[CaptureInfo], int]:
|
) -> tuple[bool, list[CaptureInfo], int]:
|
||||||
"""Capture a series of images checking that sharpest image central.
|
"""Capture a series of images checking that sharpest image central.
|
||||||
|
|
||||||
|
|
@ -703,8 +675,6 @@ class AutofocusThing(lt.Thing):
|
||||||
:param check_turning_points: Whether to check the number of turning points in
|
:param check_turning_points: Whether to check the number of turning points in
|
||||||
the sharpnesses of the images in the stack is exactly 1. (May fail with
|
the sharpnesses of the images in the stack is exactly 1. (May fail with
|
||||||
thick samples)
|
thick samples)
|
||||||
:param cam: Camera Dependency to be passed through from the calling action
|
|
||||||
:param stage: Stage Dependency to be passed through from the calling action
|
|
||||||
|
|
||||||
:returns: A tuple of
|
:returns: A tuple of
|
||||||
|
|
||||||
|
|
@ -714,14 +684,14 @@ class AutofocusThing(lt.Thing):
|
||||||
"""
|
"""
|
||||||
# Move down by the height of the z stack, plus an overshoot
|
# Move down by the height of the z stack, plus an overshoot
|
||||||
# Better to start too low and take too many images than too high and need to refocus
|
# Better to start too low and take too many images than too high and need to refocus
|
||||||
stage.move_relative(
|
self._stage.move_relative(
|
||||||
z=-(
|
z=-(
|
||||||
stack_parameters.steps_undershoot
|
stack_parameters.steps_undershoot
|
||||||
+ stack_parameters.backlash_correction
|
+ stack_parameters.backlash_correction
|
||||||
+ stack_parameters.stack_z_range / 2
|
+ stack_parameters.stack_z_range / 2
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
stage.move_relative(z=stack_parameters.backlash_correction)
|
self._stage.move_relative(z=stack_parameters.backlash_correction)
|
||||||
|
|
||||||
captures = []
|
captures = []
|
||||||
# Always check for focus using the the last `min_images_to_test` in the
|
# Always check for focus using the the last `min_images_to_test` in the
|
||||||
|
|
@ -735,11 +705,7 @@ class AutofocusThing(lt.Thing):
|
||||||
|
|
||||||
# Append a new image to the stack
|
# Append a new image to the stack
|
||||||
captures.append(
|
captures.append(
|
||||||
self.capture_stack_image(
|
self.capture_stack_image(buffer_max=stack_parameters.min_images_to_test)
|
||||||
cam,
|
|
||||||
stage,
|
|
||||||
buffer_max=stack_parameters.min_images_to_test,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# If the number of images is enough to test, test them
|
# If the number of images is enough to test, test them
|
||||||
|
|
@ -754,33 +720,26 @@ class AutofocusThing(lt.Thing):
|
||||||
if result == "restart":
|
if result == "restart":
|
||||||
return False, captures, capture_id
|
return False, captures, capture_id
|
||||||
# If reached here the result was "continue"
|
# If reached here the result was "continue"
|
||||||
stage.move_relative(z=stack_parameters.stack_dz)
|
self._stage.move_relative(z=stack_parameters.stack_dz)
|
||||||
return False, captures, capture_id
|
return False, captures, capture_id
|
||||||
|
|
||||||
def capture_stack_image(
|
def capture_stack_image(self, buffer_max: int) -> CaptureInfo:
|
||||||
self,
|
|
||||||
cam: CameraClient,
|
|
||||||
stage: Stage,
|
|
||||||
buffer_max: int,
|
|
||||||
) -> CaptureInfo:
|
|
||||||
"""Capture another image and return the capture information.
|
"""Capture another image and return the capture information.
|
||||||
|
|
||||||
The capture is stored by the camera Thing, and can be saved by ID.
|
The capture is stored by the camera Thing, and can be saved by ID.
|
||||||
|
|
||||||
:param cam: Camera Dependency to be passed through from the calling action
|
|
||||||
:param stage: Stage Dependency to be passed through from the calling action
|
|
||||||
:param buffer_max: The maximum number of images to tell the camera to keep in memory
|
:param buffer_max: The maximum number of images to tell the camera to keep in memory
|
||||||
for saving once the stack is complete
|
for saving once the stack is complete
|
||||||
|
|
||||||
:returns: A CaptureInfo object containing the capture information including its
|
:returns: A CaptureInfo object containing the capture information including its
|
||||||
camera buffer_id needed for saving.
|
camera buffer_id needed for saving.
|
||||||
"""
|
"""
|
||||||
stage_location = stage.position
|
stage_location = self._stage.position
|
||||||
buffer_id = cam.capture_to_memory(buffer_max=buffer_max)
|
buffer_id = self._cam.capture_to_memory(buffer_max=buffer_max)
|
||||||
return CaptureInfo(
|
return CaptureInfo(
|
||||||
buffer_id=buffer_id,
|
buffer_id=buffer_id,
|
||||||
position=stage_location,
|
position=stage_location,
|
||||||
sharpness=cam.grab_jpeg_size(stream_name="lores"),
|
sharpness=self._cam.grab_jpeg_size(stream_name="lores"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Silence too many returns in this situation as refactoring to reduce returns is
|
# Silence too many returns in this situation as refactoring to reduce returns is
|
||||||
|
|
|
||||||
|
|
@ -48,22 +48,23 @@ def fake_sharpness_data(
|
||||||
def test_looping_autofocus(start_z, max_loc, centre, attempts_expected, passes, mocker):
|
def test_looping_autofocus(start_z, max_loc, centre, attempts_expected, passes, mocker):
|
||||||
"""Test the high level looping autofocus algorithm."""
|
"""Test the high level looping autofocus algorithm."""
|
||||||
dz = 2000
|
dz = 2000
|
||||||
|
autofocus_thing = create_thing_without_server(AutofocusThing, mock_all_slots=True)
|
||||||
|
|
||||||
# Make a mock stage where move_absolute abs and relative updates the position counter.
|
# Make a mock stage where move_absolute abs and relative updates the position counter.
|
||||||
stage = mocker.Mock()
|
autofocus_thing._stage.position = {"x": 0, "y": 0, "z": start_z}
|
||||||
stage.position = {"x": 0, "y": 0, "z": start_z}
|
|
||||||
|
|
||||||
def set_pos(**kwargs: int) -> None:
|
def set_pos(**kwargs: int) -> None:
|
||||||
"""Move absolute should update position. So make a side effect for the mock."""
|
"""Move absolute should update position. So make a side effect for the mock."""
|
||||||
for axis, value in kwargs.items():
|
for axis, value in kwargs.items():
|
||||||
stage.position[axis] = value
|
autofocus_thing._stage.position[axis] = value
|
||||||
|
|
||||||
def adjust_pos(**kwargs: int) -> None:
|
def adjust_pos(**kwargs: int) -> None:
|
||||||
"""Move relative should update position. So make a side effect for the mock."""
|
"""Move relative should update position. So make a side effect for the mock."""
|
||||||
for axis, value in kwargs.items():
|
for axis, value in kwargs.items():
|
||||||
stage.position[axis] += value
|
autofocus_thing._stage.position[axis] += value
|
||||||
|
|
||||||
stage.move_absolute.side_effect = set_pos
|
autofocus_thing._stage.move_absolute.side_effect = set_pos
|
||||||
stage.move_relative.side_effect = adjust_pos
|
autofocus_thing._stage.move_relative.side_effect = adjust_pos
|
||||||
|
|
||||||
# Make a mock sharpness monitor that can generate sharpness data.
|
# Make a mock sharpness monitor that can generate sharpness data.
|
||||||
sharpness_monitor = mocker.MagicMock()
|
sharpness_monitor = mocker.MagicMock()
|
||||||
|
|
@ -73,27 +74,25 @@ def test_looping_autofocus(start_z, max_loc, centre, attempts_expected, passes,
|
||||||
"""Generate sharpnesses based on parameterised input, and mock stage position."""
|
"""Generate sharpnesses based on parameterised input, and mock stage position."""
|
||||||
return fake_sharpness_data(
|
return fake_sharpness_data(
|
||||||
dz=dz,
|
dz=dz,
|
||||||
start_z=stage.position["z"],
|
start_z=autofocus_thing._stage.position["z"],
|
||||||
max_loc=max_loc,
|
max_loc=max_loc,
|
||||||
)
|
)
|
||||||
|
|
||||||
sharpness_monitor.move_data.side_effect = return_sharpness
|
sharpness_monitor.move_data.side_effect = return_sharpness
|
||||||
|
|
||||||
autofocus_thing = create_thing_without_server(AutofocusThing)
|
# Mock the context manager call so out MagicMock sharpness monitor is returned.
|
||||||
|
mock_context = mocker.patch(
|
||||||
|
"openflexure_microscope_server.things.autofocus.JPEGSharpnessMonitor"
|
||||||
|
)
|
||||||
|
mock_context.return_value.__enter__.return_value = sharpness_monitor
|
||||||
|
mock_context.return_value.__exit__.return_value = None
|
||||||
|
|
||||||
if passes:
|
if passes:
|
||||||
autofocus_thing.looping_autofocus(
|
autofocus_thing.looping_autofocus(dz=dz, start="centre" if centre else "base")
|
||||||
stage=stage,
|
|
||||||
sharpness_monitor=sharpness_monitor,
|
|
||||||
dz=dz,
|
|
||||||
start="centre" if centre else "base",
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
with pytest.raises(NoFocusFoundError):
|
with pytest.raises(NoFocusFoundError):
|
||||||
autofocus_thing.looping_autofocus(
|
autofocus_thing.looping_autofocus(
|
||||||
stage=stage,
|
dz=dz, start="centre" if centre else "base"
|
||||||
sharpness_monitor=sharpness_monitor,
|
|
||||||
dz=dz,
|
|
||||||
start="centre" if centre else "base",
|
|
||||||
)
|
)
|
||||||
assert sharpness_monitor.focus_rel.call_count == attempts_expected
|
assert sharpness_monitor.focus_rel.call_count == attempts_expected
|
||||||
assert abs(max_loc - stage.position["z"]) < dz / 40
|
assert abs(max_loc - autofocus_thing._stage.position["z"]) < dz / 40
|
||||||
|
|
|
||||||
|
|
@ -265,7 +265,7 @@ def test_retrieval_of_captures(start):
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def autofocus_thing():
|
def autofocus_thing():
|
||||||
"""Return an autofocus thing connected to a server."""
|
"""Return an autofocus thing connected to a server."""
|
||||||
return create_thing_without_server(AutofocusThing)
|
return create_thing_without_server(AutofocusThing, mock_all_slots=True)
|
||||||
|
|
||||||
|
|
||||||
def test_create_stack(autofocus_thing, caplog):
|
def test_create_stack(autofocus_thing, caplog):
|
||||||
|
|
@ -352,9 +352,6 @@ def test_coercing_stack_save_ims(
|
||||||
@pytest.mark.parametrize("pass_on", [1, 2, 3, 4])
|
@pytest.mark.parametrize("pass_on", [1, 2, 3, 4])
|
||||||
def test_run_smart_stack(pass_on, autofocus_thing, mocker):
|
def test_run_smart_stack(pass_on, autofocus_thing, mocker):
|
||||||
"""Test Running smart stack with the stack passing on different attempts."""
|
"""Test Running smart stack with the stack passing on different attempts."""
|
||||||
cam = mocker.Mock()
|
|
||||||
stage = mocker.Mock()
|
|
||||||
sharpness_monitor = mocker.MagicMock()
|
|
||||||
stack_params = autofocus_thing.create_stack_params(
|
stack_params = autofocus_thing.create_stack_params(
|
||||||
autofocus_dz=2000,
|
autofocus_dz=2000,
|
||||||
images_dir="/this/is/fake",
|
images_dir="/this/is/fake",
|
||||||
|
|
@ -386,9 +383,6 @@ def test_run_smart_stack(pass_on, autofocus_thing, mocker):
|
||||||
|
|
||||||
# Run it
|
# Run it
|
||||||
success, final_z = autofocus_thing.run_smart_stack(
|
success, final_z = autofocus_thing.run_smart_stack(
|
||||||
cam=cam,
|
|
||||||
stage=stage,
|
|
||||||
sharpness_monitor=sharpness_monitor,
|
|
||||||
stack_parameters=stack_params,
|
stack_parameters=stack_params,
|
||||||
save_on_failure=False,
|
save_on_failure=False,
|
||||||
check_turning_points=True,
|
check_turning_points=True,
|
||||||
|
|
@ -403,16 +397,16 @@ def test_run_smart_stack(pass_on, autofocus_thing, mocker):
|
||||||
n_stacks = min(pass_on, stack_params.max_attempts)
|
n_stacks = min(pass_on, stack_params.max_attempts)
|
||||||
assert autofocus_thing.z_stack.call_count == n_stacks
|
assert autofocus_thing.z_stack.call_count == n_stacks
|
||||||
# Move absolute should be 1 less time that the number of times z_stack_run
|
# Move absolute should be 1 less time that the number of times z_stack_run
|
||||||
assert stage.move_absolute.call_count == n_stacks - 1
|
assert autofocus_thing._stage.move_absolute.call_count == n_stacks - 1
|
||||||
# As should looping autofocus
|
# As should looping autofocus
|
||||||
assert autofocus_thing.looping_autofocus.call_count == n_stacks - 1
|
assert autofocus_thing.looping_autofocus.call_count == n_stacks - 1
|
||||||
|
|
||||||
# Check rest stack is moving to the first image in the stack.
|
# Check rest stack is moving to the first image in the stack.
|
||||||
if n_stacks > 1:
|
if n_stacks > 1:
|
||||||
assert stage.move_absolute.call_args.kwargs["z"] == -99
|
assert autofocus_thing._stage.move_absolute.call_args.kwargs["z"] == -99
|
||||||
|
|
||||||
# Mock called to save image
|
# Mock called to save image
|
||||||
assert cam.save_from_memory.call_count == (1 if success else 0)
|
assert autofocus_thing._cam.save_from_memory.call_count == (1 if success else 0)
|
||||||
|
|
||||||
|
|
||||||
def setup_and_run_z_stack(check_returns, check_turning_points, autofocus_thing, mocker):
|
def setup_and_run_z_stack(check_returns, check_turning_points, autofocus_thing, mocker):
|
||||||
|
|
@ -430,8 +424,6 @@ def setup_and_run_z_stack(check_returns, check_turning_points, autofocus_thing,
|
||||||
)
|
)
|
||||||
stack_params.settling_time = 0 # Don't settle or tests take forever.
|
stack_params.settling_time = 0 # Don't settle or tests take forever.
|
||||||
|
|
||||||
stage = mocker.Mock()
|
|
||||||
cam = mocker.Mock()
|
|
||||||
autofocus_thing.capture_stack_image = mocker.Mock()
|
autofocus_thing.capture_stack_image = mocker.Mock()
|
||||||
if isinstance(check_returns, list):
|
if isinstance(check_returns, list):
|
||||||
autofocus_thing.check_stack_result = mocker.Mock(side_effect=check_returns)
|
autofocus_thing.check_stack_result = mocker.Mock(side_effect=check_returns)
|
||||||
|
|
@ -440,8 +432,6 @@ def setup_and_run_z_stack(check_returns, check_turning_points, autofocus_thing,
|
||||||
return autofocus_thing.z_stack(
|
return autofocus_thing.z_stack(
|
||||||
stack_parameters=stack_params,
|
stack_parameters=stack_params,
|
||||||
check_turning_points=check_turning_points,
|
check_turning_points=check_turning_points,
|
||||||
cam=cam,
|
|
||||||
stage=stage,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -506,20 +496,16 @@ def test_z_stack_return(autofocus_thing, mocker):
|
||||||
assert ret[0]
|
assert ret[0]
|
||||||
|
|
||||||
|
|
||||||
def test_capture_stack_image(autofocus_thing, mocker):
|
def test_capture_stack_image(autofocus_thing):
|
||||||
"""Check that capture stack image calls the expected functions and returns the expected data."""
|
"""Check that capture stack image calls the expected functions and returns the expected data."""
|
||||||
stage = mocker.Mock()
|
autofocus_thing._stage.position = {"x": 123, "y": 456, "z": 789}
|
||||||
stage.position = {"x": 123, "y": 456, "z": 789}
|
autofocus_thing._cam.capture_to_memory.return_value = "fake_buffer_id"
|
||||||
cam = mocker.Mock()
|
autofocus_thing._cam.grab_jpeg_size.return_value = 54321
|
||||||
cam.capture_to_memory.return_value = "fake_buffer_id"
|
|
||||||
cam.grab_jpeg_size.return_value = 54321
|
|
||||||
buffer_max = 11
|
buffer_max = 11
|
||||||
|
|
||||||
info = autofocus_thing.capture_stack_image(
|
info = autofocus_thing.capture_stack_image(buffer_max=buffer_max)
|
||||||
cam=cam, stage=stage, buffer_max=buffer_max
|
assert autofocus_thing._cam.capture_to_memory.call_count == 1
|
||||||
)
|
assert autofocus_thing._cam.grab_jpeg_size.call_count == 1
|
||||||
assert cam.capture_to_memory.call_count == 1
|
|
||||||
assert cam.grab_jpeg_size.call_count == 1
|
|
||||||
assert info.buffer_id == "fake_buffer_id"
|
assert info.buffer_id == "fake_buffer_id"
|
||||||
assert info.position == {"x": 123, "y": 456, "z": 789}
|
assert info.position == {"x": 123, "y": 456, "z": 789}
|
||||||
assert info.sharpness == 54321
|
assert info.sharpness == 54321
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue