diff --git a/src/openflexure_microscope_server/things/stage_measure.py b/src/openflexure_microscope_server/things/stage_measure.py index c7d2e698..924663a7 100644 --- a/src/openflexure_microscope_server/things/stage_measure.py +++ b/src/openflexure_microscope_server/things/stage_measure.py @@ -9,12 +9,12 @@ of increasing pixel sizes in the opposite direction until motion is detected. Th position is taken as the true final position. Throughout the test, parasitic motion(motion in the axis not being measured) -is tracked and an error is raised if it exceeds a reasonable amount. +is tracked and an error is raised if it exceeds a minimum amount. Currently +this is 10% of the expected motion is the measured axis. """ import numpy as np import cv2 -import json from typing import Literal from PIL import Image import time @@ -54,11 +54,15 @@ def _generate_move_dicts( return { "x": (fov_perc / 100) * stream_resolution[0] * factor * direction, "y": (fov_perc / 100) * stream_resolution[1] * factor * direction, - } + } # factor used for creating minimum offsets. def _predict_z( - positions: list, axis: Literal["x", "y"], relative_move: float, stage: StageDep, csm: CSMDep + positions: list, + axis: Literal["x", "y"], + relative_move: float, + stage: StageDep, + csm: CSMDep, ) -> float: """Predict the next z position for a move using previous positions. @@ -67,6 +71,8 @@ def _predict_z( :params axis: The axis in which the stage is moving. This must be 'x' or 'y'. :params relative_move: The move which the function is trying to predict z for. Here, this is inputted with units of pixels. + :param stage: A direct_thing_client dependency for the the microscope stage. + :param csm: A direct_thing_client dependency for camera stage mapping. :return: A number of pixels the stage needs to move in z. """ pixel_step = { @@ -74,7 +80,7 @@ def _predict_z( "y": 1 / csm.image_to_stage_displacement_matrix[1][0], } - lateral_positions = [i[axis] for i in positions] + lateral_positions = [i[axis] for i in positions] # x or y positions z_positions = [i["z"] for i in positions] parameters, _ = curve_fit(quadratic, lateral_positions, z_positions) z_dest = quadratic( @@ -101,6 +107,9 @@ def _move_and_measure( :params data: The object used to track stage coordinates, correlation and delta. :params image1: An image taken before moving to be correlated with image2. :params autofocus_proc: If true, looping.autofocus will be used after the stage moves. + :param csm: A direct_thing_client dependency for camera stage mapping. + :param autofocus: A direct_thing_client dependency for autofocus. + :param camera: A raw_thing_client depeendency for the camera. :return: All required data for the next move. This includes the updated delta value and offset. Also returns what wrong_axis is i.e. if the direction is 'x', wrong_axis = 'y'. """ @@ -144,6 +153,10 @@ def _acquire_z_predict_points( :param direction: The direction the stage moves. :params axis: The axis which is being measured. This must be 'x' or 'y'. :params data: The object used to track stage coordinates, correlation and delta. + :param csm: A direct_thing_client dependency for camera stage mapping. + :param camera: A raw_thing_client depeendency for the camera. + :param stage: A raw_thing_client depeendency for the microscope stage. + :param autofocus: A direct_thing_client dependency for autofocus. :return: Stage_coords and cor_lat_steps are lists of data tracked throughout the test. Delta is updated and tracked after each move. """ @@ -154,7 +167,7 @@ def _acquire_z_predict_points( for _loop in range(5): image1 = cv2.resize( np.array(Image.open(cam.grab_jpeg().open())), dsize=(0, 0), fx=1, fy=1 - ) + ) # Captures image with option to resize offset, wrong_axis = _move_and_measure( step_size=_generate_move_dicts(medium_step, stream_resolution, direction), axis=axis, @@ -196,13 +209,17 @@ def _check_stage_operation( :params data: The object used to track stage coordinates, correlation and delta. :params minimum_offset_small: A dictionary containing the minimum values for a successful correlation. + :param csm: A direct_thing_client dependency for camera stage mapping. + :param camera: A raw_thing_client depeendency for the camera. + :param stage: A raw_thing_client depeendency for the microscope stage. + :param autofocus: A direct_thing_client dependency for autofocus. :return: Stage_coords and cor_lat_steps are lists of data tracked throughout the test. Delta is updated and tracked after each move. """ failure_count = 0 wrong_axis_max_small = _generate_move_dicts( small_step, stream_resolution, direction, factor=0.1 - ) + ) # Dictionary with maximum allowed parasitic motion for _loop in range(3): image1 = cv2.resize( @@ -220,6 +237,7 @@ def _check_stage_operation( ) logger.info(f"Offset measured as {data.delta[axis]}") + # If correlation is too small, refocuses and capture new image 3 times. while ( np.abs(data.delta[axis]) < np.abs(minimum_offset_small[axis]) and failure_count < 3 @@ -272,6 +290,9 @@ def _motion_detection( :params axis: The axis in which the stage is moving. This must be 'x' or 'y'. :params direction: The direction in which the stage was moving previous to motion detection being used. + :param csm: A direct_thing_client dependency for camera stage mapping. + :param stage: A raw_thing_client depeendency for the microscope stage. + :param camera: A raw_thing_client depeendency for the camera. :return: The stage coordinates where motion was detected. """ displacements = [ @@ -285,7 +306,7 @@ def _motion_detection( 128, 256, 512, - ] # Array of increasing step sizes + ] # Array of increasing pixel sizes motion_minimum = 20 # minimum number of pixels for motion to be detected this_motion_step = {"x": 0, "y": 0} @@ -353,10 +374,14 @@ class RangeofMotionThing(lt.Thing): csm: CSMDep, logger: lt.deps.InvocationLogger, axis: Literal["x", "y"], - direction: Literal[1,-1], + direction: Literal[1, -1], ) -> dict: """Measure the range of motion in a single axis and direction. + :param stage: A raw_thing_client depeendency for the microscope stage. + :param cam: A raw_thing_client depeendency for the camera. + :param csm: A raw_thing_client depeendency for camera stage mapping. + :param logger: A raw_thing_client depeendency for the logger. :params axis: The axis which is being measured. This must be 'x' or 'y'. :params direction: The direction which is being measured. This must be 1 or -1. :return: Results dictionary containing stage positions, @@ -366,11 +391,10 @@ class RangeofMotionThing(lt.Thing): starting_position = list(stage.position.values()) - rom_data = RomDataTracker() + rom_data = RomDataTracker() # initialise data tracking object axis_results = {} try: - logger.info( f"Beginning the {axis}-axis in the {'positive' if direction == 1 else 'negative'} direction" ) @@ -482,6 +506,11 @@ class RangeofMotionThing(lt.Thing): ): """Measures the range of motion of the stage across the x and y axes. + :param autofocus: A raw_thing_client dependency for autofocus. + :param stage: A raw_thing_client depeendency for the microscope stage. + :param cam: A raw_thing_client depeendency for the camera. + :param csm: A raw_thing_client depeendency for camera stage mapping. + :param logger: A raw_thing_client depeendency for the logger. :return: Results dictionary separated into keys of each axis and direction. """ logger.info( @@ -491,6 +520,7 @@ class RangeofMotionThing(lt.Thing): start_time = time.time() rom_results = {} + # Loop through all axes and directions. for axis_dir in [["x", 1], ["x", -1], ["y", 1], ["y", -1]]: axis_dir_results = self.rom_axis( autofocus, @@ -501,6 +531,7 @@ class RangeofMotionThing(lt.Thing): axis=axis_dir[0], direction=axis_dir[1], ) + # Save results from single axis and direction rom_results[f"{axis_dir}"] = axis_dir_results end_time = time.time() @@ -515,7 +546,7 @@ class RangeofMotionThing(lt.Thing): - rom_results["['y', -1]"]["final_position"]["y"] ) step_range = [x_range, y_range] - logger.info(f"Range of motion is {x_range} X {y_range}") + logger.info(f"Range of motion is {x_range} X {y_range} steps") rom_results["Time"] = total_time rom_results["CSM Matrix"] = csm.image_to_stage_displacement_matrix @@ -523,9 +554,6 @@ class RangeofMotionThing(lt.Thing): self.last_calibration = DenumpifyingDict(rom_results).model_dump() - with open("/var/openflexure/ROM_Test_Results.json", "w") as file_object: - json.dump(rom_results, file_object, indent=3) - return rom_results last_calibration = lt.ThingSetting(initial_value=None, model=dict, readonly=True)