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,184 +139,193 @@ def sharpness_edge(image: np.ndarray):
### Autofocus extension ### Autofocus extension
def measure_sharpness(microscope: Microscope, metric_fn: Callable = sharpness_sum_lap2): class AutofocusExtension(BaseExtension):
"""Measure the sharpness of the camera's current view.""" 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( self.add_view(
getattr(microscope.camera, "array") 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)) """Measure the sharpness of the camera's current view."""
else:
raise RuntimeError(f"Object {microscope.camera} has no method `array`")
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( def autofocus(
microscope: Microscope, self,
dz: List[int], microscope: Microscope,
settle: float = 0.5, dz: List[int],
metric_fn: Callable = sharpness_sum_lap2, 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, """Perform a simple autofocus routine.
and at each position an image is captured and the sharpness function The stage is moved to z positions (relative to current position) in dz,
evaulated. We then move back to the position where the sharpness was and at each position an image is captured and the sharpness function
highest. No interpolation is performed. evaulated. We then move back to the position where the sharpness was
dz is assumed to be in ascending order (starting at -ve values) highest. No interpolation is performed.
""" dz is assumed to be in ascending order (starting at -ve values)
camera = microscope.camera """
stage = microscope.stage camera = microscope.camera
stage = microscope.stage
with set_properties(stage, backlash=256), stage.lock, camera.lock: with set_properties(stage, backlash=256), stage.lock, camera.lock:
sharpnesses = [] sharpnesses = []
positions = [] positions = []
# Some cameras may not have annotate_text. Reset if it does # Some cameras may not have annotate_text. Reset if it does
if getattr(camera, "annotate_text"): if getattr(camera, "annotate_text"):
setattr(camera, "annotate_text", "") setattr(camera, "annotate_text", "")
for _ in stage.scan_z(dz, return_to_start=False): for _ in stage.scan_z(dz, return_to_start=False):
if current_action() and current_action().stopped: if current_action() and current_action().stopped:
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 """Make a relative Z move and return the peak sharpness position"""
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:
with monitor_sharpness(microscope) as m: with monitor_sharpness(microscope) as m:
# Move to (-dz / 2) m.focus_rel(dz)
m.focus_rel(-dz / 2) return m.sharpest_z_on_move(0)
# Move to dz while monitoring sharpness
# i: Sharpness monitor index for this move def move_and_measure(self, microscope: Microscope, dz: int):
# z: Final z position after move """Make a relative Z move and return the sharpness data"""
i, z = m.focus_rel(dz) with monitor_sharpness(microscope) as m:
# Get the z position with highest sharpness from the previous move (index i) m.focus_rel(dz)
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() 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( def fast_up_down_up_autofocus(
microscope: Microscope, self,
dz: int = 2000, microscope: Microscope,
target_z: int = 0, dz: int = 2000,
initial_move_up: bool = True, target_z: int = 0,
mini_backlash: int = 25, initial_move_up: bool = True,
): 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.
The sequence of moves it performs is: 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 \\ 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 \\ estimate how much further we need to go. Make this move, to reach our \\
target position. target position.
Moving back to the target position in two steps allows us to correct for 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. backlash, by using the sharpness-vs-z curve as a rough encoder for Z.
Parameters: Parameters:
dz: number of steps over which to scan (optional, default 2000) 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 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. 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. 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 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 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. 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 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 3 to help counteract backlash. It should be small enough that you
would always expect there to be greater backlash than this. Too small 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 might slightly hurt accuracy, but is unlikely to be a big issue. Too big
may cause you to overshoot, which is a problem. may cause you to overshoot, which is a problem.
""" """
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:
# Ensure the MJPEG stream has started # Ensure the MJPEG stream has started
microscope.camera.start_stream() microscope.camera.start_stream()
logging.debug("Initial move") logging.debug("Initial move")
if initial_move_up: if initial_move_up:
m.focus_rel(dz / 2) m.focus_rel(dz / 2)
# move down # move down
logging.debug("Move down") logging.debug("Move down")
i, z = m.focus_rel(-dz) i, z = m.focus_rel(-dz)
# now inspect where the sharpest point is, and estimate the sharpness # now inspect where the sharpest point is, and estimate the sharpness
# (JPEG size) that we should find at the start of the Z stack # (JPEG size) that we should find at the start of the Z stack
_, jz, js = m.move_data(i) _, jz, js = m.move_data(i)
best_z = jz[np.argmax(js)] best_z = jz[np.argmax(js)]
# now move to the start of the z stack # now move to the start of the z stack
logging.debug("Move to the start of the z stack") logging.debug("Move to the start of the z stack")
i, z = m.focus_rel( i, z = m.focus_rel(
best_z + target_z - z + mini_backlash best_z + target_z - z + mini_backlash
) # takes us to the start of the stack ) # takes us to the start of the stack
# We've deliberately undershot - figure out how much further we should move based on the curve # We've deliberately undershot - figure out how much further we should move based on the curve
logging.debug("Calculate remining movement") logging.debug("Calculate remining movement")
current_js = m.camera.stream.last.size current_js = m.camera.stream.last.size
imax = np.argmax(js) # we want to crop out just the bit below the peak imax = np.argmax(js) # we want to crop out just the bit below the peak
js = js[imax:] # NB z is in DECREASING order js = js[imax:] # NB z is in DECREASING order
jz = jz[imax:] jz = jz[imax:]
inow = np.argmax( inow = np.argmax(
js < current_js js < current_js
) # use the curve we recorded to estimate our position ) # use the curve we recorded to estimate our position
# So, the Z position corresponding to our current sharpness value is zs[inow] # So, the Z position corresponding to our current sharpness value is zs[inow]
# That means we should move forwards, by best_z - zs[inow] # That means we should move forwards, by best_z - zs[inow]
logging.debug("Correction move") logging.debug("Correction move")
correction_move = best_z + target_z - jz[inow] correction_move = best_z + target_z - jz[inow]
logging.debug( logging.debug(
"Fast autofocus scan: correcting backlash by moving %s steps", "Fast autofocus scan: correcting backlash by moving %s steps",
(correction_move), (correction_move),
) )
m.focus_rel(correction_move) m.focus_rel(correction_move)
return m.data_dict() return m.data_dict()
class MeasureSharpnessAPI(View): class MeasureSharpnessAPI(View):
@ -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,23 +42,43 @@ def pause_stream(scamera: BaseCamera):
scamera.start_stream() scamera.start_stream()
def recalibrate(microscope: Microscope): class LSTExtension(BaseExtension):
"""Reset the camera's settings. 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 self.add_view(RecalibrateView, "/recalibrate", endpoint="recalibrate")
table such that the background is as uniform as possible self.add_view(
with a gray level of 230. It takes a little while to run. FlattenLSTView,
""" "/flatten_lens_shading_table",
with pause_stream(microscope.camera) as scamera: endpoint="flatten_lens_shading_table",
if hasattr(scamera, "picamera"): )
picamera_obj: picamerax.PiCamera = getattr(scamera, "picamera") self.add_view(
auto_expose_and_freeze_settings(picamera_obj) DeleteLSTView,
recalibrate_camera(picamera_obj) "/delete_lens_shading_table",
microscope.save_settings() endpoint="delete_lens_shading_table",
else: )
raise RuntimeError(
"Recalibrate can only be used with a Raspberry Pi camera" 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): class RecalibrateView(ActionView):
@ -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]