Add tests for checking stack result
This commit is contained in:
parent
a15f23bec2
commit
e5d2ebbb79
2 changed files with 165 additions and 2 deletions
|
|
@ -845,7 +845,7 @@ class AutofocusThing(lt.Thing):
|
||||||
# edge of the stack.
|
# edge of the stack.
|
||||||
if turning < edge_size - 0.5 or sharpest_index < edge_size:
|
if turning < edge_size - 0.5 or sharpest_index < edge_size:
|
||||||
return "restart", capture_id
|
return "restart", capture_id
|
||||||
if turning > n_imgs - (edge_size - 0.5) or sharpest_index >= n_imgs - edge_size:
|
if turning > n_imgs - edge_size - 0.5 or sharpest_index >= n_imgs - edge_size:
|
||||||
return "continue", capture_id
|
return "continue", capture_id
|
||||||
|
|
||||||
if check_turning_points:
|
if check_turning_points:
|
||||||
|
|
@ -886,7 +886,8 @@ def _get_peak_turning_point(sharpnesses: np.ndarray) -> float:
|
||||||
ci_high = a + 2 * sigma_a
|
ci_high = a + 2 * sigma_a
|
||||||
# If the high 95% confindence interval of the peak is not negative then the
|
# If the high 95% confindence interval of the peak is not negative then the
|
||||||
# fit isn't sure if this is a peak or a U shape, so we continue.
|
# fit isn't sure if this is a peak or a U shape, so we continue.
|
||||||
if ci_high >= 0:
|
# -1e-9 is used to guard against weird effects for flat and straight data
|
||||||
|
if ci_high > -1e-9:
|
||||||
raise NotAPeakError("Not a peak to within 95% confidence.")
|
raise NotAPeakError("Not a peak to within 95% confidence.")
|
||||||
return turning
|
return turning
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ from openflexure_microscope_server.things.autofocus import (
|
||||||
_get_capture_by_id,
|
_get_capture_by_id,
|
||||||
_get_capture_index_by_id,
|
_get_capture_index_by_id,
|
||||||
EXTRA_STACK_CAPTURES,
|
EXTRA_STACK_CAPTURES,
|
||||||
|
NotAPeakError,
|
||||||
|
_get_peak_turning_point,
|
||||||
|
_count_turning_points,
|
||||||
)
|
)
|
||||||
from openflexure_microscope_server.scan_directories import IMAGE_REGEX
|
from openflexure_microscope_server.scan_directories import IMAGE_REGEX
|
||||||
|
|
||||||
|
|
@ -526,3 +529,162 @@ def test_capture_stack_image(autofocus_thing, mocker):
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
def mock_capture(buffer_id: int, sharpness: int) -> CaptureInfo:
|
||||||
|
"""Create a CaptureInfo instance with a dummy position."""
|
||||||
|
return CaptureInfo(
|
||||||
|
buffer_id=buffer_id,
|
||||||
|
position={"x": 0, "y": 0, "z": buffer_id},
|
||||||
|
sharpness=sharpness,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_stack_single_image_returns_success(autofocus_thing):
|
||||||
|
"""A single image is always successful."""
|
||||||
|
captures = [mock_capture("mock-id", 10)]
|
||||||
|
result, cap_id = autofocus_thing.check_stack_result(
|
||||||
|
captures, check_turning_points=False
|
||||||
|
)
|
||||||
|
assert result == "success"
|
||||||
|
assert cap_id == "mock-id"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("sharpnesses", "expected"),
|
||||||
|
[
|
||||||
|
([5, 10, 3], "success"),
|
||||||
|
([10, 4, 2], "restart"),
|
||||||
|
([1, 2, 10], "continue"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_check_stack_three_image_logic(sharpnesses, expected, autofocus_thing):
|
||||||
|
"""For 3 images, success is the highest one is central."""
|
||||||
|
captures = [mock_capture(i, s) for i, s in enumerate(sharpnesses)]
|
||||||
|
result, _ = autofocus_thing.check_stack_result(captures, check_turning_points=False)
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
def _run_check_stack_with_good_peak(autofocus_thing, count_turnings=False):
|
||||||
|
"""Run check stack on a good peak that should pass, and return the result.
|
||||||
|
|
||||||
|
This can be used to check how other mocked results of subfunctions affects the
|
||||||
|
result.
|
||||||
|
"""
|
||||||
|
# Create an obvious peak that would normally pass.
|
||||||
|
sharpnesses = [1, 2, 4, 7, 12, 7, 4, 2, 1]
|
||||||
|
captures = [mock_capture(i, s) for i, s in enumerate(sharpnesses)]
|
||||||
|
|
||||||
|
result, cap_id = autofocus_thing.check_stack_result(
|
||||||
|
captures, check_turning_points=count_turnings
|
||||||
|
)
|
||||||
|
# Nothing a mocked function does should change which is the sharpedt image.
|
||||||
|
assert cap_id == 4
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_stack_continues_if_no_tuning_point(autofocus_thing, mocker):
|
||||||
|
"""Check that continue is returned if no turning point is found."""
|
||||||
|
# Mock to simulate not finding a peak
|
||||||
|
mocker.patch(
|
||||||
|
"openflexure_microscope_server.things.autofocus._get_peak_turning_point",
|
||||||
|
side_effect=NotAPeakError,
|
||||||
|
)
|
||||||
|
result = _run_check_stack_with_good_peak(autofocus_thing)
|
||||||
|
# Check that the NotAPeakError causes it to continue instead.
|
||||||
|
assert result == "continue"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("location", "expected"),
|
||||||
|
[
|
||||||
|
(-10, "restart"), # Restart if lower than 1.5 (halfway between im 2 and 3)
|
||||||
|
(-1, "restart"),
|
||||||
|
(0, "restart"),
|
||||||
|
(1, "restart"),
|
||||||
|
(1.49, "restart"),
|
||||||
|
(1.5, "success"), # Success up to 6.5 (as we have 9 images, final index is 8)
|
||||||
|
(2.5, "success"),
|
||||||
|
(4.5, "success"),
|
||||||
|
(6.5, "success"),
|
||||||
|
(6.51, "continue"), # Continue if thrung point is after 6.5
|
||||||
|
(7, "continue"),
|
||||||
|
(8.1, "continue"),
|
||||||
|
(123, "continue"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_check_stack_affected_by_turning_point_location(
|
||||||
|
location, expected, autofocus_thing, mocker
|
||||||
|
):
|
||||||
|
"""Check that the turning point location affects the return as expected."""
|
||||||
|
# Mock to give the turning point location specified
|
||||||
|
mocker.patch(
|
||||||
|
"openflexure_microscope_server.things.autofocus._get_peak_turning_point",
|
||||||
|
return_value=location,
|
||||||
|
)
|
||||||
|
result = _run_check_stack_with_good_peak(autofocus_thing)
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_stack_affected_by_number_of_turning_points(autofocus_thing, mocker):
|
||||||
|
"""Check that the turning point location affects the return as expected."""
|
||||||
|
# Set the turning point to the centre
|
||||||
|
mocker.patch(
|
||||||
|
"openflexure_microscope_server.things.autofocus._get_peak_turning_point",
|
||||||
|
return_value=5,
|
||||||
|
)
|
||||||
|
mocker.patch(
|
||||||
|
"openflexure_microscope_server.things.autofocus._count_turning_points",
|
||||||
|
return_value=1,
|
||||||
|
)
|
||||||
|
result = _run_check_stack_with_good_peak(autofocus_thing, count_turnings=True)
|
||||||
|
# Successful with 1 peak
|
||||||
|
assert result == "success"
|
||||||
|
|
||||||
|
# Change return to be 2 peaks
|
||||||
|
mocker.patch(
|
||||||
|
"openflexure_microscope_server.things.autofocus._count_turning_points",
|
||||||
|
return_value=2,
|
||||||
|
)
|
||||||
|
result = _run_check_stack_with_good_peak(autofocus_thing, count_turnings=True)
|
||||||
|
# Continue with 2 peaks
|
||||||
|
assert result == "continue"
|
||||||
|
# Unless this check is turned off
|
||||||
|
result = _run_check_stack_with_good_peak(autofocus_thing, count_turnings=False)
|
||||||
|
assert result == "success"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_peak_turning_point():
|
||||||
|
"""Check that the peak fitting returns expected value (or error)."""
|
||||||
|
with pytest.raises(NotAPeakError):
|
||||||
|
_get_peak_turning_point(np.ones(9))
|
||||||
|
|
||||||
|
linear = np.arange(9)
|
||||||
|
u_shape = 2 * (linear - 4) ** 2 + 17
|
||||||
|
peak = -2 * (linear - 4) ** 2 + 55
|
||||||
|
|
||||||
|
with pytest.raises(NotAPeakError):
|
||||||
|
_get_peak_turning_point(linear)
|
||||||
|
|
||||||
|
with pytest.raises(NotAPeakError):
|
||||||
|
_get_peak_turning_point(u_shape)
|
||||||
|
|
||||||
|
# Should be 4 to within a fitting error
|
||||||
|
assert abs(_get_peak_turning_point(peak) - 4) < 1e-7
|
||||||
|
|
||||||
|
|
||||||
|
def test_count_turning_points():
|
||||||
|
"""Check the turing point count works as expected."""
|
||||||
|
linear = np.arange(9)
|
||||||
|
u_shape = 2 * (linear - 4) ** 2 + 17
|
||||||
|
peak = -2 * (linear - 4) ** 2 + 55
|
||||||
|
assert _count_turning_points(np.ones(9)) == 0
|
||||||
|
assert _count_turning_points(linear) == 0
|
||||||
|
assert _count_turning_points(u_shape) == 1
|
||||||
|
assert _count_turning_points(peak) == 1
|
||||||
|
|
||||||
|
assert _count_turning_points(np.array([1, 2, 3, 4, 5, 4, 3, 2, 1])) == 1
|
||||||
|
# Double peak is 3 points
|
||||||
|
assert _count_turning_points(np.array([1, 2, 3, 4, 2, 4, 3, 2, 1])) == 3
|
||||||
|
# But only one if the dip isn't prominent
|
||||||
|
assert _count_turning_points(np.array([1, 2, 3, 4, 3.8, 4, 3, 2, 1])) == 1
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue