diff --git a/tests/unit_tests/data/sharpness_test_cases.json b/tests/unit_tests/data/sharpness_test_cases.json new file mode 100644 index 00000000..0a50f44f --- /dev/null +++ b/tests/unit_tests/data/sharpness_test_cases.json @@ -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"]}, + {"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"]}, + {"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"} +] \ No newline at end of file diff --git a/tests/unit_tests/test_stack_examples.py b/tests/unit_tests/test_stack_examples.py new file mode 100644 index 00000000..217eedf7 --- /dev/null +++ b/tests/unit_tests/test_stack_examples.py @@ -0,0 +1,80 @@ +"""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 + +from labthings_fastapi.testing import create_thing_without_server + +from openflexure_microscope_server.things.autofocus import AutofocusThing + + +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 test_stack_labelling(): + """Test stack classification accuracy against labelled sharpness cases. + + This test loads predefined sharpness profiles and their expected labels + from a JSON file, converts them into mock capture objects, and evaluates + the classification returned by `check_stack_result`. + + The test asserts that at least 90% of cases are correctly classified. + + Expected labels may be a single value or a list of acceptable values. + """ + autofocus_thing = create_thing_without_server(AutofocusThing, mock_all_slots=True) + with open(r"tests\unit_tests\data\sharpness_test_cases.json") as f: + data = json.load(f) + + success = 0 + total = len(data) + + for _i, case in enumerate(data): + sharpnesses = case["sharpnesses"] + expected = case["label"] + + # Allow multiple acceptable labels + if not isinstance(expected, list): + expected = [expected] + + # Convert to capture objects + captures = make_captures(sharpnesses) + + # Call the method under test + result, _ = autofocus_thing.check_stack_result( + captures, check_turning_points=False + ) + if result in expected: + success += 1 + + assert success > 0.8 * total