diff --git a/src/openflexure_microscope_server/things/scan_workflows.py b/src/openflexure_microscope_server/things/scan_workflows.py index 2f237d04..fd072bcc 100644 --- a/src/openflexure_microscope_server/things/scan_workflows.py +++ b/src/openflexure_microscope_server/things/scan_workflows.py @@ -422,20 +422,22 @@ class HistoScanWorkflow(ScanWorkflow[HistoScanSettingsModel]): :return: A tuple of whether an image was taken, and the z-position for focus. If failed to find focus, returns for the focus z-position. """ - # If skipping background, take an image to check if current field of view is background if settings.skip_background: + # If skipping background, take an image to check if current field of view + # is background image_array = self._cam.grab_as_array(stream_name="lores") capture_image, bg_message = self._background_detector.image_is_sample( image_array ) del image_array - if not capture_image: - msg = f"Skipping {xyz_pos} as it is {bg_message}." - self.logger.info(msg) - return False, None + if not capture_image: + # Return early if the image is background. + msg = f"Skipping {xyz_pos} as it is {bg_message}." + self.logger.info(msg) + return False, None - save_on_failure = settings.skip_background + save_on_failure = not settings.skip_background focus_height: Optional[int] focused, focus_height = self._autofocus.run_smart_stack( @@ -445,6 +447,10 @@ class HistoScanWorkflow(ScanWorkflow[HistoScanSettingsModel]): # An image was captured if we are focussed or we are not skipping background. imaged = focused or save_on_failure + if not imaged: + msg = f"Stack failed at {xyz_pos} treating as background." + self.logger.info(msg) + # run_smart_stage always returns a focus height for the sharpest image even # if it failed to find a good focus. Set to None if not focussed. if not focused: diff --git a/tests/unit_tests/test_scan_workflows.py b/tests/unit_tests/test_scan_workflows.py index 705e01cc..6fb2ff70 100644 --- a/tests/unit_tests/test_scan_workflows.py +++ b/tests/unit_tests/test_scan_workflows.py @@ -9,6 +9,7 @@ from pydantic import BaseModel import labthings_fastapi as lt from labthings_fastapi.testing import create_thing_without_server +from openflexure_microscope_server.scan_planners import SmartSpiral from openflexure_microscope_server.stitching import StitchingSettings from openflexure_microscope_server.things.autofocus import SmartStackParams from openflexure_microscope_server.things.camera_stage_mapping import csm_img_to_stage @@ -190,6 +191,146 @@ def test_histo_calculate_overlap(csm_matrix, overlap, expected_steps, histo_work assert (dx, dy) == expected_steps +def test_histo_pre_scan_routine(histo_workflow, mocker): + """Check the pre-scan routine does autofocusses.""" + # Rather than create a whole Setting class, just create a mock with the value + # we need set + mock_settings = mocker.Mock() + mock_settings.smart_stack_params.autofocus_dz = 1234 + # Run the function + histo_workflow.pre_scan_routine(mock_settings) + # Check the autofocus was run using the mocked slot. + assert histo_workflow._autofocus.looping_autofocus.call_count == 1 + call_kwargs = histo_workflow._autofocus.looping_autofocus.call_args.kwargs + assert call_kwargs["dz"] == 1234 + assert call_kwargs["start"] == "centre" + + +def test_histo_new_scan_planner(histo_workflow, mocker): + """Check the pre-scan routine does autofocusses.""" + # Rather than create a whole Setting class, just create a mock with the 3 values + # we need set + mock_settings = mocker.Mock() + mock_settings.dx = 666 + mock_settings.dy = 999 + mock_settings.max_dist = 54321 + + planner = histo_workflow.new_scan_planner( + mock_settings, position={"x": 1, "y": 2, "z": 3} + ) + assert isinstance(planner, SmartSpiral) + # Check the settings were read + assert planner._dx == 666 + assert planner._dy == 999 + assert planner._max_dist == 54321 + + +def test_histo_acquisition_with_background_detected(histo_workflow, mocker, caplog): + """If background is detected it should return without a stack.""" + # Mocking for settings as above + mock_settings = mocker.Mock() + mock_settings.skip_background = True + + histo_workflow._background_detector.image_is_sample.return_value = ( + False, + "totally empty", + ) + + with caplog.at_level(logging.INFO): + imaged, focus_height = histo_workflow.acquisition_routine( + mock_settings, xyz_pos=(1, 2, 3) + ) + + # Check returns + assert imaged is False + assert focus_height is None # None meaning not focussed + # Check there is a log explaining what happened + assert len(caplog.records) == 1 + assert caplog.records[0].message == "Skipping (1, 2, 3) as it is totally empty." + assert caplog.records[0].levelname == "INFO" + + # And that smart stack never ran + assert histo_workflow._autofocus.run_smart_stack.call_count == 0 + + +def test_histo_acquisition_not_skipping_background(histo_workflow, mocker, caplog): + """Check when not skipping background an image is always saved.""" + # Mocking for settings as above + mock_settings = mocker.Mock() + mock_settings.skip_background = False + + # Set up background detector to say it is empty, this shouldn't stop stacking. + histo_workflow._background_detector.image_is_sample.return_value = ( + False, + "totally empty", + ) + + with caplog.at_level(logging.INFO): + # First set smart stack to report failure + histo_workflow._autofocus.run_smart_stack.return_value = (False, 123) + imaged, focus_height = histo_workflow.acquisition_routine( + mock_settings, xyz_pos=(1, 2, 3) + ) + # Check return + assert imaged is True # Always images if not skipping background + assert focus_height is None # None meaning didn't focus + + # And again reporting a successful stack + histo_workflow._autofocus.run_smart_stack.return_value = (True, 123) + imaged, focus_height = histo_workflow.acquisition_routine( + mock_settings, xyz_pos=(1, 2, 3) + ) + # Check return + assert imaged is True # Always images if not skipping background + assert focus_height == 123 + + # Should never warn + assert len(caplog.records) == 0 + + # And that smart stack never ran + assert histo_workflow._autofocus.run_smart_stack.call_count == 2 + + +def test_histo_acquisition_on_sample(histo_workflow, mocker, caplog): + """Check when skipping background, but background not detected.""" + # Mocking for settings as above + mock_settings = mocker.Mock() + mock_settings.skip_background = True + + # Set up background detector to say there is sample. + histo_workflow._background_detector.image_is_sample.return_value = (True, None) + + with caplog.at_level(logging.INFO): + # First set smart stack to report failure + histo_workflow._autofocus.run_smart_stack.return_value = (False, 123) + imaged, focus_height = histo_workflow.acquisition_routine( + mock_settings, xyz_pos=(1, 2, 3) + ) + # Check return: + # Don't save failed stack when skipping background, assume it is actually + # background + assert imaged is False + assert focus_height is None # None meaning didn't focus + + # And again reporting a successful stack + histo_workflow._autofocus.run_smart_stack.return_value = (True, 123) + imaged, focus_height = histo_workflow.acquisition_routine( + mock_settings, xyz_pos=(1, 2, 3) + ) + # Check return + assert imaged is True # Always images if not skipping background + assert focus_height == 123 + + # Should never warn + assert len(caplog.records) == 1 + expected_message = "Stack failed at (1, 2, 3) treating as background." + assert caplog.records[0].message == expected_message + assert caplog.records[0].levelname == "INFO" + + # And that smart stack never ran + assert histo_workflow._autofocus.run_smart_stack.call_count == 2 + + def test_histo_workflow_settings_ui(histo_workflow): """Check that the workflow specifies the expected controls.""" ui = histo_workflow.settings_ui