Merge branch 'stack-trends' into 'v3'
Stack trends Closes #752 See merge request openflexure/openflexure-microscope-server!564
This commit is contained in:
commit
924676ff5d
3 changed files with 158 additions and 5 deletions
|
|
@ -676,10 +676,11 @@ class AutofocusThing(lt.Thing):
|
|||
sharpness=self._cam.grab_jpeg_size(stream_name="lores"),
|
||||
)
|
||||
|
||||
# Silence too many returns in this situation as refactoring to reduce returns is
|
||||
# unlikely to improve readability. This function is basically a complex switch
|
||||
# statement, having an explicit return after each option is clear.
|
||||
def check_stack_result( # noqa: PLR0911
|
||||
# Silence too many returns and complexity in this situation as refactoring to
|
||||
# reduce returns is unlikely to improve readability.
|
||||
# This function is basically a complex switch statement, having an explicit
|
||||
# return after each option is clear.
|
||||
def check_stack_result( # noqa: PLR0911 C901
|
||||
self, captures: list[CaptureInfo], check_turning_points: bool
|
||||
) -> tuple[Literal["success", "continue", "restart"], int]:
|
||||
"""Check if the sharpest image in a list of captures is central enough.
|
||||
|
|
@ -688,7 +689,7 @@ class AutofocusThing(lt.Thing):
|
|||
sharpness has converged in the centre
|
||||
: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
|
||||
thick samples)
|
||||
thick samples and stacks with many images)
|
||||
|
||||
:returns: A tuple with two values:
|
||||
|
||||
|
|
@ -719,6 +720,13 @@ class AutofocusThing(lt.Thing):
|
|||
return "restart", capture_id
|
||||
return "continue", capture_id
|
||||
|
||||
# Manually test for monotomically increasing or decreasing sharpnesses, as
|
||||
# fitting can struggle with these and their behaviour is simpler to hardcode
|
||||
if np.all(sharpnesses[:-1] <= sharpnesses[1:]):
|
||||
return "continue", capture_id
|
||||
if np.all(sharpnesses[:-1] >= sharpnesses[1:]):
|
||||
return "restart", capture_id
|
||||
|
||||
try:
|
||||
turning = _get_peak_turning_point(sharpnesses)
|
||||
except NotAPeakError:
|
||||
|
|
|
|||
43
tests/unit_tests/data/sharpness_test_cases.json
Normal file
43
tests/unit_tests/data/sharpness_test_cases.json
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[
|
||||
{"sharpnesses":[100000,300000,500000,300000,100000],"label":"success"},
|
||||
{"sharpnesses":[100000,200000,400000,600000,400000,200000,100000],"label":"success"},
|
||||
{"sharpnesses":[100000,200000,300000,400000,500000],"label":"continue"},
|
||||
{"sharpnesses":[500000,400000,300000,200000,100000],"label":"restart"},
|
||||
{"sharpnesses":[100000,200000,300000,200000,100000,50000],"label":["success"]},
|
||||
{"sharpnesses":[0,100000,200000,300000,400000,500000],"label":"continue"},
|
||||
{"sharpnesses":[500000,500000,500000,500000,500000],"label":"continue"},
|
||||
{"sharpnesses":[100000,200000,300000,300000,200000,100000],"label":["success"]},
|
||||
{"sharpnesses":[100000,200000,300000,400000,400000,300000,200000],"label":["success","continue"]},
|
||||
{"sharpnesses":[100000,300000,500000,500000,500000,300000,100000],"label":["success","continue"]},
|
||||
{"sharpnesses":[100000,300000,500000,400000,500000,300000,100000],"label":["continue","success"]},
|
||||
{"sharpnesses":[100000,400000,600000,500000,300000,200000,100000],"label":"success"},
|
||||
{"sharpnesses":[100000,200000,500000,700000,600000,300000,100000],"label":"success"},
|
||||
{"sharpnesses":[100000,200000,300000,500000,700000,600000,400000],"label":["success","continue"]},
|
||||
{"sharpnesses":[700000,600000,500000,400000,300000,200000,100000,0],"label":"restart"},
|
||||
{"sharpnesses":[100000,300000,600000,900000,600000,300000,200000,100000],"label":"success"},
|
||||
{"sharpnesses":[100000,200000,400000,800000,900000,800000,400000,200000],"label":"success"},
|
||||
{"sharpnesses":[100000,200000,300000,400000,500000,600000,700000,800000],"label":"continue"},
|
||||
{"sharpnesses":[800000,700000,600000,500000,400000,300000,200000,100000],"label":"restart"},
|
||||
{"sharpnesses":[100000,200000,300000,400000,400000,400000,300000,200000],"label":["success","continue"]},
|
||||
{"sharpnesses":[100000,200000,300000,500000,500000,500000,500000,500000],"label":"continue"},
|
||||
{"sharpnesses":[500000,500000,500000,500000,500000,400000,300000,200000],"label":"restart"},
|
||||
{"sharpnesses":[100000,200000,300000,600000,900000,600000,300000,200000,100000],"label":"success"},
|
||||
{"sharpnesses":[100000,200000,400000,700000,1000000,900000,700000,400000,200000],"label":"success"},
|
||||
{"sharpnesses":[100000,200000,300000,400000,500000,600000,700000,800000,900000],"label":"continue"},
|
||||
{"sharpnesses":[900000,800000,700000,600000,500000,400000,300000,200000,100000],"label":"restart"},
|
||||
{"sharpnesses":[100000,200000,1000000,200000,1000000,200000,100000],"label":"continue"},
|
||||
{"sharpnesses":[100000,500000,100000,500000,100000,500000,100000],"label":["restart","continue"]},
|
||||
{"sharpnesses":[300000,300000,300000,300000,300000,300000,300000],"label":"continue"},
|
||||
{"sharpnesses":[500000,600000,800000,1100000,1500000,1100000,800000,600000,500000],"label":"success"},
|
||||
{"sharpnesses":[1000000,1100000,1300000,1600000,2000000,1600000,1300000,1100000,1000000],"label":"success"},
|
||||
{"sharpnesses":[2000000,1800000,1500000,1200000,1000000,900000,800000,700000,600000],"label":"restart"},
|
||||
{"sharpnesses":[600000,700000,800000,900000,1100000,1300000,1500000,1700000,1800000],"label":"continue"},
|
||||
{"sharpnesses":[500000,700000,1000000,1400000,1800000,1700000,1400000,1000000,700000],"label":"success"},
|
||||
{"sharpnesses":[500000,600000,900000,1300000,1800000,1900000,1700000,1300000,900000],"label":"success"},
|
||||
{"sharpnesses":[500000,500000,500000,500000,600000,900000,600000,500000,500000],"label":["success"],"allow_failure":true},
|
||||
{"sharpnesses":[500000,500000,500000,500000,500000,600000,900000,600000,500000],"label":["success","continue"]},
|
||||
{"sharpnesses":[500000,500000,500000,500000,500000,500000,600000,900000,600000],"label":"continue"},
|
||||
{"sharpnesses":[100000,100000,100000,100000,200000,500000,200000,100000,100000],"label":["success"],"allow_failure":true},
|
||||
{"sharpnesses":[100000,100000,100000,100000,100000,200000,500000,200000,100000],"label":["success","continue"]},
|
||||
{"sharpnesses":[100000,100000,100000,100000,100000,100000,200000,500000,200000],"label":"continue"}
|
||||
]
|
||||
102
tests/unit_tests/test_stack_examples.py
Normal file
102
tests/unit_tests/test_stack_examples.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
"""Unit tests for validating stack classification logic in AutofocusThing.
|
||||
|
||||
This module tests whether the `check_stack_result` method correctly classifies
|
||||
z-stack sharpness profiles into "success", "continue", or "restart" categories
|
||||
based on predefined test cases.
|
||||
|
||||
Sharpness profiles are loaded from a JSON file and converted into mock capture
|
||||
objects to simulate real camera captures.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from labthings_fastapi.testing import create_thing_without_server
|
||||
|
||||
from openflexure_microscope_server.things.autofocus import AutofocusThing
|
||||
|
||||
THIS_DIR = os.path.dirname(__file__)
|
||||
DATA_PATH = os.path.join(THIS_DIR, "data", "sharpness_test_cases.json")
|
||||
|
||||
|
||||
class MockCapture:
|
||||
"""Simple mock object representing a captured image.
|
||||
|
||||
This class mimics the minimal interface required by
|
||||
`check_stack_result` by adding the `sharpness` and `buffer_id`
|
||||
attributes.
|
||||
|
||||
:param sharpness: The sharpness value associated with the image.
|
||||
:param buffer_id: A unique identifier for the image buffer.
|
||||
"""
|
||||
|
||||
def __init__(self, sharpness, buffer_id):
|
||||
"""Give each capture a sharpness and buffer_id."""
|
||||
self.sharpness = sharpness
|
||||
self.buffer_id = buffer_id
|
||||
|
||||
|
||||
def make_captures(sharpness_list):
|
||||
"""Convert a list of sharpness values into mock capture objects.
|
||||
|
||||
:param sharpness_list: A list of numeric sharpness values.
|
||||
:returns: A list of MockCapture instances with sequential buffer IDs.
|
||||
"""
|
||||
return [MockCapture(s, i) for i, s in enumerate(sharpness_list)]
|
||||
|
||||
|
||||
def load_cases():
|
||||
"""Load the sharpness data from the json.
|
||||
|
||||
Includes the sharpnesses to test, manually written labels
|
||||
on the required result, and whether the test is allowed to
|
||||
fail. This ensures future tests will flag any regression on
|
||||
tests while allowing improvements.
|
||||
"""
|
||||
with open(DATA_PATH) as f:
|
||||
data = json.load(f)
|
||||
|
||||
params = []
|
||||
for i, case in enumerate(data):
|
||||
expected = case["label"]
|
||||
|
||||
# Allow multiple acceptable labels
|
||||
if not isinstance(expected, list):
|
||||
expected = [expected]
|
||||
|
||||
marks = []
|
||||
|
||||
if case.get("allow_failure", False):
|
||||
marks.append(pytest.mark.xfail(reason="Known failing case"))
|
||||
|
||||
params.append(
|
||||
pytest.param(
|
||||
case["sharpnesses"],
|
||||
expected,
|
||||
marks=marks,
|
||||
id=f"case_{i}",
|
||||
)
|
||||
)
|
||||
return params
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("sharpnesses", "expected"), load_cases())
|
||||
def test_stack_labelling(sharpnesses, expected):
|
||||
"""Test stack classification accuracy against labelled sharpness cases.
|
||||
|
||||
This test loads the test from load_cases and evaluates
|
||||
the classification returned by `check_stack_result`.
|
||||
|
||||
The test ensures that only cases marked with "allow_failure: true" can fail.
|
||||
|
||||
Expected labels may be a single value or a list of acceptable values.
|
||||
"""
|
||||
autofocus_thing = create_thing_without_server(AutofocusThing, mock_all_slots=True)
|
||||
|
||||
captures = make_captures(sharpnesses)
|
||||
|
||||
result, _ = autofocus_thing.check_stack_result(captures, check_turning_points=False)
|
||||
|
||||
assert result in expected
|
||||
Loading…
Add table
Add a link
Reference in a new issue