Looping autofocus errors on failure to find focus. Test looping autofocus algorithm
This commit is contained in:
parent
13e065a44d
commit
b9c58b70c9
2 changed files with 118 additions and 12 deletions
|
|
@ -421,11 +421,12 @@ class AutofocusThing(lt.Thing):
|
|||
is close to focus, but not quite within ``dz/2``. It will attempt to autofocus
|
||||
up to 10 times.
|
||||
"""
|
||||
attempts = 0
|
||||
attempt = 0
|
||||
backlash = 200
|
||||
|
||||
with sharpness_monitor.run():
|
||||
while attempts < 10:
|
||||
while attempt < 10:
|
||||
attempt += 1
|
||||
if start == "centre":
|
||||
stage.move_relative(x=0, y=0, z=-(backlash + dz / 2))
|
||||
stage.move_relative(x=0, y=0, z=backlash)
|
||||
|
|
@ -447,10 +448,11 @@ class AutofocusThing(lt.Thing):
|
|||
stage.move_absolute(z=peak_height)
|
||||
|
||||
if target_min < peak_height < target_max:
|
||||
# If it is within the target range then break.
|
||||
break
|
||||
|
||||
return heights.tolist(), sizes.tolist()
|
||||
# If it is within the target range then return
|
||||
return heights.tolist(), sizes.tolist()
|
||||
raise NoFocusFoundError(
|
||||
"Looping autofocus couldn't converge on a focus location."
|
||||
)
|
||||
|
||||
stack_images_to_save = lt.ThingSetting(
|
||||
initial_value=1,
|
||||
|
|
@ -611,12 +613,15 @@ class AutofocusThing(lt.Thing):
|
|||
# The z position of the first images in the previous attempt.
|
||||
initial_z_pos = captures[0].position["z"]
|
||||
# If a stack is not successful, move to the start and autofocus
|
||||
self.reset_stack(
|
||||
initial_z_pos,
|
||||
stack_parameters.autofocus_dz,
|
||||
stage,
|
||||
sharpness_monitor,
|
||||
)
|
||||
try:
|
||||
self.reset_stack(
|
||||
initial_z_pos,
|
||||
stack_parameters.autofocus_dz,
|
||||
stage,
|
||||
sharpness_monitor,
|
||||
)
|
||||
except NoFocusFoundError:
|
||||
break
|
||||
|
||||
# Save stack_parameters.image_to_save images centred on the sharpest capture.
|
||||
# If the smart_stack failed the exact number of images saved may not be
|
||||
|
|
@ -854,6 +859,10 @@ class NotAPeakError(RuntimeError):
|
|||
"""The data to fit isn't a peak."""
|
||||
|
||||
|
||||
class NoFocusFoundError(RuntimeError):
|
||||
"""No focus found during looping Autofocus."""
|
||||
|
||||
|
||||
def _get_peak_turning_point(sharpnesses: np.ndarray) -> float:
|
||||
"""Get the turing point for a sharpnesses in a z-stack.
|
||||
|
||||
|
|
|
|||
97
tests/test_autofocus.py
Normal file
97
tests/test_autofocus.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
"""Tests for the autofoucs logic.
|
||||
|
||||
This doesn't check the behaviour of the JPEG shaprness monitor.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
from openflexure_microscope_server.things.autofocus import (
|
||||
AutofocusThing,
|
||||
NoFocusFoundError,
|
||||
)
|
||||
|
||||
|
||||
def fake_sharpeness_data(
|
||||
dz: int, start_z: int, max_loc: int, length: int = 41
|
||||
) -> tuple[list[float], np.ndarray, np.ndarray]:
|
||||
"""Create some fake data for the shapeness.
|
||||
|
||||
The highest returned sharpness is closest to max_loc
|
||||
"""
|
||||
# Some fake timestamps
|
||||
times = [i / 10 + 100000 for i in range(length)]
|
||||
img_dz = dz / (length - 1)
|
||||
heights = [round(start_z + i * img_dz) for i in range(length)]
|
||||
# Shapeness is falls off linearly in this model.
|
||||
sharpnesses = [10 * dz - abs(max_loc - h) for h in heights]
|
||||
return times, np.array(heights), np.array(sharpnesses)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("start_z", "max_loc", "centre", "attempts_expected", "passes"),
|
||||
[
|
||||
# To complete the max must be in the central 1200, so -600 to 600 when looping
|
||||
# from -1000 to 1000
|
||||
(0, 550, True, 1, True), # Found in loop1 from -1000 to 1000
|
||||
(0, 650, True, 2, True), # Just outside the limit in loop1
|
||||
(0, 1300, True, 2, True), # Found in loop2 from 0 to 2000
|
||||
(0, 1300, False, 1, True), # Found in loop1 from 0 to 2000 (as start="base")
|
||||
(0, -1300, True, 2, True), # Found in loop2 from 0 to 2000
|
||||
(0, 7300, True, 8, True), # Found in loop8 from 6000 to 8000
|
||||
(0, 9300, True, 10, True), # Found in loop10 from 8000 to 10000
|
||||
(0, 9900, True, 10, False), # Still not central in loop 10, doesn't pass
|
||||
],
|
||||
)
|
||||
def test_looping_autofocus(start_z, max_loc, centre, attempts_expected, passes, mocker):
|
||||
"""Test the high level looping autofocus algorithm."""
|
||||
dz = 2000
|
||||
# Make a mock stage where move_absolute abs and relative updates the position counter.
|
||||
stage = mocker.Mock()
|
||||
stage.position = {"x": 0, "y": 0, "z": start_z}
|
||||
|
||||
def set_pos(**kwargs: int) -> None:
|
||||
"""Move absolute should update position. So make a side effect for the mock."""
|
||||
for axis, value in kwargs.items():
|
||||
stage.position[axis] = value
|
||||
|
||||
def adjust_pos(**kwargs: int) -> None:
|
||||
"""Move relative should update position. So make a side effect for the mock."""
|
||||
for axis, value in kwargs.items():
|
||||
stage.position[axis] += value
|
||||
|
||||
stage.move_absolute.side_effect = set_pos
|
||||
stage.move_relative.side_effect = adjust_pos
|
||||
|
||||
# Make a mock sharpness monitor that can generate sharpness data.
|
||||
sharpness_monitor = mocker.MagicMock()
|
||||
sharpness_monitor.focus_rel.return_value = (0, 0)
|
||||
|
||||
def return_shapness(*_args) -> tuple[list[float], np.ndarray, np.ndarray]:
|
||||
"""Generate shapenesses based on parameterised input, and mock stage position."""
|
||||
return fake_sharpeness_data(
|
||||
dz=dz,
|
||||
start_z=stage.position["z"],
|
||||
max_loc=max_loc,
|
||||
)
|
||||
|
||||
sharpness_monitor.move_data.side_effect = return_shapness
|
||||
|
||||
autofocus_thing = AutofocusThing()
|
||||
if passes:
|
||||
autofocus_thing.looping_autofocus(
|
||||
stage=stage,
|
||||
sharpness_monitor=sharpness_monitor,
|
||||
dz=dz,
|
||||
start="centre" if centre else "base",
|
||||
)
|
||||
else:
|
||||
with pytest.raises(NoFocusFoundError):
|
||||
autofocus_thing.looping_autofocus(
|
||||
stage=stage,
|
||||
sharpness_monitor=sharpness_monitor,
|
||||
dz=dz,
|
||||
start="centre" if centre else "base",
|
||||
)
|
||||
assert sharpness_monitor.focus_rel.call_count == attempts_expected
|
||||
assert abs(max_loc - stage.position["z"]) < dz / 40
|
||||
Loading…
Add table
Add a link
Reference in a new issue