Migrated scan plugin
This commit is contained in:
parent
480848b009
commit
dee77c295a
13 changed files with 442 additions and 290 deletions
|
|
@ -1,8 +1,7 @@
|
||||||
__all__ = ["Microscope", "config", "task", "utilities"]
|
__all__ = ["Microscope", "config", "utilities"]
|
||||||
__version__ = "0.1.0"
|
__version__ = "0.1.0"
|
||||||
|
|
||||||
from .microscope import Microscope
|
from .microscope import Microscope
|
||||||
from . import config
|
from . import config
|
||||||
from . import utilities
|
from . import utilities
|
||||||
from . import task
|
|
||||||
from . import common
|
from . import common
|
||||||
|
|
|
||||||
|
|
@ -317,9 +317,6 @@ class PiCameraStreamer(BaseCamera):
|
||||||
"""
|
"""
|
||||||
Change the camera zoom, handling re-centering and scaling.
|
Change the camera zoom, handling re-centering and scaling.
|
||||||
"""
|
"""
|
||||||
logging.warning(
|
|
||||||
"set_zoom is deprecated. Please use the 'zoom' property in picamera_settings."
|
|
||||||
)
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.status["zoom_value"] = float(zoom_value)
|
self.status["zoom_value"] = float(zoom_value)
|
||||||
if self.status["zoom_value"] < 1:
|
if self.status["zoom_value"] < 1:
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
|
import logging
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
from . import EXTENSION_NAME
|
from . import EXTENSION_NAME
|
||||||
|
|
||||||
|
|
||||||
def _current_labthing():
|
def _current_labthing():
|
||||||
app = current_app
|
app = current_app._get_current_object()
|
||||||
if not app:
|
if not app:
|
||||||
return None
|
return None
|
||||||
|
logging.debug("Active app extensions:")
|
||||||
|
logging.debug(app.extensions)
|
||||||
|
logging.debug("Active labthing:")
|
||||||
|
logging.debug(app.extensions[EXTENSION_NAME])
|
||||||
return app.extensions[EXTENSION_NAME]
|
return app.extensions[EXTENSION_NAME]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -30,3 +35,15 @@ def find_device(device_name, labthing_instance=None):
|
||||||
return labthing_instance.devices[device_name]
|
return labthing_instance.devices[device_name]
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def find_plugin(plugin_name, labthing_instance=None):
|
||||||
|
if not labthing_instance:
|
||||||
|
labthing_instance = _current_labthing()
|
||||||
|
|
||||||
|
logging.debug("Current labthing:")
|
||||||
|
logging.debug(_current_labthing())
|
||||||
|
|
||||||
|
if plugin_name in labthing_instance.plugins:
|
||||||
|
return labthing_instance.plugins[plugin_name]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,14 @@ class LabThing(object):
|
||||||
|
|
||||||
# TODO: Add plugin routes
|
# TODO: Add plugin routes
|
||||||
|
|
||||||
|
for plugin_view_id, plugin_view in plugin_object.views.items():
|
||||||
|
# Add route to the plugins blueprint
|
||||||
|
self.add_resource(
|
||||||
|
plugin_view["view"],
|
||||||
|
"/plugins" + plugin_view["rule"],
|
||||||
|
**plugin_view["kwargs"],
|
||||||
|
)
|
||||||
|
|
||||||
### Resource stuff
|
### Resource stuff
|
||||||
|
|
||||||
def _complete_url(self, url_part, registration_prefix):
|
def _complete_url(self, url_part, registration_prefix):
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ class BasePlugin:
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
|
self.methods = {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def views(self):
|
def views(self):
|
||||||
return self._views
|
return self._views
|
||||||
|
|
@ -102,6 +104,13 @@ class BasePlugin:
|
||||||
def _name_uri_safe(self):
|
def _name_uri_safe(self):
|
||||||
return snake_to_spine(self._name_python_safe)
|
return snake_to_spine(self._name_python_safe)
|
||||||
|
|
||||||
|
def add_method(self, method_name, method):
|
||||||
|
self.methods[method_name] = method
|
||||||
|
|
||||||
|
if not hasattr(self, method_name):
|
||||||
|
setattr(self, method_name, method)
|
||||||
|
else:
|
||||||
|
logging.warning("Unable to bind method to plugin. Method name already exists.")
|
||||||
|
|
||||||
def find_plugins(plugin_path, module_name="plugins"):
|
def find_plugins(plugin_path, module_name="plugins"):
|
||||||
print(f"Loading plugins from {plugin_path}")
|
print(f"Loading plugins from {plugin_path}")
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from functools import wraps
|
||||||
|
|
||||||
from .thread import TaskThread
|
from .thread import TaskThread
|
||||||
|
|
||||||
|
from flask import copy_current_request_context
|
||||||
|
|
||||||
class TaskMaster:
|
class TaskMaster:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
@ -26,7 +27,8 @@ class TaskMaster:
|
||||||
return {t.id: t.state for t in self._tasks}
|
return {t.id: t.state for t in self._tasks}
|
||||||
|
|
||||||
def new(self, f, *args, **kwargs):
|
def new(self, f, *args, **kwargs):
|
||||||
task = TaskThread(target=f, args=args, kwargs=kwargs)
|
# copy_current_request_context allows threads to access flask current_app
|
||||||
|
task = TaskThread(target=copy_current_request_context(f), args=args, kwargs=kwargs)
|
||||||
self._tasks.append(task)
|
self._tasks.append(task)
|
||||||
return task
|
return task
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ from openflexure_microscope.camera.mock import MockStreamer
|
||||||
|
|
||||||
from openflexure_microscope.utilities import serialise_array_b64
|
from openflexure_microscope.utilities import serialise_array_b64
|
||||||
from openflexure_microscope.plugins import PluginLoader
|
from openflexure_microscope.plugins import PluginLoader
|
||||||
from openflexure_microscope.task import TaskOrchestrator
|
|
||||||
from openflexure_microscope.common.lock import CompositeLock
|
from openflexure_microscope.common.lock import CompositeLock
|
||||||
from openflexure_microscope.config import user_settings
|
from openflexure_microscope.config import user_settings
|
||||||
|
|
||||||
|
|
@ -46,9 +45,6 @@ class Microscope:
|
||||||
# Initialise with an empty composite lock
|
# Initialise with an empty composite lock
|
||||||
self.lock = CompositeLock([])
|
self.lock = CompositeLock([])
|
||||||
|
|
||||||
# Create a task orchestrator
|
|
||||||
self.task = TaskOrchestrator()
|
|
||||||
|
|
||||||
# Apply settings loaded from file
|
# Apply settings loaded from file
|
||||||
self.apply_settings(user_settings.load())
|
self.apply_settings(user_settings.load())
|
||||||
|
|
||||||
|
|
@ -151,26 +147,6 @@ class Microscope:
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Create unified state
|
|
||||||
@property
|
|
||||||
def state(self):
|
|
||||||
"""Dictionary of the basic microscope state.
|
|
||||||
|
|
||||||
Return:
|
|
||||||
dict: Dictionary containing position data,
|
|
||||||
and :py:attr:`openflexure_microscope.camera.base.BaseCamera.status`
|
|
||||||
"""
|
|
||||||
# DEPRECATED
|
|
||||||
logging.warning(
|
|
||||||
"Microscope.state is deprecated. Use Microscope.status instead. State will be removed in a future version."
|
|
||||||
)
|
|
||||||
state = {
|
|
||||||
"camera": self.camera.status,
|
|
||||||
"stage": self.stage.status,
|
|
||||||
"plugin": self.plugins.state,
|
|
||||||
"version": pkg_resources.get_distribution("openflexure_microscope").version,
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
|
|
||||||
# Create unified status
|
# Create unified status
|
||||||
@property
|
@property
|
||||||
|
|
@ -256,22 +232,6 @@ class Microscope:
|
||||||
self.stage.save_settings()
|
self.stage.save_settings()
|
||||||
user_settings.save(current_config, backup=True)
|
user_settings.save(current_config, backup=True)
|
||||||
|
|
||||||
@property
|
|
||||||
def config(self) -> dict:
|
|
||||||
logging.warning(
|
|
||||||
"Reading microscope through config property is deprecated.\
|
|
||||||
Please use read_settings method instead."
|
|
||||||
)
|
|
||||||
return self.read_settings()
|
|
||||||
|
|
||||||
@config.setter
|
|
||||||
def config(self, config: dict) -> None:
|
|
||||||
logging.warning(
|
|
||||||
"Setting microscope through config property is deprecated.\
|
|
||||||
Please use apply_settings method instead."
|
|
||||||
)
|
|
||||||
self.apply_settings(config)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def metadata(self):
|
def metadata(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
__all__ = ["Plugin", "HelloWorldAPI", "IdentifyAPI"]
|
|
||||||
|
|
||||||
from .plugin import Plugin
|
|
||||||
from .api import HelloWorldAPI, IdentifyAPI
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
from openflexure_microscope.api.utilities import JsonResponse
|
|
||||||
from openflexure_microscope.api.views import MicroscopeViewPlugin
|
|
||||||
from openflexure_microscope.exceptions import TaskDeniedException
|
|
||||||
|
|
||||||
from flask import request, Response, escape, jsonify, abort
|
|
||||||
|
|
||||||
|
|
||||||
class IdentifyAPI(MicroscopeViewPlugin):
|
|
||||||
"""
|
|
||||||
A simple example API plugin, attached through the main microscope plugin.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
"""
|
|
||||||
Method to call when an HTTP GET request is made.
|
|
||||||
"""
|
|
||||||
data = (
|
|
||||||
self.plugin.identify()
|
|
||||||
) # Call a method from our plugin, using the MicroscopeViewPlugin.plugin shortcut
|
|
||||||
return Response(escape(data))
|
|
||||||
|
|
||||||
|
|
||||||
class HelloWorldAPI(MicroscopeViewPlugin):
|
|
||||||
"""
|
|
||||||
A method to create, set, and return a new microscope parameter.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
"""
|
|
||||||
Method to call when an HTTP GET request is made.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# If the microscope does not already contain our plugin_string attribute
|
|
||||||
if not hasattr(self.microscope, "plugin_string"):
|
|
||||||
# Make a string, using the MicroscopeViewPlugin.plugin shortcut
|
|
||||||
self.microscope.plugin_string = self.plugin.hello_world()
|
|
||||||
|
|
||||||
return Response(self.microscope.plugin_string)
|
|
||||||
|
|
||||||
def post(self):
|
|
||||||
"""
|
|
||||||
Method to call when an HTTP POST request is made.
|
|
||||||
Assumes request will include a JSON payload.
|
|
||||||
"""
|
|
||||||
# Get payload JSON
|
|
||||||
payload = JsonResponse(request)
|
|
||||||
|
|
||||||
# Extract a value from the JSON key 'plugin_string', and convert to a string. If no value is given, default to empty.
|
|
||||||
new_plugin_string = payload.param("plugin_string", default="", convert=str)
|
|
||||||
|
|
||||||
if new_plugin_string: # If not None or empty
|
|
||||||
# Set microscope attribute to the specified string
|
|
||||||
self.microscope.plugin_string = new_plugin_string
|
|
||||||
|
|
||||||
return Response(self.microscope.plugin_string)
|
|
||||||
|
|
||||||
|
|
||||||
class LongRunningAPI(MicroscopeViewPlugin):
|
|
||||||
"""
|
|
||||||
An example API plugin that uses a long-running plugin method.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def post(self):
|
|
||||||
"""
|
|
||||||
Method to call when an HTTP POST request is made.
|
|
||||||
"""
|
|
||||||
# Get payload JSON
|
|
||||||
payload = JsonResponse(request)
|
|
||||||
|
|
||||||
# Extract a values from the JSON payload.
|
|
||||||
time_to_run = payload.param("time", default=10, convert=int)
|
|
||||||
|
|
||||||
# Attach the long-running method as a microscope task
|
|
||||||
try:
|
|
||||||
task = self.microscope.task.start(self.plugin.long_running, time_to_run)
|
|
||||||
return jsonify(task.state), 201
|
|
||||||
|
|
||||||
except TaskDeniedException:
|
|
||||||
return abort(409)
|
|
||||||
|
|
||||||
|
|
||||||
class SomeExceptionAPI(MicroscopeViewPlugin):
|
|
||||||
"""
|
|
||||||
An example API plugin that uses a long-running but broken plugin method.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def post(self):
|
|
||||||
"""
|
|
||||||
Method to call when an HTTP POST request is made.
|
|
||||||
"""
|
|
||||||
# Get payload JSON
|
|
||||||
payload = JsonResponse(request)
|
|
||||||
|
|
||||||
# Attach the long-running method as a microscope task
|
|
||||||
try:
|
|
||||||
task = self.microscope.task.start(self.plugin.some_exception)
|
|
||||||
return jsonify(task.state), 201
|
|
||||||
|
|
||||||
except TaskDeniedException:
|
|
||||||
return abort(409)
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
import random
|
|
||||||
import time
|
|
||||||
from openflexure_microscope.plugins import MicroscopePlugin
|
|
||||||
|
|
||||||
from .api import IdentifyAPI, HelloWorldAPI, LongRunningAPI, SomeExceptionAPI
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin(MicroscopePlugin):
|
|
||||||
"""
|
|
||||||
A set of default plugins
|
|
||||||
"""
|
|
||||||
|
|
||||||
api_views = {
|
|
||||||
"/identify": IdentifyAPI,
|
|
||||||
"/hello": HelloWorldAPI,
|
|
||||||
"/long_running": LongRunningAPI,
|
|
||||||
"/some_exception": SomeExceptionAPI,
|
|
||||||
}
|
|
||||||
|
|
||||||
def identify(self):
|
|
||||||
"""
|
|
||||||
Demonstrate access to Microscope.camera, and Microscope.stage
|
|
||||||
"""
|
|
||||||
|
|
||||||
response = "My parent camera is {}, and my parent stage is {}.".format(
|
|
||||||
self.microscope.camera, self.microscope.stage
|
|
||||||
)
|
|
||||||
print(response)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def hello_world(self):
|
|
||||||
"""
|
|
||||||
Demonstrate passive method
|
|
||||||
"""
|
|
||||||
|
|
||||||
return "Hello world!"
|
|
||||||
|
|
||||||
def some_exception(self):
|
|
||||||
"""
|
|
||||||
Demonstrate some broken plugin method that would be long running
|
|
||||||
"""
|
|
||||||
with self.microscope.lock:
|
|
||||||
time.sleep(3)
|
|
||||||
result = 15.0 / 0
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def long_running(self, t_run):
|
|
||||||
"""
|
|
||||||
Demonstrate a long-running method that requires microscope hardware
|
|
||||||
"""
|
|
||||||
print("Starting a long-running task...")
|
|
||||||
n_array = []
|
|
||||||
|
|
||||||
print("Acquiring composite microscope lock...")
|
|
||||||
with self.microscope.camera.lock, self.microscope.stage.lock:
|
|
||||||
for _ in range(t_run):
|
|
||||||
n_array.append(random.random())
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
print("Long-running task finished! Releasing locks.")
|
|
||||||
|
|
||||||
return n_array
|
|
||||||
|
|
@ -355,6 +355,10 @@ class FastAutofocusAPI(MethodView):
|
||||||
|
|
||||||
|
|
||||||
autofocus_plugin_v2 = BasePlugin("autofocus")
|
autofocus_plugin_v2 = BasePlugin("autofocus")
|
||||||
|
|
||||||
|
autofocus_plugin_v2.add_method("fast_autofocus", fast_autofocus)
|
||||||
|
autofocus_plugin_v2.add_method("autofocus", autofocus)
|
||||||
|
|
||||||
autofocus_plugin_v2.add_view("/measure_sharpness", MeasureSharpnessAPI)
|
autofocus_plugin_v2.add_view("/measure_sharpness", MeasureSharpnessAPI)
|
||||||
autofocus_plugin_v2.add_view("/autofocus", AutofocusAPI)
|
autofocus_plugin_v2.add_view("/autofocus", AutofocusAPI)
|
||||||
autofocus_plugin_v2.add_view("/fast_autofocus", FastAutofocusAPI)
|
autofocus_plugin_v2.add_view("/fast_autofocus", FastAutofocusAPI)
|
||||||
|
|
|
||||||
399
openflexure_microscope/plugins/v2/scan.py
Normal file
399
openflexure_microscope/plugins/v2/scan.py
Normal file
|
|
@ -0,0 +1,399 @@
|
||||||
|
import numpy as np
|
||||||
|
import itertools
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from typing import Tuple
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
|
from openflexure_microscope.camera.base import generate_basename
|
||||||
|
from openflexure_microscope.common.labthings.find import find_device, find_plugin
|
||||||
|
from openflexure_microscope.common.labthings.plugins import BasePlugin
|
||||||
|
|
||||||
|
from openflexure_microscope.devel import (
|
||||||
|
JsonResponse,
|
||||||
|
request,
|
||||||
|
jsonify,
|
||||||
|
taskify,
|
||||||
|
abort,
|
||||||
|
update_task_progress,
|
||||||
|
update_task_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
from flask.views import MethodView
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
### Grid construction
|
||||||
|
|
||||||
|
def construct_grid(initial, step_sizes, n_steps, style="raster"):
|
||||||
|
"""
|
||||||
|
Given an initial position, step sizes, and number of steps,
|
||||||
|
construct a 2-dimensional list of scan x-y positions.
|
||||||
|
"""
|
||||||
|
arr = []
|
||||||
|
|
||||||
|
for i in range(n_steps[0]): # x axis
|
||||||
|
arr.append([])
|
||||||
|
for j in range(n_steps[1]): # y axis
|
||||||
|
# Create a coordinate array
|
||||||
|
coord = [initial[ax] + [i, j][ax] * step_sizes[ax] for ax in range(2)]
|
||||||
|
# Append coordinate array to position grid
|
||||||
|
arr[i].append(tuple(coord))
|
||||||
|
|
||||||
|
# Style modifiers
|
||||||
|
if style == "snake":
|
||||||
|
for i, line in enumerate(arr):
|
||||||
|
if i % 2 != 0:
|
||||||
|
line.reverse()
|
||||||
|
|
||||||
|
return arr
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_grid(grid):
|
||||||
|
"""
|
||||||
|
Convert a 3D list of scan positions into a flat list
|
||||||
|
of sequential positions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
grid = list(itertools.chain(*grid))
|
||||||
|
return grid
|
||||||
|
|
||||||
|
|
||||||
|
### Progress
|
||||||
|
|
||||||
|
_images_to_be_captured: int = 1
|
||||||
|
_images_captured_so_far: int = 0
|
||||||
|
|
||||||
|
def progress():
|
||||||
|
progress = (_images_captured_so_far / _images_to_be_captured) * 100
|
||||||
|
logging.info(progress)
|
||||||
|
return progress
|
||||||
|
|
||||||
|
|
||||||
|
### Capturing
|
||||||
|
|
||||||
|
def capture(
|
||||||
|
microscope,
|
||||||
|
basename,
|
||||||
|
scan_id,
|
||||||
|
temporary: bool = False,
|
||||||
|
use_video_port: bool = False,
|
||||||
|
resize: Tuple[int, int] = None,
|
||||||
|
bayer: bool = False,
|
||||||
|
metadata: dict = {},
|
||||||
|
tags: list = [],
|
||||||
|
):
|
||||||
|
|
||||||
|
# Construct a tile filename
|
||||||
|
filename = "{}_{}_{}_{}".format(basename, *microscope.stage.position)
|
||||||
|
folder = "SCAN_{}".format(basename)
|
||||||
|
|
||||||
|
# Create output object
|
||||||
|
output = microscope.camera.new_image(
|
||||||
|
temporary=temporary, filename=filename, folder=folder
|
||||||
|
)
|
||||||
|
|
||||||
|
# Capture
|
||||||
|
microscope.camera.capture(
|
||||||
|
output.file, use_video_port=use_video_port, resize=resize, bayer=bayer
|
||||||
|
)
|
||||||
|
|
||||||
|
# Affix metadata
|
||||||
|
if "scan" not in tags:
|
||||||
|
tags.append("scan")
|
||||||
|
|
||||||
|
# Inject system metadata
|
||||||
|
output.put_metadata(microscope.metadata, system=True)
|
||||||
|
|
||||||
|
# Insert custom metadata
|
||||||
|
output.put_metadata(metadata)
|
||||||
|
|
||||||
|
# Insert custom tags
|
||||||
|
output.put_tags(tags)
|
||||||
|
|
||||||
|
|
||||||
|
### Scanning
|
||||||
|
|
||||||
|
def tile(
|
||||||
|
microscope,
|
||||||
|
basename: str = None,
|
||||||
|
temporary: bool = False,
|
||||||
|
step_size: int = [2000, 1500, 100],
|
||||||
|
grid: list = [3, 3, 5],
|
||||||
|
style="raster",
|
||||||
|
autofocus_dz: int = 50,
|
||||||
|
use_video_port: bool = False,
|
||||||
|
resize: Tuple[int, int] = None,
|
||||||
|
bayer: bool = False,
|
||||||
|
fast_autofocus=False,
|
||||||
|
metadata: dict = {},
|
||||||
|
tags: list = [],
|
||||||
|
):
|
||||||
|
global _images_to_be_captured
|
||||||
|
global _images_captured_so_far
|
||||||
|
|
||||||
|
# Keep task progress
|
||||||
|
# TODO: Make this line not nasty
|
||||||
|
_images_to_be_captured = reduce((lambda x, y: x * y), grid)
|
||||||
|
_images_captured_so_far = 0
|
||||||
|
|
||||||
|
# Generate a basename if none given
|
||||||
|
if not basename:
|
||||||
|
basename = generate_basename()
|
||||||
|
|
||||||
|
# Generate a stack ID
|
||||||
|
scan_id = uuid.uuid4().hex
|
||||||
|
|
||||||
|
# Store initial position
|
||||||
|
initial_position = microscope.stage.position
|
||||||
|
|
||||||
|
# Add scan metadata
|
||||||
|
if "time" not in metadata:
|
||||||
|
metadata["time"] = generate_basename()
|
||||||
|
|
||||||
|
metadata.update(
|
||||||
|
{
|
||||||
|
"scan_id": scan_id,
|
||||||
|
"basename": basename,
|
||||||
|
"scan_parameters": {
|
||||||
|
"step_size": step_size,
|
||||||
|
"grid": grid,
|
||||||
|
"style": style,
|
||||||
|
"autofocus_dz": autofocus_dz,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if autofocus is enabled
|
||||||
|
autofocus_plugin = find_plugin("autofocus")
|
||||||
|
if (
|
||||||
|
autofocus_dz
|
||||||
|
and autofocus_plugin
|
||||||
|
and microscope.has_real_stage()
|
||||||
|
and microscope.has_real_camera()
|
||||||
|
):
|
||||||
|
autofocus_enabled = True
|
||||||
|
else:
|
||||||
|
autofocus_enabled = False
|
||||||
|
|
||||||
|
z_stack_dz = (
|
||||||
|
grid[2] * step_size[2] if grid[2] > 1 else 0
|
||||||
|
) # shorthand for Z stack range
|
||||||
|
|
||||||
|
# Construct an x-y grid (worry about z later)
|
||||||
|
x_y_grid = construct_grid(
|
||||||
|
initial_position, step_size[:2], grid[:2], style=style
|
||||||
|
)
|
||||||
|
|
||||||
|
# Keep the initial Z position the same as our current position
|
||||||
|
next_z = initial_position[2]
|
||||||
|
if fast_autofocus: # If fast autofocus is enabled, make
|
||||||
|
next_z += autofocus_dz / 2 # sure we start from the top of the range
|
||||||
|
initial_z = next_z # Save this value for use in raster scans
|
||||||
|
|
||||||
|
# Now step through each point in the x-y coordinate array
|
||||||
|
for line in x_y_grid:
|
||||||
|
# If rastering, rather than snake (or eventually spiral)
|
||||||
|
# Return focus to initial position
|
||||||
|
if style == "raster":
|
||||||
|
next_z = initial_z
|
||||||
|
logging.debug("Returning to initial z position")
|
||||||
|
microscope.stage.move_abs(
|
||||||
|
[line[0][0], line[0][1], next_z]
|
||||||
|
) # RWB: I think this line is redundant
|
||||||
|
|
||||||
|
for x_y in line:
|
||||||
|
# Move to new grid position without changing z
|
||||||
|
logging.debug("Moving to step {}".format([x_y[0], x_y[1], next_z]))
|
||||||
|
microscope.stage.move_abs([x_y[0], x_y[1], next_z])
|
||||||
|
# Refocus
|
||||||
|
if autofocus_enabled:
|
||||||
|
if fast_autofocus:
|
||||||
|
autofocus_plugin.fast_autofocus(
|
||||||
|
dz=autofocus_dz,
|
||||||
|
target_z=-z_stack_dz / 2.0, # Finish below the focus
|
||||||
|
initial_move_up=False, # We're already at the top of the scan
|
||||||
|
)
|
||||||
|
# TODO: save the focus data for future reference? Use it for diagnostics?
|
||||||
|
else:
|
||||||
|
logging.debug("Running autofocus")
|
||||||
|
autofocus_plugin.autofocus(
|
||||||
|
range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz)
|
||||||
|
)
|
||||||
|
logging.debug("Finished autofocus")
|
||||||
|
time.sleep(1) # TODO: Remove
|
||||||
|
|
||||||
|
# If we're not doing a z-stack, just capture
|
||||||
|
if grid[2] <= 1:
|
||||||
|
capture(
|
||||||
|
microscope,
|
||||||
|
basename,
|
||||||
|
scan_id,
|
||||||
|
temporary=temporary,
|
||||||
|
use_video_port=use_video_port,
|
||||||
|
resize=resize,
|
||||||
|
bayer=bayer,
|
||||||
|
metadata=metadata,
|
||||||
|
tags=tags,
|
||||||
|
)
|
||||||
|
# Update task progress
|
||||||
|
_images_captured_so_far += 1
|
||||||
|
update_task_progress(progress())
|
||||||
|
else:
|
||||||
|
logging.debug("Entering z-stack")
|
||||||
|
stack(
|
||||||
|
microscope=microscope,
|
||||||
|
basename=basename,
|
||||||
|
temporary=temporary,
|
||||||
|
scan_id=scan_id,
|
||||||
|
step_size=step_size[2],
|
||||||
|
steps=grid[2],
|
||||||
|
center=not fast_autofocus, # fast_autofocus does this for us!
|
||||||
|
return_to_start=not fast_autofocus,
|
||||||
|
use_video_port=use_video_port,
|
||||||
|
resize=resize,
|
||||||
|
bayer=bayer,
|
||||||
|
metadata=metadata,
|
||||||
|
tags=tags,
|
||||||
|
)
|
||||||
|
# Make sure we use our current best estimate of focus (i.e. the current position) next point
|
||||||
|
next_z = microscope.stage.position[2]
|
||||||
|
if fast_autofocus:
|
||||||
|
next_z += (
|
||||||
|
autofocus_dz / 2
|
||||||
|
) # Fast autofocus requires us to start at the top of the range
|
||||||
|
if grid[2] > 1:
|
||||||
|
next_z -= int(
|
||||||
|
grid[2] / 2.0 * step_size[2]
|
||||||
|
) # Z stacking means we're higher up to start with
|
||||||
|
|
||||||
|
logging.debug("Returning to {}".format(initial_position))
|
||||||
|
microscope.stage.move_abs(initial_position)
|
||||||
|
|
||||||
|
def stack(
|
||||||
|
microscope,
|
||||||
|
basename: str = None,
|
||||||
|
temporary: bool = False,
|
||||||
|
scan_id: str = None,
|
||||||
|
step_size: int = 100,
|
||||||
|
steps: int = 5,
|
||||||
|
center: bool = True,
|
||||||
|
return_to_start: bool = True,
|
||||||
|
use_video_port: bool = False,
|
||||||
|
resize: Tuple[int, int] = None,
|
||||||
|
bayer: bool = False,
|
||||||
|
metadata: dict = {},
|
||||||
|
tags: list = [],
|
||||||
|
):
|
||||||
|
global _images_captured_so_far
|
||||||
|
|
||||||
|
# Generate a basename if none given
|
||||||
|
if not basename:
|
||||||
|
basename = generate_basename()
|
||||||
|
|
||||||
|
# Generate a stack ID
|
||||||
|
if not scan_id:
|
||||||
|
scan_id = uuid.uuid4().hex
|
||||||
|
|
||||||
|
# Add scan metadata
|
||||||
|
if not "time" in metadata:
|
||||||
|
metadata["time"] = generate_basename()
|
||||||
|
|
||||||
|
# Store initial position
|
||||||
|
initial_position = microscope.stage.position
|
||||||
|
|
||||||
|
with microscope.lock:
|
||||||
|
# Move to center scan
|
||||||
|
if center:
|
||||||
|
logging.debug("Moving to starting position")
|
||||||
|
microscope.stage.move_rel([0, 0, int((-step_size * steps) / 2)])
|
||||||
|
|
||||||
|
for i in range(steps):
|
||||||
|
time.sleep(0.1)
|
||||||
|
logging.debug("Capturing...")
|
||||||
|
capture(
|
||||||
|
microscope,
|
||||||
|
basename,
|
||||||
|
scan_id,
|
||||||
|
temporary=temporary,
|
||||||
|
use_video_port=use_video_port,
|
||||||
|
resize=resize,
|
||||||
|
bayer=bayer,
|
||||||
|
metadata=metadata,
|
||||||
|
tags=tags,
|
||||||
|
)
|
||||||
|
# Update task progress
|
||||||
|
_images_captured_so_far += 1
|
||||||
|
update_task_progress(progress())
|
||||||
|
|
||||||
|
if i != steps - 1:
|
||||||
|
logging.debug("Moving z by {}".format(step_size))
|
||||||
|
microscope.stage.move_rel([0, 0, step_size])
|
||||||
|
if return_to_start:
|
||||||
|
logging.debug("Returning to {}".format(initial_position))
|
||||||
|
microscope.stage.move_abs(initial_position)
|
||||||
|
|
||||||
|
|
||||||
|
### Web views
|
||||||
|
|
||||||
|
class TileScanAPI(MethodView):
|
||||||
|
def post(self):
|
||||||
|
payload = JsonResponse(request)
|
||||||
|
microscope = find_device("openflexure_microscope")
|
||||||
|
|
||||||
|
if not microscope:
|
||||||
|
abort(503, "No microscope connected. Unable to autofocus.")
|
||||||
|
|
||||||
|
# Get params
|
||||||
|
filename = payload.param("filename")
|
||||||
|
temporary = payload.param("temporary", default=False, convert=bool)
|
||||||
|
|
||||||
|
step_size = payload.param("step_size", default=[2000, 1500, 100], convert=list)
|
||||||
|
step_size = [int(i) for i in step_size]
|
||||||
|
|
||||||
|
grid = payload.param("grid", default=[3, 3, 5], convert=list)
|
||||||
|
grid = [int(i) for i in grid]
|
||||||
|
|
||||||
|
style = payload.param("style", default="raster", convert=str)
|
||||||
|
autofocus_dz = payload.param("autofocus_dz", default=50, convert=int)
|
||||||
|
fast_autofocus = payload.param("fast_autofocus", default=False, convert=bool)
|
||||||
|
|
||||||
|
use_video_port = payload.param("use_video_port", default=True, convert=bool)
|
||||||
|
resize = payload.param("size", default=None)
|
||||||
|
if resize:
|
||||||
|
if ("width" in resize) and ("height" in resize):
|
||||||
|
resize = (
|
||||||
|
int(resize["width"]),
|
||||||
|
int(resize["height"]),
|
||||||
|
) # Convert dict to tuple
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
bayer = payload.param("bayer", default=False, convert=bool)
|
||||||
|
metadata = payload.param("metadata", default={}, convert=dict)
|
||||||
|
tags = payload.param("tags", default=[], convert=list)
|
||||||
|
|
||||||
|
logging.info("Running tile scan...")
|
||||||
|
task = taskify(tile)(
|
||||||
|
microscope,
|
||||||
|
basename=filename,
|
||||||
|
temporary=temporary,
|
||||||
|
step_size=step_size,
|
||||||
|
grid=grid,
|
||||||
|
style=style,
|
||||||
|
autofocus_dz=autofocus_dz,
|
||||||
|
use_video_port=use_video_port,
|
||||||
|
resize=resize,
|
||||||
|
bayer=bayer,
|
||||||
|
fast_autofocus=fast_autofocus,
|
||||||
|
metadata=metadata,
|
||||||
|
tags=tags,
|
||||||
|
)
|
||||||
|
|
||||||
|
# return a handle on the scan task
|
||||||
|
return jsonify(task.state), 201
|
||||||
|
|
||||||
|
|
||||||
|
scan_plugin_v2 = BasePlugin("scan")
|
||||||
|
|
||||||
|
scan_plugin_v2.add_view("/tile", TileScanAPI)
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
import logging
|
|
||||||
from openflexure_microscope.common.tasks import (
|
|
||||||
tasks,
|
|
||||||
states,
|
|
||||||
taskify,
|
|
||||||
cleanup_tasks,
|
|
||||||
remove_task,
|
|
||||||
)
|
|
||||||
|
|
||||||
# DEPRECATED
|
|
||||||
class TaskOrchestrator:
|
|
||||||
"""
|
|
||||||
DEPRECATED: Class responsible for spawning threaded tasks, and storing their returns.
|
|
||||||
A microscope should contain exactly one instance of `TaskOrchestrator`.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
tasks (list): List of `Task` objects
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def tasks(self):
|
|
||||||
logging.warning(
|
|
||||||
"TaskOrchestrator class is deprecated. Please use common.tasks.tasks().values() instead."
|
|
||||||
)
|
|
||||||
return tasks().values()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self):
|
|
||||||
"""
|
|
||||||
Returns a list of dictionary representations of all tasks in the session.
|
|
||||||
"""
|
|
||||||
logging.warning(
|
|
||||||
"TaskOrchestrator class is deprecated. Please use common.tasks.tasks() instead."
|
|
||||||
)
|
|
||||||
return states()
|
|
||||||
|
|
||||||
def task_from_id(self, task_id: str):
|
|
||||||
"""
|
|
||||||
Returns a particular task object with the specified `task_id`.
|
|
||||||
"""
|
|
||||||
return tasks()[task_id]
|
|
||||||
|
|
||||||
def start(self, function, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Attach and start a new long-running task, if allowed by `start_condition()`.
|
|
||||||
Returns an instance of :py:class:`openflexure_microscope.task.Task`, which can be used to check the state
|
|
||||||
or eventual return of the task.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
function (function): The target function to run in the background
|
|
||||||
args, kwargs: Arguments that will be passed to `function` at runtime.
|
|
||||||
"""
|
|
||||||
logging.warning(
|
|
||||||
"TaskOrchestrator class is deprecated. Please use common.tasks.taskify() instead."
|
|
||||||
)
|
|
||||||
return taskify(function)(*args, **kwargs)
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
"""
|
|
||||||
Remove all inactive tasks from the task array.
|
|
||||||
This will remove tasks that either finished or never started.
|
|
||||||
"""
|
|
||||||
logging.warning(
|
|
||||||
"TaskOrchestrator class is deprecated. Please use common.tasks.cleanup_tasks() instead."
|
|
||||||
)
|
|
||||||
cleanup_tasks()
|
|
||||||
|
|
||||||
def delete(self, task_id: str):
|
|
||||||
"""
|
|
||||||
Delete a given task by ID, only if task is not currently running.
|
|
||||||
"""
|
|
||||||
logging.warning(
|
|
||||||
"TaskOrchestrator class is deprecated. Please use common.tasks.remove_task() instead."
|
|
||||||
)
|
|
||||||
remove_task(task_id)
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue