moved find_microscope into functions

This commit is contained in:
Richard 2021-07-13 22:50:42 +01:00
parent 65f5a69bad
commit 9892c7079a

View file

@ -21,6 +21,9 @@ from labthings.utilities import get_docstring, get_summary
class JPEGSharpnessMonitor: class JPEGSharpnessMonitor:
"""Monitor JPEG frame size in a background thread
This class starts a background thread """
def __init__(self, microscope: Microscope): def __init__(self, microscope: Microscope):
self.microscope: Microscope = microscope self.microscope: Microscope = microscope
self.camera: BaseCamera = microscope.camera self.camera: BaseCamera = microscope.camera
@ -146,37 +149,36 @@ def sharpness_edge(image: np.ndarray) -> float:
) )
def find_microscope() -> dict: def find_microscope() -> Microscope:
"""Find the microscope component or raise an exception. """Find the microscope component or raise an exception.
This function will fail with HTTPError extensions if it can't This function will fail with HTTPError extensions if it can't
find the appropriate hardware. find the appropriate hardware.
It is returned as a dictionary with one key, "microscope". We return a `Microscope` object
This means it can be easily merged into an arguments dictionary.
""" """
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
if not microscope: if not microscope:
abort(503, "No microscope connected. Unable to autofocus.") abort(503, "No microscope connected. Unable to autofocus.")
return {"microscope": microscope} return microscope
def find_microscope_with_real_stage() -> dict: def find_microscope_with_real_stage() -> Microscope:
"""Find the microscope and ensure it has a real stage. """Find the microscope and ensure it has a real stage.
This function wraps `find_microscope()` and additionally asserts This function wraps `find_microscope()` and additionally asserts
that there is a real stage, raising a `503` code if not. that there is a real stage, raising a `503` code if not.
""" """
args = find_microscope() microscope = find_microscope()
if not args["microscope"].has_real_stage(): if not microscope.has_real_stage():
abort(503, "No stage connected. Unable to autofocus.") abort(503, "No stage connected. Unable to autofocus.")
return args return microscope
def extension_action(args=None, extra_args_functions=None): def extension_action(args=None):
"""A decorator to auto-create an Action endpoint for a method """A decorator to auto-create an Action endpoint for a method
Use this decorator on any method of an extension (`BaseExtension` subclass) Use this decorator on any method of an extension (`BaseExtension` subclass)
@ -187,13 +189,8 @@ def extension_action(args=None, extra_args_functions=None):
or dictionary, determining the datatype of the arguments, which will be or dictionary, determining the datatype of the arguments, which will be
taken from the JSON payload of the POST request initiating the action). taken from the JSON payload of the POST request initiating the action).
It is also possible to specify a list of functions, which will be executed
in order, and their results will be added to the arguments. This allows,
for example, hardware components to be injected into the arguments. This
feature is experimental and should not be relied upon.
The parsed arguments dictionary is expanded as the function arguments, The parsed arguments dictionary is expanded as the function arguments,
i.e. we call `decorated_method(self, **kwargs). This means that any i.e. we call `decorated_method(self, **kwargs)`. This means that any
unused arguments will cause an error, which is probably good practice... unused arguments will cause an error, which is probably good practice...
NB this decorator does **not** replace the function with a `View` or NB this decorator does **not** replace the function with a `View` or
@ -213,13 +210,9 @@ def extension_action(args=None, extra_args_functions=None):
__doc__ = class_docstring __doc__ = class_docstring
args = supplied_args args = supplied_args
def post(self, args): def post(self, arguments):
# Inject arguments, if functions are supplied (TODO: I don't love this...)
if extra_args_functions:
for f in extra_args_functions:
args.update(f())
# Run the action # Run the action
return func(**args) return func(**arguments)
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
# Explicitly wrap the `get` method to allow us to add a docstring # Explicitly wrap the `get` method to allow us to add a docstring
@ -250,6 +243,18 @@ def extension_action(args=None, extra_args_functions=None):
ActionViewWrapper.post.__doc__ = ActionViewWrapper.post.description ActionViewWrapper.post.__doc__ = ActionViewWrapper.post.description
ActionViewWrapper.__name__ = func.__name__ ActionViewWrapper.__name__ = func.__name__
ActionViewWrapper.get.summary = f"List running and completed `{func.__name__}` actions." ActionViewWrapper.get.summary = f"List running and completed `{func.__name__}` actions."
ActionViewWrapper.get.description = (
ActionViewWrapper.get.summary +
"\n\n" +
inspect.cleandoc(
f"""
This `GET` request will return a list of `Action` objects corresponding
to the `{func.__name__}` action. It will include all the times it has
been run since the server was last restarted, including running, completed,
and failed attempts.
"""
)
)
func.flask_view = ActionViewWrapper func.flask_view = ActionViewWrapper
return func return func
@ -286,7 +291,7 @@ class AutofocusExtension(BaseExtension):
self.add_view(obj.flask_view, f"/{name}", name) self.add_view(obj.flask_view, f"/{name}", name)
def measure_sharpness( def measure_sharpness(
self, microscope: Microscope, metric_fn: Callable = sharpness_sum_lap2 self, microscope: Optional[Microscope] = None, metric_fn: Callable = sharpness_sum_lap2
) -> float: ) -> float:
"""Measure the sharpness from the MJPEG stream """Measure the sharpness from the MJPEG stream
@ -294,6 +299,8 @@ class AutofocusExtension(BaseExtension):
and return its size. This is the sharpness metric used by the fast autofocus and return its size. This is the sharpness metric used by the fast autofocus
method. method.
""" """
if not microscope:
microscope = find_microscope()
if hasattr(microscope.camera, "array") and callable( if hasattr(microscope.camera, "array") and callable(
getattr(microscope.camera, "array") getattr(microscope.camera, "array")
): ):
@ -302,12 +309,17 @@ class AutofocusExtension(BaseExtension):
raise RuntimeError(f"Object {microscope.camera} has no method `array`") raise RuntimeError(f"Object {microscope.camera} has no method `array`")
@extension_action( @extension_action(
args = {"dz": fields.List(fields.Int())}, args = {
extra_args_functions = [find_microscope_with_real_stage], "dz": fields.List(
fields.Int(),
description="An ascending list of relative z positions",
example = [int(x) for x in np.linspace(-300, 300, 7)],
)
},
) )
def autofocus( def autofocus(
self, self,
microscope: Microscope, microscope: Optional[Microscope] = None,
dz: Optional[List[int]] = None, dz: Optional[List[int]] = None,
settle: float = 0.5, settle: float = 0.5,
metric_fn: Callable = sharpness_sum_lap2, metric_fn: Callable = sharpness_sum_lap2,
@ -318,8 +330,11 @@ class AutofocusExtension(BaseExtension):
and at each position an image is captured and the sharpness function and at each position an image is captured and the sharpness function
evaulated. We then move back to the position where the sharpness was evaulated. We then move back to the position where the sharpness was
highest. No interpolation is performed. highest. No interpolation is performed.
dz is assumed to be in ascending order (starting at -ve values) dz is assumed to be in ascending order (starting at -ve values)
""" """
if not microscope:
microscope = find_microscope_with_real_stage()
camera: BaseCamera = microscope.camera camera: BaseCamera = microscope.camera
stage: BaseStage = microscope.stage stage: BaseStage = microscope.stage
if not dz: if not dz:
@ -345,18 +360,19 @@ class AutofocusExtension(BaseExtension):
return positions, sharpnesses return positions, sharpnesses
def move_and_find_focus(self, microscope: Microscope, dz: int) -> int: def move_and_find_focus(self, microscope: Optional[Microscope] = None, dz: int = 0) -> int:
"""Make a relative Z move and return the peak sharpness position""" """Make a relative Z move and return the peak sharpness position"""
if not microscope:
microscope = find_microscope_with_real_stage()
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)
@extension_action( @extension_action(
args={"dz": fields.Int(required=True)}, args={"dz": fields.Int(required=True, description="The relative Z move to make")},
extra_args_functions=[find_microscope_with_real_stage],
) )
def move_and_measure( def move_and_measure(
self, microscope: Microscope, dz: int self, microscope: Optional[Microscope] = None, dz: int = 0
) -> Dict[str, np.ndarray]: ) -> Dict[str, np.ndarray]:
"""Make a relative move in Z and measure dynamic sharpness """Make a relative move in Z and measure dynamic sharpness
@ -365,16 +381,23 @@ class AutofocusExtension(BaseExtension):
each JPEG frame in the preview stream. It returns a dictionary with each JPEG frame in the preview stream. It returns a dictionary with
stage position vs time and image size (i.e. sharpness) vs time. stage position vs time and image size (i.e. sharpness) vs time.
""" """
if not microscope:
microscope = find_microscope_with_real_stage()
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()
@extension_action( @extension_action(
args={"dz": fields.Int(missing=2000, example=2000)}, args={
extra_args_functions=[find_microscope_with_real_stage], "dz": fields.Int(
missing=2000,
example=2000,
description="Total Z range to search over (in stage steps)"
)
},
) )
def fast_autofocus( def fast_autofocus(
self, microscope: Microscope, dz: int = 2000 self, microscope: Optional[Microscope] = None, dz: int = 2000
) -> Dict[str, np.ndarray]: ) -> Dict[str, np.ndarray]:
"""Perform a fast down-up-down-up autofocus """Perform a fast down-up-down-up autofocus
@ -417,16 +440,16 @@ class AutofocusExtension(BaseExtension):
images! That's what lets us analyse 30 images/second even on the very images! That's what lets us analyse 30 images/second even on the very
limited processing power available to the Raspberry Pi 3. limited processing power available to the Raspberry Pi 3.
> *Warning* ## Warning: frame independence
> We assume that JPEG frames are independent. This is only true if the We assume that JPEG frames are independent. This is only true if the
> MJPEG stream is encoded at *constant quality* without any additional MJPEG stream is encoded at *constant quality* without any additional
> bit rate control. By default, many streams will reduce the quality bit rate control. By default, many streams will reduce the quality
> factor if they exceed a target bit rate, which badly affects this factor if they exceed a target bit rate, which badly affects this
> method. We turn off bit rate limiting for the Raspberry Pi camera, method. We turn off bit rate limiting for the Raspberry Pi camera,
> which fixes the problem, at the expense of sometimes failing if which fixes the problem, at the expense of sometimes failing if
> particularly sharp images appear in the stream, as there is a fairly particularly sharp images appear in the stream, as there is a fairly
> small maximum size for each JPEG frame beyond which empty images are small maximum size for each JPEG frame beyond which empty images are
> returned. returned.
## Estimation of sharpness vs z ## Estimation of sharpness vs z
What we record during an autofocus is two time series, from two parallel What we record during an autofocus is two time series, from two parallel
@ -439,6 +462,8 @@ class AutofocusExtension(BaseExtension):
JPEG size as a function of `z` and hence determine the `z` value at JPEG size as a function of `z` and hence determine the `z` value at
which sharpness is maximised. which sharpness is maximised.
""" """
if not microscope:
microscope = find_microscope_with_real_stage()
with microscope.lock(timeout=1), microscope.camera.lock, microscope.stage.lock: with microscope.lock(timeout=1), microscope.camera.lock, microscope.stage.lock:
with monitor_sharpness(microscope) as m: with monitor_sharpness(microscope) as m:
# Move to (-dz / 2) # Move to (-dz / 2)
@ -460,14 +485,30 @@ class AutofocusExtension(BaseExtension):
@extension_action( @extension_action(
args = { args = {
"dz": fields.Int(missing=2000, example=2000), "dz": fields.Int(
"backlash": fields.Int(missing=25, minimum=0), missing = 2000,
example = 2000,
description = "Total Z range to search over (in stage steps)"
),
"target_z": fields.Int(
missing = 0,
example = -100,
description = "Target finishing position, relative to the focus."
),
"initial_move_up": fields.Bool(
missing = True,
description = "Set to Flase to disable the initial move upwards"
),
"backlash": fields.Int(
missing=25,
minimum=0,
description="Distance to undershoot, before correction move."
),
}, },
extra_args_functions=[find_microscope_with_real_stage],
) )
def fast_up_down_up_autofocus( def fast_up_down_up_autofocus(
self, self,
microscope: Microscope, microscope: Optional[Microscope] = None,
dz: int = 2000, dz: int = 2000,
target_z: int = 0, target_z: int = 0,
initial_move_up: bool = True, initial_move_up: bool = True,
@ -501,25 +542,27 @@ class AutofocusExtension(BaseExtension):
moved. moved.
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.
backlash: (optional, default 25) is a small extra move made in step * **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.
mini_backlash: (optional, default 25) is an alias for `backlash` and will be * **mini_backlash**: (optional, default 25) is an alias for `backlash` and will be
removed in due course. removed in due course.
""" """
if not microscope:
microscope = find_microscope_with_real_stage()
if not mini_backlash: # I renamed the argument, if not mini_backlash: # I renamed the argument,
mini_backlash = backlash or 25 mini_backlash = backlash or 25
with microscope.lock(timeout=1), microscope.camera.lock, microscope.stage.lock: with microscope.lock(timeout=1), microscope.camera.lock, microscope.stage.lock:
@ -575,6 +618,4 @@ class AutofocusExtension(BaseExtension):
class MeasureSharpnessAPI(View): class MeasureSharpnessAPI(View):
__doc__ = AutofocusExtension.measure_sharpness.__doc__ __doc__ = AutofocusExtension.measure_sharpness.__doc__
def post(self): def post(self):
microscope = find_microscope()["microscope"] return {"sharpness": self.extension.measure_sharpness()}
return {"sharpness": self.extension.measure_sharpness(microscope)}