Update move_and_measure, add unit tests
This commit is contained in:
parent
e93d9777a2
commit
563bfcf7be
2 changed files with 170 additions and 4 deletions
|
|
@ -536,20 +536,30 @@ class AutofocusThing(lt.Thing):
|
||||||
self,
|
self,
|
||||||
dz: Sequence[int],
|
dz: Sequence[int],
|
||||||
wait: float = 0,
|
wait: float = 0,
|
||||||
|
sharpness_metric: SharpnessMethod = SharpnessMethod.JPEG,
|
||||||
|
record: Optional[int] = None,
|
||||||
) -> SharpnessDataArrays:
|
) -> SharpnessDataArrays:
|
||||||
"""Make a move (or a series of moves) and monitor sharpness.
|
"""Make a move (or a series of moves) and monitor sharpness.
|
||||||
|
|
||||||
This method will will make a series of relative moves in z, and
|
This method will will make a series of relative moves in z, and
|
||||||
return the sharpness (JPEG size and FOM) vs time, along with timestamps
|
return the sharpness (JPEG size and/or FOM) vs time, along with timestamps
|
||||||
for the moves. This can be used to calibrate autofocus.
|
for the moves. This can be used to calibrate autofocus.
|
||||||
|
|
||||||
Each move is relative to the last one, i.e. we will finish at
|
Each move is relative to the last one, i.e. we will finish at
|
||||||
``sum(dz)`` relative to the starting position.
|
``sum(dz)`` relative to the starting position.
|
||||||
|
|
||||||
If ``wait`` is specified, we will wait for that many seconds
|
:param dz: A list of moves to make in z (in steps).
|
||||||
between moves.
|
:param wait: The time to pause between movements, in seconds.
|
||||||
|
:param sharpness_metric: Sharpness metric used for evaluation. Either
|
||||||
|
JPEG file size (1) or inbuilt figure of metric (2).
|
||||||
|
:param record: Optional bitmask of sharpness metrics to record during
|
||||||
|
acquisition. Sharpness metrics can be selected as the sum of their int
|
||||||
|
value (see sharpness_metric).
|
||||||
|
If None, defaults to recording only the selected sharpness_metric.
|
||||||
"""
|
"""
|
||||||
with JPEGSharpnessMonitor(self._stage, self._cam) as sharpness_monitor:
|
with JPEGSharpnessMonitor(
|
||||||
|
self._stage, self._cam, sharpness_metric, record
|
||||||
|
) 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)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ from labthings_fastapi.testing import create_thing_without_server
|
||||||
|
|
||||||
from openflexure_microscope_server.things.autofocus import (
|
from openflexure_microscope_server.things.autofocus import (
|
||||||
AutofocusThing,
|
AutofocusThing,
|
||||||
|
JPEGSharpnessMonitor,
|
||||||
NoFocusFoundError,
|
NoFocusFoundError,
|
||||||
|
SharpnessMethod,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -98,3 +100,157 @@ def test_looping_autofocus(start_z, max_loc, centre, attempts_expected, passes,
|
||||||
)
|
)
|
||||||
assert sharpness_monitor.focus_rel.call_count == attempts_expected
|
assert sharpness_monitor.focus_rel.call_count == attempts_expected
|
||||||
assert abs(max_loc - autofocus_thing._stage.position["z"]) < dz / 40
|
assert abs(max_loc - autofocus_thing._stage.position["z"]) < dz / 40
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_camera(mocker):
|
||||||
|
"""Mock a camera to return fom properties."""
|
||||||
|
camera = mocker.MagicMock()
|
||||||
|
camera.supports_focus_fom = True
|
||||||
|
camera.focus_fom = 123
|
||||||
|
return camera
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_stage(mocker):
|
||||||
|
"""Mock a stage to return position."""
|
||||||
|
stage = mocker.MagicMock()
|
||||||
|
stage.position = {"x": 0, "y": 0, "z": 0}
|
||||||
|
return stage
|
||||||
|
|
||||||
|
|
||||||
|
def test_record_defaults_to_method(mock_stage, mock_camera):
|
||||||
|
"""If record=None, only the selected method is recorded."""
|
||||||
|
monitor = JPEGSharpnessMonitor(
|
||||||
|
mock_stage,
|
||||||
|
mock_camera,
|
||||||
|
method=SharpnessMethod.FOCUS_FOM,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert monitor.record == SharpnessMethod.FOCUS_FOM
|
||||||
|
|
||||||
|
|
||||||
|
def test_record_must_include_selected_method(mock_stage, mock_camera):
|
||||||
|
"""The selected autofocus metric must also be recorded."""
|
||||||
|
with pytest.raises(ValueError, match="not being recorded"):
|
||||||
|
JPEGSharpnessMonitor(
|
||||||
|
mock_stage,
|
||||||
|
mock_camera,
|
||||||
|
method=SharpnessMethod.FOCUS_FOM,
|
||||||
|
record=SharpnessMethod.JPEG,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_focus_fom_requires_camera_support(mock_stage, mock_camera):
|
||||||
|
"""FocusFoM recording requires camera support."""
|
||||||
|
mock_camera.supports_focus_fom = False
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="doesn't support"):
|
||||||
|
JPEGSharpnessMonitor(
|
||||||
|
mock_stage,
|
||||||
|
mock_camera,
|
||||||
|
method=SharpnessMethod.FOCUS_FOM,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_jpeg_sizes_property_requires_recording(mock_stage, mock_camera):
|
||||||
|
"""JPEG sizes should error if JPEG recording disabled."""
|
||||||
|
monitor = JPEGSharpnessMonitor(
|
||||||
|
mock_stage,
|
||||||
|
mock_camera,
|
||||||
|
method=SharpnessMethod.FOCUS_FOM,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="JPEG sizes are not being recorded"):
|
||||||
|
_ = monitor.jpeg_sizes
|
||||||
|
|
||||||
|
|
||||||
|
def test_focus_fom_property_requires_recording(mock_stage, mock_camera):
|
||||||
|
"""FocusFoM values should error if FOM recording disabled."""
|
||||||
|
monitor = JPEGSharpnessMonitor(
|
||||||
|
mock_stage,
|
||||||
|
mock_camera,
|
||||||
|
method=SharpnessMethod.JPEG,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="FocusFoM values are not being recorded"):
|
||||||
|
_ = monitor.focus_foms
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("method", "expected"),
|
||||||
|
[
|
||||||
|
(SharpnessMethod.JPEG, [100, 200, 300]),
|
||||||
|
(SharpnessMethod.FOCUS_FOM, [1, 5, 2]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_move_data_uses_selected_method(
|
||||||
|
method,
|
||||||
|
expected,
|
||||||
|
mock_stage,
|
||||||
|
mock_camera,
|
||||||
|
):
|
||||||
|
"""move_data should select the correct sharpness metric."""
|
||||||
|
monitor = JPEGSharpnessMonitor(
|
||||||
|
mock_stage,
|
||||||
|
mock_camera,
|
||||||
|
method=method,
|
||||||
|
record=SharpnessMethod.JPEG + SharpnessMethod.FOCUS_FOM,
|
||||||
|
)
|
||||||
|
|
||||||
|
monitor._jpeg_times = [1.0, 2.0, 3.0]
|
||||||
|
monitor._jpeg_sizes = [100, 200, 300]
|
||||||
|
monitor._focus_foms = [1, 5, 2]
|
||||||
|
|
||||||
|
monitor._stage_times = [0.5, 3.5]
|
||||||
|
monitor._stage_positions = [
|
||||||
|
{"z": 0},
|
||||||
|
{"z": 100},
|
||||||
|
]
|
||||||
|
|
||||||
|
_, _, sharpnesses = monitor.move_data(0)
|
||||||
|
|
||||||
|
assert sharpnesses.tolist() == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_monitor_records_both_metrics(mock_stage, mock_camera):
|
||||||
|
"""Both JPEG and FocusFoM metrics can be recorded together."""
|
||||||
|
monitor = JPEGSharpnessMonitor(
|
||||||
|
mock_stage,
|
||||||
|
mock_camera,
|
||||||
|
method=SharpnessMethod.JPEG,
|
||||||
|
record=SharpnessMethod.JPEG + SharpnessMethod.FOCUS_FOM,
|
||||||
|
)
|
||||||
|
|
||||||
|
monitor._jpeg_sizes.append(999)
|
||||||
|
monitor._focus_foms.append(321)
|
||||||
|
|
||||||
|
assert monitor.jpeg_sizes == [999]
|
||||||
|
assert monitor.focus_foms == [321]
|
||||||
|
|
||||||
|
|
||||||
|
def test_move_data_uses_focus_fom(mock_stage, mock_camera):
|
||||||
|
"""Test that move_data returns the chosen metric."""
|
||||||
|
monitor = JPEGSharpnessMonitor(
|
||||||
|
mock_stage,
|
||||||
|
mock_camera,
|
||||||
|
method=SharpnessMethod.FOCUS_FOM,
|
||||||
|
record=SharpnessMethod.JPEG + SharpnessMethod.FOCUS_FOM,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Record both sharpness metrics
|
||||||
|
monitor._jpeg_times = [1.0, 2.0, 3.0]
|
||||||
|
monitor._jpeg_sizes = [100, 200, 300]
|
||||||
|
monitor._focus_foms = [10, 99, 20]
|
||||||
|
|
||||||
|
monitor._stage_times = [0.5, 3.5]
|
||||||
|
monitor._stage_positions = [
|
||||||
|
{"z": 0},
|
||||||
|
{"z": 100},
|
||||||
|
]
|
||||||
|
|
||||||
|
# sharpnesses chosen by the chosen method
|
||||||
|
_, _, sharpnesses = monitor.move_data(0)
|
||||||
|
|
||||||
|
# ensure sharpnesses matches focus_foms
|
||||||
|
assert sharpnesses.tolist() == [10, 99, 20]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue