diff --git a/src/openflexure_microscope_server/things/camera_stage_mapping.py b/src/openflexure_microscope_server/things/camera_stage_mapping.py index a05a3385..57f3e742 100644 --- a/src/openflexure_microscope_server/things/camera_stage_mapping.py +++ b/src/openflexure_microscope_server/things/camera_stage_mapping.py @@ -268,10 +268,18 @@ class CameraStageMapper(lt.Thing): an image usually helps resolve any ambiguity. """ self.assert_calibrated() + stage.move_relative(**self.convert_image_to_stage_coordinates(x=x, y=y)) + + @lt.thing_action + def convert_image_to_stage_coordinates( + self, x: float, y: float + ) -> Mapping[str, int]: + """Convert image coordinates to stage coordinates.""" + self.assert_calibrated() relative_move: np.ndarray = np.dot( np.array([y, x]), np.array(self.image_to_stage_displacement_matrix) ) - stage.move_relative(x=relative_move[0], y=relative_move[1]) + return {"x": int(relative_move[0]), "y": int(relative_move[1])} @lt.thing_property def thing_state(self) -> Mapping[str, Any]: diff --git a/src/openflexure_microscope_server/things/stage_measure.py b/src/openflexure_microscope_server/things/stage_measure.py index 9dcce848..06e3d030 100644 --- a/src/openflexure_microscope_server/things/stage_measure.py +++ b/src/openflexure_microscope_server/things/stage_measure.py @@ -74,30 +74,23 @@ class RomDataTracker: def predict_z_displacement( self, - movement: dict[str, int], + axis: Literal["x", "y"], + stage_movement: dict[str, int], stage_position: dict[str, int], - csm_matrix: list[list[int]], ) -> int: """Predict the z-displacement needed for a given movement. - :param movement: The movement to be performed in image coordinates. + :param axis: The axis which is being measured. This must be 'x' or 'y'. + :param stage_movement: The movement to be performed in stage coordinates. :param stage_position: The current stage position in stage coordinates. - :param csm_matrix: The camera stage mapping matrix. :returns: The predicted relative z displacement needed to stay in focus. """ - pixel_step = { - "x": 1 / csm_matrix[0][1], - "y": 1 / csm_matrix[1][0], - } - - axis = _axis_from_movement_dict(movement) - - lateral_positions = [i[axis] for i in self.stage_coords] # x or y positions + # x or y positions + lateral_positions = [i[axis] for i in self.stage_coords] z_positions = [i["z"] for i in self.stage_coords] fit_params, *_others = curve_fit(quadratic, lateral_positions, z_positions) - z_dest = quadratic( - stage_position[axis] + (movement[axis] / pixel_step[axis]), *fit_params - ) + z_dest = quadratic(stage_position[axis] + stage_movement[axis], *fit_params) + return int(z_dest - stage_position["z"]) @@ -248,7 +241,6 @@ class RangeofMotionThing(lt.Thing): self._big_z_corrected_movement( axis=axis, direction=direction, rom_deps=rom_deps ) - # Autofocus and record position rom_deps.autofocus.looping_autofocus(dz=800) self._rom_data.stage_coords.append(rom_deps.stage.position) @@ -334,7 +326,6 @@ class RangeofMotionThing(lt.Thing): ) for _loop in range(5): offset = self._move_and_measure(movement=movement, rom_deps=rom_deps) - rom_deps.logger.info(f"Offset measured as {offset[axis]}") self._rom_data.record_movement(rom_deps.stage.position, offset) @@ -351,16 +342,19 @@ class RangeofMotionThing(lt.Thing): :param direction: The direction to move in. :param rom_deps: All dependencies that were passed to the calling Action """ - big_movement = self._movement_in_img_coords( + movement = self._movement_in_img_coords( fov_perc=BIG_STEP, axis=axis, direction=direction ) + # Convert to stage coordinates + stage_movemenet = rom_deps.csm.convert_image_to_stage_coordinates(**movement) + z_disp = self._rom_data.predict_z_displacement( - movement=big_movement, + axis=axis, + stage_movement=stage_movemenet, stage_position=rom_deps.stage.position, - csm_matrix=rom_deps.csm.image_to_stage_displacement_matrix, ) rom_deps.stage.move_relative(z=z_disp) - rom_deps.csm.move_in_image_coordinates(**big_movement) + rom_deps.csm.move_in_image_coordinates(**movement) def _stage_still_moves( self, diff --git a/tests/test_stage_measure.py b/tests/test_stage_measure.py index 85460972..b2d2e5e8 100644 --- a/tests/test_stage_measure.py +++ b/tests/test_stage_measure.py @@ -45,7 +45,7 @@ def increasing_xyz_dict_generator(*_args, **_kwargs): def csm_matrix(): """Return an example CSM matrix.""" return [ - [0.03061156624485296, 1.8031242270940833], + [0.03061156624485296, -1.8031242270940833], [1.773236372778601, 0.006660431608601435], ] @@ -70,12 +70,12 @@ def example_rom_data(): return rom_data -def test_predict_z(csm_matrix, example_rom_data): +def test_predict_z(example_rom_data): """Check that the prediction for the next z position is correct.""" mock_z_diff = example_rom_data.predict_z_displacement( - movement={"x": 2908, "y": 0}, + axis="x", + stage_movement={"x": 5243, "y": 0}, stage_position={"x": 3635, "y": 10, "z": 617}, - csm_matrix=csm_matrix, ) expected_z_diff = 1343 assert mock_z_diff == expected_z_diff @@ -157,9 +157,16 @@ def rom_thing(example_rom_data) -> stage_measure.RangeofMotionThing: @pytest.fixture def mock_rom_deps(csm_matrix, mocker) -> stage_measure.RomDeps: """Return a RomDeps object full of mocks, except the logger which is LOGGER.""" + + def apply_csm(x: float, y: float) -> dict[str, int]: + """Convert image coordinates to stage coordinates.""" + vec = np.dot(np.array([y, x]), np.array(csm_matrix)) + return {"x": int(vec[0]), "y": int(vec[1])} + mock_csm = mocker.Mock() # Set up mock csm to return a CSM matrix mock_csm.image_to_stage_displacement_matrix = csm_matrix + mock_csm.convert_image_to_stage_coordinates.side_effect = apply_csm return stage_measure.RomDeps( autofocus=mocker.Mock(), @@ -367,7 +374,7 @@ def test_big_z_corrected_movement(rom_thing, mock_rom_deps): assert "x" not in move_kwargs assert "y" not in move_kwargs assert "z" in move_kwargs - move_kwargs["z"] = 1162 + assert move_kwargs["z"] == 1148 # And one move in image coordinates assert mock_rom_deps.csm.move_in_image_coordinates.call_count == 1