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,8 +171,8 @@ 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,
@ -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,8 +241,8 @@ 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,
@ -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]