Merge branch '2.9-dev' into 'master'

v2.9.0

See merge request openflexure/openflexure-microscope-server!101
This commit is contained in:
Joel Collins 2020-12-07 14:01:02 +00:00
commit 9cf0a9badf
45 changed files with 1558 additions and 1113 deletions

View file

@ -10,9 +10,15 @@ This includes installing the server in a mode better suited for active developme
# Developer guidelines # Developer guidelines
The Raspberry Pi image we use currently ships with Python 3.7.3. For local development, please use PyEnv or similar to make sure you're running on this version. For example, Windows users can use [Scoop](https://scoop.sh/) to install specific Python versions.
## Installation ## Installation
* `git clone https://gitlab.com/openflexure/openflexure-microscope-server.git` * `git clone https://gitlab.com/openflexure/openflexure-microscope-server.git`
* `cd openflexure-microscope-server`
* (Optional) Set local Python version
* `pyenv init`
* `pyenv install 3.7.3`
* `pyenv local 3.7.3`
* `poetry install` * `poetry install`
* Building the static interface will require a valid Node.js installation * Building the static interface will require a valid Node.js installation
* To build on a Raspberry Pi: * To build on a Raspberry Pi:

View file

@ -2,35 +2,35 @@ from labthings import find_component
from labthings.extensions import BaseExtension from labthings.extensions import BaseExtension
def identify(): # Create the extension class
""" class MyExtension(BaseExtension):
Demonstrate access to Microscope.camera, and Microscope.stage def __init__(self):
""" # Superclass init function
microscope = find_component("org.openflexure.microscope") super().__init__("com.myname.myextension", version="0.0.0")
response = ( def identify(self):
f"My name is {microscope.name}. " """
f"My parent camera is {microscope.camera}, " Demonstrate access to Microscope.camera, and Microscope.stage
f"and my parent stage is {microscope.stage}." """
) microscope = find_component("org.openflexure.microscope")
return response response = (
f"My name is {microscope.name}. "
f"My parent camera is {microscope.camera}, "
f"and my parent stage is {microscope.stage}."
)
return response
def rename(self, new_name):
"""
Rename the microscope
"""
microscope = find_component("org.openflexure.microscope")
microscope.name = new_name
microscope.save_settings()
def rename(new_name): LABTHINGS_EXTENSIONS = (MyExtension,)
"""
Rename the microscope
"""
microscope = find_component("org.openflexure.microscope")
microscope.name = new_name
microscope.save_settings()
# Create your extension object
my_extension = BaseExtension("com.myname.myextension", version="0.0.0")
# Add methods to your extension
my_extension.add_method(identify, "identify")
my_extension.add_method(rename, "rename")

View file

@ -1,41 +0,0 @@
from labthings import find_component
from labthings.extensions import BaseExtension
# Create the extension class
class MyExtension(BaseExtension):
def __init__(self):
# Create some instance variable
self.state_variable = "An example of a persistant instance variable"
# Superclass init function
super().__init__("com.myname.myextension", version="0.0.0")
def identify(self):
"""
Demonstrate access to Microscope.camera, and Microscope.stage
"""
microscope = find_component("org.openflexure.microscope")
response = (
f"My name is {microscope.name}. "
f"My parent camera is {microscope.camera}, "
f"and my parent stage is {microscope.stage}."
)
return response
def rename(self, new_name):
"""
Rename the microscope
"""
microscope = find_component("org.openflexure.microscope")
microscope.name = new_name
microscope.save_settings()
# Create your extension object
my_extension = MyExtension()

View file

@ -2,42 +2,45 @@ from labthings import fields, find_component
from labthings.extensions import BaseExtension from labthings.extensions import BaseExtension
from labthings.views import View from labthings.views import View
## Extension methods
# Create the extension class
class MyExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("com.myname.myextension", version="0.0.0")
def identify(microscope): # Add our API Views (defined below MyExtension)
""" self.add_view(ExampleIdentifyView, "/identify")
Demonstrate access to Microscope.camera, and Microscope.stage self.add_view(ExampleRenameView, "/rename")
"""
response = ( def identify(self, microscope):
f"My name is {microscope.name}. " """
f"My parent camera is {microscope.camera}, " Demonstrate access to Microscope.camera, and Microscope.stage
f"and my parent stage is {microscope.stage}." """
) response = (
f"My name is {microscope.name}. "
f"My parent camera is {microscope.camera}, "
f"and my parent stage is {microscope.stage}."
)
return response return response
def rename(self, microscope, new_name):
def rename(microscope, new_name): """
""" Rename the microscope
Rename the microscope """
""" microscope.name = new_name
microscope.save_settings()
microscope.name = new_name
microscope.save_settings()
## Extension views ## Extension views
class ExampleIdentifyView(View): class ExampleIdentifyView(View):
def get(self): def get(self):
# Find our microscope component # Find our microscope component
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
# Return our identify function's output # Return our identify function's output
return identify(microscope) return self.extension.identify(microscope)
class ExampleRenameView(View): class ExampleRenameView(View):
@ -53,21 +56,10 @@ class ExampleRenameView(View):
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
# Pass microscope and new name to our rename function # Pass microscope and new name to our rename function
rename(microscope, new_name) self.extension.rename(microscope, new_name)
# Return our identify function's output # Return our identify function's output
return identify(microscope) return self.extension.identify(microscope)
## Create extension LABTHINGS_EXTENSIONS = (MyExtension,)
# Create your extension object
my_extension = BaseExtension("com.myname.myextension", version="0.0.0")
# Add methods to your extension
my_extension.add_method(identify, "identify")
my_extension.add_method(rename, "rename")
# Add API views to your extension
my_extension.add_view(ExampleIdentifyView, "/identify")
my_extension.add_view(ExampleRenameView, "/rename")

View file

@ -2,7 +2,23 @@ from labthings import Schema, fields, find_component
from labthings.extensions import BaseExtension from labthings.extensions import BaseExtension
from labthings.views import View from labthings.views import View
## Extension methods
# Create the extension class
class MyExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("com.myname.myextension", version="0.0.0")
# Add our API Views (defined below MyExtension)
self.add_view(ExampleIdentifyView, "/identify")
self.add_view(ExampleRenameView, "/rename")
def rename(self, microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
# Define which properties of a Microscope object we care about, # Define which properties of a Microscope object we care about,
@ -15,18 +31,7 @@ class MicroscopeIdentifySchema(Schema):
stage = fields.String() # Stage object (represented as a string) stage = fields.String() # Stage object (represented as a string)
def rename(microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
## Extension views ## Extension views
class ExampleIdentifyView(View): class ExampleIdentifyView(View):
# Format our returned object using MicroscopeIdentifySchema # Format our returned object using MicroscopeIdentifySchema
schema = MicroscopeIdentifySchema() schema = MicroscopeIdentifySchema()
@ -54,21 +59,11 @@ class ExampleRenameView(View):
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
# Pass microscope and new name to our rename function # Pass microscope and new name to our rename function
rename(microscope, new_name) self.extension.rename(microscope, new_name)
# Return our microscope object, # Return our microscope object,
# let schema handle formatting the output # let schema handle formatting the output
return microscope return microscope
## Create extension LABTHINGS_EXTENSIONS = (MyExtension,)
# Create your extension object
my_extension = BaseExtension("com.myname.myextension", version="0.0.0")
# Add methods to your extension
my_extension.add_method(rename, "rename")
# Add API views to your extension
my_extension.add_view(ExampleIdentifyView, "/identify")
my_extension.add_view(ExampleRenameView, "/rename")

View file

@ -2,7 +2,23 @@ from labthings import Schema, fields, find_component
from labthings.extensions import BaseExtension from labthings.extensions import BaseExtension
from labthings.views import PropertyView from labthings.views import PropertyView
## Extension methods
# Create the extension class
class MyExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("com.myname.myextension", version="0.0.0")
# Add our API Views (defined below MyExtension)
self.add_view(ExampleIdentifyView, "/identify")
self.add_view(ExampleRenameView, "/rename")
def rename(self, microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
# Define which properties of a Microscope object we care about, # Define which properties of a Microscope object we care about,
@ -15,16 +31,7 @@ class MicroscopeIdentifySchema(Schema):
stage = fields.String() # Stage object (represented as a string) stage = fields.String() # Stage object (represented as a string)
def rename(microscope, new_name): ## Extension viewss
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
## Extension views
# Since we only have a GET method here, it'll register as a read-only property # Since we only have a GET method here, it'll register as a read-only property
class ExampleIdentifyView(PropertyView): class ExampleIdentifyView(PropertyView):
@ -68,21 +75,11 @@ class ExampleRenameView(PropertyView):
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
# Pass microscope and new name to our rename function # Pass microscope and new name to our rename function
rename(microscope, new_name) self.extension.rename(microscope, new_name)
# Return our microscope object, # Return our microscope object,
# let schema handle formatting the output # let schema handle formatting the output
return microscope return microscope
## Create extension LABTHINGS_EXTENSIONS = (MyExtension,)
# Create your extension object
my_extension = BaseExtension("com.myname.myextension", version="0.0.0")
# Add methods to your extension
my_extension.add_method(rename, "rename")
# Add API views to your extension
my_extension.add_view(ExampleIdentifyView, "/identify")
my_extension.add_view(ExampleRenameView, "/rename")

View file

@ -5,7 +5,23 @@ from labthings import Schema, fields, find_component
from labthings.extensions import BaseExtension from labthings.extensions import BaseExtension
from labthings.views import ActionView, PropertyView from labthings.views import ActionView, PropertyView
## Extension methods
# Create the extension class
class MyExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("com.myname.myextension", version="0.0.0")
# Add our API Views (defined below MyExtension)
self.add_view(ExampleIdentifyView, "/identify")
self.add_view(ExampleRenameView, "/rename")
def rename(self, microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
# Define which properties of a Microscope object we care about, # Define which properties of a Microscope object we care about,
@ -18,15 +34,6 @@ class MicroscopeIdentifySchema(Schema):
stage = fields.String() # Stage object (represented as a string) stage = fields.String() # Stage object (represented as a string)
def rename(microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
## Extension views ## Extension views
# Since we only have a GET method here, it'll register as a read-only property # Since we only have a GET method here, it'll register as a read-only property
@ -71,7 +78,7 @@ class ExampleRenameView(PropertyView):
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
# Pass microscope and new name to our rename function # Pass microscope and new name to our rename function
rename(microscope, new_name) self.extension.rename(microscope, new_name)
# Return our microscope object, # Return our microscope object,
# let schema handle formatting the output # let schema handle formatting the output
@ -109,15 +116,4 @@ class QuickCaptureAPI(ActionView):
return send_file(io.BytesIO(stream.read()), mimetype="image/jpeg") return send_file(io.BytesIO(stream.read()), mimetype="image/jpeg")
## Create extension LABTHINGS_EXTENSIONS = (MyExtension,)
# Create your extension object
my_extension = BaseExtension("com.myname.myextension", version="0.0.0")
# Add methods to your extension
my_extension.add_method(rename, "rename")
# Add API views to your extension
my_extension.add_view(ExampleIdentifyView, "/identify")
my_extension.add_view(ExampleRenameView, "/rename")
my_extension.add_view(QuickCaptureAPI, "/quick-capture")

View file

@ -7,52 +7,59 @@ from labthings.views import ActionView
# Used in our timelapse function # Used in our timelapse function
from openflexure_microscope.captures.capture_manager import generate_basename from openflexure_microscope.captures.capture_manager import generate_basename
## Extension methods
# Create the extension class
class TimelapseExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("org.openflexure.timelapse-extension", version="0.0.0")
def timelapse(microscope, n_images, t_between): # Add our API views
""" self.add_view(TimelapseAPIView, "/timelapse")
Save a set of images in a timelapse
Args: def timelapse(self, microscope, n_images, t_between):
microscope: Microscope object """
n_images (int): Number of images to take Save a set of images in a timelapse
t_between (int/float): Time, in seconds, between sequential captures
"""
base_file_name = generate_basename()
folder = "TIMELAPSE_{}".format(base_file_name)
# Take exclusive control over both the camera and stage Args:
with microscope.camera.lock, microscope.stage.lock: microscope: Microscope object
for n in range(n_images): n_images (int): Number of images to take
# Elegantly handle action cancellation t_between (int/float): Time, in seconds, between sequential captures
if current_action() and current_action().stopped: """
return base_file_name = generate_basename()
# Generate a filename folder = "TIMELAPSE_{}".format(base_file_name)
filename = f"{base_file_name}_image{n}"
# Create a file to save the image to
output = microscope.camera.new_image(
filename=filename, folder=folder, temporary=False
)
# Capture # Take exclusive control over both the camera and stage
microscope.camera.capture(output) with microscope.camera.lock, microscope.stage.lock:
for n in range(n_images):
# Elegantly handle action cancellation
if current_action() and current_action().stopped:
return
# Generate a filename
filename = f"{base_file_name}_image{n}"
# Create a file to save the image to
output = microscope.camera.new_image(
filename=filename, folder=folder, temporary=False
)
# Add system metadata # Capture
output.put_metadata(microscope.metadata, system=True) microscope.camera.capture(output)
# Update task progress (only does anyting if the function is running in a LabThings task) # Add system metadata
progress_pct = ((n + 1) / n_images) * 100 # Progress, in percent output.put_metadata(microscope.metadata, system=True)
update_action_progress(progress_pct)
# Wait for the specified time # Update task progress (only does anyting if the function is running in a LabThings task)
time.sleep(t_between) progress_pct = ((n + 1) / n_images) * 100 # Progress, in percent
update_action_progress(progress_pct)
# Wait for the specified time
time.sleep(t_between)
## Extension views ## Extension views
class TimelapseAPI(ActionView): class TimelapseAPIView(ActionView):
""" """
Take a series of images in a timelapse Take a series of images in a timelapse
""" """
@ -71,16 +78,9 @@ class TimelapseAPI(ActionView):
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
# Start "timelapse" # Start "timelapse"
return timelapse(microscope, args.get("n_images"), args.get("t_between")) return self.extension.timelapse(
microscope, args.get("n_images"), args.get("t_between")
)
## Create extension LABTHINGS_EXTENSIONS = (TimelapseExtension,)
# Create your extension object
my_extension = BaseExtension("com.myname.timelapse-extension", version="0.0.0")
# Add methods to your extension
my_extension.add_method(timelapse, "timelapse")
# Add API views to your extension
my_extension.add_view(TimelapseAPI, "/timelapse")

View file

@ -10,54 +10,91 @@ from openflexure_microscope.api.utilities.gui import build_gui
# Used in our timelapse function # Used in our timelapse function
from openflexure_microscope.captures.capture_manager import generate_basename from openflexure_microscope.captures.capture_manager import generate_basename
## Extension methods
# Create the extension class
class TimelapseExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("org.openflexure.timelapse-extension", version="0.0.0")
def timelapse(microscope, n_images, t_between): # Add our API views
""" self.add_view(TimelapseAPIView, "/timelapse")
Save a set of images in a timelapse
Args: # Add our GUI description
microscope: Microscope object gui_description = {
n_images (int): Number of images to take "icon": "timelapse", # Name of an icon from https://material.io/resources/icons/
t_between (int/float): Time, in seconds, between sequential captures "forms": [ # List of forms. Each form is a collapsible accordion panel
""" {
base_file_name = generate_basename() "name": "Start a timelapse", # Form title
folder = "TIMELAPSE_{}".format(base_file_name) "route": "/timelapse", # The URL rule (as given by "add_view") of your submission view
"isTask": True, # This forms submission starts a background task
"isCollapsible": False, # This form cannot be collapsed into an accordion
"submitLabel": "Start", # Label for the form submit button
"schema": [ # List of dictionaries. Each element is a form component.
{
"fieldType": "numberInput",
"name": "n_images", # Name of the view arg this value corresponds to
"label": "Number of images",
"min": 1, # HTML number input attribute
"default": 5, # HTML number input attribute
},
{
"fieldType": "numberInput",
"name": "t_between",
"label": "Time (seconds) between images",
"min": 0.1, # HTML number input attribute
"step": 0.1, # HTML number input attribute
"default": 1, # HTML number input attribute
},
],
}
],
}
self.add_meta("gui", build_gui(gui_description, self))
# Take exclusive control over both the camera and stage def timelapse(self, microscope, n_images, t_between):
with microscope.camera.lock, microscope.stage.lock: """
for n in range(n_images): Save a set of images in a timelapse
# Elegantly handle action cancellation
if current_action() and current_action().stopped:
return
# Generate a filename
filename = f"{base_file_name}_image{n}"
# Create a file to save the image to
output = microscope.camera.new_image(
filename=filename, folder=folder, temporary=False
)
# Capture Args:
microscope.camera.capture(output) microscope: Microscope object
n_images (int): Number of images to take
t_between (int/float): Time, in seconds, between sequential captures
"""
base_file_name = generate_basename()
folder = "TIMELAPSE_{}".format(base_file_name)
# Add system metadata # Take exclusive control over both the camera and stage
output.put_metadata(microscope.metadata, system=True) with microscope.camera.lock, microscope.stage.lock:
for n in range(n_images):
# Elegantly handle action cancellation
if current_action() and current_action().stopped:
return
# Generate a filename
filename = f"{base_file_name}_image{n}"
# Create a file to save the image to
output = microscope.camera.new_image(
filename=filename, folder=folder, temporary=False
)
# Update task progress (only does anyting if the function is running in a LabThings task) # Capture
progress_pct = ((n + 1) / n_images) * 100 # Progress, in percent microscope.camera.capture(output)
update_action_progress(progress_pct)
# Wait for the specified time # Add system metadata
time.sleep(t_between) output.put_metadata(microscope.metadata, system=True)
# Update task progress (only does anyting if the function is running in a LabThings task)
progress_pct = ((n + 1) / n_images) * 100 # Progress, in percent
update_action_progress(progress_pct)
# Wait for the specified time
time.sleep(t_between)
## Extension views ## Extension views
class TimelapseAPIView(ActionView):
class TimelapseAPI(ActionView):
""" """
Take a series of images in a timelapse, running as a background task Take a series of images in a timelapse
""" """
args = { args = {
@ -73,53 +110,10 @@ class TimelapseAPI(ActionView):
# Find our microscope component # Find our microscope component
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
# Create and start "timelapse", running in a background task # Start "timelapse"
return timelapse(microscope, args.get("n_images"), args.get("t_between")) return self.extension.timelapse(
microscope, args.get("n_images"), args.get("t_between")
)
## Extension GUI (OpenFlexure eV) LABTHINGS_EXTENSIONS = (TimelapseExtension,)
# Alternate form without any dynamic parts
extension_gui = {
"icon": "timelapse", # Name of an icon from https://material.io/resources/icons/
"forms": [ # List of forms. Each form is a collapsible accordion panel
{
"name": "Start a timelapse", # Form title
"route": "/timelapse", # The URL rule (as given by "add_view") of your submission view
"isTask": True, # This forms submission starts a background task
"isCollapsible": False, # This form cannot be collapsed into an accordion
"submitLabel": "Start", # Label for the form submit button
"schema": [ # List of dictionaries. Each element is a form component.
{
"fieldType": "numberInput",
"name": "n_images", # Name of the view arg this value corresponds to
"label": "Number of images",
"min": 1, # HTML number input attribute
"default": 5, # HTML number input attribute
},
{
"fieldType": "numberInput",
"name": "t_between",
"label": "Time (seconds) between images",
"min": 0.1, # HTML number input attribute
"step": 0.1, # HTML number input attribute
"default": 1, # HTML number input attribute
},
],
}
],
}
## Create extension
# Create your extension object
my_extension = BaseExtension("com.myname.timelapse-extension", version="0.0.0")
# Add methods to your extension
my_extension.add_method(timelapse, "timelapse")
# Add API views to your extension
my_extension.add_view(TimelapseAPI, "/timelapse")
# Add OpenFlexure eV GUI to your extension
my_extension.add_meta("gui", build_gui(extension_gui, my_extension))

View file

@ -16,6 +16,15 @@ Package extensions
------------------ ------------------
Generally, for adding anything other than very simple functionality, extensions should be written as `package distributions <https://packaging.python.org/tutorials/packaging-projects/>`_. This has the advantage of allowing relative imports, so functionality can be easily split over several files. For example, class definitions associated with API routes can be separated from class definitions associated with the microscope extension. Generally, for adding anything other than very simple functionality, extensions should be written as `package distributions <https://packaging.python.org/tutorials/packaging-projects/>`_. This has the advantage of allowing relative imports, so functionality can be easily split over several files. For example, class definitions associated with API routes can be separated from class definitions associated with the microscope extension.
Your module must be a folder within the extensions folder (by default ``/var/openflexure/extensions/microscope_extensions``), and include a top-level ``__init__.py`` file which includes (or imports) all of your extension objects. Your module must be a folder within the extensions folder (by default ``/var/openflexure/extensions/microscope_extensions``), and include a top-level ``__init__.py`` file which includes (or imports) all of your extension classes, and includes them in a global constant `LABTHINGS_EXTENSIONS` list.
For example, if your extension classes are defined in a file ``my_extension.py``, your adjascent ``__init__.py`` file may look like:
.. code-block:: python
from .my_extension import MyExtensionClass, MyOtherExtensionClass
LABTHINGS_EXTENSIONS = (MyExtensionClass, MyOtherExtensionClass)
In order to enable a globally installed, packaged extension, create a file in the applications extensions directory (by default ``/var/openflexure/extensions/microscope_extensions``) which imports your extension object(s) from your module. In order to enable a globally installed, packaged extension, create a file in the applications extensions directory (by default ``/var/openflexure/extensions/microscope_extensions``) which imports your extension object(s) from your module.

View file

@ -1,9 +1,11 @@
Basic extension structure Basic extension structure
========================= =========================
An extension starts as a simple instance of :py:class:`labthings.extensions.BaseExtension`. An extension starts as a subclass of :py:class:`labthings.extensions.BaseExtension`.
Each extension is described by a single ``BaseExtension`` instance, containing any number of methods, API views, and additional hardware components. Each extension is described by a single ``BaseExtension`` instance, containing any number of methods, API views, and additional hardware components.
You will build your extension by subclassing :py:class:`labthings.extensions.BaseExtension`, and adding the class to a top-level `LABTHINGS_EXTENSIONS` list.
In order to access the currently running microscope object, use the :py:func:`labthings.find_component` function, with the argument ``"org.openflexure.microscope"``. Likewise, any new components attached by other extensions can be found using their full name, as above. In order to access the currently running microscope object, use the :py:func:`labthings.find_component` function, with the argument ``"org.openflexure.microscope"``. Likewise, any new components attached by other extensions can be found using their full name, as above.
A simple extension file, with no API views but application-available methods may look like: A simple extension file, with no API views but application-available methods may look like:
@ -24,13 +26,3 @@ Once this extension is loaded, any other extensions will have access to your met
# Call a function from your extension # Call a function from your extension
if my_found_extension: if my_found_extension:
my_found_extension.identify() my_found_extension.identify()
Subclassing ``BaseExtension``
-------------------------------
The syntax used above allows novice programmers to easily start building extensions, without having to deal with subclassing. However, for more complex extensions which require persistent state, subclassing :py:class:`labthings.extensions.BaseExtension` is recommended.
The same simple extension as seen above can be written using subclassing:
.. literalinclude:: ./example_extension/01b_basic_structure_subclass.py

View file

@ -1,6 +1,14 @@
Adding web API views Adding web API views
==================== ====================
Key terminology
---------------
API View (or View)
++++++++++++++++++
*"A view function is the code you write to respond to requests to your application [...] For RESTful APIs its especially helpful to execute a different function for each HTTP method. With the [View class] you can easily do that. Each HTTP method maps to a function with the same name (just in lowercase)"* - `Flask documentation <https://flask.palletsprojects.com/en/1.1.x/views/>`_
Introduction Introduction
------------ ------------
Extensions can create views to expose extension functionality via the web API. Creating API views for your extension is strongly recommended, as this is the primary way we encourage interaction with the microscope device. Extensions can create views to expose extension functionality via the web API. Creating API views for your extension is strongly recommended, as this is the primary way we encourage interaction with the microscope device.
@ -13,6 +21,8 @@ Continuing our example on the previous page, and discussed below, adding API vie
Note that we are now passing our microscope object as an argument to our API methods. Finding the microscope component is performed by the API view at request-time, and passed onto the functions. Note that we are now passing our microscope object as an argument to our API methods. Finding the microscope component is performed by the API view at request-time, and passed onto the functions.
Your extension functions can be accessed from within an API View by using ``self.extension``. Once your view has been added to your extension, this will point to the extension object, allowing your API views to use your extension functionality.
In this case, our extension will have two new API views at `/identify` and `/rename`. The `/identify` view only accepts GET requests, and the `/rename` view only accepts POST requests. In this case, our extension will have two new API views at `/identify` and `/rename`. The `/identify` view only accepts GET requests, and the `/rename` view only accepts POST requests.
Request arguments Request arguments

View file

@ -1,6 +1,11 @@
import logging import logging
import traceback import traceback
from contextlib import contextmanager from contextlib import contextmanager
from typing import List, Type
from labthings.extensions import BaseExtension
LABTHINGS_EXTENSIONS: List[Type[BaseExtension]] = []
@contextmanager @contextmanager
@ -17,14 +22,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
with handle_extension_error("camera stage mapping"):
from camera_stage_mapping.ofm_extension import csm_extension LABTHINGS_EXTENSIONS.append(AutostorageExtension)
with handle_extension_error("lens shading calibration"): with handle_extension_error("lens shading calibration"):
from .picamera_autocalibrate import lst_extension_v2 from .picamera_autocalibrate import LSTExtension
LABTHINGS_EXTENSIONS.append(LSTExtension)
with handle_extension_error("camera stage mapping"):
from .camera_stage_mapping import CSMExtension
LABTHINGS_EXTENSIONS.append(CSMExtension)

View file

@ -1,7 +1,7 @@
import logging import logging
import time import time
from contextlib import contextmanager from contextlib import contextmanager
from typing import Callable, List, Optional, Tuple from typing import Callable, Dict, List, Optional, Tuple
import numpy as np import numpy as np
from labthings import current_action, fields, find_component from labthings import current_action, fields, find_component
@ -39,7 +39,7 @@ class JPEGSharpnessMonitor:
self.camera.stream.stop_tracking() self.camera.stream.stop_tracking()
self.camera.stream.reset_tracking() self.camera.stream.reset_tracking()
def focus_rel(self, dz: int, backlash: bool = False, **kwargs): def focus_rel(self, dz: int, backlash: bool = False, **kwargs) -> Tuple[int, int]:
# Store the start time and position # Store the start time and position
self.camera.stream.start_tracking() self.camera.stream.start_tracking()
self.stage_times.append(time.time()) self.stage_times.append(time.time())
@ -62,22 +62,28 @@ class JPEGSharpnessMonitor:
self.camera.stream.reset_tracking() self.camera.stream.reset_tracking()
# Index of the data for this movement # Index of the data for this movement
data_index = len(self.stage_positions) - 2 data_index: int = len(self.stage_positions) - 2
# Final z position after move # Final z position after move
final_z_position = self.stage_positions[-1][2] final_z_position: int = self.stage_positions[-1][2]
return data_index, final_z_position return data_index, final_z_position
def move_data(self, istart: int, istop: Optional[int] = None): def move_data(
self, istart: int, istop: Optional[int] = None
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""Extract sharpness as a function of (interpolated) z""" """Extract sharpness as a function of (interpolated) z"""
if istop is None: if istop is None:
istop = istart + 2 istop = istart + 2
jpeg_times = np.array(self.jpeg_times) jpeg_times: np.ndarray = np.array(self.jpeg_times) # np.ndarray[float]
jpeg_sizes = np.array(self.jpeg_sizes) jpeg_sizes: np.ndarray = np.array(self.jpeg_sizes) # np.ndarray[int]
stage_times = np.array(self.stage_times)[istart:istop] stage_times: np.ndarray = np.array(self.stage_times)[
stage_zs = np.array(self.stage_positions)[istart:istop, 2] istart:istop
] # np.ndarray[float]
stage_zs: np.ndarray = np.array(self.stage_positions)[
istart:istop, 2
] # np.ndarray[int]
try: try:
start = np.argmax(jpeg_times > stage_times[0]) start: int = int(np.argmax(jpeg_times > stage_times[0]))
stop = np.argmax(jpeg_times > stage_times[1]) stop: int = int(np.argmax(jpeg_times > stage_times[1]))
except ValueError as e: except ValueError as e:
if np.sum(jpeg_times > stage_times[0]) == 0: if np.sum(jpeg_times > stage_times[0]) == 0:
raise ValueError( raise ValueError(
@ -89,10 +95,12 @@ class JPEGSharpnessMonitor:
stop = len(jpeg_times) stop = len(jpeg_times)
logging.debug("changing stop to %s", (stop)) logging.debug("changing stop to %s", (stop))
jpeg_times = jpeg_times[start:stop] jpeg_times = jpeg_times[start:stop]
jpeg_zs = np.interp(jpeg_times, stage_times, stage_zs) jpeg_zs: np.ndarray = np.interp(
jpeg_times, stage_times, stage_zs
) # np.ndarray[float]
return jpeg_times, jpeg_zs, jpeg_sizes[start:stop] return jpeg_times, jpeg_zs, jpeg_sizes[start:stop]
def sharpest_z_on_move(self, index: int): def sharpest_z_on_move(self, index: int) -> int:
"""Return the z position of the sharpest image on a given move""" """Return the z position of the sharpest image on a given move"""
_, jz, js = self.move_data(index) _, jz, js = self.move_data(index)
if len(js) == 0: if len(js) == 0:
@ -101,7 +109,7 @@ class JPEGSharpnessMonitor:
) )
return jz[np.argmax(js)] return jz[np.argmax(js)]
def data_dict(self): def data_dict(self) -> Dict[str, np.ndarray]:
"""Return the gathered data as a single convenient dictionary""" """Return the gathered data as a single convenient dictionary"""
data = {} data = {}
for k in ["jpeg_times", "jpeg_sizes", "stage_times", "stage_positions"]: for k in ["jpeg_times", "jpeg_sizes", "stage_times", "stage_positions"]:
@ -109,18 +117,28 @@ class JPEGSharpnessMonitor:
return data return data
def sharpness_sum_lap2(rgb_image: np.ndarray): @contextmanager
def monitor_sharpness(microscope: Microscope):
m: JPEGSharpnessMonitor = JPEGSharpnessMonitor(microscope)
m.start()
try:
yield m
finally:
m.stop()
def sharpness_sum_lap2(rgb_image: np.ndarray) -> np.float:
"""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.float = np.mean(rgb_image, 2)
image_lap = ndimage.filters.laplace(image_bw) image_lap: np.float = ndimage.filters.laplace(image_bw)
return np.mean(image_lap.astype(np.float) ** 4) return np.mean(image_lap.astype(np.float) ** 4)
def sharpness_edge(image: np.ndarray): def sharpness_edge(image: np.ndarray) -> np.float:
"""Return a sharpness metric optimised for vertical lines""" """Return a sharpness metric optimised for vertical lines"""
gray = np.mean(image.astype(float), 2) gray: np.float = np.mean(image.astype(float), 2)
n = 20 n: int = 20
edge = np.array([[-1] * n + [1] * n]) edge: np.ndarray = np.array([[-1] * n + [1] * n])
return np.sum( return np.sum(
[np.sum(ndimage.filters.convolve(gray, W) ** 2) for W in [edge, edge.T]] [np.sum(ndimage.filters.convolve(gray, W) ** 2) for W in [edge, edge.T]]
) )
@ -129,184 +147,203 @@ 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"
): )
return metric_fn(getattr(microscope.camera, "array")(use_video_port=True))
else:
raise RuntimeError(f"Object {microscope.camera} has no method `array`")
def measure_sharpness(
self, microscope: Microscope, metric_fn: Callable = sharpness_sum_lap2
) -> np.float:
"""Measure the sharpness of the camera's current view."""
def autofocus( if hasattr(microscope.camera, "array") and callable(
microscope: Microscope, getattr(microscope.camera, "array")
dz: List[int], ):
settle: float = 0.5, return metric_fn(getattr(microscope.camera, "array")(use_video_port=True))
metric_fn: Callable = sharpness_sum_lap2, else:
): raise RuntimeError(f"Object {microscope.camera} has no method `array`")
"""Perform a simple autofocus routine.
The stage is moved to z positions (relative to current position) in dz,
and at each position an image is captured and the sharpness function
evaulated. We then move back to the position where the sharpness was
highest. No interpolation is performed.
dz is assumed to be in ascending order (starting at -ve values)
"""
camera = microscope.camera
stage = microscope.stage
with set_properties(stage, backlash=256), stage.lock, camera.lock: def autofocus(
sharpnesses = [] self,
positions = [] microscope: Microscope,
dz: List[int],
settle: float = 0.5,
metric_fn: Callable = sharpness_sum_lap2,
) -> Tuple[List[int], List[np.float]]:
"""Perform a simple autofocus routine.
The stage is moved to z positions (relative to current position) in dz,
and at each position an image is captured and the sharpness function
evaulated. We then move back to the position where the sharpness was
highest. No interpolation is performed.
dz is assumed to be in ascending order (starting at -ve values)
"""
camera: BaseCamera = microscope.camera
stage: BaseStage = microscope.stage
# Some cameras may not have annotate_text. Reset if it does with set_properties(stage, backlash=256), stage.lock, camera.lock:
if getattr(camera, "annotate_text"): sharpnesses: List[np.float] = []
setattr(camera, "annotate_text", "") positions: List[int] = []
for _ in stage.scan_z(dz, return_to_start=False): # Some cameras may not have annotate_text. Reset if it does
if current_action() and current_action().stopped: if getattr(camera, "annotate_text", None):
return setattr(camera, "annotate_text", "")
positions.append(stage.position[2])
time.sleep(settle)
sharpnesses.append(measure_sharpness(microscope, metric_fn))
newposition = positions[np.argmax(sharpnesses)] for _ in stage.scan_z(dz, return_to_start=False):
stage.move_rel((0, 0, newposition - stage.position[2])) if current_action() and current_action().stopped:
return [], []
positions.append(stage.position[2])
time.sleep(settle)
sharpnesses.append(self.measure_sharpness(microscope, metric_fn))
return positions, sharpnesses newposition: int = positions[int(np.argmax(sharpnesses))]
stage.move_rel((0, 0, newposition - stage.position[2]))
return positions, sharpnesses
@contextmanager def move_and_find_focus(self, microscope: Microscope, dz: int) -> int:
def monitor_sharpness(microscope: Microscope): """Make a relative Z move and return the peak sharpness position"""
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(
# z: Final z position after move self, microscope: Microscope, dz: int
i, z = m.focus_rel(dz) ) -> Dict[str, np.ndarray]:
# Get the z position with highest sharpness from the previous move (index i) """Make a relative Z move and return the sharpness data"""
fz = m.sharpest_z_on_move(i) with monitor_sharpness(microscope) as m:
# Move all the way to the start so it's consistent m.focus_rel(dz)
# 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
) -> Dict[str, np.ndarray]:
"""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: int = 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. ) -> Dict[str, np.ndarray]:
"""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: int
# now inspect where the sharpest point is, and estimate the sharpness z: int
# (JPEG size) that we should find at the start of the Z stack i, z = m.focus_rel(-dz)
_, jz, js = m.move_data(i) # now inspect where the sharpest point is, and estimate the sharpness
best_z = jz[np.argmax(js)] # (JPEG size) that we should find at the start of the Z stack
jz: np.ndarray # np.ndarray[float]
js: np.ndarray # np.ndarray[float]
_, jz, js = m.move_data(i)
best_z: int = 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: int = int(
js = js[imax:] # NB z is in DECREASING order np.argmax(js)
jz = jz[imax:] ) # we want to crop out just the bit below the peak
inow = np.argmax( js = js[imax:] # NB z is in DECREASING order
js < current_js jz = jz[imax:]
) # use the curve we recorded to estimate our position inow: int = int(
np.argmax(js < current_js)
) # 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: int = 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 +353,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 +372,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 +398,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 +422,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 +454,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,21 +235,18 @@ 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 self.extension.get_locations()
class PreferredLocationView(PropertyView): 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,10 @@ 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

@ -0,0 +1,351 @@
"""
OpenFlexure Microscope API extension for stage calibration
This file contains the HTTP API for camera/stage calibration. It
includes calibration functions that measure the relationship between
stage coordinates and camera coordinates, as well as functions that
move by a specified displacement in pixels, perform closed-loop moves,
and return the calibration data.
This module is only intended to be called from the OpenFlexure Microscope
server, and depends on that server and its underlying LabThings library.
"""
import io
import json
import logging
import os
import time
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Tuple
import numpy as np
import PIL
from camera_stage_mapping.camera_stage_calibration_1d import (
calibrate_backlash_1d,
image_to_stage_displacement_from_1d,
)
from camera_stage_mapping.camera_stage_tracker import Tracker
from camera_stage_mapping.closed_loop_move import closed_loop_move, closed_loop_scan
from camera_stage_mapping.scan_coords_times import ordered_spiral
from labthings import fields
from labthings.extensions import BaseExtension
from labthings.find import find_component
from labthings.utilities import create_from_path, get_by_path, set_by_path
from labthings.views import ActionView, PropertyView
from openflexure_microscope.config import JSONEncoder
from openflexure_microscope.microscope import Microscope
from openflexure_microscope.paths import data_file_path
CSM_DATAFILE_NAME = "csm_calibration.json"
CSM_DATAFILE_PATH = data_file_path(CSM_DATAFILE_NAME)
CoordinateType = Tuple[float, float, float]
XYCoordinateType = Tuple[float, float]
class MoveHistory(NamedTuple):
times: List[float]
stage_positions: List[CoordinateType]
class LoggingMoveWrapper:
"""Wrap a move function, and maintain a log position/time.
This class is callable, so it doesn't change the signature
of the function it wraps - it just makes it possible to get
a list of all the moves we've made, and how long they took.
Said list is intended to be useful for calibrating the stage
so we can estimate how long moves will take.
"""
def __init__(self, move_function: Callable):
self._move_function: Callable = move_function
self._current_position: Optional[CoordinateType] = None
self.clear_history()
def __call__(self, new_position: CoordinateType, *args, **kwargs):
"""Move to a new position, and record it"""
self._history.append((time.time(), self._current_position))
self._move_function(new_position, *args, **kwargs)
self._current_position = new_position
self._history.append((time.time(), self._current_position))
@property
def history(self) -> MoveHistory:
"""The history, as a numpy array of times and another of positions"""
times: List[float] = [t for t, p in self._history if p is not None]
positions: List[CoordinateType] = [p for t, p in self._history if p is not None]
return MoveHistory(times, positions)
def clear_history(self):
"""Reset our history to be an empty list"""
self._history: List[Tuple[float, Optional[CoordinateType]]] = []
class CSMExtension(BaseExtension):
"""
Use the camera as an encoder, so we can relate camera and stage coordinates
"""
def __init__(self):
BaseExtension.__init__(
self, "org.openflexure.camera_stage_mapping", version="0.0.1"
)
self.add_view(Calibrate1DView, "/calibrate_1d", endpoint="calibrate_1d")
self.add_view(CalibrateXYView, "/calibrate_xy", endpoint="calibrate_xy")
self.add_view(
MoveInImageCoordinatesView,
"/move_in_image_coordinates",
endpoint="move_in_image_coordinates",
)
self.add_view(
ClosedLoopMoveInImageCoordinatesView,
"/closed_loop_move_in_image_coordinates",
)
self.add_view(
TestClosedLoopSpiralScanView,
"/test_closed_loop_spiral_scan",
endpoint="test_closed_loop_spiral_scan",
)
self.add_view(
GetCalibrationFile, "/get_calibration", endpoint="get_calibration"
)
_microscope: Optional[Microscope] = None
@property
def microscope(self):
# TODO: does caching the microscope actually help?
if self._microscope is None:
self._microscope = find_component("org.openflexure.microscope")
return self._microscope
def update_settings(self, settings):
"""Update the stored extension settings dictionary"""
keys: List[str] = ["extensions", self.name]
dictionary: dict = create_from_path(keys)
set_by_path(dictionary, keys, settings)
logging.info("Updating settings with %s", dictionary)
self.microscope.update_settings(dictionary)
self.microscope.save_settings()
def get_settings(self) -> Dict[str, Any]:
"""Retrieve the settings for this extension"""
keys: List[str] = ["extensions", self.name]
return get_by_path(self.microscope.read_settings(), keys)
def camera_stage_functions(self) -> Tuple[Callable, Callable, Callable, Callable]:
"""Return functions that allow us to interface with the microscope"""
def grab_image():
jpeg: bytes = self.microscope.camera.get_frame()
return np.array(PIL.Image.open(io.BytesIO(jpeg)))
def get_position() -> CoordinateType:
return self.microscope.stage.position
move: Callable = self.microscope.stage.move_abs
def wait():
time.sleep(0.2)
return grab_image, get_position, move, wait
def calibrate_1d(self, direction: Tuple[float, float, float]) -> dict:
"""Move a microscope's stage in 1D, and figure out the relationship with the camera"""
grab_image: Callable
get_position: Callable
move: Callable
wait: Callable
grab_image, get_position, move, wait = self.camera_stage_functions()
move = LoggingMoveWrapper(move) # log positions and times for stage calibration
tracker = Tracker(grab_image, get_position, settle=wait)
direction_array: np.ndarray = np.array(direction)
result: dict = calibrate_backlash_1d(tracker, move, direction_array)
result["move_history"] = move.history
return result
def calibrate_xy(self) -> Dict[str, dict]:
"""Move the microscope's stage in X and Y, to calibrate its relationship to the camera"""
logging.info("Calibrating X axis:")
cal_x: dict = self.calibrate_1d((1, 0, 0))
logging.info("Calibrating Y axis:")
cal_y: dict = self.calibrate_1d((0, 1, 0))
# Combine X and Y calibrations to make a 2D calibration
cal_xy: dict = image_to_stage_displacement_from_1d([cal_x, cal_y])
self.update_settings(cal_xy)
data: Dict[str, dict] = {
"camera_stage_mapping_calibration": cal_xy,
"linear_calibration_x": cal_x,
"linear_calibration_y": cal_y,
}
with open(CSM_DATAFILE_PATH, "w") as f:
json.dump(data, f, cls=JSONEncoder)
return data
@property
def image_to_stage_displacement_matrix(self) -> np.ndarray: # 2x2 integer array
"""A 2x2 matrix that converts displacement in image coordinates to stage coordinates."""
displacement_matrix = self.get_settings().get("image_to_stage_displacement")
if not displacement_matrix:
raise ValueError("The microscope has not yet been calibrated.")
return np.array(displacement_matrix)
def move_in_image_coordinates(self, displacement_in_pixels: XYCoordinateType):
"""Move by a given number of pixels on the camera"""
relative_move: np.ndarray = np.dot(
np.array(displacement_in_pixels), self.image_to_stage_displacement_matrix
)
self.microscope.stage.move_rel([relative_move[0], relative_move[1], 0])
def closed_loop_move_in_image_coordinates(
self, displacement_in_pixels: XYCoordinateType, **kwargs
):
"""Move by a given number of pixels on the camera, using the camera as an encoder."""
grab_image, get_position, _, wait = self.camera_stage_functions()
tracker = Tracker(grab_image, get_position, settle=wait)
tracker.acquire_template()
closed_loop_move(
tracker,
self.move_in_image_coordinates,
np.array(displacement_in_pixels),
**kwargs
)
def closed_loop_scan(
self, scan_path: List[XYCoordinateType], **kwargs
) -> List[CoordinateType]:
"""Perform closed-loop moves to each point defined in scan_path.
This returns a generator, which will move the stage to each point in
``scan_path``, then yield ``i, pos`` where ``i``
is the index of the scan point, and ``pos`` is the estimated position
in pixels relative to the starting point. To use it properly, you
should iterate over it, for example::
for i, pos in self.extension.closed_loop_scan(scan_path):
capture_image(f"image_{i}.jpg")
``scan_path`` should be an Nx2 array defining
the points to visit in pixels relative to the current position.
If an exception occurs during the scan, we automatically return to the
starting point. Keyword arguments are passed to
``closed_loop_move.closed_loop_scan``.
"""
grab_image, get_position, move, wait = self.camera_stage_functions()
tracker = Tracker(grab_image, get_position, settle=wait)
tracker.acquire_template()
return closed_loop_scan(
tracker, self.move_in_image_coordinates, move, np.array(scan_path), **kwargs
)
def test_closed_loop_spiral_scan(
self, step_size: Tuple[int, int], N: int, **kwargs
):
"""Move the microscope in a spiral scan, and return the positions."""
scan_path: List[XYCoordinateType] = ordered_spiral(0, 0, N, *step_size)
for _ in self.closed_loop_scan(scan_path, **kwargs):
pass
class Calibrate1DView(ActionView):
args = {"direction": fields.List(fields.Float(), required=True, example=[1, 0, 0])}
def post(self, args):
"""Calibrate one axis of the microscope stage against the camera."""
direction: Tuple[float, float, float] = args.get("direction")
return self.extension.calibrate_1d(direction)
class CalibrateXYView(ActionView):
def post(self):
"""Calibrate both axes of the microscope stage against the camera."""
return self.extension.calibrate_xy()
class MoveInImageCoordinatesView(ActionView):
args = {
"x": fields.Float(
description="The number of pixels to move in X", required=True, example=100
),
"y": fields.Float(
description="The number of pixels to move in Y", required=True, example=100
),
}
def post(self, args):
"""Move the microscope stage, such that we move by a given number of pixels on the camera"""
logging.debug("moving in pixels")
self.extension.move_in_image_coordinates((args.get("x"), args.get("y")))
return self.extension.microscope.state["stage"]["position"]
class ClosedLoopMoveInImageCoordinatesView(ActionView):
args = {
"x": fields.Float(
description="The number of pixels to move in X", required=True, example=100
),
"y": fields.Float(
description="The number of pixels to move in Y", required=True, example=100
),
}
def post(self, args):
"""Move the microscope stage, such that we move by a given number of pixels on the camera"""
logging.debug("moving in pixels")
self.extension.closed_loop_move_in_image_coordinates(
(args.get("x"), args.get("y"))
)
return self.extension.microscope.state["stage"]["position"]
class TestClosedLoopSpiralScanView(ActionView):
args = {
"x_step": fields.Float(
description="The number of pixels to move in X", required=True, example=100
),
"y_step": fields.Float(
description="The number of pixels to move in Y", required=True, example=100
),
"N": fields.Int(
description="The number of rings in the spiral scan",
required=True,
example=3,
),
}
def post(self, args):
"""Move the microscope stage, such that we move by a given number of pixels on the camera"""
logging.debug("moving in pixels")
return self.extension.test_closed_loop_spiral_scan(
(args.get("x"), args.get("y")), args.get("N")
)
class GetCalibrationFile(PropertyView):
def get(self):
"""Get the calibration data in JSON format."""
datafile_path = CSM_DATAFILE_PATH
if os.path.isfile(datafile_path):
with open(datafile_path, "rb") as f:
return json.load(f)
else:
return {}

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

@ -8,14 +8,16 @@ from picamerax import PiCamera
from picamerax.array import PiBayerArray, PiRGBArray from picamerax.array import PiBayerArray, PiRGBArray
def rgb_image(camera: PiCamera, resize: Optional[Tuple[int, int]] = None, **kwargs): def rgb_image(
camera: PiCamera, resize: Optional[Tuple[int, int]] = None, **kwargs
) -> PiRGBArray:
"""Capture an image and return an RGB numpy array""" """Capture an image and return an RGB numpy array"""
with PiRGBArray(camera, size=resize) as output: with PiRGBArray(camera, size=resize) as output:
camera.capture(output, format="rgb", resize=resize, **kwargs) camera.capture(output, format="rgb", resize=resize, **kwargs)
return output.array return output.array
def flat_lens_shading_table(camera: PiCamera): def flat_lens_shading_table(camera: PiCamera) -> np.ndarray:
"""Return a flat (i.e. unity gain) lens shading table. """Return a flat (i.e. unity gain) lens shading table.
This is mostly useful because it makes it easy to get the size This is mostly useful because it makes it easy to get the size
@ -78,10 +80,12 @@ def auto_expose_and_freeze_settings(camera: PiCamera):
def channels_from_bayer_array(bayer_array: np.ndarray) -> np.ndarray: def channels_from_bayer_array(bayer_array: np.ndarray) -> np.ndarray:
"""Given the 'array' from a PiBayerArray, return the 4 channels.""" """Given the 'array' from a PiBayerArray, return the 4 channels."""
bayer_pattern: List[Tuple[int, int]] = [(0, 0), (0, 1), (1, 0), (1, 1)] bayer_pattern: List[Tuple[int, int]] = [(0, 0), (0, 1), (1, 0), (1, 1)]
channels: np.ndarray = np.zeros( channels_shape: Tuple[int, ...] = (
(4, bayer_array.shape[0] // 2, bayer_array.shape[1] // 2), 4,
dtype=bayer_array.dtype, bayer_array.shape[0] // 2,
bayer_array.shape[1] // 2,
) )
channels: np.ndarray = np.zeros(channels_shape, dtype=bayer_array.dtype)
for i, offset in enumerate(bayer_pattern): for i, offset in enumerate(bayer_pattern):
# We simplify life by dealing with only one channel at a time. # We simplify life by dealing with only one channel at a time.
channels[i, :, :] = np.sum( channels[i, :, :] = np.sum(
@ -105,9 +109,13 @@ def lst_from_channels(channels: np.ndarray) -> np.ndarray:
[channels.shape[0]] + lst_resolution, dtype=np.float [channels.shape[0]] + lst_resolution, dtype=np.float
) )
for i in range(lens_shading.shape[0]): for i in range(lens_shading.shape[0]):
image_channel = channels[i, :, :] image_channel: np.ndarray = channels[i, :, :]
iw: int
ih: int
iw, ih = image_channel.shape iw, ih = image_channel.shape
ls_channel = lens_shading[i, :, :] ls_channel: np.ndarray = lens_shading[i, :, :]
lw: int
lh: int
lw, lh = ls_channel.shape lw, lh = ls_channel.shape
# The lens shading table is rounded **up** in size to 1/64th of the size of # The lens shading table is rounded **up** in size to 1/64th of the size of
# the image. Rather than handle edge images separately, I'm just going to # the image. Rather than handle edge images separately, I'm just going to
@ -116,7 +124,7 @@ def lst_from_channels(channels: np.ndarray) -> np.ndarray:
# half the size of the full image - remember the Bayer pattern... This # half the size of the full image - remember the Bayer pattern... This
# should give results very close to 6by9's solution, albeit considerably # should give results very close to 6by9's solution, albeit considerably
# less computationally efficient! # less computationally efficient!
padded_image_channel = np.pad( padded_image_channel: np.ndarray = np.pad(
image_channel, [(0, lw * 32 - iw), (0, lh * 32 - ih)], mode="edge" image_channel, [(0, lw * 32 - iw), (0, lh * 32 - ih)], mode="edge"
) # Pad image to the right and bottom ) # Pad image to the right and bottom
logging.info( logging.info(
@ -129,7 +137,7 @@ def lst_from_channels(channels: np.ndarray) -> np.ndarray:
) )
# Next, fill the shading table (except edge pixels). Please excuse the # Next, fill the shading table (except edge pixels). Please excuse the
# for loop - I know it's not fast but this code needn't be! # for loop - I know it's not fast but this code needn't be!
box = 3 # We average together a square of this side length for each pixel. box: int = 3 # We average together a square of this side length for each pixel.
# NB this isn't quite what 6by9's program does - it averages 3 pixels # NB this isn't quite what 6by9's program does - it averages 3 pixels
# horizontally, but not vertically. # horizontally, but not vertically.
for dx in np.arange(box) - box // 2: for dx in np.arange(box) - box // 2:
@ -150,10 +158,10 @@ def lst_from_channels(channels: np.ndarray) -> np.ndarray:
# What we actually want to calculate is the gains needed to compensate for the # What we actually want to calculate is the gains needed to compensate for the
# lens shading - that's 1/lens_shading_table_float as we currently have it. # lens shading - that's 1/lens_shading_table_float as we currently have it.
gains = 32.0 / lens_shading # 32 is unity gain gains: np.ndarray = 32.0 / lens_shading # 32 is unity gain
gains[gains > 255] = 255 # clip at 255, maximum gain is 255/32 gains[gains > 255] = 255 # clip at 255, maximum gain is 255/32
gains[gains < 32] = 32 # clip at 32, minimum gain is 1 (is this necessary?) gains[gains < 32] = 32 # clip at 32, minimum gain is 1 (is this necessary?)
lens_shading_table = gains.astype(np.uint8) lens_shading_table: np.ndarray = gains.astype(np.uint8)
return lens_shading_table[::-1, :, :].copy() return lens_shading_table[::-1, :, :].copy()

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.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 DevToolsExtension
LABTHINGS_EXTENSIONS = [DevToolsExtension]

View file

@ -6,6 +6,17 @@ from labthings.extensions import BaseExtension
from labthings.views import ActionView from labthings.views import ActionView
class DevToolsExtension(BaseExtension):
def __init__(self) -> None:
super().__init__(
"org.openflexure.dev.tools",
version="0.1.0",
description="Actions to cause various traumatic events in the microscope, used for testing.",
)
self.add_view(RaiseException, "/raise")
self.add_view(SleepFor, "/sleep")
class RaiseException(ActionView): class RaiseException(ActionView):
def post(self): def post(self):
raise Exception("The developer raised an exception") raise Exception("The developer raised an exception")
@ -23,13 +34,3 @@ class SleepFor(ActionView):
end = time.time() end = time.time()
logging.info("Waking up!") logging.info("Waking up!")
return {"TimeAsleep": (end - start)} return {"TimeAsleep": (end - start)}
devtools_extension_v2 = BaseExtension(
"org.openflexure.dev.tools",
version="0.1.0",
description="Actions to cause various traumatic events in the microscope, used for testing.",
)
devtools_extension_v2.add_view(RaiseException, "/raise")
devtools_extension_v2.add_view(SleepFor, "/sleep")

View file

@ -2308,9 +2308,9 @@
} }
}, },
"@types/express-serve-static-core": { "@types/express-serve-static-core": {
"version": "4.17.13", "version": "4.17.14",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.14.tgz",
"integrity": "sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA==", "integrity": "sha512-uFTLwu94TfUFMToXNgRZikwPuZdOtDgs3syBtAIr/OXorL1kJqUJT9qCLnRZ5KBOWfZQikQ2xKgR2tnDj1OgDA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/node": "*", "@types/node": "*",
@ -2403,9 +2403,9 @@
"dev": true "dev": true
}, },
"@types/serve-static": { "@types/serve-static": {
"version": "1.13.7", "version": "1.13.8",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.7.tgz", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz",
"integrity": "sha512-3diZWucbR+xTmbDlU+FRRxBf+31OhFew7cJXML/zh9NmvSPTNoFecAwHB66BUqFgENJtqMiyl7JAwUE/siqdLw==", "integrity": "sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/mime": "*", "@types/mime": "*",
@ -2442,9 +2442,9 @@
} }
}, },
"@types/webpack": { "@types/webpack": {
"version": "4.41.24", "version": "4.41.25",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.24.tgz", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.25.tgz",
"integrity": "sha512-1A0MXPwZiMOD3DPMuOKUKcpkdPo8Lq33UGggZ7xio6wJ/jV1dAu5cXDrOfGDnldUroPIRLsr/DT43/GqOA4RFQ==", "integrity": "sha512-cr6kZ+4m9lp86ytQc1jPOJXgINQyz3kLLunZ57jznW+WIAL0JqZbGubQk4GlD42MuQL5JGOABrxdpqqWeovlVQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/anymatch": "*", "@types/anymatch": "*",
@ -2477,9 +2477,9 @@
} }
}, },
"@types/webpack-sources": { "@types/webpack-sources": {
"version": "2.0.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-2.1.0.tgz",
"integrity": "sha512-a5kPx98CNFRKQ+wqawroFunvFqv7GHm/3KOI52NY9xWADgc8smu4R6prt4EU/M4QfVjvgBkMqU4fBhw3QfMVkg==", "integrity": "sha512-LXn/oYIpBeucgP1EIJbKQ2/4ZmpvRl+dlrFdX7+94SKRUV3Evy3FsfMZY318vGhkWUS5MPhtOM3w1/hCOAOXcg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/node": "*", "@types/node": "*",
@ -2594,9 +2594,9 @@
} }
}, },
"@vue/cli-overlay": { "@vue/cli-overlay": {
"version": "4.5.8", "version": "4.5.9",
"resolved": "https://registry.npmjs.org/@vue/cli-overlay/-/cli-overlay-4.5.8.tgz", "resolved": "https://registry.npmjs.org/@vue/cli-overlay/-/cli-overlay-4.5.9.tgz",
"integrity": "sha512-M6q4QPKQfErHk54ycxBASgBadgdsK45f6v2NWUTAiFXdTtqv1Z7gR8JAbQhiXbni/m9571bLJnZFv5c8uhAHnw==", "integrity": "sha512-E2PWv6tCdUz+eEDj2Th2oxiKmzMe02qi0PcxiNaO7oaqggmEOrp1rLgop7DWpiLDBiqUZk2x0vjK/q2Tz8z/eg==",
"dev": true "dev": true
}, },
"@vue/cli-plugin-babel": { "@vue/cli-plugin-babel": {
@ -2708,18 +2708,18 @@
} }
}, },
"@vue/cli-plugin-router": { "@vue/cli-plugin-router": {
"version": "4.5.8", "version": "4.5.9",
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-router/-/cli-plugin-router-4.5.8.tgz", "resolved": "https://registry.npmjs.org/@vue/cli-plugin-router/-/cli-plugin-router-4.5.9.tgz",
"integrity": "sha512-tJwVLdX4jj0Ia/1hsBejRbo4gs0hb2z1z5ert+4Ve5RNdpOKUG69OaPQgXPqvuYSQh9MW7bqG0iJmYtVD+KBNw==", "integrity": "sha512-eBBfbZpQ1sJrdlx8i7iReFxSnuzwmrv+s2OCT3kjBd6uWRqGnD4VihpS4srC7vZLzDQrDplumSn0a93L9Qf3wQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@vue/cli-shared-utils": "^4.5.8" "@vue/cli-shared-utils": "^4.5.9"
}, },
"dependencies": { "dependencies": {
"@vue/cli-shared-utils": { "@vue/cli-shared-utils": {
"version": "4.5.8", "version": "4.5.9",
"resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.8.tgz", "resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.9.tgz",
"integrity": "sha512-pa6oenhBO/5HeDLRSokiwVN01gROACEDy3ESXWuPmragOREGNmmFKtkPHlqeYavGEX6LFp7f0VK3uMX6UYS5mQ==", "integrity": "sha512-anvsrv+rkQC+hgxaT2nQQxnSWSsIzyysZ36LO7qPjXvDRBvcvKLAAviFlUkYbZ+ntbV8puzJ3zw+gUhQw4SEVA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@hapi/joi": "^15.0.1", "@hapi/joi": "^15.0.1",
@ -2798,15 +2798,15 @@
} }
}, },
"@vue/cli-plugin-vuex": { "@vue/cli-plugin-vuex": {
"version": "4.5.8", "version": "4.5.9",
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.8.tgz", "resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.9.tgz",
"integrity": "sha512-wU9WPhay1zBxtdd/HLRYhFRLBbG9lw3YTBJep9sTnYwUeXiEgum4O88Q2j6cwBBPPHMgeMrKMPHS85Jf4hMc0g==", "integrity": "sha512-mFNIJhYiJjzCgytkDHX00ROy5Yzl7prkZpUbeDE0biwcLteMf2s3qZVbESOQl6GcviqcfEt2f3tHQQtLNa+OLg==",
"dev": true "dev": true
}, },
"@vue/cli-service": { "@vue/cli-service": {
"version": "4.5.8", "version": "4.5.9",
"resolved": "https://registry.npmjs.org/@vue/cli-service/-/cli-service-4.5.8.tgz", "resolved": "https://registry.npmjs.org/@vue/cli-service/-/cli-service-4.5.9.tgz",
"integrity": "sha512-YcVEJvA5vQOcfRzhCQDfHxRW9WHvKqlQeiuSvNddfV2uQqKD3ifhsNYiZZuFvbev9qgRUmtmxSafpwYzj/LLBw==", "integrity": "sha512-E3XlfM0q+UnnjbC9rwLIWNo2umZCRwnlMJY0KOhY1hFvqisGIYzFmQQ4o01KGyTx2BZNMuQg7Kw+BZ5gyM1Wig==",
"dev": true, "dev": true,
"requires": { "requires": {
"@intervolga/optimize-cssnano-plugin": "^1.0.5", "@intervolga/optimize-cssnano-plugin": "^1.0.5",
@ -2815,10 +2815,10 @@
"@types/minimist": "^1.2.0", "@types/minimist": "^1.2.0",
"@types/webpack": "^4.0.0", "@types/webpack": "^4.0.0",
"@types/webpack-dev-server": "^3.11.0", "@types/webpack-dev-server": "^3.11.0",
"@vue/cli-overlay": "^4.5.8", "@vue/cli-overlay": "^4.5.9",
"@vue/cli-plugin-router": "^4.5.8", "@vue/cli-plugin-router": "^4.5.9",
"@vue/cli-plugin-vuex": "^4.5.8", "@vue/cli-plugin-vuex": "^4.5.9",
"@vue/cli-shared-utils": "^4.5.8", "@vue/cli-shared-utils": "^4.5.9",
"@vue/component-compiler-utils": "^3.1.2", "@vue/component-compiler-utils": "^3.1.2",
"@vue/preload-webpack-plugin": "^1.1.0", "@vue/preload-webpack-plugin": "^1.1.0",
"@vue/web-component-wrapper": "^1.2.0", "@vue/web-component-wrapper": "^1.2.0",
@ -2868,9 +2868,9 @@
}, },
"dependencies": { "dependencies": {
"@vue/cli-shared-utils": { "@vue/cli-shared-utils": {
"version": "4.5.8", "version": "4.5.9",
"resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.8.tgz", "resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.9.tgz",
"integrity": "sha512-pa6oenhBO/5HeDLRSokiwVN01gROACEDy3ESXWuPmragOREGNmmFKtkPHlqeYavGEX6LFp7f0VK3uMX6UYS5mQ==", "integrity": "sha512-anvsrv+rkQC+hgxaT2nQQxnSWSsIzyysZ36LO7qPjXvDRBvcvKLAAviFlUkYbZ+ntbV8puzJ3zw+gUhQw4SEVA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@hapi/joi": "^15.0.1", "@hapi/joi": "^15.0.1",
@ -2928,16 +2928,16 @@
} }
}, },
"browserslist": { "browserslist": {
"version": "4.14.7", "version": "4.15.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.7.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.15.0.tgz",
"integrity": "sha512-BSVRLCeG3Xt/j/1cCGj1019Wbty0H+Yvu2AOuZSuoaUWn3RatbL33Cxk+Q4jRMRAbOm0p7SLravLjpnT6s0vzQ==", "integrity": "sha512-IJ1iysdMkGmjjYeRlDU8PQejVwxvVO5QOfXH7ylW31GO6LwNRSmm/SgRXtNsEXqMLl2e+2H5eEJ7sfynF8TCaQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"caniuse-lite": "^1.0.30001157", "caniuse-lite": "^1.0.30001164",
"colorette": "^1.2.1", "colorette": "^1.2.1",
"electron-to-chromium": "^1.3.591", "electron-to-chromium": "^1.3.612",
"escalade": "^3.1.1", "escalade": "^3.1.1",
"node-releases": "^1.1.66" "node-releases": "^1.1.67"
} }
}, },
"cacache": { "cacache": {
@ -2967,9 +2967,9 @@
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001157", "version": "1.0.30001165",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001157.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001165.tgz",
"integrity": "sha512-gOerH9Wz2IRZ2ZPdMfBvyOi3cjaz4O4dgNwPGzx8EhqAs4+2IL/O+fJsbt+znSigujoZG8bVcIAUM/I/E5K3MA==", "integrity": "sha512-8cEsSMwXfx7lWSUMA2s08z9dIgsnR5NAqjXP23stdsU3AUWkCr/rr4s4OFtHXn5XXr6+7kam3QFVoYyXNPdJPA==",
"dev": true "dev": true
}, },
"color-convert": { "color-convert": {
@ -2990,9 +2990,9 @@
"optional": true "optional": true
}, },
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.592", "version": "1.3.615",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.592.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.615.tgz",
"integrity": "sha512-kGNowksvqQiPb1pUSQKpd8JFoGPLxYOwduNRCqCxGh/2Q1qE2JdmwouCW41lUzDxOb/2RIV4lR0tVIfboWlO9A==", "integrity": "sha512-fNYTQXoUhNc6RmHDlGN4dgcLURSBIqQCN7ls6MuQ741+NJyLNRz8DxAC+pZpOKfRs6cfY0lv2kWdy8Oxf9j4+A==",
"dev": true "dev": true
}, },
"emojis-list": { "emojis-list": {
@ -3083,9 +3083,9 @@
} }
}, },
"node-releases": { "node-releases": {
"version": "1.1.66", "version": "1.1.67",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.66.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.67.tgz",
"integrity": "sha512-JHEQ1iWPGK+38VLB2H9ef2otU4l8s3yAMt9Xf934r6+ojCYDMHPMqvCc9TnzfeFSP1QEOeU6YZEd3+De0LTCgg==", "integrity": "sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==",
"dev": true "dev": true
}, },
"p-limit": { "p-limit": {
@ -3235,9 +3235,9 @@
} }
}, },
"vue-loader-v16": { "vue-loader-v16": {
"version": "npm:vue-loader@16.1.0", "version": "npm:vue-loader@16.1.1",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.0.tgz", "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.1.tgz",
"integrity": "sha512-fTtCdI7VeyNK0HP4q4y9Z9ts8TUeaF+2/FjKx8CJ/7/Oem1rCX7zIJe+d+jLrVnVNQjENd3gqmANraLcdRWwnQ==", "integrity": "sha512-wz/+HFg/3SBayHWAlZXARcnDTl3VOChrfW9YnxvAweiuyKX/7IGx1ad/4yJHmwhgWlOVYMAbTiI7GV8G33PfGQ==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"requires": { "requires": {
@ -3872,34 +3872,34 @@
}, },
"dependencies": { "dependencies": {
"browserslist": { "browserslist": {
"version": "4.14.7", "version": "4.15.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.7.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.15.0.tgz",
"integrity": "sha512-BSVRLCeG3Xt/j/1cCGj1019Wbty0H+Yvu2AOuZSuoaUWn3RatbL33Cxk+Q4jRMRAbOm0p7SLravLjpnT6s0vzQ==", "integrity": "sha512-IJ1iysdMkGmjjYeRlDU8PQejVwxvVO5QOfXH7ylW31GO6LwNRSmm/SgRXtNsEXqMLl2e+2H5eEJ7sfynF8TCaQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"caniuse-lite": "^1.0.30001157", "caniuse-lite": "^1.0.30001164",
"colorette": "^1.2.1", "colorette": "^1.2.1",
"electron-to-chromium": "^1.3.591", "electron-to-chromium": "^1.3.612",
"escalade": "^3.1.1", "escalade": "^3.1.1",
"node-releases": "^1.1.66" "node-releases": "^1.1.67"
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001157", "version": "1.0.30001165",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001157.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001165.tgz",
"integrity": "sha512-gOerH9Wz2IRZ2ZPdMfBvyOi3cjaz4O4dgNwPGzx8EhqAs4+2IL/O+fJsbt+znSigujoZG8bVcIAUM/I/E5K3MA==", "integrity": "sha512-8cEsSMwXfx7lWSUMA2s08z9dIgsnR5NAqjXP23stdsU3AUWkCr/rr4s4OFtHXn5XXr6+7kam3QFVoYyXNPdJPA==",
"dev": true "dev": true
}, },
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.592", "version": "1.3.615",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.592.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.615.tgz",
"integrity": "sha512-kGNowksvqQiPb1pUSQKpd8JFoGPLxYOwduNRCqCxGh/2Q1qE2JdmwouCW41lUzDxOb/2RIV4lR0tVIfboWlO9A==", "integrity": "sha512-fNYTQXoUhNc6RmHDlGN4dgcLURSBIqQCN7ls6MuQ741+NJyLNRz8DxAC+pZpOKfRs6cfY0lv2kWdy8Oxf9j4+A==",
"dev": true "dev": true
}, },
"node-releases": { "node-releases": {
"version": "1.1.66", "version": "1.1.67",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.66.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.67.tgz",
"integrity": "sha512-JHEQ1iWPGK+38VLB2H9ef2otU4l8s3yAMt9Xf934r6+ojCYDMHPMqvCc9TnzfeFSP1QEOeU6YZEd3+De0LTCgg==", "integrity": "sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==",
"dev": true "dev": true
} }
} }
@ -4864,16 +4864,16 @@
} }
}, },
"cli-highlight": { "cli-highlight": {
"version": "2.1.4", "version": "2.1.9",
"resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.4.tgz", "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.9.tgz",
"integrity": "sha512-s7Zofobm20qriqDoU9sXptQx0t2R9PEgac92mENNm7xaEe1hn71IIMsXMK+6encA6WRCWWxIGQbipr3q998tlQ==", "integrity": "sha512-t8RNIZgiI24i/mslZ8XT8o660RUj5ZbUJpEZrZa/BNekTzdC2LfMRAnt0Y7sgzNM4FGW5tmWg/YnbTH8o1eIOQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"chalk": "^3.0.0", "chalk": "^4.0.0",
"highlight.js": "^9.6.0", "highlight.js": "^10.0.0",
"mz": "^2.4.0", "mz": "^2.4.0",
"parse5": "^5.1.1", "parse5": "^5.1.1",
"parse5-htmlparser2-tree-adapter": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0",
"yargs": "^15.0.0" "yargs": "^15.0.0"
}, },
"dependencies": { "dependencies": {
@ -4887,9 +4887,9 @@
} }
}, },
"chalk": { "chalk": {
"version": "3.0.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true, "dev": true,
"requires": { "requires": {
"ansi-styles": "^4.1.0", "ansi-styles": "^4.1.0",
@ -5628,28 +5628,28 @@
"dev": true "dev": true
}, },
"csso": { "csso": {
"version": "4.1.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/csso/-/csso-4.1.0.tgz", "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz",
"integrity": "sha512-h+6w/W1WqXaJA4tb1dk7r5tVbOm97MsKxzwnvOR04UQ6GILroryjMWu3pmCCtL2mLaEStQ0fZgeGiy99mo7iyg==", "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==",
"dev": true, "dev": true,
"requires": { "requires": {
"css-tree": "^1.0.0" "css-tree": "^1.1.2"
}, },
"dependencies": { "dependencies": {
"css-tree": { "css-tree": {
"version": "1.0.0", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0.tgz", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.2.tgz",
"integrity": "sha512-CdVYz/Yuqw0VdKhXPBIgi8DO3NicJVYZNWeX9XcIuSp9ZoFT5IcleVRW07O5rMjdcx1mb+MEJPknTTEW7DdsYw==", "integrity": "sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"mdn-data": "2.0.12", "mdn-data": "2.0.14",
"source-map": "^0.6.1" "source-map": "^0.6.1"
} }
}, },
"mdn-data": { "mdn-data": {
"version": "2.0.12", "version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.12.tgz", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-ULbAlgzVb8IqZ0Hsxm6hHSlQl3Jckst2YEQS7fODu9ilNWy2LvcoSY7TRFIktABP2mdppBioc66va90T+NUs8Q==", "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
"dev": true "dev": true
}, },
"source-map": { "source-map": {
@ -6089,9 +6089,9 @@
}, },
"dependencies": { "dependencies": {
"domelementtype": { "domelementtype": {
"version": "2.0.2", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.2.tgz", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz",
"integrity": "sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA==", "integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==",
"dev": true "dev": true
} }
} }
@ -6620,9 +6620,9 @@
} }
}, },
"eslint-plugin-prettier": { "eslint-plugin-prettier": {
"version": "3.1.4", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.2.0.tgz",
"integrity": "sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg==", "integrity": "sha512-kOUSJnFjAUFKwVxuzy6sA5yyMx6+o9ino4gCdShzBNx4eyFRudWRYKCFolKjoM40PEiuU6Cn7wBLfq3WsGg7qg==",
"dev": true, "dev": true,
"requires": { "requires": {
"prettier-linter-helpers": "^1.0.0" "prettier-linter-helpers": "^1.0.0"
@ -8216,9 +8216,9 @@
"dev": true "dev": true
}, },
"highlight.js": { "highlight.js": {
"version": "9.18.3", "version": "10.4.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.3.tgz", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.4.1.tgz",
"integrity": "sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ==", "integrity": "sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg==",
"dev": true "dev": true
}, },
"hmac-drbg": { "hmac-drbg": {
@ -9521,9 +9521,9 @@
} }
}, },
"loglevel": { "loglevel": {
"version": "1.7.0", "version": "1.7.1",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.0.tgz", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz",
"integrity": "sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ==", "integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==",
"dev": true "dev": true
}, },
"loose-envify": { "loose-envify": {
@ -10206,11 +10206,49 @@
"dev": true "dev": true
}, },
"object-is": { "object-is": {
"version": "1.1.3", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.3.tgz", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.4.tgz",
"integrity": "sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==", "integrity": "sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg==",
"dev": true, "dev": true,
"requires": { "requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3"
}
},
"object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true
},
"object-visit": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
"integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
"dev": true,
"requires": {
"isobject": "^3.0.0"
}
},
"object.assign": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
"integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
"dev": true,
"requires": {
"define-properties": "^1.1.2",
"function-bind": "^1.1.1",
"has-symbols": "^1.0.0",
"object-keys": "^1.0.11"
}
},
"object.getownpropertydescriptors": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz",
"integrity": "sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==",
"dev": true,
"requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3", "define-properties": "^1.1.3",
"es-abstract": "^1.18.0-next.1" "es-abstract": "^1.18.0-next.1"
}, },
@ -10251,9 +10289,9 @@
} }
}, },
"object-inspect": { "object-inspect": {
"version": "1.8.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz",
"integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==",
"dev": true "dev": true
}, },
"object.assign": { "object.assign": {
@ -10270,43 +10308,6 @@
} }
} }
}, },
"object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true
},
"object-visit": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
"integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
"dev": true,
"requires": {
"isobject": "^3.0.0"
}
},
"object.assign": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
"integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
"dev": true,
"requires": {
"define-properties": "^1.1.2",
"function-bind": "^1.1.1",
"has-symbols": "^1.0.0",
"object-keys": "^1.0.11"
}
},
"object.getownpropertydescriptors": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz",
"integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==",
"dev": true,
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.0-next.1"
}
},
"object.pick": { "object.pick": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
@ -10317,15 +10318,70 @@
} }
}, },
"object.values": { "object.values": {
"version": "1.1.1", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.2.tgz",
"integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", "integrity": "sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag==",
"dev": true, "dev": true,
"requires": { "requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3", "define-properties": "^1.1.3",
"es-abstract": "^1.17.0-next.1", "es-abstract": "^1.18.0-next.1",
"function-bind": "^1.1.1",
"has": "^1.0.3" "has": "^1.0.3"
},
"dependencies": {
"es-abstract": {
"version": "1.18.0-next.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
"integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
"dev": true,
"requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
"is-negative-zero": "^2.0.0",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
},
"is-callable": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
"integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==",
"dev": true
},
"is-regex": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
"integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
"dev": true,
"requires": {
"has-symbols": "^1.0.1"
}
},
"object-inspect": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz",
"integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==",
"dev": true
},
"object.assign": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
"integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
"dev": true,
"requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3",
"has-symbols": "^1.0.1",
"object-keys": "^1.1.1"
}
}
} }
}, },
"obuf": { "obuf": {
@ -10561,12 +10617,20 @@
"dev": true "dev": true
}, },
"parse5-htmlparser2-tree-adapter": { "parse5-htmlparser2-tree-adapter": {
"version": "5.1.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-5.1.1.tgz", "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
"integrity": "sha512-CF+TKjXqoqyDwHqBhFQ+3l5t83xYi6fVT1tQNg+Ye0JRLnTxWvIroCjEp1A0k4lneHNBGnICUf0cfYVYGEazqw==", "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==",
"dev": true, "dev": true,
"requires": { "requires": {
"parse5": "^5.1.1" "parse5": "^6.0.1"
},
"dependencies": {
"parse5": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
"dev": true
}
} }
}, },
"parseurl": { "parseurl": {
@ -10783,9 +10847,9 @@
}, },
"dependencies": { "dependencies": {
"debug": { "debug": {
"version": "3.2.6", "version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"ms": "^2.1.1" "ms": "^2.1.1"
@ -12452,9 +12516,9 @@
}, },
"dependencies": { "dependencies": {
"debug": { "debug": {
"version": "3.2.6", "version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"ms": "^2.1.1" "ms": "^2.1.1"
@ -12770,68 +12834,13 @@
} }
}, },
"string.prototype.trimend": { "string.prototype.trimend": {
"version": "1.0.2", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz",
"integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==", "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==",
"dev": true, "dev": true,
"requires": { "requires": {
"define-properties": "^1.1.3", "call-bind": "^1.0.0",
"es-abstract": "^1.18.0-next.1" "define-properties": "^1.1.3"
},
"dependencies": {
"es-abstract": {
"version": "1.18.0-next.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
"integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
"dev": true,
"requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
"is-negative-zero": "^2.0.0",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
},
"is-callable": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
"integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==",
"dev": true
},
"is-regex": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
"integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
"dev": true,
"requires": {
"has-symbols": "^1.0.1"
}
},
"object-inspect": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
"integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==",
"dev": true
},
"object.assign": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
"integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
"dev": true,
"requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3",
"has-symbols": "^1.0.1",
"object-keys": "^1.1.1"
}
}
} }
}, },
"string.prototype.trimleft": { "string.prototype.trimleft": {
@ -12855,68 +12864,13 @@
} }
}, },
"string.prototype.trimstart": { "string.prototype.trimstart": {
"version": "1.0.2", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz",
"integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==", "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==",
"dev": true, "dev": true,
"requires": { "requires": {
"define-properties": "^1.1.3", "call-bind": "^1.0.0",
"es-abstract": "^1.18.0-next.1" "define-properties": "^1.1.3"
},
"dependencies": {
"es-abstract": {
"version": "1.18.0-next.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
"integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
"dev": true,
"requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
"is-negative-zero": "^2.0.0",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
},
"is-callable": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
"integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==",
"dev": true
},
"is-regex": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
"integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
"dev": true,
"requires": {
"has-symbols": "^1.0.1"
}
},
"object-inspect": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
"integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==",
"dev": true
},
"object.assign": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
"integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
"dev": true,
"requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3",
"has-symbols": "^1.0.1",
"object-keys": "^1.1.1"
}
}
} }
}, },
"string_decoder": { "string_decoder": {
@ -13387,9 +13341,9 @@
} }
}, },
"uikit": { "uikit": {
"version": "3.5.9", "version": "3.5.10",
"resolved": "https://registry.npmjs.org/uikit/-/uikit-3.5.9.tgz", "resolved": "https://registry.npmjs.org/uikit/-/uikit-3.5.10.tgz",
"integrity": "sha512-0197WceeBMlCLscsmds6zqL8K3TOtdUCvoHPJEuYqYY2gKa7JttlA6MiyzU0yBPzDlcawXF2TdOmQ/DWy6ZDEw==", "integrity": "sha512-X+OUD02atmks1yYG9C3FSo1EcQ0xfV0+NCHspTShd2uMKYcC5LiMq6o/MyxBxx2bQSeKXh6Kh8TqQ/v4LWI4Tg==",
"dev": true "dev": true
}, },
"unicode-canonical-property-names-ecmascript": { "unicode-canonical-property-names-ecmascript": {
@ -13699,9 +13653,9 @@
} }
}, },
"object-inspect": { "object-inspect": {
"version": "1.8.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz",
"integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==",
"dev": true "dev": true
}, },
"object.assign": { "object.assign": {
@ -13915,9 +13869,9 @@
"dev": true "dev": true
}, },
"vuex": { "vuex": {
"version": "3.5.1", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.5.1.tgz", "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.0.tgz",
"integrity": "sha512-w7oJzmHQs0FM9LXodfskhw9wgKBiaB+totOdb8sNzbTB2KDCEEwEs29NzBZFh/lmEK1t5tDmM1vtsO7ubG1DFw==", "integrity": "sha512-W74OO2vCJPs9/YjNjW8lLbj+jzT24waTo2KShI8jLvJW8OaIkgb3wuAMA7D+ZiUxDOx3ubwSZTaJBip9G8a3aQ==",
"dev": true "dev": true
}, },
"watchpack": { "watchpack": {

View file

@ -18,25 +18,25 @@
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^3.12.1", "@vue/cli-plugin-babel": "^3.12.1",
"@vue/cli-plugin-eslint": "^4.5.9", "@vue/cli-plugin-eslint": "^4.5.9",
"@vue/cli-service": "^4.5.8", "@vue/cli-service": "^4.5.9",
"@vue/eslint-config-prettier": "^6.0.0", "@vue/eslint-config-prettier": "^6.0.0",
"axios": "^0.19.2", "axios": "^0.19.2",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"css-loader": "^3.6.0", "css-loader": "^3.6.0",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-plugin-prettier": "^3.1.4", "eslint-plugin-prettier": "^3.2.0",
"eslint-plugin-vue": "^6.2.2", "eslint-plugin-vue": "^6.2.2",
"less": "^3.12.2", "less": "^3.12.2",
"less-loader": "^5.0.0", "less-loader": "^5.0.0",
"mdns-js": "^1.0.3", "mdns-js": "^1.0.3",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"uikit": "^3.5.9", "uikit": "^3.5.10",
"vue": "^2.6.12", "vue": "^2.6.12",
"vue-friendly-iframe": "^0.17.0", "vue-friendly-iframe": "^0.17.0",
"vue-template-compiler": "^2.6.12", "vue-template-compiler": "^2.6.12",
"vue-tour": "^1.5.0", "vue-tour": "^1.5.0",
"vuejs-paginate": "^2.1.0", "vuejs-paginate": "^2.1.0",
"vuex": "^3.5.1" "vuex": "^3.6.0"
}, },
"postcss": { "postcss": {
"plugins": { "plugins": {

View file

@ -98,7 +98,9 @@ export default {
this.$emit("input", newSelected); this.$emit("input", newSelected);
// Move focus back to key textbox // Move focus back to key textbox
this.$refs.textboxKey.focus(); if (this.$refs.textboxKey) {
this.$refs.textboxKey.focus();
}
}, },
delMetadataKey: function(key) { delMetadataKey: function(key) {

View file

@ -198,7 +198,6 @@ export default {
if (!error) { if (!error) {
error = Error("Unknown error"); error = Error("Unknown error");
} }
this.$emit("error", error); this.$emit("error", error);
this.$emit("finished"); this.$emit("finished");
}) })

View file

@ -248,9 +248,8 @@
:submit-data="scanPayload" :submit-data="scanPayload"
:submit-label="'Start Scan'" :submit-label="'Start Scan'"
:button-primary="true" :button-primary="true"
@submit="onScanSubmit"
@response="onScanResponse" @response="onScanResponse"
@error="onScanError" @error="modalError"
> >
</taskSubmitter> </taskSubmitter>
</div> </div>
@ -433,14 +432,10 @@ export default {
}); });
}, },
onScanSubmit: function() {},
onScanResponse: function() { onScanResponse: function() {
this.modalNotify("Finished scan."); this.modalNotify("Finished scan.");
}, // Emit signal to update capture list
this.$root.$emit("globalUpdateCaptures");
onScanError: function(error) {
this.modalError(error);
} }
} }
}; };

View file

@ -119,7 +119,6 @@ export default {
deleteAll: function() { deleteAll: function() {
axios.all(this.allURLs.map(l => axios.delete(l))).then(() => { axios.all(this.allURLs.map(l => axios.delete(l))).then(() => {
console.log("Delete finished")
// Emit signal to update capture list // Emit signal to update capture list
this.$root.$emit("globalUpdateCaptures"); this.$root.$emit("globalUpdateCaptures");
}); });

View file

@ -18,7 +18,7 @@
:submit-label="'Create ZIP'" :submit-label="'Create ZIP'"
:submit-data="captureIds" :submit-data="captureIds"
@response="onResponse" @response="onResponse"
@error="onError" @error="modalError"
> >
</taskSubmitter> </taskSubmitter>
</div> </div>
@ -163,10 +163,6 @@ export default {
this.lastSessionId = response.output.id; this.lastSessionId = response.output.id;
this.downloadUrl = `${this.zipGetterUri}/${this.lastSessionId}`; this.downloadUrl = `${this.zipGetterUri}/${this.lastSessionId}`;
this.downloadReady = true; this.downloadReady = true;
},
onError: function(error) {
this.modalError(error); // Let mixin handle error
} }
} }
}; };

View file

@ -197,7 +197,7 @@ export default {
var captures = []; var captures = [];
for (var capture of this.captures) { for (var capture of this.captures) {
// Add to capture list if matched // Add to capture list if matched
if (!capture.dataset) { if (!this.isDatasetPopulated(capture.dataset)) {
captures.push(capture); captures.push(capture);
} }
} }
@ -212,7 +212,7 @@ export default {
for (var capture of this.captures) { for (var capture of this.captures) {
var dataset = capture.dataset; var dataset = capture.dataset;
if (dataset) { if (this.isDatasetPopulated(dataset)) {
var id = dataset["id"]; var id = dataset["id"];
// If this scan ID hasn't been seen before // If this scan ID hasn't been seen before
@ -361,6 +361,18 @@ export default {
} }
}, },
isDatasetPopulated: function(dataset) {
if (
!dataset || // If no dataset key
(dataset.constructor === Object && // Or dataset is an object...
Object.keys(dataset).length === 0) // ...but it's empty
) {
return false;
} else {
return true;
}
},
filterCaptures: function(list, filterTags) { filterCaptures: function(list, filterTags) {
// Filter a list of captures by an array of tags // Filter a list of captures by an array of tags
var result = []; var result = [];

View file

@ -11,7 +11,11 @@
<!-- Left side controls --> <!-- Left side controls -->
<div <div
class="uk-navbar-left uk-padding-remove-top uk-padding-remove-bottom" class="uk-navbar-left uk-padding-remove-top uk-padding-remove-bottom"
></div> >
<select v-model="filterLevel" class="uk-select">
<option v-for="level in allLevels" :key="level">{{ level }}</option>
</select>
</div>
<!-- Right side buttons --> <!-- Right side buttons -->
<div class="uk-navbar-right"> <div class="uk-navbar-right">
@ -40,15 +44,14 @@
v-for="item in pagedItems" v-for="item in pagedItems"
:key="item.timestamp" :key="item.timestamp"
uk-alert uk-alert
class="logging-entry"
:class="{ :class="{
'uk-alert-warning uk-alert': item.data.levelname == 'WARNING', 'uk-alert-warning uk-alert': item.data.levelname == 'WARNING',
'uk-alert-danger uk-alert': item.data.levelname == 'ERROR' 'uk-alert-danger uk-alert': item.data.levelname == 'ERROR'
}" }"
> >
<p> <b>{{ formatDateTime(item.data.created) }}</b>
<b>{{ formatDateTime(item.data.created) }}</b> <div class="logging-message">{{ formatMessage(item) }}</div>
</p>
{{ item.data.levelname }}: {{ item.data.message }}
</div> </div>
<Paginate <Paginate
@ -84,11 +87,28 @@ export default {
return { return {
maxitems: 20, maxitems: 20,
page: 1, page: 1,
logs: [] logs: [],
allLevels: ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
filterLevel: "WARNING"
}; };
}, },
computed: { computed: {
filteredLevels: function() {
let cutoffIndex = this.allLevels.indexOf(this.filterLevel);
return this.allLevels.slice(cutoffIndex, -1);
},
filteredItems: function() {
var items = [];
for (var item of this.logs) {
// Add to capture list if matched
if (this.filteredLevels.includes(item.data.levelname)) {
items.push(item);
}
}
return items;
},
loggingUri: function() { loggingUri: function() {
return `${this.$store.getters.baseUri}/api/v2/events/logging`; return `${this.$store.getters.baseUri}/api/v2/events/logging`;
}, },
@ -97,10 +117,10 @@ export default {
}, },
pagedItems: function() { pagedItems: function() {
let startIndex = (this.page - 1) * this.maxitems; let startIndex = (this.page - 1) * this.maxitems;
return this.logs.slice(startIndex, startIndex + this.maxitems); return this.filteredItems.slice(startIndex, startIndex + this.maxitems);
}, },
numberOfPages: function() { numberOfPages: function() {
return Math.floor(this.logs.length / this.maxitems); return Math.floor(this.filteredItems.length / this.maxitems);
} }
}, },
@ -134,6 +154,9 @@ export default {
formatDateTime: function(isoDateTimeString) { formatDateTime: function(isoDateTimeString) {
let date = new Date(isoDateTimeString); let date = new Date(isoDateTimeString);
return date.toLocaleDateString() + " " + date.toLocaleTimeString(); return date.toLocaleDateString() + " " + date.toLocaleTimeString();
},
formatMessage: function(item) {
return item.data.levelname + ": " + item.data.message;
} }
} }
}; };
@ -147,4 +170,10 @@ export default {
margin-bottom: 30px; margin-bottom: 30px;
height: 80px; height: 80px;
} }
.logging-entry {
white-space: break-spaces;
}
.logging-message {
font-family: monospace;
}
</style> </style>

View file

@ -148,6 +148,7 @@
:submit-on-event="'globalFastAutofocusEvent'" :submit-on-event="'globalFastAutofocusEvent'"
@taskStarted="isAutofocusing = 1" @taskStarted="isAutofocusing = 1"
@finished="isAutofocusing = 0" @finished="isAutofocusing = 0"
@error="modalError"
></taskSubmitter> ></taskSubmitter>
</div> </div>
@ -160,6 +161,7 @@
:button-primary="false" :button-primary="false"
@taskStarted="isAutofocusing = 2" @taskStarted="isAutofocusing = 2"
@finished="isAutofocusing = 0" @finished="isAutofocusing = 0"
@error="modalError"
></taskSubmitter> ></taskSubmitter>
</div> </div>
@ -172,6 +174,7 @@
:button-primary="false" :button-primary="false"
@taskStarted="isAutofocusing = 3" @taskStarted="isAutofocusing = 3"
@finished="isAutofocusing = 0" @finished="isAutofocusing = 0"
@error="modalError"
></taskSubmitter> ></taskSubmitter>
</div> </div>
</div> </div>

View file

@ -12,7 +12,7 @@
:submit-url="recalibrationLinks.calibrate_xy.href" :submit-url="recalibrationLinks.calibrate_xy.href"
:submit-label="'Auto-Calibrate using camera'" :submit-label="'Auto-Calibrate using camera'"
@response="onRecalibrateResponse" @response="onRecalibrateResponse"
@error="onRecalibrateError" @error="modalError"
> >
</taskSubmitter> </taskSubmitter>
</div> </div>
@ -149,10 +149,6 @@ export default {
this.modalNotify("Finished stage-to-camera calibration."); this.modalNotify("Finished stage-to-camera calibration.");
// Update local settings // Update local settings
this.updateSettings(); this.updateSettings();
},
onRecalibrateError: function(error) {
this.modalError(error); // Let mixin handle error
} }
} }
}; };

View file

@ -11,7 +11,7 @@
:submit-url="recalibrationLinks.recalibrate.href" :submit-url="recalibrationLinks.recalibrate.href"
:submit-label="'Auto-Calibrate'" :submit-label="'Auto-Calibrate'"
@response="onRecalibrateResponse" @response="onRecalibrateResponse"
@error="onRecalibrateError" @error="modalError"
> >
</taskSubmitter> </taskSubmitter>
</div> </div>
@ -128,10 +128,6 @@ export default {
this.modalNotify("Finished recalibration."); this.modalNotify("Finished recalibration.");
}, },
onRecalibrateError: function(error) {
this.modalError(error); // Let mixin handle error
},
flattenLensShadingTableRequest: function() { flattenLensShadingTableRequest: function() {
axios.post(this.recalibrationLinks.flatten_lens_shading_table.href); axios.post(this.recalibrationLinks.flatten_lens_shading_table.href);
}, },

View file

@ -95,7 +95,7 @@
submit-label="Start scan" submit-label="Start scan"
@submit="scanRunning = true" @submit="scanRunning = true"
@response="scanRunning = false" @response="scanRunning = false"
@error="scanRunning = false" @error="onScanError"
></taskSubmitter> ></taskSubmitter>
<br /> <br />
@ -233,6 +233,11 @@ export default {
}); });
}, },
onScanError: function(error) {
this.scanRunning = false;
this.modalError(error);
},
decrement: function() { decrement: function() {
if (this.stepValue > 0) { if (this.stepValue > 0) {
this.stepValue = this.stepValue - 1; this.stepValue = this.stepValue - 1;

View file

@ -79,19 +79,25 @@ class CaptureSchema(ImageSchema):
"href": url_for(CaptureView.endpoint, id_=capture_id, _external=True), "href": url_for(CaptureView.endpoint, id_=capture_id, _external=True),
"mimetype": "application/json", "mimetype": "application/json",
**description_from_view(CaptureView), **description_from_view(CaptureView),
}, }
if CaptureView.endpoint
else {},
"tags": { "tags": {
"href": url_for(CaptureTags.endpoint, id_=capture_id, _external=True), "href": url_for(CaptureTags.endpoint, id_=capture_id, _external=True),
"mimetype": "application/json", "mimetype": "application/json",
**description_from_view(CaptureTags), **description_from_view(CaptureTags),
}, }
if CaptureTags.endpoint
else {},
"annotations": { "annotations": {
"href": url_for( "href": url_for(
CaptureAnnotations.endpoint, id_=capture_id, _external=True CaptureAnnotations.endpoint, id_=capture_id, _external=True
), ),
"mimetype": "application/json", "mimetype": "application/json",
**description_from_view(CaptureAnnotations), **description_from_view(CaptureAnnotations),
}, }
if CaptureAnnotations.endpoint
else {},
"download": { "download": {
"href": url_for( "href": url_for(
CaptureDownload.endpoint, CaptureDownload.endpoint,
@ -101,7 +107,9 @@ class CaptureSchema(ImageSchema):
), ),
"mimetype": "image/jpeg", "mimetype": "image/jpeg",
**description_from_view(CaptureDownload), **description_from_view(CaptureDownload),
}, }
if CaptureDownload.endpoint
else {},
} }
if isinstance(data, dict): if isinstance(data, dict):

View file

@ -3,14 +3,16 @@ import io
import logging import logging
import time import time
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from collections import namedtuple
from types import TracebackType from types import TracebackType
from typing import BinaryIO, List, Optional, Tuple, Type, Union from typing import BinaryIO, List, NamedTuple, Optional, Tuple, Type, Union
from labthings import ClientEvent, StrictLock from labthings import ClientEvent, StrictLock
# Class to store a frames metadata # Class to store a frames metadata
TrackerFrame = namedtuple("TrackerFrame", ["size", "time"]) class TrackerFrame(NamedTuple):
size: int
time: float
class FrameStream(io.BytesIO): class FrameStream(io.BytesIO):
@ -170,6 +172,21 @@ class BaseCamera(metaclass=ABCMeta):
thumbnail: Dimensions and quality (x, y, quality) of a thumbnail to generate, if supported thumbnail: Dimensions and quality (x, y, quality) of a thumbnail to generate, if supported
""" """
def start_worker(self, **_) -> bool:
"""Start the background camera thread if it isn't running yet."""
logging.warning(
"`start_worker` method has been deprecated and is no longer required. Please avoid calling this method."
)
return True
def get_frame(self) -> bytes:
"""
Return the current camera frame.
Just an alias of self.stream.getframe()
"""
return self.stream.getframe()
def __enter__(self): def __enter__(self):
"""Create camera on context enter.""" """Create camera on context enter."""
return self return self

View file

@ -178,7 +178,7 @@ class MissingCamera(BaseCamera):
""" """
Change the camera zoom, handling re-centering and scaling. Change the camera zoom, handling re-centering and scaling.
""" """
logging.warning("Zoom not implemented in mock camera") logging.info("Zoom not implemented in mock camera")
def start_stream(self): def start_stream(self):
pass pass
@ -188,11 +188,11 @@ class MissingCamera(BaseCamera):
def start_preview(self, *_, **__): def start_preview(self, *_, **__):
"""Start the on board GPU camera preview.""" """Start the on board GPU camera preview."""
logging.warning("GPU preview not implemented in mock camera") logging.info("GPU preview not implemented in mock camera")
def stop_preview(self): def stop_preview(self):
"""Stop the on board GPU camera preview.""" """Stop the on board GPU camera preview."""
logging.warning("GPU preview not implemented in mock camera") logging.info("GPU preview not implemented in mock camera")
def start_recording(self, *_, **__): def start_recording(self, *_, **__):
"""Start recording. """Start recording.

View file

@ -22,7 +22,7 @@ try:
except Exception as e: # pylint: disable=W0703 except Exception as e: # pylint: disable=W0703
logging.error(e) logging.error(e)
logging.warning("Unable to import PiCameraStreamer") logging.warning("Unable to import PiCameraStreamer")
from labthings import CompositeLock, StrictLock from labthings import CompositeLock
from openflexure_microscope.config import user_configuration, user_settings from openflexure_microscope.config import user_configuration, user_settings
@ -48,7 +48,7 @@ class Microscope:
# Initialise with an empty composite lock # Initialise with an empty composite lock
#: :py:class:`labthings.CompositeLock`: Composite lock for locking both camera and stage #: :py:class:`labthings.CompositeLock`: Composite lock for locking both camera and stage
self.lock: Union[CompositeLock, StrictLock] = CompositeLock([]) self.lock: CompositeLock = CompositeLock([])
self.camera: BaseCamera = None #: Currently connected camera object self.camera: BaseCamera = None #: Currently connected camera object
self.stage: BaseStage = None #: Currently connected stage object self.stage: BaseStage = None #: Currently connected stage object

View file

@ -5,6 +5,8 @@ import numpy as np
from labthings import StrictLock from labthings import StrictLock
from typing_extensions import Literal from typing_extensions import Literal
CoordinateType = Tuple[int, int, int]
class BaseStage(metaclass=ABCMeta): class BaseStage(metaclass=ABCMeta):
""" """
@ -41,7 +43,7 @@ class BaseStage(metaclass=ABCMeta):
@property @property
@abstractmethod @abstractmethod
def position(self) -> Tuple[int, int, int]: def position(self) -> CoordinateType:
"""The current position, as a list""" """The current position, as a list"""
@property @property
@ -64,7 +66,7 @@ class BaseStage(metaclass=ABCMeta):
@abstractmethod @abstractmethod
def move_rel( def move_rel(
self, self,
displacement: Union[int, Tuple[int, int, int]], displacement: Union[int, CoordinateType],
axis: Optional[Literal["x", "y", "z"]] = None, axis: Optional[Literal["x", "y", "z"]] = None,
backlash: bool = True, backlash: bool = True,
): ):
@ -74,7 +76,7 @@ class BaseStage(metaclass=ABCMeta):
""" """
@abstractmethod @abstractmethod
def move_abs(self, final: Tuple[int, int, int], **kwargs): def move_abs(self, final: CoordinateType, **kwargs):
"""Make an absolute move to a position""" """Make an absolute move to a position"""
@abstractmethod @abstractmethod
@ -87,7 +89,7 @@ class BaseStage(metaclass=ABCMeta):
def scan_linear( def scan_linear(
self, self,
rel_positions: List[Tuple[int, int, int]], rel_positions: List[CoordinateType],
backlash: bool = True, backlash: bool = True,
return_to_start: bool = True, return_to_start: bool = True,
): ):

View file

@ -1,6 +1,7 @@
import logging import logging
import time import time
from collections.abc import Iterable from collections.abc import Iterable
from types import GeneratorType
from typing import Optional, Tuple, Union from typing import Optional, Tuple, Union
import numpy as np import numpy as np
@ -11,6 +12,19 @@ from openflexure_microscope.stage.base import BaseStage
from openflexure_microscope.utilities import axes_to_array from openflexure_microscope.utilities import axes_to_array
def _displacement_to_array(
displacement: int, axis: Literal["x", "y", "z"]
) -> np.ndarray:
# Create the displacement array
return np.array(
[
displacement if axis == "x" else 0,
displacement if axis == "y" else 0,
displacement if axis == "z" else 0,
]
)
class SangaStage(BaseStage): class SangaStage(BaseStage):
""" """
Sangaboard v0.2 and v0.3 powered Stage object Sangaboard v0.2 and v0.3 powered Stage object
@ -30,11 +44,9 @@ class SangaStage(BaseStage):
self.port = port self.port = port
self.board = Sangaboard(port, **kwargs) self.board = Sangaboard(port, **kwargs)
self._backlash = ( # Initialise backlash storage, used by property setter/getter
None # Initialise backlash storage, used by property setter/getter self._backlash = None
)
self.settle_time = 0.2 # Default move settle time self.settle_time = 0.2 # Default move settle time
self._position_on_enter = None self._position_on_enter = None
@property @property
@ -56,11 +68,11 @@ class SangaStage(BaseStage):
return 3 return 3
@property @property
def position(self): def position(self) -> Tuple[int, int, int]:
return self.board.position return self.board.position
@property @property
def backlash(self): def backlash(self) -> np.ndarray:
"""The distance used for backlash compensation. """The distance used for backlash compensation.
Software backlash compensation is enabled by setting this property to a value Software backlash compensation is enabled by setting this property to a value
other than `None`. The value can either be an array-like object (list, tuple, other than `None`. The value can either be an array-like object (list, tuple,
@ -75,8 +87,12 @@ class SangaStage(BaseStage):
back by ``backlash[i]``. This is computed per-axis, so if some axes are moving back by ``backlash[i]``. This is computed per-axis, so if some axes are moving
in the same direction as ``backlash``, they won't do two moves. in the same direction as ``backlash``, they won't do two moves.
""" """
if self._backlash is not None: if isinstance(self._backlash, np.ndarray):
return self._backlash return self._backlash
elif isinstance(self._backlash, list):
return np.array(self._backlash)
elif isinstance(self._backlash, int):
return np.array([self._backlash] * self.n_axes)
else: else:
return np.array([0] * self.n_axes) return np.array([0] * self.n_axes)
@ -98,13 +114,16 @@ class SangaStage(BaseStage):
if "backlash" in config: if "backlash" in config:
# Construct backlash array # Construct backlash array
backlash = axes_to_array(config["backlash"], ["x", "y", "z"], [0, 0, 0]) backlash = axes_to_array(config["backlash"], ["x", "y", "z"], [0, 0, 0])
self.backlash = backlash self.backlash = np.array(backlash)
if "settle_time" in config: if "settle_time" in config:
self.settle_time = config.get("settle_time") self.settle_time = config.get("settle_time")
def read_settings(self) -> dict: def read_settings(self) -> dict:
"""Return the current settings as a dictionary""" """Return the current settings as a dictionary"""
blsh = self.backlash.tolist() if self.backlash is not None:
blsh = self.backlash.tolist()
else:
blsh = None
config = { config = {
"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]}, "backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]},
"settle_time": self.settle_time, "settle_time": self.settle_time,
@ -114,7 +133,7 @@ class SangaStage(BaseStage):
def move_rel( def move_rel(
self, self,
displacement: Union[int, Tuple[int, int, int]], displacement: Union[int, Tuple[int, int, int], np.ndarray],
axis: Optional[Literal["x", "y", "z"]] = None, axis: Optional[Literal["x", "y", "z"]] = None,
backlash: bool = True, backlash: bool = True,
): ):
@ -122,11 +141,19 @@ class SangaStage(BaseStage):
displacement: integer or array/list of 3 integers displacement: integer or array/list of 3 integers
axis: None (for 3-axis moves) or one of 'x','y','z' axis: None (for 3-axis moves) or one of 'x','y','z'
backlash: (default: True) whether to correct for backlash. backlash: (default: True) whether to correct for backlash.
Backlash Correction:
This backlash correction strategy ensures we're always approaching the
end point from the same direction, while minimising the amount of extra
motion. It's a good option if you're scanning in a line, for example,
as it will kick in when moving to the start of the line, but not for each
point on the line.
For each axis where we're moving in the *opposite*
direction to self.backlash, we deliberately overshoot:
""" """
with self.lock: with self.lock:
logging.debug("Moving sangaboard by %s", displacement) logging.debug("Moving sangaboard by %s", displacement)
if not backlash or self.backlash is None:
return self.board.move_rel(displacement, axis=axis)
# If we specify an axis name and a displacement int, convert to a displacement tuple # If we specify an axis name and a displacement int, convert to a displacement tuple
if axis: if axis:
# Displacement MUST be an integer if axis name is specified # Displacement MUST be an integer if axis name is specified
@ -137,37 +164,42 @@ class SangaStage(BaseStage):
# Axis name MUST be x, y, or z # Axis name MUST be x, y, or z
if axis not in ("x", "y", "z"): if axis not in ("x", "y", "z"):
raise ValueError("axis must be one of x, y, or z") raise ValueError("axis must be one of x, y, or z")
move = ( # Calculate displacement array
displacement if axis == "x" else 0, displacement_array: np.ndarray = _displacement_to_array(
displacement if axis == "y" else 0, displacement, axis
displacement if axis == "z" else 0,
) )
displacement = move elif isinstance(displacement, np.ndarray):
displacement_array = displacement
elif isinstance(displacement, (list, tuple, GeneratorType)):
# Convert our displacement tuple/generator into a numpy array
displacement_array = np.array(list(displacement))
else:
raise TypeError(f"Unsupported displacement type {type(displacement)}")
initial_move = np.array(displacement, dtype=np.int) # Handle simple case, no backlash
# Backlash Correction if not backlash or self.backlash is None:
# This backlash correction strategy ensures we're always approaching the return self.board.move_rel(displacement_array)
# end point from the same direction, while minimising the amount of extra
# motion. It's a good option if you're scanning in a line, for example, # Handle move with backlash correction
# as it will kick in when moving to the start of the line, but not for each # Calculate main movement
# point on the line. initial_move: np.ndarray = displacement_array
# For each axis where we're moving in the *opposite*
# direction to self.backlash, we deliberately overshoot:
initial_move -= np.where( initial_move -= np.where(
self.backlash * displacement < 0, self.backlash * displacement_array < 0,
self.backlash, self.backlash,
np.zeros(self.n_axes, dtype=self.backlash.dtype), np.zeros(self.n_axes, dtype=self.backlash.dtype),
) )
# Make the main movement
self.board.move_rel(initial_move) self.board.move_rel(initial_move)
if np.any(displacement - initial_move != 0): # Handle backlash if required
if np.any(displacement_array - initial_move != 0):
# If backlash correction has kicked in and made us overshoot, move # If backlash correction has kicked in and made us overshoot, move
# to the correct end position (i.e. the move we were asked to make) # to the correct end position (i.e. the move we were asked to make)
self.board.move_rel(displacement - initial_move) self.board.move_rel(displacement_array - initial_move)
# Settle outside of the stage lock so that another move request # Settle outside of the stage lock so that another move request
# can just take over before settling # can just take over before settling
time.sleep(self.settle_time) time.sleep(self.settle_time)
def move_abs(self, final: Tuple[int, int, int], **kwargs): def move_abs(self, final: Union[Tuple[int, int, int], np.ndarray], **kwargs):
"""Make an absolute move to a position """Make an absolute move to a position
""" """
with self.lock: with self.lock:
@ -220,15 +252,21 @@ class SangaStage(BaseStage):
class SangaDeltaStage(SangaStage): class SangaDeltaStage(SangaStage):
def __init__( def __init__(
self, port=None, flex_h=80, flex_a=50, flex_b=50, camera_angle=0, **kwargs self,
port: Optional[str] = None,
flex_h: int = 80,
flex_a: int = 50,
flex_b: int = 50,
camera_angle: float = 0,
**kwargs,
): ):
self.flex_h = flex_h self.flex_h: int = flex_h
self.flex_a = flex_a self.flex_a: int = flex_a
self.flex_b = flex_b self.flex_b: int = flex_b
# Set up camera rotation relative to stage # Set up camera rotation relative to stage
camera_theta = (camera_angle / 180) * np.pi camera_theta: float = (camera_angle / 180) * np.pi
self.R_camera = np.array( self.R_camera: np.ndarray = np.array(
[ [
[np.cos(camera_theta), -np.sin(camera_theta), 0], [np.cos(camera_theta), -np.sin(camera_theta), 0],
[np.sin(camera_theta), np.cos(camera_theta), 0], [np.sin(camera_theta), np.cos(camera_theta), 0],
@ -239,13 +277,15 @@ class SangaDeltaStage(SangaStage):
logging.debug(self.R_camera) logging.debug(self.R_camera)
# Transformation matrix converting delta into cartesian # Transformation matrix converting delta into cartesian
x_fac = -1 * np.multiply( x_fac: np.float = -1 * np.multiply(
np.divide(2, np.sqrt(3)), np.divide(self.flex_b, self.flex_h) np.divide(2, np.sqrt(3)), np.divide(self.flex_b, self.flex_h)
) )
y_fac = -1 * np.divide(self.flex_b, self.flex_h) y_fac: np.float = -1 * np.divide(self.flex_b, self.flex_h)
z_fac = np.multiply(np.divide(1, 3), np.divide(self.flex_b, self.flex_a)) z_fac: np.float = np.multiply(
np.divide(1, 3), np.divide(self.flex_b, self.flex_a)
)
self.Tvd = np.array( self.Tvd: np.ndarray = np.array(
[ [
[-x_fac, x_fac, 0], [-x_fac, x_fac, 0],
[0.5 * y_fac, 0.5 * y_fac, -y_fac], [0.5 * y_fac, 0.5 * y_fac, -y_fac],
@ -254,40 +294,76 @@ class SangaDeltaStage(SangaStage):
) )
logging.debug(self.Tvd) logging.debug(self.Tvd)
self.Tdv = np.linalg.inv(self.Tvd) self.Tdv: np.ndarray = np.linalg.inv(self.Tvd)
logging.debug(self.Tdv) logging.debug(self.Tdv)
SangaStage.__init__(self, port=port, **kwargs) SangaStage.__init__(self, port=port, **kwargs)
@property
def raw_position(self) -> Tuple[int, int, int]:
return self.board.position
@property @property
def position(self): def position(self):
# TODO: Account for camera rotation # TODO: Account for camera rotation
position = np.dot(self.Tvd, self.board.position) position: np.ndarray = np.dot(self.Tvd, self.raw_position)
position = np.dot(np.linalg.inv(self.R_camera), position) position: np.ndarray = np.dot(np.linalg.inv(self.R_camera), position)
return [int(p) for p in position] return [int(p) for p in position]
def move_rel(self, displacement, axis=None, backlash=True): def move_rel(
self,
displacement: Union[int, Tuple[int, int, int], np.ndarray],
axis: Optional[Literal["x", "y", "z"]] = None,
backlash: bool = True,
):
# If we specify an axis name and a displacement int, convert to a displacement tuple
if axis:
# Displacement MUST be an integer if axis name is specified
if not isinstance(displacement, int):
raise TypeError(
"Displacement must be an integer when axis is specified"
)
# Axis name MUST be x, y, or z
if axis not in ("x", "y", "z"):
raise ValueError("axis must be one of x, y, or z")
# Calculate displacement array
cartesian_displacement_array: np.ndarray = _displacement_to_array(
displacement, axis
)
elif isinstance(displacement, np.ndarray):
cartesian_displacement_array = displacement
elif isinstance(displacement, (list, tuple, GeneratorType)):
# Convert our displacement tuple/generator into a numpy array
cartesian_displacement_array = np.array(list(displacement))
else:
raise TypeError(f"Unsupported displacement type {type(displacement)}")
# Transform into camera coordinates # Transform into camera coordinates
displacement = np.dot(self.R_camera, displacement) camera_displacement_array: np.ndarray = np.dot(
self.R_camera, cartesian_displacement_array
)
# Transform into delta coordinates # Transform into delta coordinates
displacement = np.dot(self.Tdv, displacement) delta_displacement_array: np.ndarray = np.dot(
self.Tdv, camera_displacement_array
)
logging.debug("Delta displacement: %s", (displacement)) logging.debug("Delta displacement: %s", (delta_displacement_array))
# Do the move # Do the move
SangaStage.move_rel(self, displacement, axis=None, backlash=backlash) SangaStage.move_rel(
self, delta_displacement_array, axis=None, backlash=backlash
)
def move_abs(self, final, **kwargs): def move_abs(self, final: Union[Tuple[int, int, int], np.ndarray], **kwargs):
# Transform into camera coordinates # Transform into camera coordinates
final = np.dot(self.R_camera, final) camera_final_array: np.ndarray = np.dot(self.R_camera, final)
# Transform into delta coordinates # Transform into delta coordinates
final = np.dot(self.Tdv, final) delta_final_array: np.ndarray = np.dot(self.Tdv, camera_final_array)
logging.debug("Delta final: %s", (final)) logging.debug("Delta final: %s", (final))
# Do the move # Do the move
SangaStage.move_abs(self, final, **kwargs) SangaStage.move_abs(self, delta_final_array, **kwargs)

View file

@ -1,18 +1,25 @@
import base64 import base64
import copy import copy
import logging import logging
import sys
import time import time
from contextlib import contextmanager from contextlib import contextmanager
from typing import Dict, List, Optional from typing import Dict, List, Optional, Tuple, Type, Union
import numpy as np import numpy as np
# TypedDict was added to typing in 3.8. Use typing_extensions for <3.8
if sys.version_info >= (3, 8):
from typing import TypedDict # pylint: disable=no-name-in-module
else:
from typing_extensions import TypedDict
class Timer(object): class Timer(object):
def __init__(self, name): def __init__(self, name: str):
self.name = name self.name: str = name
self.start = None self.start: Optional[float] = None
self.end = None self.end: Optional[float] = None
def __enter__(self): def __enter__(self):
self.start = time.time() self.start = time.time()
@ -22,19 +29,27 @@ class Timer(object):
logging.debug("%s time: %s", self.name, self.end - self.start) logging.debug("%s time: %s", self.name, self.end - self.start)
def deserialise_array_b64(b64_string: str, dtype: str, shape: List[int]): JSONArrayType = TypedDict(
flat_arr = np.frombuffer(base64.b64decode(b64_string), dtype) "JSONArrayType",
{"@type": str, "base64": str, "dtype": str, "shape": Tuple[int, ...]},
)
def deserialise_array_b64(
b64_string: str, dtype: Union[Type[np.dtype], str], shape: Tuple[int, ...]
):
flat_arr: np.ndarray = np.frombuffer(base64.b64decode(b64_string), dtype)
return flat_arr.reshape(shape) return flat_arr.reshape(shape)
def serialise_array_b64(npy_arr: np.ndarray): def serialise_array_b64(npy_arr: np.ndarray) -> Tuple[str, str, Tuple[int, ...]]:
b64_string = base64.b64encode(npy_arr).decode("ascii") b64_string: str = base64.b64encode(npy_arr.tobytes()).decode("ascii")
dtype = str(npy_arr.dtype) dtype: str = str(npy_arr.dtype)
shape = npy_arr.shape shape: Tuple[int, ...] = npy_arr.shape
return b64_string, dtype, shape return b64_string, dtype, shape
def ndarray_to_json(arr: np.ndarray): def ndarray_to_json(arr: np.ndarray) -> JSONArrayType:
if isinstance(arr, memoryview): if isinstance(arr, memoryview):
# We can transparently convert memoryview objects to arrays # We can transparently convert memoryview objects to arrays
# This comes in very handy for the lens shading table. # This comes in very handy for the lens shading table.
@ -43,7 +58,7 @@ def ndarray_to_json(arr: np.ndarray):
return {"@type": "ndarray", "dtype": dtype, "shape": shape, "base64": b64_string} return {"@type": "ndarray", "dtype": dtype, "shape": shape, "base64": b64_string}
def json_to_ndarray(json_dict: dict): def json_to_ndarray(json_dict: JSONArrayType):
if not json_dict.get("@type") != "ndarray": if not json_dict.get("@type") != "ndarray":
logging.warning("No valid @type attribute found. Conversion may fail.") logging.warning("No valid @type attribute found. Conversion may fail.")
for required_param in ("dtype", "shape", "base64"): for required_param in ("dtype", "shape", "base64"):
@ -52,7 +67,7 @@ def json_to_ndarray(json_dict: dict):
b64_string: Optional[str] = json_dict.get("base64") b64_string: Optional[str] = json_dict.get("base64")
dtype: Optional[str] = json_dict.get("dtype") dtype: Optional[str] = json_dict.get("dtype")
shape: Optional[List[int]] = json_dict.get("shape") shape: Optional[Tuple[int, ...]] = json_dict.get("shape")
if b64_string and dtype and shape: if b64_string and dtype and shape:
return deserialise_array_b64(b64_string, dtype, shape) return deserialise_array_b64(b64_string, dtype, shape)

81
poetry.lock generated
View file

@ -8,20 +8,20 @@ python-versions = "*"
[[package]] [[package]]
name = "apispec" name = "apispec"
version = "3.3.2" version = "4.0.0"
description = "A pluggable API specification generator. Currently supports the OpenAPI Specification (f.k.a. the Swagger specification)." description = "A pluggable API specification generator. Currently supports the OpenAPI Specification (f.k.a. the Swagger specification)."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.6"
[package.dependencies] [package.dependencies]
PyYAML = {version = ">=3.10", optional = true, markers = "extra == \"yaml\""} PyYAML = {version = ">=3.10", optional = true, markers = "extra == \"yaml\""}
[package.extras] [package.extras]
dev = ["PyYAML (>=3.10)", "prance[osv] (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock", "flake8 (==3.8.3)", "flake8-bugbear (==20.1.4)", "pre-commit (>=2.4,<3.0)", "tox"] dev = ["PyYAML (>=3.10)", "prance[osv] (>=0.11)", "marshmallow (>=3.0.0)", "pytest", "mock", "flake8 (==3.8.3)", "flake8-bugbear (==20.1.4)", "pre-commit (>=2.4,<3.0)", "tox"]
docs = ["marshmallow (>=2.19.2)", "pyyaml (==5.3.1)", "sphinx (==3.2.1)", "sphinx-issues (==1.2.0)", "sphinx-rtd-theme (==0.5.0)"] docs = ["marshmallow (>=3.0.0)", "pyyaml (==5.3.1)", "sphinx (==3.2.1)", "sphinx-issues (==1.2.0)", "sphinx-rtd-theme (==0.5.0)"]
lint = ["flake8 (==3.8.3)", "flake8-bugbear (==20.1.4)", "pre-commit (>=2.4,<3.0)"] lint = ["flake8 (==3.8.3)", "flake8-bugbear (==20.1.4)", "pre-commit (>=2.4,<3.0)"]
tests = ["PyYAML (>=3.10)", "prance[osv] (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock"] tests = ["PyYAML (>=3.10)", "prance[osv] (>=0.11)", "marshmallow (>=3.0.0)", "pytest", "mock"]
validation = ["prance[osv] (>=0.11)"] validation = ["prance[osv] (>=0.11)"]
yaml = ["PyYAML (>=3.10)"] yaml = ["PyYAML (>=3.10)"]
@ -278,7 +278,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]] [[package]]
name = "importlib-metadata" name = "importlib-metadata"
version = "3.1.0" version = "3.1.1"
description = "Read metadata from Python packages" description = "Read metadata from Python packages"
category = "main" category = "main"
optional = false optional = false
@ -288,8 +288,8 @@ python-versions = ">=3.6"
zipp = ">=0.5" zipp = ">=0.5"
[package.extras] [package.extras]
docs = ["sphinx", "rst.linker"] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["packaging", "pep517", "unittest2", "importlib-resources (>=1.3)"] testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
@ -336,28 +336,21 @@ i18n = ["Babel (>=0.8)"]
[[package]] [[package]]
name = "labthings" name = "labthings"
version = "1.1.5" version = "1.2.2"
description = "Python implementation of LabThings, based on the Flask microframework" description = "Python implementation of LabThings, based on the Flask microframework"
category = "main" category = "main"
optional = false optional = false
python-versions = "^3.6" python-versions = ">=3.6,<4.0"
develop = true
[package.dependencies] [package.dependencies]
apispec = "^3.2.0" apispec = ">=3.2,<5.0"
apispec_webframeworks = "^0.5.2" apispec_webframeworks = ">=0.5.2,<0.6.0"
Flask = "^1.1.1" Flask = ">=1.1.1,<2.0.0"
flask-cors = "^3.0.8" flask-cors = ">=3.0.8,<4.0.0"
marshmallow = "^3.4.0" marshmallow = ">=3.4.0,<4.0.0"
webargs = "^6.0.0" webargs = ">=6.0.0,<7.0.0"
zeroconf = ">=0.24.5,<0.29.0" zeroconf = ">=0.24.5,<0.29.0"
[package.source]
type = "git"
url = "https://github.com/labthings/python-labthings"
reference = "master"
resolved_reference = "e9ed2444c08e3854ebefdadaa753700401392c97"
[[package]] [[package]]
name = "lazy-object-proxy" name = "lazy-object-proxy"
version = "1.4.3" version = "1.4.3"
@ -428,6 +421,25 @@ category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
[[package]]
name = "numpy-stubs"
version = "0.0.1"
description = ""
category = "main"
optional = false
python-versions = "*"
develop = false
[package.dependencies]
numpy = ">=1.16.0"
typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
[package.source]
type = "git"
url = "https://github.com/numpy/numpy-stubs.git"
reference = "master"
resolved_reference = "c49d2d6875971a669a166ea93ef998911af283a1"
[[package]] [[package]]
name = "opencv-python-headless" name = "opencv-python-headless"
version = "4.4.0.44" version = "4.4.0.44"
@ -441,7 +453,7 @@ numpy = ">=1.17.3"
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "20.4" version = "20.7"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
category = "main" category = "main"
optional = false optional = false
@ -449,7 +461,6 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies] [package.dependencies]
pyparsing = ">=2.0.2" pyparsing = ">=2.0.2"
six = "*"
[[package]] [[package]]
name = "pastel" name = "pastel"
@ -956,7 +967,7 @@ rpi = ["RPi.GPIO"]
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.7.3" python-versions = "^3.7.3"
content-hash = "e2f1011751b49fc5837f475f5304822216724a89f877ee5daf9c281cd8830673" content-hash = "2c0faa244099ec3433edfc2cb9321f5de8bd5c0c32e135cae23f91e717cb07d7"
[metadata.files] [metadata.files]
alabaster = [ alabaster = [
@ -964,8 +975,8 @@ alabaster = [
{file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
] ]
apispec = [ apispec = [
{file = "apispec-3.3.2-py2.py3-none-any.whl", hash = "sha256:a1df9ec6b2cd0edf45039ef025abd7f0660808fa2edf737d3ba1cf5ef1a4625b"}, {file = "apispec-4.0.0-py2.py3-none-any.whl", hash = "sha256:20d271f7c8d130719be223fdb122af391ff8d59fb24958c793f632305b87f8ed"},
{file = "apispec-3.3.2.tar.gz", hash = "sha256:d23ebd5b71e541e031b02a19db10b5e6d5ef8452c552833e3e1afc836b40b1ad"}, {file = "apispec-4.0.0.tar.gz", hash = "sha256:360e28e5e84a4d7023b16de2b897327fe3da63ddc8e01f9165b9113b7fe1c48a"},
] ]
apispec-webframeworks = [ apispec-webframeworks = [
{file = "apispec-webframeworks-0.5.2.tar.gz", hash = "sha256:0db35b267914b3f8c562aca0261957dbcb4176f255eacc22520277010818dcf3"}, {file = "apispec-webframeworks-0.5.2.tar.gz", hash = "sha256:0db35b267914b3f8c562aca0261957dbcb4176f255eacc22520277010818dcf3"},
@ -1090,8 +1101,8 @@ imagesize = [
{file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"},
] ]
importlib-metadata = [ importlib-metadata = [
{file = "importlib_metadata-3.1.0-py2.py3-none-any.whl", hash = "sha256:590690d61efdd716ff82c39ca9a9d4209252adfe288a4b5721181050acbd4175"}, {file = "importlib_metadata-3.1.1-py3-none-any.whl", hash = "sha256:6112e21359ef8f344e7178aa5b72dc6e62b38b0d008e6d3cb212c5b84df72013"},
{file = "importlib_metadata-3.1.0.tar.gz", hash = "sha256:d9b8a46a0885337627a6430db287176970fff18ad421becec1d64cfc763c2099"}, {file = "importlib_metadata-3.1.1.tar.gz", hash = "sha256:b0c2d3b226157ae4517d9625decf63591461c66b3a808c2666d538946519d170"},
] ]
iniconfig = [ iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
@ -1109,7 +1120,10 @@ jinja2 = [
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
] ]
labthings = [] labthings = [
{file = "labthings-1.2.2-py3-none-any.whl", hash = "sha256:0f4fb76feed5ce9213d471f97700c4b52d9563f940954e1efb644889d3381d2d"},
{file = "labthings-1.2.2.tar.gz", hash = "sha256:62a8c5bceea25883a7f329a8e19bd95d211d4fe8ac7eb39ee2a2bd51befa4bd5"},
]
lazy-object-proxy = [ lazy-object-proxy = [
{file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"},
{file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"}, {file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"},
@ -1224,6 +1238,7 @@ numpy = [
{file = "numpy-1.19.2-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:0bfd85053d1e9f60234f28f63d4a5147ada7f432943c113a11afcf3e65d9d4c8"}, {file = "numpy-1.19.2-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:0bfd85053d1e9f60234f28f63d4a5147ada7f432943c113a11afcf3e65d9d4c8"},
{file = "numpy-1.19.2.zip", hash = "sha256:0d310730e1e793527065ad7dde736197b705d0e4c9999775f212b03c44a8484c"}, {file = "numpy-1.19.2.zip", hash = "sha256:0d310730e1e793527065ad7dde736197b705d0e4c9999775f212b03c44a8484c"},
] ]
numpy-stubs = []
opencv-python-headless = [ opencv-python-headless = [
{file = "opencv-python-headless-4.4.0.44.tar.gz", hash = "sha256:6eefcacfb9b2da305277e1a93c7bf074dcd10b7aa154a0c963ded08fc0ffc02e"}, {file = "opencv-python-headless-4.4.0.44.tar.gz", hash = "sha256:6eefcacfb9b2da305277e1a93c7bf074dcd10b7aa154a0c963ded08fc0ffc02e"},
{file = "opencv_python_headless-4.4.0.44-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:0896413b35b4b64acae42b84f740a458e0520d51f806946adf185e710a2ab300"}, {file = "opencv_python_headless-4.4.0.44-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:0896413b35b4b64acae42b84f740a458e0520d51f806946adf185e710a2ab300"},
@ -1243,8 +1258,8 @@ opencv-python-headless = [
{file = "opencv_python_headless-4.4.0.44-cp38-cp38-win_amd64.whl", hash = "sha256:93f251c28739d8e8ade8898ddcbd75dcec60f779d9534644c568e7e65e2b76de"}, {file = "opencv_python_headless-4.4.0.44-cp38-cp38-win_amd64.whl", hash = "sha256:93f251c28739d8e8ade8898ddcbd75dcec60f779d9534644c568e7e65e2b76de"},
] ]
packaging = [ packaging = [
{file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, {file = "packaging-20.7-py2.py3-none-any.whl", hash = "sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"},
{file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, {file = "packaging-20.7.tar.gz", hash = "sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236"},
] ]
pastel = [ pastel = [
{file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"},

View file

@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api"
[tool.poetry] [tool.poetry]
name = "openflexure-microscope-server" name = "openflexure-microscope-server"
version = "2.8.0" version = "2.9.0"
description = "Python module, and Flask-based web API, to run the OpenFlexure Microscope." description = "Python module, and Flask-based web API, to run the OpenFlexure Microscope."
authors = [ authors = [
@ -43,10 +43,11 @@ expiringdict = "^1.2.1"
camera-stage-mapping = "0.1.4" camera-stage-mapping = "0.1.4"
picamerax = ">=20.9.1" picamerax = ">=20.9.1"
pyyaml = "^5.3.1" pyyaml = "^5.3.1"
labthings = {git = "https://github.com/labthings/python-labthings", develop = true}
typing-extensions = "^3.7.4" # Needed for some type-hints in Python < 3.8 (e.g. Literal)
pytest-cov = "^2.10.1" pytest-cov = "^2.10.1"
piexif = "^1.1.3" piexif = "^1.1.3"
labthings = "1.2.2"
typing-extensions = "^3.7.4" # Needed for some type-hints in Python < 3.8 (e.g. Literal)
numpy-stubs = {git = "https://github.com/numpy/numpy-stubs.git"} # Needed for Numpy < 1.20
[tool.poetry.extras] [tool.poetry.extras]
rpi = ["RPi.GPIO"] rpi = ["RPi.GPIO"]
@ -66,7 +67,7 @@ poethepoet = "^0.9.0"
freezegun = "^1.0.0" freezegun = "^1.0.0"
[tool.black] [tool.black]
exclude = '(\.eggs|\.git|\.venv|node_modules/)' exclude = '(\.eggs|\.git|\.venv|\venv|node_modules/)'
[tool.isort] [tool.isort]
multi_line_output = 3 multi_line_output = 3