Updated default extensions to subclass structure

This commit is contained in:
Joel Collins 2020-11-30 16:41:39 +00:00
parent bacc111a87
commit 8d0759e9e0
8 changed files with 300 additions and 305 deletions

View file

@ -2,6 +2,8 @@ import logging
import traceback import traceback
from contextlib import contextmanager from contextlib import contextmanager
LABTHINGS_EXTENSIONS = []
@contextmanager @contextmanager
def handle_extension_error(extension_name): def handle_extension_error(extension_name):
@ -17,14 +19,28 @@ def handle_extension_error(extension_name):
with handle_extension_error("autofocus"): with handle_extension_error("autofocus"):
from .autofocus import autofocus_extension_v2 from .autofocus import AutofocusExtension
LABTHINGS_EXTENSIONS.append(AutofocusExtension)
with handle_extension_error("scan"): 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"): 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"): 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"): with handle_extension_error("camera stage mapping"):
from camera_stage_mapping.ofm_extension import csm_extension 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)

View file

@ -109,6 +109,16 @@ class JPEGSharpnessMonitor:
return data 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): def sharpness_sum_lap2(rgb_image: np.ndarray):
"""Return an image sharpness metric: sum(laplacian(image)**")""" """Return an image sharpness metric: sum(laplacian(image)**")"""
image_bw = np.mean(rgb_image, 2) image_bw = np.mean(rgb_image, 2)
@ -129,7 +139,29 @@ def sharpness_edge(image: np.ndarray):
### Autofocus extension ### Autofocus extension
def measure_sharpness(microscope: Microscope, metric_fn: Callable = sharpness_sum_lap2): 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"
)
self.add_view(
MoveAndMeasureAPI, "/move_and_measure", endpoint="move_and_measure"
)
def measure_sharpness(
self, microscope: Microscope, metric_fn: Callable = sharpness_sum_lap2
):
"""Measure the sharpness of the camera's current view.""" """Measure the sharpness of the camera's current view."""
if hasattr(microscope.camera, "array") and callable( if hasattr(microscope.camera, "array") and callable(
@ -139,13 +171,13 @@ def measure_sharpness(microscope: Microscope, metric_fn: Callable = sharpness_su
else: else:
raise RuntimeError(f"Object {microscope.camera} has no method `array`") raise RuntimeError(f"Object {microscope.camera} has no method `array`")
def autofocus(
def autofocus( self,
microscope: Microscope, microscope: Microscope,
dz: List[int], dz: List[int],
settle: float = 0.5, settle: float = 0.5,
metric_fn: Callable = sharpness_sum_lap2, metric_fn: Callable = sharpness_sum_lap2,
): ):
"""Perform a simple autofocus routine. """Perform a simple autofocus routine.
The stage is moved to z positions (relative to current position) in dz, 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 and at each position an image is captured and the sharpness function
@ -169,39 +201,26 @@ def autofocus(
return return
positions.append(stage.position[2]) positions.append(stage.position[2])
time.sleep(settle) time.sleep(settle)
sharpnesses.append(measure_sharpness(microscope, metric_fn)) sharpnesses.append(self.measure_sharpness(microscope, metric_fn))
newposition = positions[np.argmax(sharpnesses)] newposition = positions[np.argmax(sharpnesses)]
stage.move_rel((0, 0, newposition - stage.position[2])) stage.move_rel((0, 0, newposition - stage.position[2]))
return positions, sharpnesses return positions, sharpnesses
def move_and_find_focus(self, microscope: Microscope, dz: int):
@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""" """Make a relative Z move and return the peak sharpness position"""
with monitor_sharpness(microscope) as m: with monitor_sharpness(microscope) as m:
m.focus_rel(dz) m.focus_rel(dz)
return m.sharpest_z_on_move(0) return m.sharpest_z_on_move(0)
def move_and_measure(self, microscope: Microscope, dz: int):
def move_and_measure(microscope: Microscope, dz: int):
"""Make a relative Z move and return the sharpness data""" """Make a relative Z move and return the sharpness data"""
with monitor_sharpness(microscope) as m: with monitor_sharpness(microscope) as m:
m.focus_rel(dz) m.focus_rel(dz)
return m.data_dict() return m.data_dict()
def fast_autofocus(self, microscope: Microscope, dz: int = 2000):
def fast_autofocus(microscope: Microscope, dz: int = 2000):
"""Perform a down-up-down-up autofocus""" """Perform a down-up-down-up autofocus"""
with microscope.camera.lock, microscope.stage.lock: with microscope.camera.lock, microscope.stage.lock:
with monitor_sharpness(microscope) as m: with monitor_sharpness(microscope) as m:
@ -222,14 +241,14 @@ def fast_autofocus(microscope: Microscope, dz: int = 2000):
# Return all focus data # Return all focus data
return m.data_dict() return m.data_dict()
def fast_up_down_up_autofocus(
def fast_up_down_up_autofocus( self,
microscope: Microscope, microscope: Microscope,
dz: int = 2000, dz: int = 2000,
target_z: int = 0, target_z: int = 0,
initial_move_up: bool = True, initial_move_up: bool = True,
mini_backlash: int = 25, mini_backlash: int = 25,
): ):
"""Autofocus by measuring on the way down, and moving back up with feedback. """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. This autofocus method is very efficient, as it only passes the peak once.
@ -316,7 +335,7 @@ class MeasureSharpnessAPI(View):
if not microscope: if not microscope:
abort(503, "No microscope connected. Unable to measure sharpness.") abort(503, "No microscope connected. Unable to measure sharpness.")
return {"sharpness": measure_sharpness(microscope)} return {"sharpness": self.extension.measure_sharpness(microscope)}
class MoveAndMeasureAPI(ActionView): class MoveAndMeasureAPI(ActionView):
@ -335,7 +354,7 @@ class MoveAndMeasureAPI(ActionView):
if microscope.has_real_stage(): if microscope.has_real_stage():
# Acquire microscope lock with 1s timeout # Acquire microscope lock with 1s timeout
with microscope.camera.lock, microscope.stage.lock: 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: else:
abort(503, "No stage connected. Unable to autofocus.") abort(503, "No stage connected. Unable to autofocus.")
@ -361,7 +380,7 @@ class AutofocusAPI(ActionView):
logging.debug("Running autofocus...") logging.debug("Running autofocus...")
# return a handle on the autofocus task # return a handle on the autofocus task
return autofocus(microscope, dz) return self.extension.autofocus(microscope, dz)
else: else:
abort(503, "No stage connected. Unable to autofocus.") abort(503, "No stage connected. Unable to autofocus.")
@ -385,7 +404,7 @@ class FastAutofocusAPI(ActionView):
# Acquire microscope lock with 1s timeout # Acquire microscope lock with 1s timeout
with microscope.lock(timeout=1): with microscope.lock(timeout=1):
# Run fast_autofocus # Run fast_autofocus
return fast_autofocus(microscope, dz=args.get("dz")) return self.extension.fast_autofocus(microscope, dz=args.get("dz"))
else: else:
abort(503, "No stage connected. Unable to autofocus.") abort(503, "No stage connected. Unable to autofocus.")
@ -417,38 +436,9 @@ class UpDownUpAutofocusAPI(ActionView):
# Acquire microscope lock with 1s timeout # Acquire microscope lock with 1s timeout
with microscope.lock(timeout=1): with microscope.lock(timeout=1):
# Run fast_up_down_up_autofocus # 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 microscope, dz=dz, mini_backlash=backlash
) )
else: else:
abort(503, "No stage connected. Unable to autofocus.") 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"
)

View file

@ -93,12 +93,43 @@ class AutostorageExtension(BaseExtension):
# We'll store a reference to a CaptureManager object, who's capture paths will be modified # We'll store a reference to a CaptureManager object, who's capture paths will be modified
self.capture_manager: Optional[CaptureManager] = None self.capture_manager: Optional[CaptureManager] = None
self.initial_location: str = get_default_location() self.initial_location: str = get_default_location()
# Register the on_microscope function to run when the microscope is attached # Register the on_microscope function to run when the microscope is attached
self.on_component("org.openflexure.microscope", self.on_microscope) 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): def on_microscope(self, microscope_obj: Microscope):
"""Function to automatically call when the parent LabThing has a microscope attached.""" """Function to automatically call when the parent LabThing has a microscope attached."""
logging.debug("Autostorage extension found microscope %s", microscope_obj) 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()) return self.key_to_title(self.get_preferred_key())
autostorage_extension_v2 = AutostorageExtension()
class GetLocationsView(PropertyView): class GetLocationsView(PropertyView):
def get(self): def get(self):
autostorage_extension_v2.check_location() self.extension.check_location()
return autostorage_extension_v2.get_locations() return autostorage_extension_v2.get_locations()
@ -217,8 +245,8 @@ class PreferredLocationView(PropertyView):
schema = fields.String(required=True, example="Default") schema = fields.String(required=True, example="Default")
def get(self): def get(self):
autostorage_extension_v2.check_location() self.extension.check_location()
return autostorage_extension_v2.get_preferred_key() return self.extension.get_preferred_key()
def post(self, new_path_key): def post(self, new_path_key):
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
@ -226,8 +254,8 @@ class PreferredLocationView(PropertyView):
if not microscope: if not microscope:
abort(503, "No microscope connected. Unable to autofocus.") abort(503, "No microscope connected. Unable to autofocus.")
autostorage_extension_v2.check_location() self.extension.check_location()
autostorage_extension_v2.set_preferred_key(new_path_key) self.extension.set_preferred_key(new_path_key)
microscope.save_settings() microscope.save_settings()
@ -237,46 +265,11 @@ class PreferredLocationGUIView(View):
new_path_title = args.get("new_path_title") new_path_title = args.get("new_path_title")
logging.debug(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) logging.debug(new_path_key)
autostorage_extension_v2.check_location() self.extension.check_location()
autostorage_extension_v2.set_preferred_key(new_path_key) self.extension.set_preferred_key(new_path_key)
return new_path_title 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)
)

View file

@ -1 +1 @@
from .extension import lst_extension_v2 from .extension import LSTExtension

View file

@ -42,7 +42,27 @@ def pause_stream(scamera: BaseCamera):
scamera.start_stream() scamera.start_stream()
def recalibrate(microscope: Microscope): 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.",
)
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. """Reset the camera's settings.
This generates new gains, exposure time, and lens shading This generates new gains, exposure time, and lens shading
@ -70,7 +90,7 @@ class RecalibrateView(ActionView):
logging.info("Starting microscope recalibration...") logging.info("Starting microscope recalibration...")
return recalibrate(microscope) return self.extension.recalibrate(microscope)
class FlattenLSTView(ActionView): class FlattenLSTView(ActionView):
@ -102,22 +122,3 @@ class DeleteLSTView(ActionView):
with pause_stream(microscope.camera) as scamera: with pause_stream(microscope.camera) as scamera:
scamera.camera.lens_shading_table = None scamera.camera.lens_shading_table = None
microscope.save_settings() 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"
)

View file

@ -99,9 +99,12 @@ def construct_grid(
class ScanExtension(BaseExtension): class ScanExtension(BaseExtension):
def __init__(self): def __init__(self):
BaseExtension.__init__(self, "org.openflexure.scan", version="2.0.0")
self._images_to_be_captured: int = 1 self._images_to_be_captured: int = 1
self._images_captured_so_far: int = 0 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( def capture(
self, self,
@ -356,9 +359,6 @@ class ScanExtension(BaseExtension):
microscope.stage.move_abs(initial_position) microscope.stage.move_abs(initial_position)
scan_extension_v2 = ScanExtension()
class TileScanArgs(FullCaptureArgs): class TileScanArgs(FullCaptureArgs):
namemode = fields.String(missing="coordinates", example="coordinates") namemode = fields.String(missing="coordinates", example="coordinates")
grid = fields.List(fields.Integer, missing=[3, 3, 3], example=[3, 3, 3]) 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 # Acquire microscope lock with 1s timeout
with microscope.lock(timeout=1): with microscope.lock(timeout=1):
# Run scan_extension_v2 # Run scan_extension_v2
return scan_extension_v2.tile( return self.extension.scan_extension_v2.tile(
microscope, microscope,
basename=args.get("filename"), basename=args.get("filename"),
namemode=args.get("namemode"), namemode=args.get("namemode"),
@ -414,6 +414,3 @@ class TileScanAPI(ActionView):
annotations=args.get("annotations"), annotations=args.get("annotations"),
tags=args.get("tags"), tags=args.get("tags"),
) )
scan_extension_v2.add_view(TileScanAPI, "/tile", endpoint="tile")

View file

@ -131,8 +131,18 @@ class ZipManager:
zd.close() zd.close()
# Create a global ZIP manager class ZipBuilderExtension(BaseExtension):
default_zip_manager = ZipManager() 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/<string:session_id>", endpoint="get_id")
self.add_view(ZipListAPIView, "/get", endpoint="get")
self.add_view(ZipBuilderAPIView, "/build", endpoint="build")
class ZipBuilderAPIView(ActionView): class ZipBuilderAPIView(ActionView):
@ -142,7 +152,7 @@ class ZipBuilderAPIView(ActionView):
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
# Return a handle on the autofocus task # 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 microscope, args
) )
@ -151,7 +161,7 @@ class ZipListAPIView(PropertyView):
schema = ZipObjectSchema(many=True) schema = ZipObjectSchema(many=True)
def get(self): def get(self):
return default_zip_manager.session_zips.values() return self.extension.manager.session_zips.values()
class ZipGetterAPIView(View): class ZipGetterAPIView(View):
@ -163,13 +173,13 @@ class ZipGetterAPIView(View):
""" """
Download a particular capture collection ZIP file 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 return abort(404) # 404 Not Found
logging.info("Session ID: %s", session_id) logging.info("Session ID: %s", session_id)
return send_file( 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", mimetype="application/zip",
as_attachment=True, as_attachment=True,
attachment_filename=f"{session_id}.zip", attachment_filename=f"{session_id}.zip",
@ -179,26 +189,12 @@ class ZipGetterAPIView(View):
""" """
Close and delete a particular capture collection ZIP file 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 return abort(404) # 404 Not Found
# Close the file # Close the file
default_zip_manager.session_zips[session_id].close() self.extension.manager.session_zips[session_id].close()
# Delete the file reference # Delete the file reference
del default_zip_manager.session_zips[session_id] del self.extension.manager.session_zips[session_id]
return {"return": 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/<string:session_id>", endpoint="get_id"
)
zip_extension_v2.add_view(ZipListAPIView, "/get", endpoint="get")
zip_extension_v2.add_view(ZipBuilderAPIView, "/build", endpoint="build")

View file

@ -1 +1,3 @@
from .tools import devtools_extension_v2 from .tools import devtools_extension_v2
LABTHINGS_EXTENSIONS = [devtools_extension_v2]