diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index 837d7b49..1d9bd2ea 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -26,8 +26,6 @@ from .camera import RawCameraDependency as Camera from .camera import CameraDependency as WrappedCamera from .stage import StageDependency as Stage -# TODO Capture reolution should be save resolution for consistency - class StackParams: """A class for holding for scan parameters @@ -76,26 +74,29 @@ class StackParams: # attributed to be documented settling_time: float = 0.3 - """time (in seconds) between moving and capturing an image""" + """Time (in seconds) between moving and capturing an image""" backlash_correction: int = 250 """ - distance (in steps) to overshoot a move and then undo, to account for backlash + Distance (in steps) to overshoot a move and then undo, to account for backlash """ stack_height_limit: int = 15 """ - how many images can be appended to the stack after the predicted peak to test + How many images can be appended to the stack after the predicted peak to test for focus before assuming the focus was passed, and restarting the stack """ img_undershoot: int = 5 """ - how far below (in factors of stack_dz) the estimated optimal starting position to + How far below (in factors of stack_dz) the estimated optimal starting position to begin the stack. Better to start slightly too low and require many images, rather than too high and needing to autofocus and restart the stack """ + max_attempts: int = 3 + """Maximum number of times to attempt fast stack""" + @property def stack_z_range(self) -> int: """The range of the z stack, in steps @@ -129,7 +130,8 @@ class StackParams: """Return the slice of images to save given the index of the sharpest image""" images_each_side = (self.images_to_save - 1) // 2 return slice( - sharpest_index - images_each_side, sharpest_index + images_each_side + 1 + max(sharpest_index - images_each_side, 0), + sharpest_index + images_each_side + 1, ) @@ -427,7 +429,7 @@ class AutofocusThing(Thing): images_dir: str, autofocus_dz: int, save_resolution: tuple[int, int], - ) -> None: + ) -> tuple[bool, int]: """Run a smart stack, which captures images offset in z, testing whether the sharpest image is towards the centre of the stack. The sharpest image, and optionally images around the sharpest, @@ -440,6 +442,7 @@ class AutofocusThing(Thing): LabThings FastAPI Returns: + A boolean, True if stack was successfully The z position of the sharpest image """ @@ -453,9 +456,9 @@ class AutofocusThing(Thing): save_resolution=save_resolution, ) - success = False + trys = 0 # Loop until a stack is successful - while not success: # TODO add a maximum number? + while trys < stack_parameters.max_attempts: success, captures, sharpest_id = self.z_stack( stack_parameters=stack_parameters, stage=stage, @@ -475,7 +478,9 @@ class AutofocusThing(Thing): sharpness_monitor, ) - # Save the sharpest image, and images either side of focus + # Save stack_parameters.image_to_save images centred on the sharpest capture. + # If the smart_stack failed the exact number of images saved may not be + # stack_parameters.image_to_save self.save_stack( sharpest_id=sharpest_id, captures=captures, @@ -484,7 +489,7 @@ class AutofocusThing(Thing): ) # Return the z position of the sharpest image, for path planning and tracking - return _get_capture_by_id(captures, sharpest_id).position["z"] + return success, _get_capture_by_id(captures, sharpest_id).position["z"] def reset_stack( self, diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 8ca4c1a1..b206058a 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -653,20 +653,16 @@ class SmartScanThing(Thing): self._scan_logger.info(msg) continue - focused_height = self._autofocus.run_smart_stack( + focused, focused_height = self._autofocus.run_smart_stack( images_dir=self._ongoing_scan_images_dir, autofocus_dz=self._scan_data["autofocus_dz"], save_resolution=self._scan_data["save_resolution"], ) - current_pos_xyz = ( - new_pos_xyz[0], - new_pos_xyz[1], - focused_height, - ) + current_pos_xyz = (new_pos_xyz[0], new_pos_xyz[1], focused_height) route_planner.mark_location_visited( - current_pos_xyz, imaged=True, focused=True + current_pos_xyz, imaged=True, focused=focused ) # increment capure counter as thread has completed diff --git a/tests/test_stack.py b/tests/test_stack.py index c5d469bd..04c67d8b 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -170,9 +170,23 @@ def test_computed_stack_params(): sharpnesses = [50, 77, 234, 324, 390, 496, 569, 454, 333, 222, 178, 70] max_ind = np.argmax(sharpnesses) slice_to_save = stack_parameters.slice_to_save(max_ind) - # Check the slice corresponds to the inex for the 5 images centred on the sharpest + # Check the slice corresponds to the index for the 5 images centred on the sharpest assert sharpnesses[slice_to_save] == [390, 496, 569, 454, 333] + # For a failed smart stack the slice may be truncated. Check it truncates correctly + sharpnesses = [496, 569, 454, 333, 222, 178, 70, 69, 66, 50, 45, 40] + max_ind = np.argmax(sharpnesses) + slice_to_save = stack_parameters.slice_to_save(max_ind) + # Check the slice corresponds to the index for the first 4 images + assert sharpnesses[slice_to_save] == [496, 569, 454, 333] + + # And again for sharpest at the end + sharpnesses = [13, 21, 26, 31, 39, 49, 50, 77, 234, 324, 390, 496, 569] + max_ind = np.argmax(sharpnesses) + slice_to_save = stack_parameters.slice_to_save(max_ind) + # Check the slice corresponds to the index for the final 3 images + assert sharpnesses[slice_to_save] == [390, 496, 569] + def random_capture(set_id: Optional[int] = None): """