From 5d5cc7a72aa63f02ac4f5c175c703dcd8db30e1f Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Thu, 12 Mar 2020 17:51:24 +0000 Subject: [PATCH] Added persistent settings and moves --- .../camera_stage_mapping/extension.py | 155 +++++++++++++----- 1 file changed, 116 insertions(+), 39 deletions(-) diff --git a/openflexure_microscope/api/default_extensions/camera_stage_mapping/extension.py b/openflexure_microscope/api/default_extensions/camera_stage_mapping/extension.py index 25e7121e..9d0929c2 100644 --- a/openflexure_microscope/api/default_extensions/camera_stage_mapping/extension.py +++ b/openflexure_microscope/api/default_extensions/camera_stage_mapping/extension.py @@ -13,7 +13,7 @@ from labthings.core.tasks import taskify from labthings.core.utilities import get_by_path, set_by_path, create_from_path -from flask import abort +from flask import abort, jsonify import logging import time @@ -24,46 +24,105 @@ import io from .camera_stage_calibration_1d import calibrate_backlash_1d, image_to_stage_displacement_from_1d from .camera_stage_tracker import Tracker -def update_extension_settings(microscope, settings): - """Update the stored extension settings dictionary""" - keys = ["extensions","org.openflexure.camera_stage_mapping"] - dictionary = create_from_path(keys) - set_by_path(dictionary, keys, settings) +from openflexure_microscope.utilities import axes_to_array - microscope.update_settings(dictionary) - microscope.save_settings() -def get_extension_settings(microscope): - """Retrieve the settings for this extension""" - keys = ["extensions","org.openflexure.camera_stage_mapping"] - return get_by_path(microscope.read_settings, keys) +class CSMExtension(BaseExtension): + """ + Use the camera as an encoder, so we can relate camera and stage coordinates + """ + def __init__(self): + BaseExtension.__init__( + self, + "org.openflexure.camera_stage_mapping", + version="0.0.1", + ) -def camera_stage_functions(microscope): - """Return functions that allow us to interface with the microscope""" - microscope.camera.start_worker() # ensure the worker thread is running, so there is an MJPEG stream + _microscope = None + @property + def microscope(self): + # TODO: does caching the microscope actually help? + if self._microscope is None: + self._microscope = find_component("org.openflexure.microscope") + return self._microscope - def grab_image(): - jpeg = microscope.camera.get_frame() - return np.array(PIL.Image.open(io.BytesIO(jpeg))) - - def get_position(): - return microscope.stage.position - - move = microscope.stage.move_abs + def update_settings(self, settings): + """Update the stored extension settings dictionary""" + keys = ["extensions",self.name] + dictionary = create_from_path(keys) + set_by_path(dictionary, keys, settings) + logging.info(f"Updating settings with {dictionary}") + self.microscope.update_settings(dictionary) + self.microscope.save_settings() - return grab_image, get_position, move + def get_settings(self): + """Retrieve the settings for this extension""" + keys = ["extensions",self.name] + return get_by_path(self.microscope.read_settings(), keys) -def calibrate_1d(microscope, direction): - """Move a microscope's stage in 1D, and figure out the relationship with the camera""" - grab_image, get_position, move = camera_stage_functions(microscope) + def camera_stage_functions(self): + """Return functions that allow us to interface with the microscope""" + self.microscope.camera.start_worker() # ensure the worker thread is running, so there is an MJPEG stream + + def grab_image(): + jpeg = self.microscope.camera.get_frame() + return np.array(PIL.Image.open(io.BytesIO(jpeg))) + + def get_position(): + return self.microscope.stage.position + + move = self.microscope.stage.move_abs - def wait(): - time.sleep(0.2) + return grab_image, get_position, move - tracker = Tracker(grab_image, get_position, settle=wait) + def calibrate_1d(self, direction): + """Move a microscope's stage in 1D, and figure out the relationship with the camera""" + grab_image, get_position, move = self.camera_stage_functions() - return calibrate_backlash_1d(tracker, move, direction) + def wait(): + time.sleep(0.2) + tracker = Tracker(grab_image, get_position, settle=wait) + + return calibrate_backlash_1d(tracker, move, direction) + + def calibrate_xy(self): + """Move the microscope's stage in X and Y, to calibrate its relationship to the camera""" + logging.info("Calibrating X axis:") + cal_x = self.calibrate_1d(np.array([1,0,0])) + logging.info("Calibrating Y axis:") + cal_y = self.calibrate_1d(np.array([0,1,0])) + + # Combine X and Y calibrations to make a 2D calibration + cal_xy = image_to_stage_displacement_from_1d([cal_x, cal_y]) + self.update_settings(cal_xy) + return { + "camera_stage_mapping_calibration": cal_xy, + "linear_calibration_x": cal_x, + "linear_calibration_y": cal_y, + } + + @property + def image_to_stage_displacement_matrix(self): + try: + settings = self.get_settings() + return settings["image_to_stage_displacement"] + except KeyError: + raise ValueError("The microscope has not yet been calibrated.") + + def move_in_image_coordinates(self, displacement_in_pixels): + """Move by a given number of pixels on the camera""" + p = np.array(displacement_in_pixels) + relative_move = np.dot(p, self.image_to_stage_displacement_matrix) + self.microscope.stage.move_rel([relative_move[0], relative_move[1], 0]) + + + + +csm_extension = CSMExtension() + +#csm_extension.add_method(calibrate_1d, "calibrate_1d") # Not needed any more?? +#csm_extension.add_method(calibrate_xy, "calibrate_xy") @ThingAction @@ -74,21 +133,39 @@ class Calibrate1DView(View): @marshal_task def post(self, args): """Calibrate one axis of the microscope stage against the camera.""" - microscope = find_component("org.openflexure.microscope") direction = np.array(args.get("direction")) - task = taskify(calibrate_1d)(microscope, direction) + task = taskify(csm_extension.calibrate_1d)(direction) return task +csm_extension.add_view(Calibrate1DView, "/calibrate_1d") + +@ThingAction +class CalibrateXYView(View): + @marshal_task + def post(self): + """Calibrate both axes of the microscope stage against the camera.""" + task = taskify(csm_extension.calibrate_xy)() + + return task + +csm_extension.add_view(CalibrateXYView, "/calibrate_xy") +@ThingAction +class MoveInImageCoordinatesView(View): + @use_args({ + "x": fields.Float(),#description="The number of pixels to move in X", required=True, example=100), + "y": fields.Float(),#description="The number of pixels to move in Y", required=True, example=100), + }) + def post(self, args): + logging.debug("moving in pixels") + """Move the microscope stage, such that we move by a given number of pixels on the camera""" + csm_extension.move_in_image_coordinates(np.array([args.get("x"), args.get("y")])) + + return jsonify(csm_extension.microscope.state["stage"]["position"]) +csm_extension.add_view(MoveInImageCoordinatesView, "/move_in_image_coordinates") -csm_extension = BaseExtension("org.openflexure.camera_stage_mapping", version="0.0.1") - -csm_extension.add_method(calibrate_1d, "calibrate_1d") -#csm_extension.add_method(calibrate_xy, "calibrate_xy") - -csm_extension.add_view(Calibrate1DView, "/calibrate_1d") \ No newline at end of file