Merge branch '2.9-dev' into 'master'
v2.9.0 See merge request openflexure/openflexure-microscope-server!101
This commit is contained in:
commit
9cf0a9badf
45 changed files with 1558 additions and 1113 deletions
|
|
@ -10,9 +10,15 @@ This includes installing the server in a mode better suited for active developme
|
|||
|
||||
# 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
|
||||
|
||||
* `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`
|
||||
* Building the static interface will require a valid Node.js installation
|
||||
* To build on a Raspberry Pi:
|
||||
|
|
|
|||
|
|
@ -2,35 +2,35 @@ from labthings import find_component
|
|||
from labthings.extensions import BaseExtension
|
||||
|
||||
|
||||
def identify():
|
||||
"""
|
||||
Demonstrate access to Microscope.camera, and Microscope.stage
|
||||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
# Create the extension class
|
||||
class MyExtension(BaseExtension):
|
||||
def __init__(self):
|
||||
# Superclass init function
|
||||
super().__init__("com.myname.myextension", version="0.0.0")
|
||||
|
||||
response = (
|
||||
f"My name is {microscope.name}. "
|
||||
f"My parent camera is {microscope.camera}, "
|
||||
f"and my parent stage is {microscope.stage}."
|
||||
)
|
||||
def identify(self):
|
||||
"""
|
||||
Demonstrate access to Microscope.camera, and 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):
|
||||
"""
|
||||
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")
|
||||
LABTHINGS_EXTENSIONS = (MyExtension,)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -2,42 +2,45 @@ from labthings import fields, find_component
|
|||
from labthings.extensions import BaseExtension
|
||||
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):
|
||||
"""
|
||||
Demonstrate access to Microscope.camera, and Microscope.stage
|
||||
"""
|
||||
# Add our API Views (defined below MyExtension)
|
||||
self.add_view(ExampleIdentifyView, "/identify")
|
||||
self.add_view(ExampleRenameView, "/rename")
|
||||
|
||||
response = (
|
||||
f"My name is {microscope.name}. "
|
||||
f"My parent camera is {microscope.camera}, "
|
||||
f"and my parent stage is {microscope.stage}."
|
||||
)
|
||||
def identify(self, microscope):
|
||||
"""
|
||||
Demonstrate access to Microscope.camera, and 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(microscope, new_name):
|
||||
"""
|
||||
Rename the microscope
|
||||
"""
|
||||
|
||||
microscope.name = new_name
|
||||
microscope.save_settings()
|
||||
def rename(self, microscope, new_name):
|
||||
"""
|
||||
Rename the microscope
|
||||
"""
|
||||
microscope.name = new_name
|
||||
microscope.save_settings()
|
||||
|
||||
|
||||
## Extension views
|
||||
|
||||
|
||||
class ExampleIdentifyView(View):
|
||||
def get(self):
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Return our identify function's output
|
||||
return identify(microscope)
|
||||
return self.extension.identify(microscope)
|
||||
|
||||
|
||||
class ExampleRenameView(View):
|
||||
|
|
@ -53,21 +56,10 @@ class ExampleRenameView(View):
|
|||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# 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 identify(microscope)
|
||||
return self.extension.identify(microscope)
|
||||
|
||||
|
||||
## Create extension
|
||||
|
||||
# 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")
|
||||
LABTHINGS_EXTENSIONS = (MyExtension,)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,23 @@ from labthings import Schema, fields, find_component
|
|||
from labthings.extensions import BaseExtension
|
||||
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,
|
||||
|
|
@ -15,18 +31,7 @@ class MicroscopeIdentifySchema(Schema):
|
|||
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
|
||||
|
||||
|
||||
class ExampleIdentifyView(View):
|
||||
# Format our returned object using MicroscopeIdentifySchema
|
||||
schema = MicroscopeIdentifySchema()
|
||||
|
|
@ -54,21 +59,11 @@ class ExampleRenameView(View):
|
|||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Pass microscope and new name to our rename function
|
||||
rename(microscope, new_name)
|
||||
self.extension.rename(microscope, new_name)
|
||||
|
||||
# Return our microscope object,
|
||||
# let schema handle formatting the output
|
||||
return microscope
|
||||
|
||||
|
||||
## Create extension
|
||||
|
||||
# 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")
|
||||
LABTHINGS_EXTENSIONS = (MyExtension,)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,23 @@ from labthings import Schema, fields, find_component
|
|||
from labthings.extensions import BaseExtension
|
||||
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,
|
||||
|
|
@ -15,16 +31,7 @@ class MicroscopeIdentifySchema(Schema):
|
|||
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 viewss
|
||||
|
||||
# Since we only have a GET method here, it'll register as a read-only property
|
||||
class ExampleIdentifyView(PropertyView):
|
||||
|
|
@ -68,21 +75,11 @@ class ExampleRenameView(PropertyView):
|
|||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Pass microscope and new name to our rename function
|
||||
rename(microscope, new_name)
|
||||
self.extension.rename(microscope, new_name)
|
||||
|
||||
# Return our microscope object,
|
||||
# let schema handle formatting the output
|
||||
return microscope
|
||||
|
||||
|
||||
## Create extension
|
||||
|
||||
# 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")
|
||||
LABTHINGS_EXTENSIONS = (MyExtension,)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,23 @@ from labthings import Schema, fields, find_component
|
|||
from labthings.extensions import BaseExtension
|
||||
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,
|
||||
|
|
@ -18,15 +34,6 @@ class MicroscopeIdentifySchema(Schema):
|
|||
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
|
||||
|
||||
# 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")
|
||||
|
||||
# Pass microscope and new name to our rename function
|
||||
rename(microscope, new_name)
|
||||
self.extension.rename(microscope, new_name)
|
||||
|
||||
# Return our microscope object,
|
||||
# let schema handle formatting the output
|
||||
|
|
@ -109,15 +116,4 @@ class QuickCaptureAPI(ActionView):
|
|||
return send_file(io.BytesIO(stream.read()), mimetype="image/jpeg")
|
||||
|
||||
|
||||
## Create extension
|
||||
|
||||
# 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")
|
||||
LABTHINGS_EXTENSIONS = (MyExtension,)
|
||||
|
|
|
|||
|
|
@ -7,52 +7,59 @@ from labthings.views import ActionView
|
|||
# Used in our timelapse function
|
||||
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):
|
||||
"""
|
||||
Save a set of images in a timelapse
|
||||
# Add our API views
|
||||
self.add_view(TimelapseAPIView, "/timelapse")
|
||||
|
||||
Args:
|
||||
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)
|
||||
def timelapse(self, microscope, n_images, t_between):
|
||||
"""
|
||||
Save a set of images in a timelapse
|
||||
|
||||
# Take exclusive control over both the camera and stage
|
||||
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
|
||||
)
|
||||
Args:
|
||||
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)
|
||||
|
||||
# Capture
|
||||
microscope.camera.capture(output)
|
||||
# Take exclusive control over both the camera and stage
|
||||
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
|
||||
output.put_metadata(microscope.metadata, system=True)
|
||||
# Capture
|
||||
microscope.camera.capture(output)
|
||||
|
||||
# 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)
|
||||
# Add system metadata
|
||||
output.put_metadata(microscope.metadata, system=True)
|
||||
|
||||
# Wait for the specified time
|
||||
time.sleep(t_between)
|
||||
# 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
|
||||
|
||||
|
||||
class TimelapseAPI(ActionView):
|
||||
class TimelapseAPIView(ActionView):
|
||||
"""
|
||||
Take a series of images in a timelapse
|
||||
"""
|
||||
|
|
@ -71,16 +78,9 @@ class TimelapseAPI(ActionView):
|
|||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# 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
|
||||
|
||||
# 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")
|
||||
LABTHINGS_EXTENSIONS = (TimelapseExtension,)
|
||||
|
|
|
|||
|
|
@ -10,54 +10,91 @@ from openflexure_microscope.api.utilities.gui import build_gui
|
|||
# Used in our timelapse function
|
||||
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):
|
||||
"""
|
||||
Save a set of images in a timelapse
|
||||
# Add our API views
|
||||
self.add_view(TimelapseAPIView, "/timelapse")
|
||||
|
||||
Args:
|
||||
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 our GUI description
|
||||
gui_description = {
|
||||
"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
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
self.add_meta("gui", build_gui(gui_description, self))
|
||||
|
||||
# Take exclusive control over both the camera and stage
|
||||
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
|
||||
)
|
||||
def timelapse(self, microscope, n_images, t_between):
|
||||
"""
|
||||
Save a set of images in a timelapse
|
||||
|
||||
# Capture
|
||||
microscope.camera.capture(output)
|
||||
Args:
|
||||
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
|
||||
output.put_metadata(microscope.metadata, system=True)
|
||||
# Take exclusive control over both the camera and stage
|
||||
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)
|
||||
progress_pct = ((n + 1) / n_images) * 100 # Progress, in percent
|
||||
update_action_progress(progress_pct)
|
||||
# Capture
|
||||
microscope.camera.capture(output)
|
||||
|
||||
# Wait for the specified time
|
||||
time.sleep(t_between)
|
||||
# Add system metadata
|
||||
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
|
||||
|
||||
|
||||
class TimelapseAPI(ActionView):
|
||||
class TimelapseAPIView(ActionView):
|
||||
"""
|
||||
Take a series of images in a timelapse, running as a background task
|
||||
Take a series of images in a timelapse
|
||||
"""
|
||||
|
||||
args = {
|
||||
|
|
@ -73,53 +110,10 @@ class TimelapseAPI(ActionView):
|
|||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Create and start "timelapse", running in a background task
|
||||
return timelapse(microscope, args.get("n_images"), args.get("t_between"))
|
||||
# Start "timelapse"
|
||||
return self.extension.timelapse(
|
||||
microscope, args.get("n_images"), args.get("t_between")
|
||||
)
|
||||
|
||||
|
||||
## Extension GUI (OpenFlexure eV)
|
||||
# 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))
|
||||
LABTHINGS_EXTENSIONS = (TimelapseExtension,)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
if my_found_extension:
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
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 it’s 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
|
||||
------------
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Request arguments
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import logging
|
||||
import traceback
|
||||
from contextlib import contextmanager
|
||||
from typing import List, Type
|
||||
|
||||
from labthings.extensions import BaseExtension
|
||||
|
||||
LABTHINGS_EXTENSIONS: List[Type[BaseExtension]] = []
|
||||
|
||||
|
||||
@contextmanager
|
||||
|
|
@ -17,14 +22,28 @@ def handle_extension_error(extension_name):
|
|||
|
||||
|
||||
with handle_extension_error("autofocus"):
|
||||
from .autofocus import autofocus_extension_v2
|
||||
from .autofocus import AutofocusExtension
|
||||
|
||||
LABTHINGS_EXTENSIONS.append(AutofocusExtension)
|
||||
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"):
|
||||
from .zip_builder import zip_extension_v2
|
||||
from .zip_builder import ZipBuilderExtension
|
||||
|
||||
LABTHINGS_EXTENSIONS.append(ZipBuilderExtension)
|
||||
with handle_extension_error("autostorage"):
|
||||
from .autostorage import autostorage_extension_v2
|
||||
with handle_extension_error("camera stage mapping"):
|
||||
from camera_stage_mapping.ofm_extension import csm_extension
|
||||
from .autostorage import AutostorageExtension
|
||||
|
||||
LABTHINGS_EXTENSIONS.append(AutostorageExtension)
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import logging
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from typing import Callable, List, Optional, Tuple
|
||||
from typing import Callable, Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
from labthings import current_action, fields, find_component
|
||||
|
|
@ -39,7 +39,7 @@ class JPEGSharpnessMonitor:
|
|||
self.camera.stream.stop_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
|
||||
self.camera.stream.start_tracking()
|
||||
self.stage_times.append(time.time())
|
||||
|
|
@ -62,22 +62,28 @@ class JPEGSharpnessMonitor:
|
|||
self.camera.stream.reset_tracking()
|
||||
|
||||
# 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 = self.stage_positions[-1][2]
|
||||
final_z_position: int = self.stage_positions[-1][2]
|
||||
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"""
|
||||
if istop is None:
|
||||
istop = istart + 2
|
||||
jpeg_times = np.array(self.jpeg_times)
|
||||
jpeg_sizes = np.array(self.jpeg_sizes)
|
||||
stage_times = np.array(self.stage_times)[istart:istop]
|
||||
stage_zs = np.array(self.stage_positions)[istart:istop, 2]
|
||||
jpeg_times: np.ndarray = np.array(self.jpeg_times) # np.ndarray[float]
|
||||
jpeg_sizes: np.ndarray = np.array(self.jpeg_sizes) # np.ndarray[int]
|
||||
stage_times: np.ndarray = np.array(self.stage_times)[
|
||||
istart:istop
|
||||
] # np.ndarray[float]
|
||||
stage_zs: np.ndarray = np.array(self.stage_positions)[
|
||||
istart:istop, 2
|
||||
] # np.ndarray[int]
|
||||
try:
|
||||
start = np.argmax(jpeg_times > stage_times[0])
|
||||
stop = np.argmax(jpeg_times > stage_times[1])
|
||||
start: int = int(np.argmax(jpeg_times > stage_times[0]))
|
||||
stop: int = int(np.argmax(jpeg_times > stage_times[1]))
|
||||
except ValueError as e:
|
||||
if np.sum(jpeg_times > stage_times[0]) == 0:
|
||||
raise ValueError(
|
||||
|
|
@ -89,10 +95,12 @@ class JPEGSharpnessMonitor:
|
|||
stop = len(jpeg_times)
|
||||
logging.debug("changing stop to %s", (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]
|
||||
|
||||
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"""
|
||||
_, jz, js = self.move_data(index)
|
||||
if len(js) == 0:
|
||||
|
|
@ -101,7 +109,7 @@ class JPEGSharpnessMonitor:
|
|||
)
|
||||
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"""
|
||||
data = {}
|
||||
for k in ["jpeg_times", "jpeg_sizes", "stage_times", "stage_positions"]:
|
||||
|
|
@ -109,18 +117,28 @@ class JPEGSharpnessMonitor:
|
|||
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)**")"""
|
||||
image_bw = np.mean(rgb_image, 2)
|
||||
image_lap = ndimage.filters.laplace(image_bw)
|
||||
image_bw: np.float = np.mean(rgb_image, 2)
|
||||
image_lap: np.float = ndimage.filters.laplace(image_bw)
|
||||
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"""
|
||||
gray = np.mean(image.astype(float), 2)
|
||||
n = 20
|
||||
edge = np.array([[-1] * n + [1] * n])
|
||||
gray: np.float = np.mean(image.astype(float), 2)
|
||||
n: int = 20
|
||||
edge: np.ndarray = np.array([[-1] * n + [1] * n])
|
||||
return np.sum(
|
||||
[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
|
||||
|
||||
|
||||
def measure_sharpness(microscope: Microscope, metric_fn: Callable = sharpness_sum_lap2):
|
||||
"""Measure the sharpness of the camera's current view."""
|
||||
class AutofocusExtension(BaseExtension):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"org.openflexure.autofocus",
|
||||
version="2.0.0",
|
||||
description="Actions to move the microscope in Z and pick the point with the sharpest image.",
|
||||
)
|
||||
self.add_view(
|
||||
MeasureSharpnessAPI, "/measure_sharpness", endpoint="measure_sharpness"
|
||||
)
|
||||
self.add_view(AutofocusAPI, "/autofocus", endpoint="autofocus")
|
||||
self.add_view(FastAutofocusAPI, "/fast_autofocus", endpoint="fast_autofocus")
|
||||
self.add_view(
|
||||
UpDownUpAutofocusAPI, "/updownup_autofocus", endpoint="updownup_autofocus"
|
||||
)
|
||||
|
||||
if hasattr(microscope.camera, "array") and callable(
|
||||
getattr(microscope.camera, "array")
|
||||
):
|
||||
return metric_fn(getattr(microscope.camera, "array")(use_video_port=True))
|
||||
else:
|
||||
raise RuntimeError(f"Object {microscope.camera} has no method `array`")
|
||||
self.add_view(
|
||||
MoveAndMeasureAPI, "/move_and_measure", endpoint="move_and_measure"
|
||||
)
|
||||
|
||||
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(
|
||||
microscope: Microscope,
|
||||
dz: List[int],
|
||||
settle: float = 0.5,
|
||||
metric_fn: Callable = sharpness_sum_lap2,
|
||||
):
|
||||
"""Perform a simple autofocus routine.
|
||||
The stage is moved to z positions (relative to current position) in dz,
|
||||
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
|
||||
if hasattr(microscope.camera, "array") and callable(
|
||||
getattr(microscope.camera, "array")
|
||||
):
|
||||
return metric_fn(getattr(microscope.camera, "array")(use_video_port=True))
|
||||
else:
|
||||
raise RuntimeError(f"Object {microscope.camera} has no method `array`")
|
||||
|
||||
with set_properties(stage, backlash=256), stage.lock, camera.lock:
|
||||
sharpnesses = []
|
||||
positions = []
|
||||
def autofocus(
|
||||
self,
|
||||
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
|
||||
if getattr(camera, "annotate_text"):
|
||||
setattr(camera, "annotate_text", "")
|
||||
with set_properties(stage, backlash=256), stage.lock, camera.lock:
|
||||
sharpnesses: List[np.float] = []
|
||||
positions: List[int] = []
|
||||
|
||||
for _ in stage.scan_z(dz, return_to_start=False):
|
||||
if current_action() and current_action().stopped:
|
||||
return
|
||||
positions.append(stage.position[2])
|
||||
time.sleep(settle)
|
||||
sharpnesses.append(measure_sharpness(microscope, metric_fn))
|
||||
# Some cameras may not have annotate_text. Reset if it does
|
||||
if getattr(camera, "annotate_text", None):
|
||||
setattr(camera, "annotate_text", "")
|
||||
|
||||
newposition = positions[np.argmax(sharpnesses)]
|
||||
stage.move_rel((0, 0, newposition - stage.position[2]))
|
||||
for _ in stage.scan_z(dz, return_to_start=False):
|
||||
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 monitor_sharpness(microscope: Microscope):
|
||||
m = JPEGSharpnessMonitor(microscope)
|
||||
m.start()
|
||||
try:
|
||||
yield m
|
||||
finally:
|
||||
m.stop()
|
||||
|
||||
|
||||
def move_and_find_focus(microscope: Microscope, dz: int):
|
||||
"""Make a relative Z move and return the peak sharpness position"""
|
||||
with monitor_sharpness(microscope) as m:
|
||||
m.focus_rel(dz)
|
||||
return m.sharpest_z_on_move(0)
|
||||
|
||||
|
||||
def move_and_measure(microscope: Microscope, dz: int):
|
||||
"""Make a relative Z move and return the sharpness data"""
|
||||
with monitor_sharpness(microscope) as m:
|
||||
m.focus_rel(dz)
|
||||
return m.data_dict()
|
||||
|
||||
|
||||
def fast_autofocus(microscope: Microscope, dz: int = 2000):
|
||||
"""Perform a down-up-down-up autofocus"""
|
||||
with microscope.camera.lock, microscope.stage.lock:
|
||||
def move_and_find_focus(self, microscope: Microscope, dz: int) -> int:
|
||||
"""Make a relative Z move and return the peak sharpness position"""
|
||||
with monitor_sharpness(microscope) as m:
|
||||
# Move to (-dz / 2)
|
||||
m.focus_rel(-dz / 2)
|
||||
# Move to dz while monitoring sharpness
|
||||
# i: Sharpness monitor index for this move
|
||||
# z: Final z position after move
|
||||
i, z = m.focus_rel(dz)
|
||||
# Get the z position with highest sharpness from the previous move (index i)
|
||||
fz = m.sharpest_z_on_move(i)
|
||||
# Move all the way to the start so it's consistent
|
||||
# Store final absolute z position from this return move
|
||||
i, z = m.focus_rel(-dz)
|
||||
# Move to the target position fz
|
||||
# Can't do absolute move here yet so move by (fz - z)
|
||||
m.focus_rel(fz - z)
|
||||
# Return all focus data
|
||||
m.focus_rel(dz)
|
||||
return m.sharpest_z_on_move(0)
|
||||
|
||||
def move_and_measure(
|
||||
self, microscope: Microscope, dz: int
|
||||
) -> Dict[str, np.ndarray]:
|
||||
"""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(
|
||||
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(
|
||||
microscope: Microscope,
|
||||
dz: int = 2000,
|
||||
target_z: int = 0,
|
||||
initial_move_up: bool = True,
|
||||
mini_backlash: int = 25,
|
||||
):
|
||||
"""Autofocus by measuring on the way down, and moving back up with feedback.
|
||||
def fast_up_down_up_autofocus(
|
||||
self,
|
||||
microscope: Microscope,
|
||||
dz: int = 2000,
|
||||
target_z: int = 0,
|
||||
initial_move_up: bool = True,
|
||||
mini_backlash: int = 25,
|
||||
) -> 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.
|
||||
The sequence of moves it performs is:
|
||||
This autofocus method is very efficient, as it only passes the peak once.
|
||||
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 \\
|
||||
estimate how much further we need to go. Make this move, to reach our \\
|
||||
target position.
|
||||
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 \\
|
||||
target position.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Parameters:
|
||||
dz: number of steps over which to scan (optional, default 2000)
|
||||
Parameters:
|
||||
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
|
||||
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.
|
||||
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.
|
||||
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
|
||||
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.
|
||||
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
|
||||
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
|
||||
3 to help counteract backlash. It should be small enough that you
|
||||
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
|
||||
may cause you to overshoot, which is a problem.
|
||||
"""
|
||||
with microscope.camera.lock, microscope.stage.lock:
|
||||
with monitor_sharpness(microscope) as m:
|
||||
# Ensure the MJPEG stream has started
|
||||
microscope.camera.start_stream()
|
||||
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
|
||||
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
|
||||
may cause you to overshoot, which is a problem.
|
||||
"""
|
||||
with microscope.camera.lock, microscope.stage.lock:
|
||||
with monitor_sharpness(microscope) as m:
|
||||
# Ensure the MJPEG stream has started
|
||||
microscope.camera.start_stream()
|
||||
|
||||
logging.debug("Initial move")
|
||||
if initial_move_up:
|
||||
m.focus_rel(dz / 2)
|
||||
# move down
|
||||
logging.debug("Move down")
|
||||
i, z = m.focus_rel(-dz)
|
||||
# now inspect where the sharpest point is, and estimate the sharpness
|
||||
# (JPEG size) that we should find at the start of the Z stack
|
||||
_, jz, js = m.move_data(i)
|
||||
best_z = jz[np.argmax(js)]
|
||||
logging.debug("Initial move")
|
||||
if initial_move_up:
|
||||
m.focus_rel(dz / 2)
|
||||
# move down
|
||||
logging.debug("Move down")
|
||||
i: int
|
||||
z: int
|
||||
i, z = m.focus_rel(-dz)
|
||||
# now inspect where the sharpest point is, and estimate the sharpness
|
||||
# (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
|
||||
logging.debug("Move to the start of the z stack")
|
||||
i, z = m.focus_rel(
|
||||
best_z + target_z - z + mini_backlash
|
||||
) # takes us to the start of the stack
|
||||
# now move to the start of the z stack
|
||||
logging.debug("Move to the start of the z stack")
|
||||
i, z = m.focus_rel(
|
||||
best_z + target_z - z + mini_backlash
|
||||
) # takes us to the start of the stack
|
||||
|
||||
# We've deliberately undershot - figure out how much further we should move based on the curve
|
||||
logging.debug("Calculate remining movement")
|
||||
current_js = m.camera.stream.last.size
|
||||
imax = np.argmax(js) # we want to crop out just the bit below the peak
|
||||
js = js[imax:] # NB z is in DECREASING order
|
||||
jz = jz[imax:]
|
||||
inow = np.argmax(
|
||||
js < current_js
|
||||
) # use the curve we recorded to estimate our position
|
||||
# We've deliberately undershot - figure out how much further we should move based on the curve
|
||||
logging.debug("Calculate remining movement")
|
||||
current_js = m.camera.stream.last.size
|
||||
imax: int = int(
|
||||
np.argmax(js)
|
||||
) # we want to crop out just the bit below the peak
|
||||
js = js[imax:] # NB z is in DECREASING order
|
||||
jz = jz[imax:]
|
||||
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]
|
||||
# That means we should move forwards, by best_z - zs[inow]
|
||||
logging.debug("Correction move")
|
||||
correction_move = best_z + target_z - jz[inow]
|
||||
logging.debug(
|
||||
"Fast autofocus scan: correcting backlash by moving %s steps",
|
||||
(correction_move),
|
||||
)
|
||||
m.focus_rel(correction_move)
|
||||
return m.data_dict()
|
||||
# So, the Z position corresponding to our current sharpness value is zs[inow]
|
||||
# That means we should move forwards, by best_z - zs[inow]
|
||||
logging.debug("Correction move")
|
||||
correction_move: int = best_z + target_z - jz[inow]
|
||||
logging.debug(
|
||||
"Fast autofocus scan: correcting backlash by moving %s steps",
|
||||
(correction_move),
|
||||
)
|
||||
m.focus_rel(correction_move)
|
||||
return m.data_dict()
|
||||
|
||||
|
||||
class MeasureSharpnessAPI(View):
|
||||
|
|
@ -316,7 +353,7 @@ class MeasureSharpnessAPI(View):
|
|||
if not microscope:
|
||||
abort(503, "No microscope connected. Unable to measure sharpness.")
|
||||
|
||||
return {"sharpness": measure_sharpness(microscope)}
|
||||
return {"sharpness": self.extension.measure_sharpness(microscope)}
|
||||
|
||||
|
||||
class MoveAndMeasureAPI(ActionView):
|
||||
|
|
@ -335,7 +372,7 @@ class MoveAndMeasureAPI(ActionView):
|
|||
if microscope.has_real_stage():
|
||||
# Acquire microscope lock with 1s timeout
|
||||
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:
|
||||
abort(503, "No stage connected. Unable to autofocus.")
|
||||
|
|
@ -361,7 +398,7 @@ class AutofocusAPI(ActionView):
|
|||
logging.debug("Running autofocus...")
|
||||
|
||||
# return a handle on the autofocus task
|
||||
return autofocus(microscope, dz)
|
||||
return self.extension.autofocus(microscope, dz)
|
||||
|
||||
else:
|
||||
abort(503, "No stage connected. Unable to autofocus.")
|
||||
|
|
@ -385,7 +422,7 @@ class FastAutofocusAPI(ActionView):
|
|||
# Acquire microscope lock with 1s timeout
|
||||
with microscope.lock(timeout=1):
|
||||
# Run fast_autofocus
|
||||
return fast_autofocus(microscope, dz=args.get("dz"))
|
||||
return self.extension.fast_autofocus(microscope, dz=args.get("dz"))
|
||||
|
||||
else:
|
||||
abort(503, "No stage connected. Unable to autofocus.")
|
||||
|
|
@ -417,38 +454,9 @@ class UpDownUpAutofocusAPI(ActionView):
|
|||
# Acquire microscope lock with 1s timeout
|
||||
with microscope.lock(timeout=1):
|
||||
# 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
|
||||
)
|
||||
|
||||
else:
|
||||
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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -93,12 +93,43 @@ class AutostorageExtension(BaseExtension):
|
|||
|
||||
# We'll store a reference to a CaptureManager object, who's capture paths will be modified
|
||||
self.capture_manager: Optional[CaptureManager] = None
|
||||
|
||||
self.initial_location: str = get_default_location()
|
||||
|
||||
# Register the on_microscope function to run when the microscope is attached
|
||||
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):
|
||||
"""Function to automatically call when the parent LabThing has a microscope attached."""
|
||||
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())
|
||||
|
||||
|
||||
autostorage_extension_v2 = AutostorageExtension()
|
||||
|
||||
|
||||
class GetLocationsView(PropertyView):
|
||||
def get(self):
|
||||
autostorage_extension_v2.check_location()
|
||||
return autostorage_extension_v2.get_locations()
|
||||
self.extension.check_location()
|
||||
return self.extension.get_locations()
|
||||
|
||||
|
||||
class PreferredLocationView(PropertyView):
|
||||
schema = fields.String(required=True, example="Default")
|
||||
|
||||
def get(self):
|
||||
autostorage_extension_v2.check_location()
|
||||
return autostorage_extension_v2.get_preferred_key()
|
||||
self.extension.check_location()
|
||||
return self.extension.get_preferred_key()
|
||||
|
||||
def post(self, new_path_key):
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
|
@ -226,8 +254,8 @@ class PreferredLocationView(PropertyView):
|
|||
if not microscope:
|
||||
abort(503, "No microscope connected. Unable to autofocus.")
|
||||
|
||||
autostorage_extension_v2.check_location()
|
||||
autostorage_extension_v2.set_preferred_key(new_path_key)
|
||||
self.extension.check_location()
|
||||
self.extension.set_preferred_key(new_path_key)
|
||||
microscope.save_settings()
|
||||
|
||||
|
||||
|
|
@ -237,46 +265,10 @@ class PreferredLocationGUIView(View):
|
|||
new_path_title = args.get("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)
|
||||
|
||||
autostorage_extension_v2.check_location()
|
||||
autostorage_extension_v2.set_preferred_key(new_path_key)
|
||||
self.extension.check_location()
|
||||
self.extension.set_preferred_key(new_path_key)
|
||||
|
||||
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)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
@ -1 +1 @@
|
|||
from .extension import lst_extension_v2
|
||||
from .extension import LSTExtension
|
||||
|
|
|
|||
|
|
@ -42,23 +42,43 @@ def pause_stream(scamera: BaseCamera):
|
|||
scamera.start_stream()
|
||||
|
||||
|
||||
def recalibrate(microscope: Microscope):
|
||||
"""Reset the camera's settings.
|
||||
class LSTExtension(BaseExtension):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
"org.openflexure.calibration.picamera",
|
||||
version="2.0.0-beta.1",
|
||||
description="Routines to perform flat-field correction on the camera.",
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
self.add_view(RecalibrateView, "/recalibrate", endpoint="recalibrate")
|
||||
self.add_view(
|
||||
FlattenLSTView,
|
||||
"/flatten_lens_shading_table",
|
||||
endpoint="flatten_lens_shading_table",
|
||||
)
|
||||
self.add_view(
|
||||
DeleteLSTView,
|
||||
"/delete_lens_shading_table",
|
||||
endpoint="delete_lens_shading_table",
|
||||
)
|
||||
|
||||
def recalibrate(self, microscope: Microscope):
|
||||
"""Reset the camera's settings.
|
||||
|
||||
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):
|
||||
|
|
@ -70,7 +90,7 @@ class RecalibrateView(ActionView):
|
|||
|
||||
logging.info("Starting microscope recalibration...")
|
||||
|
||||
return recalibrate(microscope)
|
||||
return self.extension.recalibrate(microscope)
|
||||
|
||||
|
||||
class FlattenLSTView(ActionView):
|
||||
|
|
@ -102,22 +122,3 @@ class DeleteLSTView(ActionView):
|
|||
with pause_stream(microscope.camera) as scamera:
|
||||
scamera.camera.lens_shading_table = None
|
||||
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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,14 +8,16 @@ from picamerax import PiCamera
|
|||
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"""
|
||||
with PiRGBArray(camera, size=resize) as output:
|
||||
camera.capture(output, format="rgb", resize=resize, **kwargs)
|
||||
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.
|
||||
|
||||
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:
|
||||
"""Given the 'array' from a PiBayerArray, return the 4 channels."""
|
||||
bayer_pattern: List[Tuple[int, int]] = [(0, 0), (0, 1), (1, 0), (1, 1)]
|
||||
channels: np.ndarray = np.zeros(
|
||||
(4, bayer_array.shape[0] // 2, bayer_array.shape[1] // 2),
|
||||
dtype=bayer_array.dtype,
|
||||
channels_shape: Tuple[int, ...] = (
|
||||
4,
|
||||
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):
|
||||
# We simplify life by dealing with only one channel at a time.
|
||||
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
|
||||
)
|
||||
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
|
||||
ls_channel = lens_shading[i, :, :]
|
||||
ls_channel: np.ndarray = lens_shading[i, :, :]
|
||||
lw: int
|
||||
lh: int
|
||||
lw, lh = ls_channel.shape
|
||||
# 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
|
||||
|
|
@ -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
|
||||
# should give results very close to 6by9's solution, albeit considerably
|
||||
# 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"
|
||||
) # Pad image to the right and bottom
|
||||
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
|
||||
# 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
|
||||
# horizontally, but not vertically.
|
||||
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
|
||||
# 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 < 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()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -99,9 +99,12 @@ def construct_grid(
|
|||
|
||||
class ScanExtension(BaseExtension):
|
||||
def __init__(self):
|
||||
BaseExtension.__init__(self, "org.openflexure.scan", version="2.0.0")
|
||||
|
||||
self._images_to_be_captured: int = 1
|
||||
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(
|
||||
self,
|
||||
|
|
@ -356,9 +359,6 @@ class ScanExtension(BaseExtension):
|
|||
microscope.stage.move_abs(initial_position)
|
||||
|
||||
|
||||
scan_extension_v2 = ScanExtension()
|
||||
|
||||
|
||||
class TileScanArgs(FullCaptureArgs):
|
||||
namemode = fields.String(missing="coordinates", example="coordinates")
|
||||
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
|
||||
with microscope.lock(timeout=1):
|
||||
# Run scan_extension_v2
|
||||
return scan_extension_v2.tile(
|
||||
return self.extension.tile(
|
||||
microscope,
|
||||
basename=args.get("filename"),
|
||||
namemode=args.get("namemode"),
|
||||
|
|
@ -414,6 +414,3 @@ class TileScanAPI(ActionView):
|
|||
annotations=args.get("annotations"),
|
||||
tags=args.get("tags"),
|
||||
)
|
||||
|
||||
|
||||
scan_extension_v2.add_view(TileScanAPI, "/tile", endpoint="tile")
|
||||
|
|
|
|||
|
|
@ -131,8 +131,18 @@ class ZipManager:
|
|||
zd.close()
|
||||
|
||||
|
||||
# Create a global ZIP manager
|
||||
default_zip_manager = ZipManager()
|
||||
class ZipBuilderExtension(BaseExtension):
|
||||
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):
|
||||
|
|
@ -142,7 +152,7 @@ class ZipBuilderAPIView(ActionView):
|
|||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
|
|
@ -151,7 +161,7 @@ class ZipListAPIView(PropertyView):
|
|||
schema = ZipObjectSchema(many=True)
|
||||
|
||||
def get(self):
|
||||
return default_zip_manager.session_zips.values()
|
||||
return self.extension.manager.session_zips.values()
|
||||
|
||||
|
||||
class ZipGetterAPIView(View):
|
||||
|
|
@ -163,13 +173,13 @@ class ZipGetterAPIView(View):
|
|||
"""
|
||||
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
|
||||
|
||||
logging.info("Session ID: %s", session_id)
|
||||
|
||||
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",
|
||||
as_attachment=True,
|
||||
attachment_filename=f"{session_id}.zip",
|
||||
|
|
@ -179,26 +189,12 @@ class ZipGetterAPIView(View):
|
|||
"""
|
||||
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
|
||||
|
||||
# Close the file
|
||||
default_zip_manager.session_zips[session_id].close()
|
||||
self.extension.manager.session_zips[session_id].close()
|
||||
# Delete the file reference
|
||||
del default_zip_manager.session_zips[session_id]
|
||||
del self.extension.manager.session_zips[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")
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
from .tools import devtools_extension_v2
|
||||
from .tools import DevToolsExtension
|
||||
|
||||
LABTHINGS_EXTENSIONS = [DevToolsExtension]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,17 @@ from labthings.extensions import BaseExtension
|
|||
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):
|
||||
def post(self):
|
||||
raise Exception("The developer raised an exception")
|
||||
|
|
@ -23,13 +34,3 @@ class SleepFor(ActionView):
|
|||
end = time.time()
|
||||
logging.info("Waking up!")
|
||||
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")
|
||||
|
|
|
|||
528
openflexure_microscope/api/static/package-lock.json
generated
528
openflexure_microscope/api/static/package-lock.json
generated
|
|
@ -2308,9 +2308,9 @@
|
|||
}
|
||||
},
|
||||
"@types/express-serve-static-core": {
|
||||
"version": "4.17.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz",
|
||||
"integrity": "sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA==",
|
||||
"version": "4.17.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.14.tgz",
|
||||
"integrity": "sha512-uFTLwu94TfUFMToXNgRZikwPuZdOtDgs3syBtAIr/OXorL1kJqUJT9qCLnRZ5KBOWfZQikQ2xKgR2tnDj1OgDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
|
|
@ -2403,9 +2403,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"@types/serve-static": {
|
||||
"version": "1.13.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.7.tgz",
|
||||
"integrity": "sha512-3diZWucbR+xTmbDlU+FRRxBf+31OhFew7cJXML/zh9NmvSPTNoFecAwHB66BUqFgENJtqMiyl7JAwUE/siqdLw==",
|
||||
"version": "1.13.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz",
|
||||
"integrity": "sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/mime": "*",
|
||||
|
|
@ -2442,9 +2442,9 @@
|
|||
}
|
||||
},
|
||||
"@types/webpack": {
|
||||
"version": "4.41.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.24.tgz",
|
||||
"integrity": "sha512-1A0MXPwZiMOD3DPMuOKUKcpkdPo8Lq33UGggZ7xio6wJ/jV1dAu5cXDrOfGDnldUroPIRLsr/DT43/GqOA4RFQ==",
|
||||
"version": "4.41.25",
|
||||
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.25.tgz",
|
||||
"integrity": "sha512-cr6kZ+4m9lp86ytQc1jPOJXgINQyz3kLLunZ57jznW+WIAL0JqZbGubQk4GlD42MuQL5JGOABrxdpqqWeovlVQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/anymatch": "*",
|
||||
|
|
@ -2477,9 +2477,9 @@
|
|||
}
|
||||
},
|
||||
"@types/webpack-sources": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-2.0.0.tgz",
|
||||
"integrity": "sha512-a5kPx98CNFRKQ+wqawroFunvFqv7GHm/3KOI52NY9xWADgc8smu4R6prt4EU/M4QfVjvgBkMqU4fBhw3QfMVkg==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-2.1.0.tgz",
|
||||
"integrity": "sha512-LXn/oYIpBeucgP1EIJbKQ2/4ZmpvRl+dlrFdX7+94SKRUV3Evy3FsfMZY318vGhkWUS5MPhtOM3w1/hCOAOXcg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
|
|
@ -2594,9 +2594,9 @@
|
|||
}
|
||||
},
|
||||
"@vue/cli-overlay": {
|
||||
"version": "4.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/cli-overlay/-/cli-overlay-4.5.8.tgz",
|
||||
"integrity": "sha512-M6q4QPKQfErHk54ycxBASgBadgdsK45f6v2NWUTAiFXdTtqv1Z7gR8JAbQhiXbni/m9571bLJnZFv5c8uhAHnw==",
|
||||
"version": "4.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@vue/cli-overlay/-/cli-overlay-4.5.9.tgz",
|
||||
"integrity": "sha512-E2PWv6tCdUz+eEDj2Th2oxiKmzMe02qi0PcxiNaO7oaqggmEOrp1rLgop7DWpiLDBiqUZk2x0vjK/q2Tz8z/eg==",
|
||||
"dev": true
|
||||
},
|
||||
"@vue/cli-plugin-babel": {
|
||||
|
|
@ -2708,18 +2708,18 @@
|
|||
}
|
||||
},
|
||||
"@vue/cli-plugin-router": {
|
||||
"version": "4.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-router/-/cli-plugin-router-4.5.8.tgz",
|
||||
"integrity": "sha512-tJwVLdX4jj0Ia/1hsBejRbo4gs0hb2z1z5ert+4Ve5RNdpOKUG69OaPQgXPqvuYSQh9MW7bqG0iJmYtVD+KBNw==",
|
||||
"version": "4.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-router/-/cli-plugin-router-4.5.9.tgz",
|
||||
"integrity": "sha512-eBBfbZpQ1sJrdlx8i7iReFxSnuzwmrv+s2OCT3kjBd6uWRqGnD4VihpS4srC7vZLzDQrDplumSn0a93L9Qf3wQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@vue/cli-shared-utils": "^4.5.8"
|
||||
"@vue/cli-shared-utils": "^4.5.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/cli-shared-utils": {
|
||||
"version": "4.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.8.tgz",
|
||||
"integrity": "sha512-pa6oenhBO/5HeDLRSokiwVN01gROACEDy3ESXWuPmragOREGNmmFKtkPHlqeYavGEX6LFp7f0VK3uMX6UYS5mQ==",
|
||||
"version": "4.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.9.tgz",
|
||||
"integrity": "sha512-anvsrv+rkQC+hgxaT2nQQxnSWSsIzyysZ36LO7qPjXvDRBvcvKLAAviFlUkYbZ+ntbV8puzJ3zw+gUhQw4SEVA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@hapi/joi": "^15.0.1",
|
||||
|
|
@ -2798,15 +2798,15 @@
|
|||
}
|
||||
},
|
||||
"@vue/cli-plugin-vuex": {
|
||||
"version": "4.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.8.tgz",
|
||||
"integrity": "sha512-wU9WPhay1zBxtdd/HLRYhFRLBbG9lw3YTBJep9sTnYwUeXiEgum4O88Q2j6cwBBPPHMgeMrKMPHS85Jf4hMc0g==",
|
||||
"version": "4.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.9.tgz",
|
||||
"integrity": "sha512-mFNIJhYiJjzCgytkDHX00ROy5Yzl7prkZpUbeDE0biwcLteMf2s3qZVbESOQl6GcviqcfEt2f3tHQQtLNa+OLg==",
|
||||
"dev": true
|
||||
},
|
||||
"@vue/cli-service": {
|
||||
"version": "4.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/cli-service/-/cli-service-4.5.8.tgz",
|
||||
"integrity": "sha512-YcVEJvA5vQOcfRzhCQDfHxRW9WHvKqlQeiuSvNddfV2uQqKD3ifhsNYiZZuFvbev9qgRUmtmxSafpwYzj/LLBw==",
|
||||
"version": "4.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@vue/cli-service/-/cli-service-4.5.9.tgz",
|
||||
"integrity": "sha512-E3XlfM0q+UnnjbC9rwLIWNo2umZCRwnlMJY0KOhY1hFvqisGIYzFmQQ4o01KGyTx2BZNMuQg7Kw+BZ5gyM1Wig==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@intervolga/optimize-cssnano-plugin": "^1.0.5",
|
||||
|
|
@ -2815,10 +2815,10 @@
|
|||
"@types/minimist": "^1.2.0",
|
||||
"@types/webpack": "^4.0.0",
|
||||
"@types/webpack-dev-server": "^3.11.0",
|
||||
"@vue/cli-overlay": "^4.5.8",
|
||||
"@vue/cli-plugin-router": "^4.5.8",
|
||||
"@vue/cli-plugin-vuex": "^4.5.8",
|
||||
"@vue/cli-shared-utils": "^4.5.8",
|
||||
"@vue/cli-overlay": "^4.5.9",
|
||||
"@vue/cli-plugin-router": "^4.5.9",
|
||||
"@vue/cli-plugin-vuex": "^4.5.9",
|
||||
"@vue/cli-shared-utils": "^4.5.9",
|
||||
"@vue/component-compiler-utils": "^3.1.2",
|
||||
"@vue/preload-webpack-plugin": "^1.1.0",
|
||||
"@vue/web-component-wrapper": "^1.2.0",
|
||||
|
|
@ -2868,9 +2868,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@vue/cli-shared-utils": {
|
||||
"version": "4.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.8.tgz",
|
||||
"integrity": "sha512-pa6oenhBO/5HeDLRSokiwVN01gROACEDy3ESXWuPmragOREGNmmFKtkPHlqeYavGEX6LFp7f0VK3uMX6UYS5mQ==",
|
||||
"version": "4.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.9.tgz",
|
||||
"integrity": "sha512-anvsrv+rkQC+hgxaT2nQQxnSWSsIzyysZ36LO7qPjXvDRBvcvKLAAviFlUkYbZ+ntbV8puzJ3zw+gUhQw4SEVA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@hapi/joi": "^15.0.1",
|
||||
|
|
@ -2928,16 +2928,16 @@
|
|||
}
|
||||
},
|
||||
"browserslist": {
|
||||
"version": "4.14.7",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.7.tgz",
|
||||
"integrity": "sha512-BSVRLCeG3Xt/j/1cCGj1019Wbty0H+Yvu2AOuZSuoaUWn3RatbL33Cxk+Q4jRMRAbOm0p7SLravLjpnT6s0vzQ==",
|
||||
"version": "4.15.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.15.0.tgz",
|
||||
"integrity": "sha512-IJ1iysdMkGmjjYeRlDU8PQejVwxvVO5QOfXH7ylW31GO6LwNRSmm/SgRXtNsEXqMLl2e+2H5eEJ7sfynF8TCaQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"caniuse-lite": "^1.0.30001157",
|
||||
"caniuse-lite": "^1.0.30001164",
|
||||
"colorette": "^1.2.1",
|
||||
"electron-to-chromium": "^1.3.591",
|
||||
"electron-to-chromium": "^1.3.612",
|
||||
"escalade": "^3.1.1",
|
||||
"node-releases": "^1.1.66"
|
||||
"node-releases": "^1.1.67"
|
||||
}
|
||||
},
|
||||
"cacache": {
|
||||
|
|
@ -2967,9 +2967,9 @@
|
|||
}
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001157",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001157.tgz",
|
||||
"integrity": "sha512-gOerH9Wz2IRZ2ZPdMfBvyOi3cjaz4O4dgNwPGzx8EhqAs4+2IL/O+fJsbt+znSigujoZG8bVcIAUM/I/E5K3MA==",
|
||||
"version": "1.0.30001165",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001165.tgz",
|
||||
"integrity": "sha512-8cEsSMwXfx7lWSUMA2s08z9dIgsnR5NAqjXP23stdsU3AUWkCr/rr4s4OFtHXn5XXr6+7kam3QFVoYyXNPdJPA==",
|
||||
"dev": true
|
||||
},
|
||||
"color-convert": {
|
||||
|
|
@ -2990,9 +2990,9 @@
|
|||
"optional": true
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.592",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.592.tgz",
|
||||
"integrity": "sha512-kGNowksvqQiPb1pUSQKpd8JFoGPLxYOwduNRCqCxGh/2Q1qE2JdmwouCW41lUzDxOb/2RIV4lR0tVIfboWlO9A==",
|
||||
"version": "1.3.615",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.615.tgz",
|
||||
"integrity": "sha512-fNYTQXoUhNc6RmHDlGN4dgcLURSBIqQCN7ls6MuQ741+NJyLNRz8DxAC+pZpOKfRs6cfY0lv2kWdy8Oxf9j4+A==",
|
||||
"dev": true
|
||||
},
|
||||
"emojis-list": {
|
||||
|
|
@ -3083,9 +3083,9 @@
|
|||
}
|
||||
},
|
||||
"node-releases": {
|
||||
"version": "1.1.66",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.66.tgz",
|
||||
"integrity": "sha512-JHEQ1iWPGK+38VLB2H9ef2otU4l8s3yAMt9Xf934r6+ojCYDMHPMqvCc9TnzfeFSP1QEOeU6YZEd3+De0LTCgg==",
|
||||
"version": "1.1.67",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.67.tgz",
|
||||
"integrity": "sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==",
|
||||
"dev": true
|
||||
},
|
||||
"p-limit": {
|
||||
|
|
@ -3235,9 +3235,9 @@
|
|||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.0.tgz",
|
||||
"integrity": "sha512-fTtCdI7VeyNK0HP4q4y9Z9ts8TUeaF+2/FjKx8CJ/7/Oem1rCX7zIJe+d+jLrVnVNQjENd3gqmANraLcdRWwnQ==",
|
||||
"version": "npm:vue-loader@16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.1.tgz",
|
||||
"integrity": "sha512-wz/+HFg/3SBayHWAlZXARcnDTl3VOChrfW9YnxvAweiuyKX/7IGx1ad/4yJHmwhgWlOVYMAbTiI7GV8G33PfGQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
|
|
@ -3872,34 +3872,34 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"browserslist": {
|
||||
"version": "4.14.7",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.7.tgz",
|
||||
"integrity": "sha512-BSVRLCeG3Xt/j/1cCGj1019Wbty0H+Yvu2AOuZSuoaUWn3RatbL33Cxk+Q4jRMRAbOm0p7SLravLjpnT6s0vzQ==",
|
||||
"version": "4.15.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.15.0.tgz",
|
||||
"integrity": "sha512-IJ1iysdMkGmjjYeRlDU8PQejVwxvVO5QOfXH7ylW31GO6LwNRSmm/SgRXtNsEXqMLl2e+2H5eEJ7sfynF8TCaQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"caniuse-lite": "^1.0.30001157",
|
||||
"caniuse-lite": "^1.0.30001164",
|
||||
"colorette": "^1.2.1",
|
||||
"electron-to-chromium": "^1.3.591",
|
||||
"electron-to-chromium": "^1.3.612",
|
||||
"escalade": "^3.1.1",
|
||||
"node-releases": "^1.1.66"
|
||||
"node-releases": "^1.1.67"
|
||||
}
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001157",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001157.tgz",
|
||||
"integrity": "sha512-gOerH9Wz2IRZ2ZPdMfBvyOi3cjaz4O4dgNwPGzx8EhqAs4+2IL/O+fJsbt+znSigujoZG8bVcIAUM/I/E5K3MA==",
|
||||
"version": "1.0.30001165",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001165.tgz",
|
||||
"integrity": "sha512-8cEsSMwXfx7lWSUMA2s08z9dIgsnR5NAqjXP23stdsU3AUWkCr/rr4s4OFtHXn5XXr6+7kam3QFVoYyXNPdJPA==",
|
||||
"dev": true
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.592",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.592.tgz",
|
||||
"integrity": "sha512-kGNowksvqQiPb1pUSQKpd8JFoGPLxYOwduNRCqCxGh/2Q1qE2JdmwouCW41lUzDxOb/2RIV4lR0tVIfboWlO9A==",
|
||||
"version": "1.3.615",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.615.tgz",
|
||||
"integrity": "sha512-fNYTQXoUhNc6RmHDlGN4dgcLURSBIqQCN7ls6MuQ741+NJyLNRz8DxAC+pZpOKfRs6cfY0lv2kWdy8Oxf9j4+A==",
|
||||
"dev": true
|
||||
},
|
||||
"node-releases": {
|
||||
"version": "1.1.66",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.66.tgz",
|
||||
"integrity": "sha512-JHEQ1iWPGK+38VLB2H9ef2otU4l8s3yAMt9Xf934r6+ojCYDMHPMqvCc9TnzfeFSP1QEOeU6YZEd3+De0LTCgg==",
|
||||
"version": "1.1.67",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.67.tgz",
|
||||
"integrity": "sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
|
|
@ -4864,16 +4864,16 @@
|
|||
}
|
||||
},
|
||||
"cli-highlight": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.4.tgz",
|
||||
"integrity": "sha512-s7Zofobm20qriqDoU9sXptQx0t2R9PEgac92mENNm7xaEe1hn71IIMsXMK+6encA6WRCWWxIGQbipr3q998tlQ==",
|
||||
"version": "2.1.9",
|
||||
"resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.9.tgz",
|
||||
"integrity": "sha512-t8RNIZgiI24i/mslZ8XT8o660RUj5ZbUJpEZrZa/BNekTzdC2LfMRAnt0Y7sgzNM4FGW5tmWg/YnbTH8o1eIOQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^3.0.0",
|
||||
"highlight.js": "^9.6.0",
|
||||
"chalk": "^4.0.0",
|
||||
"highlight.js": "^10.0.0",
|
||||
"mz": "^2.4.0",
|
||||
"parse5": "^5.1.1",
|
||||
"parse5-htmlparser2-tree-adapter": "^5.1.1",
|
||||
"parse5-htmlparser2-tree-adapter": "^6.0.0",
|
||||
"yargs": "^15.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
@ -4887,9 +4887,9 @@
|
|||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
|
||||
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
|
|
@ -5628,28 +5628,28 @@
|
|||
"dev": true
|
||||
},
|
||||
"csso": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/csso/-/csso-4.1.0.tgz",
|
||||
"integrity": "sha512-h+6w/W1WqXaJA4tb1dk7r5tVbOm97MsKxzwnvOR04UQ6GILroryjMWu3pmCCtL2mLaEStQ0fZgeGiy99mo7iyg==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz",
|
||||
"integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"css-tree": "^1.0.0"
|
||||
"css-tree": "^1.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"css-tree": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0.tgz",
|
||||
"integrity": "sha512-CdVYz/Yuqw0VdKhXPBIgi8DO3NicJVYZNWeX9XcIuSp9ZoFT5IcleVRW07O5rMjdcx1mb+MEJPknTTEW7DdsYw==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.2.tgz",
|
||||
"integrity": "sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mdn-data": "2.0.12",
|
||||
"mdn-data": "2.0.14",
|
||||
"source-map": "^0.6.1"
|
||||
}
|
||||
},
|
||||
"mdn-data": {
|
||||
"version": "2.0.12",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.12.tgz",
|
||||
"integrity": "sha512-ULbAlgzVb8IqZ0Hsxm6hHSlQl3Jckst2YEQS7fODu9ilNWy2LvcoSY7TRFIktABP2mdppBioc66va90T+NUs8Q==",
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
|
|
@ -6089,9 +6089,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"domelementtype": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.2.tgz",
|
||||
"integrity": "sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz",
|
||||
"integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
|
|
@ -6620,9 +6620,9 @@
|
|||
}
|
||||
},
|
||||
"eslint-plugin-prettier": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz",
|
||||
"integrity": "sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.2.0.tgz",
|
||||
"integrity": "sha512-kOUSJnFjAUFKwVxuzy6sA5yyMx6+o9ino4gCdShzBNx4eyFRudWRYKCFolKjoM40PEiuU6Cn7wBLfq3WsGg7qg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"prettier-linter-helpers": "^1.0.0"
|
||||
|
|
@ -8216,9 +8216,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"highlight.js": {
|
||||
"version": "9.18.3",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.3.tgz",
|
||||
"integrity": "sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ==",
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.4.1.tgz",
|
||||
"integrity": "sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg==",
|
||||
"dev": true
|
||||
},
|
||||
"hmac-drbg": {
|
||||
|
|
@ -9521,9 +9521,9 @@
|
|||
}
|
||||
},
|
||||
"loglevel": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.0.tgz",
|
||||
"integrity": "sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ==",
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz",
|
||||
"integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==",
|
||||
"dev": true
|
||||
},
|
||||
"loose-envify": {
|
||||
|
|
@ -10206,11 +10206,49 @@
|
|||
"dev": true
|
||||
},
|
||||
"object-is": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.3.tgz",
|
||||
"integrity": "sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.4.tgz",
|
||||
"integrity": "sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg==",
|
||||
"dev": true,
|
||||
"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",
|
||||
"es-abstract": "^1.18.0-next.1"
|
||||
},
|
||||
|
|
@ -10251,9 +10289,9 @@
|
|||
}
|
||||
},
|
||||
"object-inspect": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
|
||||
"integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==",
|
||||
"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": {
|
||||
|
|
@ -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": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
|
||||
|
|
@ -10317,15 +10318,70 @@
|
|||
}
|
||||
},
|
||||
"object.values": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz",
|
||||
"integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.2.tgz",
|
||||
"integrity": "sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"define-properties": "^1.1.3",
|
||||
"es-abstract": "^1.17.0-next.1",
|
||||
"function-bind": "^1.1.1",
|
||||
"es-abstract": "^1.18.0-next.1",
|
||||
"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": {
|
||||
|
|
@ -10561,12 +10617,20 @@
|
|||
"dev": true
|
||||
},
|
||||
"parse5-htmlparser2-tree-adapter": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-5.1.1.tgz",
|
||||
"integrity": "sha512-CF+TKjXqoqyDwHqBhFQ+3l5t83xYi6fVT1tQNg+Ye0JRLnTxWvIroCjEp1A0k4lneHNBGnICUf0cfYVYGEazqw==",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
|
||||
"integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==",
|
||||
"dev": true,
|
||||
"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": {
|
||||
|
|
@ -10783,9 +10847,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
|
|
@ -12452,9 +12516,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
|
|
@ -12770,68 +12834,13 @@
|
|||
}
|
||||
},
|
||||
"string.prototype.trimend": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz",
|
||||
"integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz",
|
||||
"integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"define-properties": "^1.1.3",
|
||||
"es-abstract": "^1.18.0-next.1"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
"call-bind": "^1.0.0",
|
||||
"define-properties": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"string.prototype.trimleft": {
|
||||
|
|
@ -12855,68 +12864,13 @@
|
|||
}
|
||||
},
|
||||
"string.prototype.trimstart": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz",
|
||||
"integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz",
|
||||
"integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"define-properties": "^1.1.3",
|
||||
"es-abstract": "^1.18.0-next.1"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
"call-bind": "^1.0.0",
|
||||
"define-properties": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
|
|
@ -13387,9 +13341,9 @@
|
|||
}
|
||||
},
|
||||
"uikit": {
|
||||
"version": "3.5.9",
|
||||
"resolved": "https://registry.npmjs.org/uikit/-/uikit-3.5.9.tgz",
|
||||
"integrity": "sha512-0197WceeBMlCLscsmds6zqL8K3TOtdUCvoHPJEuYqYY2gKa7JttlA6MiyzU0yBPzDlcawXF2TdOmQ/DWy6ZDEw==",
|
||||
"version": "3.5.10",
|
||||
"resolved": "https://registry.npmjs.org/uikit/-/uikit-3.5.10.tgz",
|
||||
"integrity": "sha512-X+OUD02atmks1yYG9C3FSo1EcQ0xfV0+NCHspTShd2uMKYcC5LiMq6o/MyxBxx2bQSeKXh6Kh8TqQ/v4LWI4Tg==",
|
||||
"dev": true
|
||||
},
|
||||
"unicode-canonical-property-names-ecmascript": {
|
||||
|
|
@ -13699,9 +13653,9 @@
|
|||
}
|
||||
},
|
||||
"object-inspect": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
|
||||
"integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==",
|
||||
"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": {
|
||||
|
|
@ -13915,9 +13869,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"vuex": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.5.1.tgz",
|
||||
"integrity": "sha512-w7oJzmHQs0FM9LXodfskhw9wgKBiaB+totOdb8sNzbTB2KDCEEwEs29NzBZFh/lmEK1t5tDmM1vtsO7ubG1DFw==",
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.0.tgz",
|
||||
"integrity": "sha512-W74OO2vCJPs9/YjNjW8lLbj+jzT24waTo2KShI8jLvJW8OaIkgb3wuAMA7D+ZiUxDOx3ubwSZTaJBip9G8a3aQ==",
|
||||
"dev": true
|
||||
},
|
||||
"watchpack": {
|
||||
|
|
|
|||
|
|
@ -18,25 +18,25 @@
|
|||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.12.1",
|
||||
"@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",
|
||||
"axios": "^0.19.2",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"css-loader": "^3.6.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-prettier": "^3.2.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"less": "^3.12.2",
|
||||
"less-loader": "^5.0.0",
|
||||
"mdns-js": "^1.0.3",
|
||||
"prettier": "^1.19.1",
|
||||
"uikit": "^3.5.9",
|
||||
"uikit": "^3.5.10",
|
||||
"vue": "^2.6.12",
|
||||
"vue-friendly-iframe": "^0.17.0",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"vue-tour": "^1.5.0",
|
||||
"vuejs-paginate": "^2.1.0",
|
||||
"vuex": "^3.5.1"
|
||||
"vuex": "^3.6.0"
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
|
|
|
|||
|
|
@ -98,7 +98,9 @@ export default {
|
|||
this.$emit("input", newSelected);
|
||||
|
||||
// Move focus back to key textbox
|
||||
this.$refs.textboxKey.focus();
|
||||
if (this.$refs.textboxKey) {
|
||||
this.$refs.textboxKey.focus();
|
||||
}
|
||||
},
|
||||
|
||||
delMetadataKey: function(key) {
|
||||
|
|
|
|||
|
|
@ -198,7 +198,6 @@ export default {
|
|||
if (!error) {
|
||||
error = Error("Unknown error");
|
||||
}
|
||||
|
||||
this.$emit("error", error);
|
||||
this.$emit("finished");
|
||||
})
|
||||
|
|
|
|||
|
|
@ -248,9 +248,8 @@
|
|||
:submit-data="scanPayload"
|
||||
:submit-label="'Start Scan'"
|
||||
:button-primary="true"
|
||||
@submit="onScanSubmit"
|
||||
@response="onScanResponse"
|
||||
@error="onScanError"
|
||||
@error="modalError"
|
||||
>
|
||||
</taskSubmitter>
|
||||
</div>
|
||||
|
|
@ -433,14 +432,10 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
onScanSubmit: function() {},
|
||||
|
||||
onScanResponse: function() {
|
||||
this.modalNotify("Finished scan.");
|
||||
},
|
||||
|
||||
onScanError: function(error) {
|
||||
this.modalError(error);
|
||||
// Emit signal to update capture list
|
||||
this.$root.$emit("globalUpdateCaptures");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -119,7 +119,6 @@ export default {
|
|||
|
||||
deleteAll: function() {
|
||||
axios.all(this.allURLs.map(l => axios.delete(l))).then(() => {
|
||||
console.log("Delete finished")
|
||||
// Emit signal to update capture list
|
||||
this.$root.$emit("globalUpdateCaptures");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
:submit-label="'Create ZIP'"
|
||||
:submit-data="captureIds"
|
||||
@response="onResponse"
|
||||
@error="onError"
|
||||
@error="modalError"
|
||||
>
|
||||
</taskSubmitter>
|
||||
</div>
|
||||
|
|
@ -163,10 +163,6 @@ export default {
|
|||
this.lastSessionId = response.output.id;
|
||||
this.downloadUrl = `${this.zipGetterUri}/${this.lastSessionId}`;
|
||||
this.downloadReady = true;
|
||||
},
|
||||
|
||||
onError: function(error) {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ export default {
|
|||
var captures = [];
|
||||
for (var capture of this.captures) {
|
||||
// Add to capture list if matched
|
||||
if (!capture.dataset) {
|
||||
if (!this.isDatasetPopulated(capture.dataset)) {
|
||||
captures.push(capture);
|
||||
}
|
||||
}
|
||||
|
|
@ -212,7 +212,7 @@ export default {
|
|||
for (var capture of this.captures) {
|
||||
var dataset = capture.dataset;
|
||||
|
||||
if (dataset) {
|
||||
if (this.isDatasetPopulated(dataset)) {
|
||||
var id = dataset["id"];
|
||||
|
||||
// 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) {
|
||||
// Filter a list of captures by an array of tags
|
||||
var result = [];
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@
|
|||
<!-- Left side controls -->
|
||||
<div
|
||||
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 -->
|
||||
<div class="uk-navbar-right">
|
||||
|
|
@ -40,15 +44,14 @@
|
|||
v-for="item in pagedItems"
|
||||
:key="item.timestamp"
|
||||
uk-alert
|
||||
class="logging-entry"
|
||||
:class="{
|
||||
'uk-alert-warning uk-alert': item.data.levelname == 'WARNING',
|
||||
'uk-alert-danger uk-alert': item.data.levelname == 'ERROR'
|
||||
}"
|
||||
>
|
||||
<p>
|
||||
<b>{{ formatDateTime(item.data.created) }}</b>
|
||||
</p>
|
||||
{{ item.data.levelname }}: {{ item.data.message }}
|
||||
<b>{{ formatDateTime(item.data.created) }}</b>
|
||||
<div class="logging-message">{{ formatMessage(item) }}</div>
|
||||
</div>
|
||||
|
||||
<Paginate
|
||||
|
|
@ -84,11 +87,28 @@ export default {
|
|||
return {
|
||||
maxitems: 20,
|
||||
page: 1,
|
||||
logs: []
|
||||
logs: [],
|
||||
allLevels: ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
||||
filterLevel: "WARNING"
|
||||
};
|
||||
},
|
||||
|
||||
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() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/events/logging`;
|
||||
},
|
||||
|
|
@ -97,10 +117,10 @@ export default {
|
|||
},
|
||||
pagedItems: function() {
|
||||
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() {
|
||||
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) {
|
||||
let date = new Date(isoDateTimeString);
|
||||
return date.toLocaleDateString() + " " + date.toLocaleTimeString();
|
||||
},
|
||||
formatMessage: function(item) {
|
||||
return item.data.levelname + ": " + item.data.message;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -147,4 +170,10 @@ export default {
|
|||
margin-bottom: 30px;
|
||||
height: 80px;
|
||||
}
|
||||
.logging-entry {
|
||||
white-space: break-spaces;
|
||||
}
|
||||
.logging-message {
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@
|
|||
:submit-on-event="'globalFastAutofocusEvent'"
|
||||
@taskStarted="isAutofocusing = 1"
|
||||
@finished="isAutofocusing = 0"
|
||||
@error="modalError"
|
||||
></taskSubmitter>
|
||||
</div>
|
||||
|
||||
|
|
@ -160,6 +161,7 @@
|
|||
:button-primary="false"
|
||||
@taskStarted="isAutofocusing = 2"
|
||||
@finished="isAutofocusing = 0"
|
||||
@error="modalError"
|
||||
></taskSubmitter>
|
||||
</div>
|
||||
|
||||
|
|
@ -172,6 +174,7 @@
|
|||
:button-primary="false"
|
||||
@taskStarted="isAutofocusing = 3"
|
||||
@finished="isAutofocusing = 0"
|
||||
@error="modalError"
|
||||
></taskSubmitter>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
:submit-url="recalibrationLinks.calibrate_xy.href"
|
||||
:submit-label="'Auto-Calibrate using camera'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="onRecalibrateError"
|
||||
@error="modalError"
|
||||
>
|
||||
</taskSubmitter>
|
||||
</div>
|
||||
|
|
@ -149,10 +149,6 @@ export default {
|
|||
this.modalNotify("Finished stage-to-camera calibration.");
|
||||
// Update local settings
|
||||
this.updateSettings();
|
||||
},
|
||||
|
||||
onRecalibrateError: function(error) {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
:submit-url="recalibrationLinks.recalibrate.href"
|
||||
:submit-label="'Auto-Calibrate'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="onRecalibrateError"
|
||||
@error="modalError"
|
||||
>
|
||||
</taskSubmitter>
|
||||
</div>
|
||||
|
|
@ -128,10 +128,6 @@ export default {
|
|||
this.modalNotify("Finished recalibration.");
|
||||
},
|
||||
|
||||
onRecalibrateError: function(error) {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
},
|
||||
|
||||
flattenLensShadingTableRequest: function() {
|
||||
axios.post(this.recalibrationLinks.flatten_lens_shading_table.href);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@
|
|||
submit-label="Start scan"
|
||||
@submit="scanRunning = true"
|
||||
@response="scanRunning = false"
|
||||
@error="scanRunning = false"
|
||||
@error="onScanError"
|
||||
></taskSubmitter>
|
||||
<br />
|
||||
|
||||
|
|
@ -233,6 +233,11 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
onScanError: function(error) {
|
||||
this.scanRunning = false;
|
||||
this.modalError(error);
|
||||
},
|
||||
|
||||
decrement: function() {
|
||||
if (this.stepValue > 0) {
|
||||
this.stepValue = this.stepValue - 1;
|
||||
|
|
|
|||
|
|
@ -79,19 +79,25 @@ class CaptureSchema(ImageSchema):
|
|||
"href": url_for(CaptureView.endpoint, id_=capture_id, _external=True),
|
||||
"mimetype": "application/json",
|
||||
**description_from_view(CaptureView),
|
||||
},
|
||||
}
|
||||
if CaptureView.endpoint
|
||||
else {},
|
||||
"tags": {
|
||||
"href": url_for(CaptureTags.endpoint, id_=capture_id, _external=True),
|
||||
"mimetype": "application/json",
|
||||
**description_from_view(CaptureTags),
|
||||
},
|
||||
}
|
||||
if CaptureTags.endpoint
|
||||
else {},
|
||||
"annotations": {
|
||||
"href": url_for(
|
||||
CaptureAnnotations.endpoint, id_=capture_id, _external=True
|
||||
),
|
||||
"mimetype": "application/json",
|
||||
**description_from_view(CaptureAnnotations),
|
||||
},
|
||||
}
|
||||
if CaptureAnnotations.endpoint
|
||||
else {},
|
||||
"download": {
|
||||
"href": url_for(
|
||||
CaptureDownload.endpoint,
|
||||
|
|
@ -101,7 +107,9 @@ class CaptureSchema(ImageSchema):
|
|||
),
|
||||
"mimetype": "image/jpeg",
|
||||
**description_from_view(CaptureDownload),
|
||||
},
|
||||
}
|
||||
if CaptureDownload.endpoint
|
||||
else {},
|
||||
}
|
||||
|
||||
if isinstance(data, dict):
|
||||
|
|
|
|||
|
|
@ -3,14 +3,16 @@ import io
|
|||
import logging
|
||||
import time
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from collections import namedtuple
|
||||
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
|
||||
|
||||
|
||||
# Class to store a frames metadata
|
||||
TrackerFrame = namedtuple("TrackerFrame", ["size", "time"])
|
||||
class TrackerFrame(NamedTuple):
|
||||
size: int
|
||||
time: float
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
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):
|
||||
"""Create camera on context enter."""
|
||||
return self
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ class MissingCamera(BaseCamera):
|
|||
"""
|
||||
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):
|
||||
pass
|
||||
|
|
@ -188,11 +188,11 @@ class MissingCamera(BaseCamera):
|
|||
|
||||
def start_preview(self, *_, **__):
|
||||
"""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):
|
||||
"""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, *_, **__):
|
||||
"""Start recording.
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ try:
|
|||
except Exception as e: # pylint: disable=W0703
|
||||
logging.error(e)
|
||||
logging.warning("Unable to import PiCameraStreamer")
|
||||
from labthings import CompositeLock, StrictLock
|
||||
from labthings import CompositeLock
|
||||
|
||||
from openflexure_microscope.config import user_configuration, user_settings
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ class Microscope:
|
|||
|
||||
# Initialise with an empty composite lock
|
||||
#: :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.stage: BaseStage = None #: Currently connected stage object
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import numpy as np
|
|||
from labthings import StrictLock
|
||||
from typing_extensions import Literal
|
||||
|
||||
CoordinateType = Tuple[int, int, int]
|
||||
|
||||
|
||||
class BaseStage(metaclass=ABCMeta):
|
||||
"""
|
||||
|
|
@ -41,7 +43,7 @@ class BaseStage(metaclass=ABCMeta):
|
|||
|
||||
@property
|
||||
@abstractmethod
|
||||
def position(self) -> Tuple[int, int, int]:
|
||||
def position(self) -> CoordinateType:
|
||||
"""The current position, as a list"""
|
||||
|
||||
@property
|
||||
|
|
@ -64,7 +66,7 @@ class BaseStage(metaclass=ABCMeta):
|
|||
@abstractmethod
|
||||
def move_rel(
|
||||
self,
|
||||
displacement: Union[int, Tuple[int, int, int]],
|
||||
displacement: Union[int, CoordinateType],
|
||||
axis: Optional[Literal["x", "y", "z"]] = None,
|
||||
backlash: bool = True,
|
||||
):
|
||||
|
|
@ -74,7 +76,7 @@ class BaseStage(metaclass=ABCMeta):
|
|||
"""
|
||||
|
||||
@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"""
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -87,7 +89,7 @@ class BaseStage(metaclass=ABCMeta):
|
|||
|
||||
def scan_linear(
|
||||
self,
|
||||
rel_positions: List[Tuple[int, int, int]],
|
||||
rel_positions: List[CoordinateType],
|
||||
backlash: bool = True,
|
||||
return_to_start: bool = True,
|
||||
):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
import time
|
||||
from collections.abc import Iterable
|
||||
from types import GeneratorType
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
|
|
@ -11,6 +12,19 @@ from openflexure_microscope.stage.base import BaseStage
|
|||
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):
|
||||
"""
|
||||
Sangaboard v0.2 and v0.3 powered Stage object
|
||||
|
|
@ -30,11 +44,9 @@ class SangaStage(BaseStage):
|
|||
self.port = port
|
||||
self.board = Sangaboard(port, **kwargs)
|
||||
|
||||
self._backlash = (
|
||||
None # Initialise backlash storage, used by property setter/getter
|
||||
)
|
||||
# Initialise backlash storage, used by property setter/getter
|
||||
self._backlash = None
|
||||
self.settle_time = 0.2 # Default move settle time
|
||||
|
||||
self._position_on_enter = None
|
||||
|
||||
@property
|
||||
|
|
@ -56,11 +68,11 @@ class SangaStage(BaseStage):
|
|||
return 3
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
def position(self) -> Tuple[int, int, int]:
|
||||
return self.board.position
|
||||
|
||||
@property
|
||||
def backlash(self):
|
||||
def backlash(self) -> np.ndarray:
|
||||
"""The distance used for backlash compensation.
|
||||
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,
|
||||
|
|
@ -75,8 +87,12 @@ class SangaStage(BaseStage):
|
|||
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.
|
||||
"""
|
||||
if self._backlash is not None:
|
||||
if isinstance(self._backlash, np.ndarray):
|
||||
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:
|
||||
return np.array([0] * self.n_axes)
|
||||
|
||||
|
|
@ -98,13 +114,16 @@ class SangaStage(BaseStage):
|
|||
if "backlash" in config:
|
||||
# Construct backlash array
|
||||
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:
|
||||
self.settle_time = config.get("settle_time")
|
||||
|
||||
def read_settings(self) -> dict:
|
||||
"""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 = {
|
||||
"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]},
|
||||
"settle_time": self.settle_time,
|
||||
|
|
@ -114,7 +133,7 @@ class SangaStage(BaseStage):
|
|||
|
||||
def move_rel(
|
||||
self,
|
||||
displacement: Union[int, Tuple[int, int, int]],
|
||||
displacement: Union[int, Tuple[int, int, int], np.ndarray],
|
||||
axis: Optional[Literal["x", "y", "z"]] = None,
|
||||
backlash: bool = True,
|
||||
):
|
||||
|
|
@ -122,11 +141,19 @@ class SangaStage(BaseStage):
|
|||
displacement: integer or array/list of 3 integers
|
||||
axis: None (for 3-axis moves) or one of 'x','y','z'
|
||||
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:
|
||||
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 axis:
|
||||
# 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
|
||||
if axis not in ("x", "y", "z"):
|
||||
raise ValueError("axis must be one of x, y, or z")
|
||||
move = (
|
||||
displacement if axis == "x" else 0,
|
||||
displacement if axis == "y" else 0,
|
||||
displacement if axis == "z" else 0,
|
||||
# Calculate displacement array
|
||||
displacement_array: np.ndarray = _displacement_to_array(
|
||||
displacement, axis
|
||||
)
|
||||
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)
|
||||
# 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:
|
||||
# Handle simple case, no backlash
|
||||
if not backlash or self.backlash is None:
|
||||
return self.board.move_rel(displacement_array)
|
||||
|
||||
# Handle move with backlash correction
|
||||
# Calculate main movement
|
||||
initial_move: np.ndarray = displacement_array
|
||||
initial_move -= np.where(
|
||||
self.backlash * displacement < 0,
|
||||
self.backlash * displacement_array < 0,
|
||||
self.backlash,
|
||||
np.zeros(self.n_axes, dtype=self.backlash.dtype),
|
||||
)
|
||||
# Make the main movement
|
||||
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
|
||||
# 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
|
||||
# can just take over before settling
|
||||
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
|
||||
"""
|
||||
with self.lock:
|
||||
|
|
@ -220,15 +252,21 @@ class SangaStage(BaseStage):
|
|||
|
||||
class SangaDeltaStage(SangaStage):
|
||||
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_a = flex_a
|
||||
self.flex_b = flex_b
|
||||
self.flex_h: int = flex_h
|
||||
self.flex_a: int = flex_a
|
||||
self.flex_b: int = flex_b
|
||||
|
||||
# Set up camera rotation relative to stage
|
||||
camera_theta = (camera_angle / 180) * np.pi
|
||||
self.R_camera = np.array(
|
||||
camera_theta: float = (camera_angle / 180) * np.pi
|
||||
self.R_camera: np.ndarray = np.array(
|
||||
[
|
||||
[np.cos(camera_theta), -np.sin(camera_theta), 0],
|
||||
[np.sin(camera_theta), np.cos(camera_theta), 0],
|
||||
|
|
@ -239,13 +277,15 @@ class SangaDeltaStage(SangaStage):
|
|||
logging.debug(self.R_camera)
|
||||
|
||||
# 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)
|
||||
)
|
||||
y_fac = -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))
|
||||
y_fac: np.float = -1 * np.divide(self.flex_b, self.flex_h)
|
||||
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],
|
||||
[0.5 * y_fac, 0.5 * y_fac, -y_fac],
|
||||
|
|
@ -254,40 +294,76 @@ class SangaDeltaStage(SangaStage):
|
|||
)
|
||||
logging.debug(self.Tvd)
|
||||
|
||||
self.Tdv = np.linalg.inv(self.Tvd)
|
||||
self.Tdv: np.ndarray = np.linalg.inv(self.Tvd)
|
||||
logging.debug(self.Tdv)
|
||||
|
||||
SangaStage.__init__(self, port=port, **kwargs)
|
||||
|
||||
@property
|
||||
def raw_position(self) -> Tuple[int, int, int]:
|
||||
return self.board.position
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
# 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]
|
||||
|
||||
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
|
||||
displacement = np.dot(self.R_camera, displacement)
|
||||
camera_displacement_array: np.ndarray = np.dot(
|
||||
self.R_camera, cartesian_displacement_array
|
||||
)
|
||||
|
||||
# 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
|
||||
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
|
||||
final = np.dot(self.R_camera, final)
|
||||
camera_final_array: np.ndarray = np.dot(self.R_camera, final)
|
||||
|
||||
# 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))
|
||||
|
||||
# Do the move
|
||||
SangaStage.move_abs(self, final, **kwargs)
|
||||
SangaStage.move_abs(self, delta_final_array, **kwargs)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,25 @@
|
|||
import base64
|
||||
import copy
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List, Optional, Tuple, Type, Union
|
||||
|
||||
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):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.start = None
|
||||
self.end = None
|
||||
def __init__(self, name: str):
|
||||
self.name: str = name
|
||||
self.start: Optional[float] = None
|
||||
self.end: Optional[float] = None
|
||||
|
||||
def __enter__(self):
|
||||
self.start = time.time()
|
||||
|
|
@ -22,19 +29,27 @@ class Timer(object):
|
|||
logging.debug("%s time: %s", self.name, self.end - self.start)
|
||||
|
||||
|
||||
def deserialise_array_b64(b64_string: str, dtype: str, shape: List[int]):
|
||||
flat_arr = np.frombuffer(base64.b64decode(b64_string), dtype)
|
||||
JSONArrayType = TypedDict(
|
||||
"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)
|
||||
|
||||
|
||||
def serialise_array_b64(npy_arr: np.ndarray):
|
||||
b64_string = base64.b64encode(npy_arr).decode("ascii")
|
||||
dtype = str(npy_arr.dtype)
|
||||
shape = npy_arr.shape
|
||||
def serialise_array_b64(npy_arr: np.ndarray) -> Tuple[str, str, Tuple[int, ...]]:
|
||||
b64_string: str = base64.b64encode(npy_arr.tobytes()).decode("ascii")
|
||||
dtype: str = str(npy_arr.dtype)
|
||||
shape: Tuple[int, ...] = npy_arr.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):
|
||||
# We can transparently convert memoryview objects to arrays
|
||||
# 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}
|
||||
|
||||
|
||||
def json_to_ndarray(json_dict: dict):
|
||||
def json_to_ndarray(json_dict: JSONArrayType):
|
||||
if not json_dict.get("@type") != "ndarray":
|
||||
logging.warning("No valid @type attribute found. Conversion may fail.")
|
||||
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")
|
||||
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:
|
||||
return deserialise_array_b64(b64_string, dtype, shape)
|
||||
|
|
|
|||
81
poetry.lock
generated
81
poetry.lock
generated
|
|
@ -8,20 +8,20 @@ python-versions = "*"
|
|||
|
||||
[[package]]
|
||||
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)."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
PyYAML = {version = ">=3.10", optional = true, markers = "extra == \"yaml\""}
|
||||
|
||||
[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"]
|
||||
docs = ["marshmallow (>=2.19.2)", "pyyaml (==5.3.1)", "sphinx (==3.2.1)", "sphinx-issues (==1.2.0)", "sphinx-rtd-theme (==0.5.0)"]
|
||||
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 (>=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)"]
|
||||
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)"]
|
||||
yaml = ["PyYAML (>=3.10)"]
|
||||
|
||||
|
|
@ -278,7 +278,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "3.1.0"
|
||||
version = "3.1.1"
|
||||
description = "Read metadata from Python packages"
|
||||
category = "main"
|
||||
optional = false
|
||||
|
|
@ -288,8 +288,8 @@ python-versions = ">=3.6"
|
|||
zipp = ">=0.5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx", "rst.linker"]
|
||||
testing = ["packaging", "pep517", "unittest2", "importlib-resources (>=1.3)"]
|
||||
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
|
||||
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]]
|
||||
name = "iniconfig"
|
||||
|
|
@ -336,28 +336,21 @@ i18n = ["Babel (>=0.8)"]
|
|||
|
||||
[[package]]
|
||||
name = "labthings"
|
||||
version = "1.1.5"
|
||||
version = "1.2.2"
|
||||
description = "Python implementation of LabThings, based on the Flask microframework"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "^3.6"
|
||||
develop = true
|
||||
python-versions = ">=3.6,<4.0"
|
||||
|
||||
[package.dependencies]
|
||||
apispec = "^3.2.0"
|
||||
apispec_webframeworks = "^0.5.2"
|
||||
Flask = "^1.1.1"
|
||||
flask-cors = "^3.0.8"
|
||||
marshmallow = "^3.4.0"
|
||||
webargs = "^6.0.0"
|
||||
apispec = ">=3.2,<5.0"
|
||||
apispec_webframeworks = ">=0.5.2,<0.6.0"
|
||||
Flask = ">=1.1.1,<2.0.0"
|
||||
flask-cors = ">=3.0.8,<4.0.0"
|
||||
marshmallow = ">=3.4.0,<4.0.0"
|
||||
webargs = ">=6.0.0,<7.0.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]]
|
||||
name = "lazy-object-proxy"
|
||||
version = "1.4.3"
|
||||
|
|
@ -428,6 +421,25 @@ category = "main"
|
|||
optional = false
|
||||
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]]
|
||||
name = "opencv-python-headless"
|
||||
version = "4.4.0.44"
|
||||
|
|
@ -441,7 +453,7 @@ numpy = ">=1.17.3"
|
|||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "20.4"
|
||||
version = "20.7"
|
||||
description = "Core utilities for Python packages"
|
||||
category = "main"
|
||||
optional = false
|
||||
|
|
@ -449,7 +461,6 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|||
|
||||
[package.dependencies]
|
||||
pyparsing = ">=2.0.2"
|
||||
six = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pastel"
|
||||
|
|
@ -956,7 +967,7 @@ rpi = ["RPi.GPIO"]
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.7.3"
|
||||
content-hash = "e2f1011751b49fc5837f475f5304822216724a89f877ee5daf9c281cd8830673"
|
||||
content-hash = "2c0faa244099ec3433edfc2cb9321f5de8bd5c0c32e135cae23f91e717cb07d7"
|
||||
|
||||
[metadata.files]
|
||||
alabaster = [
|
||||
|
|
@ -964,8 +975,8 @@ alabaster = [
|
|||
{file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
|
||||
]
|
||||
apispec = [
|
||||
{file = "apispec-3.3.2-py2.py3-none-any.whl", hash = "sha256:a1df9ec6b2cd0edf45039ef025abd7f0660808fa2edf737d3ba1cf5ef1a4625b"},
|
||||
{file = "apispec-3.3.2.tar.gz", hash = "sha256:d23ebd5b71e541e031b02a19db10b5e6d5ef8452c552833e3e1afc836b40b1ad"},
|
||||
{file = "apispec-4.0.0-py2.py3-none-any.whl", hash = "sha256:20d271f7c8d130719be223fdb122af391ff8d59fb24958c793f632305b87f8ed"},
|
||||
{file = "apispec-4.0.0.tar.gz", hash = "sha256:360e28e5e84a4d7023b16de2b897327fe3da63ddc8e01f9165b9113b7fe1c48a"},
|
||||
]
|
||||
apispec-webframeworks = [
|
||||
{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"},
|
||||
]
|
||||
importlib-metadata = [
|
||||
{file = "importlib_metadata-3.1.0-py2.py3-none-any.whl", hash = "sha256:590690d61efdd716ff82c39ca9a9d4209252adfe288a4b5721181050acbd4175"},
|
||||
{file = "importlib_metadata-3.1.0.tar.gz", hash = "sha256:d9b8a46a0885337627a6430db287176970fff18ad421becec1d64cfc763c2099"},
|
||||
{file = "importlib_metadata-3.1.1-py3-none-any.whl", hash = "sha256:6112e21359ef8f344e7178aa5b72dc6e62b38b0d008e6d3cb212c5b84df72013"},
|
||||
{file = "importlib_metadata-3.1.1.tar.gz", hash = "sha256:b0c2d3b226157ae4517d9625decf63591461c66b3a808c2666d538946519d170"},
|
||||
]
|
||||
iniconfig = [
|
||||
{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.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 = [
|
||||
{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"},
|
||||
|
|
@ -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.zip", hash = "sha256:0d310730e1e793527065ad7dde736197b705d0e4c9999775f212b03c44a8484c"},
|
||||
]
|
||||
numpy-stubs = []
|
||||
opencv-python-headless = [
|
||||
{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"},
|
||||
|
|
@ -1243,8 +1258,8 @@ opencv-python-headless = [
|
|||
{file = "opencv_python_headless-4.4.0.44-cp38-cp38-win_amd64.whl", hash = "sha256:93f251c28739d8e8ade8898ddcbd75dcec60f779d9534644c568e7e65e2b76de"},
|
||||
]
|
||||
packaging = [
|
||||
{file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"},
|
||||
{file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"},
|
||||
{file = "packaging-20.7-py2.py3-none-any.whl", hash = "sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"},
|
||||
{file = "packaging-20.7.tar.gz", hash = "sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236"},
|
||||
]
|
||||
pastel = [
|
||||
{file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api"
|
|||
|
||||
[tool.poetry]
|
||||
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."
|
||||
|
||||
authors = [
|
||||
|
|
@ -43,10 +43,11 @@ expiringdict = "^1.2.1"
|
|||
camera-stage-mapping = "0.1.4"
|
||||
picamerax = ">=20.9.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"
|
||||
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]
|
||||
rpi = ["RPi.GPIO"]
|
||||
|
|
@ -66,7 +67,7 @@ poethepoet = "^0.9.0"
|
|||
freezegun = "^1.0.0"
|
||||
|
||||
[tool.black]
|
||||
exclude = '(\.eggs|\.git|\.venv|node_modules/)'
|
||||
exclude = '(\.eggs|\.git|\.venv|\venv|node_modules/)'
|
||||
|
||||
[tool.isort]
|
||||
multi_line_output = 3
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue