diff --git a/openflexure_microscope/api/default_extensions/__init__.py b/openflexure_microscope/api/default_extensions/__init__.py index 561239af..1c080a93 100644 --- a/openflexure_microscope/api/default_extensions/__init__.py +++ b/openflexure_microscope/api/default_extensions/__init__.py @@ -2,6 +2,8 @@ import logging import traceback from contextlib import contextmanager +LABTHINGS_EXTENSIONS = [] + @contextmanager def handle_extension_error(extension_name): @@ -17,14 +19,28 @@ def handle_extension_error(extension_name): with handle_extension_error("autofocus"): - from .autofocus import autofocus_extension_v2 + from .autofocus import AutofocusExtension + + LABTHINGS_EXTENSIONS.append(AutofocusExtension) with handle_extension_error("scan"): - from .scan import scan_extension_v2 + from .scan import ScanExtension + + LABTHINGS_EXTENSIONS.append(ScanExtension) with handle_extension_error("zip builder"): - from .zip_builder import zip_extension_v2 + from .zip_builder import ZipBuilderExtension + + LABTHINGS_EXTENSIONS.append(ZipBuilderExtension) with handle_extension_error("autostorage"): - from .autostorage import autostorage_extension_v2 + from .autostorage import AutostorageExtension + + LABTHINGS_EXTENSIONS.append(AutostorageExtension) + +with handle_extension_error("lens shading calibration"): + from .picamera_autocalibrate import LSTExtension + + LABTHINGS_EXTENSIONS.append(LSTExtension) + with handle_extension_error("camera stage mapping"): from camera_stage_mapping.ofm_extension import csm_extension -with handle_extension_error("lens shading calibration"): - from .picamera_autocalibrate import lst_extension_v2 + + LABTHINGS_EXTENSIONS.append(csm_extension) diff --git a/openflexure_microscope/api/default_extensions/autofocus.py b/openflexure_microscope/api/default_extensions/autofocus.py index 4ed1a672..d2c616a5 100644 --- a/openflexure_microscope/api/default_extensions/autofocus.py +++ b/openflexure_microscope/api/default_extensions/autofocus.py @@ -109,6 +109,16 @@ class JPEGSharpnessMonitor: return data +@contextmanager +def monitor_sharpness(microscope: Microscope): + m = JPEGSharpnessMonitor(microscope) + m.start() + try: + yield m + finally: + m.stop() + + def sharpness_sum_lap2(rgb_image: np.ndarray): """Return an image sharpness metric: sum(laplacian(image)**")""" image_bw = np.mean(rgb_image, 2) @@ -129,184 +139,193 @@ def sharpness_edge(image: np.ndarray): ### Autofocus extension -def measure_sharpness(microscope: Microscope, metric_fn: Callable = sharpness_sum_lap2): - """Measure the sharpness of the camera's current view.""" +class AutofocusExtension(BaseExtension): + def __init__(self): + super().__init__( + "org.openflexure.autofocus", + version="2.0.0", + description="Actions to move the microscope in Z and pick the point with the sharpest image.", + ) + self.add_view( + MeasureSharpnessAPI, "/measure_sharpness", endpoint="measure_sharpness" + ) + self.add_view(AutofocusAPI, "/autofocus", endpoint="autofocus") + self.add_view(FastAutofocusAPI, "/fast_autofocus", endpoint="fast_autofocus") + self.add_view( + UpDownUpAutofocusAPI, "/updownup_autofocus", endpoint="updownup_autofocus" + ) - if hasattr(microscope.camera, "array") and callable( - getattr(microscope.camera, "array") + self.add_view( + MoveAndMeasureAPI, "/move_and_measure", endpoint="move_and_measure" + ) + + def measure_sharpness( + self, microscope: Microscope, metric_fn: Callable = sharpness_sum_lap2 ): - return metric_fn(getattr(microscope.camera, "array")(use_video_port=True)) - else: - raise RuntimeError(f"Object {microscope.camera} has no method `array`") + """Measure the sharpness of the camera's current view.""" + if hasattr(microscope.camera, "array") and callable( + getattr(microscope.camera, "array") + ): + return metric_fn(getattr(microscope.camera, "array")(use_video_port=True)) + else: + raise RuntimeError(f"Object {microscope.camera} has no method `array`") -def autofocus( - microscope: Microscope, - dz: List[int], - settle: float = 0.5, - metric_fn: Callable = sharpness_sum_lap2, -): - """Perform a simple autofocus routine. - The stage is moved to z positions (relative to current position) in dz, - and at each position an image is captured and the sharpness function - evaulated. We then move back to the position where the sharpness was - highest. No interpolation is performed. - dz is assumed to be in ascending order (starting at -ve values) - """ - camera = microscope.camera - stage = microscope.stage + def autofocus( + self, + microscope: Microscope, + dz: List[int], + settle: float = 0.5, + metric_fn: Callable = sharpness_sum_lap2, + ): + """Perform a simple autofocus routine. + The stage is moved to z positions (relative to current position) in dz, + and at each position an image is captured and the sharpness function + evaulated. We then move back to the position where the sharpness was + highest. No interpolation is performed. + dz is assumed to be in ascending order (starting at -ve values) + """ + camera = microscope.camera + stage = microscope.stage - with set_properties(stage, backlash=256), stage.lock, camera.lock: - sharpnesses = [] - positions = [] + with set_properties(stage, backlash=256), stage.lock, camera.lock: + sharpnesses = [] + positions = [] - # Some cameras may not have annotate_text. Reset if it does - if getattr(camera, "annotate_text"): - setattr(camera, "annotate_text", "") + # Some cameras may not have annotate_text. Reset if it does + if getattr(camera, "annotate_text"): + setattr(camera, "annotate_text", "") - for _ in stage.scan_z(dz, return_to_start=False): - if current_action() and current_action().stopped: - return - positions.append(stage.position[2]) - time.sleep(settle) - sharpnesses.append(measure_sharpness(microscope, metric_fn)) + for _ in stage.scan_z(dz, return_to_start=False): + if current_action() and current_action().stopped: + return + positions.append(stage.position[2]) + time.sleep(settle) + sharpnesses.append(self.measure_sharpness(microscope, metric_fn)) - newposition = positions[np.argmax(sharpnesses)] - stage.move_rel((0, 0, newposition - stage.position[2])) + newposition = positions[np.argmax(sharpnesses)] + stage.move_rel((0, 0, newposition - stage.position[2])) - return positions, sharpnesses + return positions, sharpnesses - -@contextmanager -def monitor_sharpness(microscope: Microscope): - m = JPEGSharpnessMonitor(microscope) - m.start() - try: - yield m - finally: - m.stop() - - -def move_and_find_focus(microscope: Microscope, dz: int): - """Make a relative Z move and return the peak sharpness position""" - with monitor_sharpness(microscope) as m: - m.focus_rel(dz) - return m.sharpest_z_on_move(0) - - -def move_and_measure(microscope: Microscope, dz: int): - """Make a relative Z move and return the sharpness data""" - with monitor_sharpness(microscope) as m: - m.focus_rel(dz) - return m.data_dict() - - -def fast_autofocus(microscope: Microscope, dz: int = 2000): - """Perform a down-up-down-up autofocus""" - with microscope.camera.lock, microscope.stage.lock: + def move_and_find_focus(self, microscope: Microscope, dz: int): + """Make a relative Z move and return the peak sharpness position""" with monitor_sharpness(microscope) as m: - # Move to (-dz / 2) - m.focus_rel(-dz / 2) - # Move to dz while monitoring sharpness - # i: Sharpness monitor index for this move - # z: Final z position after move - i, z = m.focus_rel(dz) - # Get the z position with highest sharpness from the previous move (index i) - fz = m.sharpest_z_on_move(i) - # Move all the way to the start so it's consistent - # Store final absolute z position from this return move - i, z = m.focus_rel(-dz) - # Move to the target position fz - # Can't do absolute move here yet so move by (fz - z) - m.focus_rel(fz - z) - # Return all focus data + m.focus_rel(dz) + return m.sharpest_z_on_move(0) + + def move_and_measure(self, microscope: Microscope, dz: int): + """Make a relative Z move and return the sharpness data""" + with monitor_sharpness(microscope) as m: + m.focus_rel(dz) return m.data_dict() + def fast_autofocus(self, microscope: Microscope, dz: int = 2000): + """Perform a down-up-down-up autofocus""" + with microscope.camera.lock, microscope.stage.lock: + with monitor_sharpness(microscope) as m: + # Move to (-dz / 2) + m.focus_rel(-dz / 2) + # Move to dz while monitoring sharpness + # i: Sharpness monitor index for this move + # z: Final z position after move + i, z = m.focus_rel(dz) + # Get the z position with highest sharpness from the previous move (index i) + fz = m.sharpest_z_on_move(i) + # Move all the way to the start so it's consistent + # Store final absolute z position from this return move + i, z = m.focus_rel(-dz) + # Move to the target position fz + # Can't do absolute move here yet so move by (fz - z) + m.focus_rel(fz - z) + # Return all focus data + return m.data_dict() -def fast_up_down_up_autofocus( - microscope: Microscope, - dz: int = 2000, - target_z: int = 0, - initial_move_up: bool = True, - mini_backlash: int = 25, -): - """Autofocus by measuring on the way down, and moving back up with feedback. + def fast_up_down_up_autofocus( + self, + microscope: Microscope, + dz: int = 2000, + target_z: int = 0, + initial_move_up: bool = True, + mini_backlash: int = 25, + ): + """Autofocus by measuring on the way down, and moving back up with feedback. - This autofocus method is very efficient, as it only passes the peak once. - The sequence of moves it performs is: + This autofocus method is very efficient, as it only passes the peak once. + The sequence of moves it performs is: - 1. Move to the top of the range `dz/2` (can be disabled) + 1. Move to the top of the range `dz/2` (can be disabled) - 2. Move down by `dz` while monitoring JPEG size to find the focus. + 2. Move down by `dz` while monitoring JPEG size to find the focus. - 3. Move back up to the `target_z` position, relative to the sharpest image. + 3. Move back up to the `target_z` position, relative to the sharpest image. - 4. Measure the sharpness, and compare against the curve recorded in (2) to \\ - estimate how much further we need to go. Make this move, to reach our \\ - target position. + 4. Measure the sharpness, and compare against the curve recorded in (2) to \\ + estimate how much further we need to go. Make this move, to reach our \\ + target position. - Moving back to the target position in two steps allows us to correct for - backlash, by using the sharpness-vs-z curve as a rough encoder for Z. + Moving back to the target position in two steps allows us to correct for + backlash, by using the sharpness-vs-z curve as a rough encoder for Z. - Parameters: - dz: number of steps over which to scan (optional, default 2000) + Parameters: + dz: number of steps over which to scan (optional, default 2000) - target_z: we aim to finish at this position, relative to focus. This may - be useful if, for example, you want to acquire a stack of images in Z. - It is optional, and the default value of 0 will finish at the focus. + target_z: we aim to finish at this position, relative to focus. This may + be useful if, for example, you want to acquire a stack of images in Z. + It is optional, and the default value of 0 will finish at the focus. - initial_move_up: (optional, default True) set this to `False` to move down - from the starting position. Mostly useful if you're able to combine - the initial move with something else, e.g. moving to the next scan point. + initial_move_up: (optional, default True) set this to `False` to move down + from the starting position. Mostly useful if you're able to combine + the initial move with something else, e.g. moving to the next scan point. - mini_backlash: (optional, default 25) is a small extra move made in step - 3 to help counteract backlash. It should be small enough that you - would always expect there to be greater backlash than this. Too small - might slightly hurt accuracy, but is unlikely to be a big issue. Too big - may cause you to overshoot, which is a problem. - """ - with microscope.camera.lock, microscope.stage.lock: - with monitor_sharpness(microscope) as m: - # Ensure the MJPEG stream has started - microscope.camera.start_stream() + mini_backlash: (optional, default 25) is a small extra move made in step + 3 to help counteract backlash. It should be small enough that you + would always expect there to be greater backlash than this. Too small + might slightly hurt accuracy, but is unlikely to be a big issue. Too big + may cause you to overshoot, which is a problem. + """ + with microscope.camera.lock, microscope.stage.lock: + with monitor_sharpness(microscope) as m: + # Ensure the MJPEG stream has started + microscope.camera.start_stream() - logging.debug("Initial move") - if initial_move_up: - m.focus_rel(dz / 2) - # move down - logging.debug("Move down") - i, z = m.focus_rel(-dz) - # now inspect where the sharpest point is, and estimate the sharpness - # (JPEG size) that we should find at the start of the Z stack - _, jz, js = m.move_data(i) - best_z = jz[np.argmax(js)] + logging.debug("Initial move") + if initial_move_up: + m.focus_rel(dz / 2) + # move down + logging.debug("Move down") + i, z = m.focus_rel(-dz) + # now inspect where the sharpest point is, and estimate the sharpness + # (JPEG size) that we should find at the start of the Z stack + _, jz, js = m.move_data(i) + best_z = jz[np.argmax(js)] - # now move to the start of the z stack - logging.debug("Move to the start of the z stack") - i, z = m.focus_rel( - best_z + target_z - z + mini_backlash - ) # takes us to the start of the stack + # now move to the start of the z stack + logging.debug("Move to the start of the z stack") + i, z = m.focus_rel( + best_z + target_z - z + mini_backlash + ) # takes us to the start of the stack - # We've deliberately undershot - figure out how much further we should move based on the curve - logging.debug("Calculate remining movement") - current_js = m.camera.stream.last.size - imax = np.argmax(js) # we want to crop out just the bit below the peak - js = js[imax:] # NB z is in DECREASING order - jz = jz[imax:] - inow = np.argmax( - js < current_js - ) # use the curve we recorded to estimate our position + # We've deliberately undershot - figure out how much further we should move based on the curve + logging.debug("Calculate remining movement") + current_js = m.camera.stream.last.size + imax = np.argmax(js) # we want to crop out just the bit below the peak + js = js[imax:] # NB z is in DECREASING order + jz = jz[imax:] + inow = np.argmax( + js < current_js + ) # use the curve we recorded to estimate our position - # So, the Z position corresponding to our current sharpness value is zs[inow] - # That means we should move forwards, by best_z - zs[inow] - logging.debug("Correction move") - correction_move = best_z + target_z - jz[inow] - logging.debug( - "Fast autofocus scan: correcting backlash by moving %s steps", - (correction_move), - ) - m.focus_rel(correction_move) - return m.data_dict() + # So, the Z position corresponding to our current sharpness value is zs[inow] + # That means we should move forwards, by best_z - zs[inow] + logging.debug("Correction move") + correction_move = best_z + target_z - jz[inow] + logging.debug( + "Fast autofocus scan: correcting backlash by moving %s steps", + (correction_move), + ) + m.focus_rel(correction_move) + return m.data_dict() class MeasureSharpnessAPI(View): @@ -316,7 +335,7 @@ class MeasureSharpnessAPI(View): if not microscope: abort(503, "No microscope connected. Unable to measure sharpness.") - return {"sharpness": measure_sharpness(microscope)} + return {"sharpness": self.extension.measure_sharpness(microscope)} class MoveAndMeasureAPI(ActionView): @@ -335,7 +354,7 @@ class MoveAndMeasureAPI(ActionView): if microscope.has_real_stage(): # Acquire microscope lock with 1s timeout with microscope.camera.lock, microscope.stage.lock: - return move_and_measure(microscope, dz=args.get("dz")) + return self.extension.move_and_measure(microscope, dz=args.get("dz")) else: abort(503, "No stage connected. Unable to autofocus.") @@ -361,7 +380,7 @@ class AutofocusAPI(ActionView): logging.debug("Running autofocus...") # return a handle on the autofocus task - return autofocus(microscope, dz) + return self.extension.autofocus(microscope, dz) else: abort(503, "No stage connected. Unable to autofocus.") @@ -385,7 +404,7 @@ class FastAutofocusAPI(ActionView): # Acquire microscope lock with 1s timeout with microscope.lock(timeout=1): # Run fast_autofocus - return fast_autofocus(microscope, dz=args.get("dz")) + return self.extension.fast_autofocus(microscope, dz=args.get("dz")) else: abort(503, "No stage connected. Unable to autofocus.") @@ -417,38 +436,9 @@ class UpDownUpAutofocusAPI(ActionView): # Acquire microscope lock with 1s timeout with microscope.lock(timeout=1): # Run fast_up_down_up_autofocus - return fast_up_down_up_autofocus( + return self.extension.fast_up_down_up_autofocus( microscope, dz=dz, mini_backlash=backlash ) else: abort(503, "No stage connected. Unable to autofocus.") - - -autofocus_extension_v2 = BaseExtension( - "org.openflexure.autofocus", - version="2.0.0", - description="Actions to move the microscope in Z and pick the point with the sharpest image.", -) - -autofocus_extension_v2.add_method(fast_autofocus, "fast_autofocus") -autofocus_extension_v2.add_method( - fast_up_down_up_autofocus, "fast_up_down_up_autofocus" -) -autofocus_extension_v2.add_method(autofocus, "autofocus") -autofocus_extension_v2.add_method(move_and_measure, "move_and_measure") - -autofocus_extension_v2.add_view( - MeasureSharpnessAPI, "/measure_sharpness", endpoint="measure_sharpness" -) -autofocus_extension_v2.add_view(AutofocusAPI, "/autofocus", endpoint="autofocus") -autofocus_extension_v2.add_view( - FastAutofocusAPI, "/fast_autofocus", endpoint="fast_autofocus" -) -autofocus_extension_v2.add_view( - UpDownUpAutofocusAPI, "/updownup_autofocus", endpoint="updownup_autofocus" -) - -autofocus_extension_v2.add_view( - MoveAndMeasureAPI, "/move_and_measure", endpoint="move_and_measure" -) diff --git a/openflexure_microscope/api/default_extensions/autostorage.py b/openflexure_microscope/api/default_extensions/autostorage.py index 7d97d766..2847bff0 100644 --- a/openflexure_microscope/api/default_extensions/autostorage.py +++ b/openflexure_microscope/api/default_extensions/autostorage.py @@ -93,12 +93,43 @@ class AutostorageExtension(BaseExtension): # We'll store a reference to a CaptureManager object, who's capture paths will be modified self.capture_manager: Optional[CaptureManager] = None - self.initial_location: str = get_default_location() # Register the on_microscope function to run when the microscope is attached self.on_component("org.openflexure.microscope", self.on_microscope) + self.add_view(GetLocationsView, "/list-locations") + self.add_view(PreferredLocationView, "/location") + self.add_view(PreferredLocationGUIView, "/location-from-title") + self.add_meta("gui", build_gui(self.dynamic_form, self)) + + def dynamic_form(self): + self.check_location() + return { + "icon": "sd_storage", + "title": "Storage", + "viewPanel": "gallery", + "forms": [ + { + "name": "Autostorage", + "isCollapsible": False, + "isTask": False, + "route": "/location-from-title", + "emitOnResponse": "globalUpdateCaptures", + "submitLabel": "Set path", + "schema": [ + { + "fieldType": "selectList", + "name": "new_path_title", + "label": "Capture storage path", + "options": self.get_titles(), + "value": self.get_preferred_title(), + } + ], + } + ], + } + def on_microscope(self, microscope_obj: Microscope): """Function to automatically call when the parent LabThing has a microscope attached.""" logging.debug("Autostorage extension found microscope %s", microscope_obj) @@ -204,12 +235,9 @@ class AutostorageExtension(BaseExtension): return self.key_to_title(self.get_preferred_key()) -autostorage_extension_v2 = AutostorageExtension() - - class GetLocationsView(PropertyView): def get(self): - autostorage_extension_v2.check_location() + self.extension.check_location() return autostorage_extension_v2.get_locations() @@ -217,8 +245,8 @@ class PreferredLocationView(PropertyView): schema = fields.String(required=True, example="Default") def get(self): - autostorage_extension_v2.check_location() - return autostorage_extension_v2.get_preferred_key() + self.extension.check_location() + return self.extension.get_preferred_key() def post(self, new_path_key): microscope = find_component("org.openflexure.microscope") @@ -226,8 +254,8 @@ class PreferredLocationView(PropertyView): if not microscope: abort(503, "No microscope connected. Unable to autofocus.") - autostorage_extension_v2.check_location() - autostorage_extension_v2.set_preferred_key(new_path_key) + self.extension.check_location() + self.extension.set_preferred_key(new_path_key) microscope.save_settings() @@ -237,46 +265,11 @@ class PreferredLocationGUIView(View): new_path_title = args.get("new_path_title") logging.debug(new_path_title) - new_path_key = autostorage_extension_v2.title_to_key(new_path_title) + new_path_key = self.extension.title_to_key(new_path_title) logging.debug(new_path_key) - autostorage_extension_v2.check_location() - autostorage_extension_v2.set_preferred_key(new_path_key) + self.extension.check_location() + self.extension.set_preferred_key(new_path_key) return new_path_title - -def dynamic_form(): - autostorage_extension_v2.check_location() - return { - "icon": "sd_storage", - "title": "Storage", - "viewPanel": "gallery", - "forms": [ - { - "name": "Autostorage", - "isCollapsible": False, - "isTask": False, - "route": "/location-from-title", - "emitOnResponse": "globalUpdateCaptures", - "submitLabel": "Set path", - "schema": [ - { - "fieldType": "selectList", - "name": "new_path_title", - "label": "Capture storage path", - "options": autostorage_extension_v2.get_titles(), - "value": autostorage_extension_v2.get_preferred_title(), - } - ], - } - ], - } - - -autostorage_extension_v2.add_view(GetLocationsView, "/list-locations") -autostorage_extension_v2.add_view(PreferredLocationView, "/location") -autostorage_extension_v2.add_view(PreferredLocationGUIView, "/location-from-title") -autostorage_extension_v2.add_meta( - "gui", build_gui(dynamic_form, autostorage_extension_v2) -) diff --git a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/__init__.py b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/__init__.py index 49d03097..fd920b86 100644 --- a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/__init__.py +++ b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/__init__.py @@ -1 +1 @@ -from .extension import lst_extension_v2 +from .extension import LSTExtension diff --git a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/extension.py b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/extension.py index c10491c4..3b7de2ea 100644 --- a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/extension.py +++ b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/extension.py @@ -42,23 +42,43 @@ def pause_stream(scamera: BaseCamera): scamera.start_stream() -def recalibrate(microscope: Microscope): - """Reset the camera's settings. +class LSTExtension(BaseExtension): + def __init__(self) -> None: + super().__init__( + "org.openflexure.calibration.picamera", + version="2.0.0-beta.1", + description="Routines to perform flat-field correction on the camera.", + ) - This generates new gains, exposure time, and lens shading - table such that the background is as uniform as possible - with a gray level of 230. It takes a little while to run. - """ - with pause_stream(microscope.camera) as scamera: - if hasattr(scamera, "picamera"): - picamera_obj: picamerax.PiCamera = getattr(scamera, "picamera") - auto_expose_and_freeze_settings(picamera_obj) - recalibrate_camera(picamera_obj) - microscope.save_settings() - else: - raise RuntimeError( - "Recalibrate can only be used with a Raspberry Pi camera" - ) + self.add_view(RecalibrateView, "/recalibrate", endpoint="recalibrate") + self.add_view( + FlattenLSTView, + "/flatten_lens_shading_table", + endpoint="flatten_lens_shading_table", + ) + self.add_view( + DeleteLSTView, + "/delete_lens_shading_table", + endpoint="delete_lens_shading_table", + ) + + def recalibrate(self, microscope: Microscope): + """Reset the camera's settings. + + This generates new gains, exposure time, and lens shading + table such that the background is as uniform as possible + with a gray level of 230. It takes a little while to run. + """ + with pause_stream(microscope.camera) as scamera: + if hasattr(scamera, "picamera"): + picamera_obj: picamerax.PiCamera = getattr(scamera, "picamera") + auto_expose_and_freeze_settings(picamera_obj) + recalibrate_camera(picamera_obj) + microscope.save_settings() + else: + raise RuntimeError( + "Recalibrate can only be used with a Raspberry Pi camera" + ) class RecalibrateView(ActionView): @@ -70,7 +90,7 @@ class RecalibrateView(ActionView): logging.info("Starting microscope recalibration...") - return recalibrate(microscope) + return self.extension.recalibrate(microscope) class FlattenLSTView(ActionView): @@ -102,22 +122,3 @@ class DeleteLSTView(ActionView): with pause_stream(microscope.camera) as scamera: scamera.camera.lens_shading_table = None microscope.save_settings() - - -lst_extension_v2 = BaseExtension( - "org.openflexure.calibration.picamera", - version="2.0.0-beta.1", - description="Routines to perform flat-field correction on the camera.", -) - -lst_extension_v2.add_method( - recalibrate, "org.openflexure.calibration.picamera.recalibrate" -) - -lst_extension_v2.add_view(RecalibrateView, "/recalibrate", endpoint="recalibrate") -lst_extension_v2.add_view( - FlattenLSTView, "/flatten_lens_shading_table", endpoint="flatten_lens_shading_table" -) -lst_extension_v2.add_view( - DeleteLSTView, "/delete_lens_shading_table", endpoint="delete_lens_shading_table" -) diff --git a/openflexure_microscope/api/default_extensions/scan.py b/openflexure_microscope/api/default_extensions/scan.py index 6cf5abd8..8caa6c13 100644 --- a/openflexure_microscope/api/default_extensions/scan.py +++ b/openflexure_microscope/api/default_extensions/scan.py @@ -99,9 +99,12 @@ def construct_grid( class ScanExtension(BaseExtension): def __init__(self): + BaseExtension.__init__(self, "org.openflexure.scan", version="2.0.0") + self._images_to_be_captured: int = 1 self._images_captured_so_far: int = 0 - BaseExtension.__init__(self, "org.openflexure.scan", version="2.0.0") + + self.add_view(TileScanAPI, "/tile", endpoint="tile") def capture( self, @@ -356,9 +359,6 @@ class ScanExtension(BaseExtension): microscope.stage.move_abs(initial_position) -scan_extension_v2 = ScanExtension() - - class TileScanArgs(FullCaptureArgs): namemode = fields.String(missing="coordinates", example="coordinates") grid = fields.List(fields.Integer, missing=[3, 3, 3], example=[3, 3, 3]) @@ -398,7 +398,7 @@ class TileScanAPI(ActionView): # Acquire microscope lock with 1s timeout with microscope.lock(timeout=1): # Run scan_extension_v2 - return scan_extension_v2.tile( + return self.extension.scan_extension_v2.tile( microscope, basename=args.get("filename"), namemode=args.get("namemode"), @@ -414,6 +414,3 @@ class TileScanAPI(ActionView): annotations=args.get("annotations"), tags=args.get("tags"), ) - - -scan_extension_v2.add_view(TileScanAPI, "/tile", endpoint="tile") diff --git a/openflexure_microscope/api/default_extensions/zip_builder.py b/openflexure_microscope/api/default_extensions/zip_builder.py index 93da215d..74444a07 100644 --- a/openflexure_microscope/api/default_extensions/zip_builder.py +++ b/openflexure_microscope/api/default_extensions/zip_builder.py @@ -131,8 +131,18 @@ class ZipManager: zd.close() -# Create a global ZIP manager -default_zip_manager = ZipManager() +class ZipBuilderExtension(BaseExtension): + def __init__(self): + super().__init__( + "org.openflexure.zipbuilder", + version="2.0.0", + description="Build and download capture collections as ZIP files", + ) + self.manager = ZipManager() + + self.add_view(ZipGetterAPIView, "/get/", endpoint="get_id") + self.add_view(ZipListAPIView, "/get", endpoint="get") + self.add_view(ZipBuilderAPIView, "/build", endpoint="build") class ZipBuilderAPIView(ActionView): @@ -142,7 +152,7 @@ class ZipBuilderAPIView(ActionView): microscope = find_component("org.openflexure.microscope") # Return a handle on the autofocus task - return default_zip_manager.marshaled_build_zip_from_capture_ids( + return self.extension.manager.marshaled_build_zip_from_capture_ids( microscope, args ) @@ -151,7 +161,7 @@ class ZipListAPIView(PropertyView): schema = ZipObjectSchema(many=True) def get(self): - return default_zip_manager.session_zips.values() + return self.extension.manager.session_zips.values() class ZipGetterAPIView(View): @@ -163,13 +173,13 @@ class ZipGetterAPIView(View): """ Download a particular capture collection ZIP file """ - if not session_id in default_zip_manager.session_zips: + if not session_id in self.extension.manager.session_zips: return abort(404) # 404 Not Found logging.info("Session ID: %s", session_id) return send_file( - default_zip_manager.zip_fp_from_id(session_id).name, + self.extension.manager.zip_fp_from_id(session_id).name, mimetype="application/zip", as_attachment=True, attachment_filename=f"{session_id}.zip", @@ -179,26 +189,12 @@ class ZipGetterAPIView(View): """ Close and delete a particular capture collection ZIP file """ - if not session_id in default_zip_manager.session_zips: + if not session_id in self.extension.manager.session_zips: return abort(404) # 404 Not Found # Close the file - default_zip_manager.session_zips[session_id].close() + self.extension.manager.session_zips[session_id].close() # Delete the file reference - del default_zip_manager.session_zips[session_id] + del self.extension.manager.session_zips[session_id] return {"return": session_id} - - -zip_extension_v2 = BaseExtension( - "org.openflexure.zipbuilder", - version="2.0.0", - description="Build and download capture collections as ZIP files", -) - -zip_extension_v2.add_view( - ZipGetterAPIView, "/get/", endpoint="get_id" -) -zip_extension_v2.add_view(ZipListAPIView, "/get", endpoint="get") - -zip_extension_v2.add_view(ZipBuilderAPIView, "/build", endpoint="build") diff --git a/openflexure_microscope/api/dev_extensions/__init__.py b/openflexure_microscope/api/dev_extensions/__init__.py index 01e565b4..c5a0a811 100644 --- a/openflexure_microscope/api/dev_extensions/__init__.py +++ b/openflexure_microscope/api/dev_extensions/__init__.py @@ -1 +1,3 @@ from .tools import devtools_extension_v2 + +LABTHINGS_EXTENSIONS = [devtools_extension_v2]