From 5091ee8983d7001c6dfd0e136880618806bbf14a Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 30 Nov 2020 13:52:41 +0000 Subject: [PATCH 01/35] Updated to LabThings 1.1.6 --- poetry.lock | 44 ++++++++++++++++++++------------------------ pyproject.toml | 2 +- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/poetry.lock b/poetry.lock index d7a4f526..7cfd28ce 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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)"] @@ -336,28 +336,21 @@ i18n = ["Babel (>=0.8)"] [[package]] name = "labthings" -version = "1.1.5" +version = "1.1.6" 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" @@ -956,7 +949,7 @@ rpi = ["RPi.GPIO"] [metadata] lock-version = "1.1" python-versions = "^3.7.3" -content-hash = "e2f1011751b49fc5837f475f5304822216724a89f877ee5daf9c281cd8830673" +content-hash = "6309f2d5e874193ed420e2b7a4b399be69c82c7d2ee1eaaad8634706daa3422e" [metadata.files] alabaster = [ @@ -964,8 +957,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"}, @@ -1109,7 +1102,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.1.6-py3-none-any.whl", hash = "sha256:ca3c906b29f7b7ba86ee11d858ecf05d7a34f2d277f3e87af8a255d5825b3992"}, + {file = "labthings-1.1.6.tar.gz", hash = "sha256:30901ea6b1fb1f7468c92a0ebfc5a138e324a2886928720ace1a58761f369ab8"}, +] 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"}, diff --git a/pyproject.toml b/pyproject.toml index 68b9d60d..f2fa51ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,10 +43,10 @@ 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.1.6" [tool.poetry.extras] rpi = ["RPi.GPIO"] From 7a849c725a795215527424b9cc78eb5fdee15e90 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 30 Nov 2020 16:23:02 +0000 Subject: [PATCH 02/35] Test on new LabThings branch --- poetry.lock | 33 ++++++++++++++++++--------------- pyproject.toml | 2 +- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7cfd28ce..5ca93cb1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -340,17 +340,24 @@ version = "1.1.6" description = "Python implementation of LabThings, based on the Flask microframework" category = "main" optional = false -python-versions = ">=3.6,<4.0" +python-versions = "^3.6" +develop = true [package.dependencies] 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" +apispec_webframeworks = "^0.5.2" +Flask = "^1.1.1" +flask-cors = "^3.0.8" +marshmallow = "^3.4.0" +webargs = "^6.0.0" zeroconf = ">=0.24.5,<0.29.0" +[package.source] +type = "git" +url = "https://github.com/labthings/python-labthings" +reference = "new-extension-manifest" +resolved_reference = "427b12e4f739fc35bad05a25c099c29c3bba173d" + [[package]] name = "lazy-object-proxy" version = "1.4.3" @@ -434,7 +441,7 @@ numpy = ">=1.17.3" [[package]] name = "packaging" -version = "20.4" +version = "20.7" description = "Core utilities for Python packages" category = "main" optional = false @@ -442,7 +449,6 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] pyparsing = ">=2.0.2" -six = "*" [[package]] name = "pastel" @@ -949,7 +955,7 @@ rpi = ["RPi.GPIO"] [metadata] lock-version = "1.1" python-versions = "^3.7.3" -content-hash = "6309f2d5e874193ed420e2b7a4b399be69c82c7d2ee1eaaad8634706daa3422e" +content-hash = "61ef63ad367aade04025dd0383286671481252c2efba2c068315522b11664206" [metadata.files] alabaster = [ @@ -1102,10 +1108,7 @@ 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 = [ - {file = "labthings-1.1.6-py3-none-any.whl", hash = "sha256:ca3c906b29f7b7ba86ee11d858ecf05d7a34f2d277f3e87af8a255d5825b3992"}, - {file = "labthings-1.1.6.tar.gz", hash = "sha256:30901ea6b1fb1f7468c92a0ebfc5a138e324a2886928720ace1a58761f369ab8"}, -] +labthings = [] 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"}, @@ -1239,8 +1242,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"}, diff --git a/pyproject.toml b/pyproject.toml index f2fa51ac..332b4692 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ pyyaml = "^5.3.1" 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.1.6" +labthings = {git = "https://github.com/labthings/python-labthings", branch="new-extension-manifest", develop = true} [tool.poetry.extras] rpi = ["RPi.GPIO"] From bacc111a87d9b198f6a129f3ed1994d59a719a98 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 30 Nov 2020 16:23:28 +0000 Subject: [PATCH 03/35] Updated documentation to subclass-based extensions --- .../example_extension/01_basic_structure.py | 56 +++--- .../01b_basic_structure_subclass.py | 41 ----- .../example_extension/02_adding_views.py | 65 +++---- .../example_extension/03_marshaling_data.py | 42 ++--- .../example_extension/04_properties.py | 42 ++--- .../example_extension/05_actions.py | 41 ++--- .../example_extension/06_tasks_locks.py | 88 ++++----- .../extensions/example_extension/07_ev_gui.py | 168 +++++++++--------- docs/source/extensions/structure.rst | 14 +- docs/source/extensions/views.rst | 10 ++ 10 files changed, 249 insertions(+), 318 deletions(-) delete mode 100644 docs/source/extensions/example_extension/01b_basic_structure_subclass.py diff --git a/docs/source/extensions/example_extension/01_basic_structure.py b/docs/source/extensions/example_extension/01_basic_structure.py index eec8f362..21301820 100644 --- a/docs/source/extensions/example_extension/01_basic_structure.py +++ b/docs/source/extensions/example_extension/01_basic_structure.py @@ -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,) diff --git a/docs/source/extensions/example_extension/01b_basic_structure_subclass.py b/docs/source/extensions/example_extension/01b_basic_structure_subclass.py deleted file mode 100644 index 9d1b93f3..00000000 --- a/docs/source/extensions/example_extension/01b_basic_structure_subclass.py +++ /dev/null @@ -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() diff --git a/docs/source/extensions/example_extension/02_adding_views.py b/docs/source/extensions/example_extension/02_adding_views.py index d22a9ae2..69aa6d6f 100644 --- a/docs/source/extensions/example_extension/02_adding_views.py +++ b/docs/source/extensions/example_extension/02_adding_views.py @@ -2,42 +2,44 @@ 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") + # Add our API Views (defined below MyExtension) + self.add_view(ExampleIdentifyView, "/identify") + self.add_view(ExampleRenameView, "/rename") -def identify(microscope): - """ - Demonstrate access to Microscope.camera, and 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}." + ) - 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 +55,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,) diff --git a/docs/source/extensions/example_extension/03_marshaling_data.py b/docs/source/extensions/example_extension/03_marshaling_data.py index aceefcea..63a73575 100644 --- a/docs/source/extensions/example_extension/03_marshaling_data.py +++ b/docs/source/extensions/example_extension/03_marshaling_data.py @@ -2,7 +2,22 @@ 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 +30,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 +58,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,) diff --git a/docs/source/extensions/example_extension/04_properties.py b/docs/source/extensions/example_extension/04_properties.py index 3bedbad0..9fc7862d 100644 --- a/docs/source/extensions/example_extension/04_properties.py +++ b/docs/source/extensions/example_extension/04_properties.py @@ -2,7 +2,22 @@ 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 +30,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 +74,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,) diff --git a/docs/source/extensions/example_extension/05_actions.py b/docs/source/extensions/example_extension/05_actions.py index 52b1b024..dcee9ad8 100644 --- a/docs/source/extensions/example_extension/05_actions.py +++ b/docs/source/extensions/example_extension/05_actions.py @@ -5,7 +5,22 @@ 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 +33,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 +77,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 +115,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,) diff --git a/docs/source/extensions/example_extension/06_tasks_locks.py b/docs/source/extensions/example_extension/06_tasks_locks.py index 710b4e73..79f9b1ef 100644 --- a/docs/source/extensions/example_extension/06_tasks_locks.py +++ b/docs/source/extensions/example_extension/06_tasks_locks.py @@ -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,) diff --git a/docs/source/extensions/example_extension/07_ev_gui.py b/docs/source/extensions/example_extension/07_ev_gui.py index a71d9053..0782af73 100644 --- a/docs/source/extensions/example_extension/07_ev_gui.py +++ b/docs/source/extensions/example_extension/07_ev_gui.py @@ -4,60 +4,97 @@ from labthings import current_action, fields, find_component, update_action_prog from labthings.extensions import BaseExtension from labthings.views import ActionView -# Used to convert our GUI dictionary into a complete eV extension GUI -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 +# Used to convert our GUI dictionary into a complete eV extension GUI +from openflexure_microscope.api.utilities.gui import build_gui -def timelapse(microscope, n_images, t_between): - """ - Save a set of images in a timelapse +# Create the extension class +class TimelapseExtension(BaseExtension): + def __init__(self): + # Superclass init function + super().__init__("org.openflexure.timelapse-extension", version="0.0.0") - 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 API views + self.add_view(TimelapseAPIView, "/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 - ) + # 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)) - # Capture - microscope.camera.capture(output) + def timelapse(self, microscope, n_images, t_between): + """ + Save a set of images in a timelapse - # Add system metadata - output.put_metadata(microscope.metadata, system=True) + 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) - # 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) + # 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 + ) - # Wait for the specified time - time.sleep(t_between) + # Capture + microscope.camera.capture(output) + + # 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,) diff --git a/docs/source/extensions/structure.rst b/docs/source/extensions/structure.rst index ee3ee0ce..c05091f3 100644 --- a/docs/source/extensions/structure.rst +++ b/docs/source/extensions/structure.rst @@ -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 diff --git a/docs/source/extensions/views.rst b/docs/source/extensions/views.rst index be8364fb..ff2e5f57 100644 --- a/docs/source/extensions/views.rst +++ b/docs/source/extensions/views.rst @@ -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 `_ + 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 From 8d0759e9e08dc679214dc78eb271e34473fd2642 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 30 Nov 2020 16:41:39 +0000 Subject: [PATCH 04/35] Updated default extensions to subclass structure --- .../api/default_extensions/__init__.py | 28 +- .../api/default_extensions/autofocus.py | 358 +++++++++--------- .../api/default_extensions/autostorage.py | 87 ++--- .../picamera_autocalibrate/__init__.py | 2 +- .../picamera_autocalibrate/extension.py | 73 ++-- .../api/default_extensions/scan.py | 13 +- .../api/default_extensions/zip_builder.py | 42 +- .../api/dev_extensions/__init__.py | 2 + 8 files changed, 300 insertions(+), 305 deletions(-) diff --git a/openflexure_microscope/api/default_extensions/__init__.py b/openflexure_microscope/api/default_extensions/__init__.py index 561239af..1c080a93 100644 --- a/openflexure_microscope/api/default_extensions/__init__.py +++ b/openflexure_microscope/api/default_extensions/__init__.py @@ -2,6 +2,8 @@ import logging import traceback from contextlib import contextmanager +LABTHINGS_EXTENSIONS = [] + @contextmanager def handle_extension_error(extension_name): @@ -17,14 +19,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 + from .autostorage import AutostorageExtension + + LABTHINGS_EXTENSIONS.append(AutostorageExtension) + +with handle_extension_error("lens shading calibration"): + from .picamera_autocalibrate import LSTExtension + + LABTHINGS_EXTENSIONS.append(LSTExtension) + with handle_extension_error("camera stage mapping"): from camera_stage_mapping.ofm_extension import csm_extension -with handle_extension_error("lens shading calibration"): - from .picamera_autocalibrate import lst_extension_v2 + + LABTHINGS_EXTENSIONS.append(csm_extension) diff --git a/openflexure_microscope/api/default_extensions/autofocus.py b/openflexure_microscope/api/default_extensions/autofocus.py index 4ed1a672..d2c616a5 100644 --- a/openflexure_microscope/api/default_extensions/autofocus.py +++ b/openflexure_microscope/api/default_extensions/autofocus.py @@ -109,6 +109,16 @@ class JPEGSharpnessMonitor: return data +@contextmanager +def monitor_sharpness(microscope: Microscope): + m = JPEGSharpnessMonitor(microscope) + m.start() + try: + yield m + finally: + m.stop() + + def sharpness_sum_lap2(rgb_image: np.ndarray): """Return an image sharpness metric: sum(laplacian(image)**")""" image_bw = np.mean(rgb_image, 2) @@ -129,184 +139,193 @@ 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") + self.add_view( + MoveAndMeasureAPI, "/move_and_measure", endpoint="move_and_measure" + ) + + def measure_sharpness( + self, microscope: Microscope, metric_fn: Callable = sharpness_sum_lap2 ): - return metric_fn(getattr(microscope.camera, "array")(use_video_port=True)) - else: - raise RuntimeError(f"Object {microscope.camera} has no method `array`") + """Measure the sharpness of the camera's current view.""" + if hasattr(microscope.camera, "array") and callable( + getattr(microscope.camera, "array") + ): + return metric_fn(getattr(microscope.camera, "array")(use_video_port=True)) + else: + raise RuntimeError(f"Object {microscope.camera} has no method `array`") -def autofocus( - 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 + def autofocus( + self, + 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 - with set_properties(stage, backlash=256), stage.lock, camera.lock: - sharpnesses = [] - positions = [] + with set_properties(stage, backlash=256), stage.lock, camera.lock: + sharpnesses = [] + positions = [] - # Some cameras may not have annotate_text. Reset if it does - if getattr(camera, "annotate_text"): - setattr(camera, "annotate_text", "") + # Some cameras may not have annotate_text. Reset if it does + if getattr(camera, "annotate_text"): + setattr(camera, "annotate_text", "") - 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)) + 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)) - newposition = positions[np.argmax(sharpnesses)] - stage.move_rel((0, 0, newposition - stage.position[2])) + newposition = positions[np.argmax(sharpnesses)] + stage.move_rel((0, 0, newposition - stage.position[2])) - return positions, sharpnesses + 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): + """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): + """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): + """Perform a down-up-down-up autofocus""" + with microscope.camera.lock, microscope.stage.lock: + with monitor_sharpness(microscope) as m: + # Move to (-dz / 2) + m.focus_rel(-dz / 2) + # Move to dz while monitoring sharpness + # i: Sharpness monitor index for this move + # z: Final z position after move + i, z = m.focus_rel(dz) + # Get the z position with highest sharpness from the previous move (index i) + fz = m.sharpest_z_on_move(i) + # Move all the way to the start so it's consistent + # Store final absolute z position from this return move + i, z = m.focus_rel(-dz) + # Move to the target position fz + # Can't do absolute move here yet so move by (fz - z) + m.focus_rel(fz - z) + # Return all focus data + return m.data_dict() -def fast_up_down_up_autofocus( - 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, + ): + """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, 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)] - # 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 = 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 - # 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 = 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 +335,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 +354,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 +380,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 +404,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 +436,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" -) diff --git a/openflexure_microscope/api/default_extensions/autostorage.py b/openflexure_microscope/api/default_extensions/autostorage.py index 7d97d766..2847bff0 100644 --- a/openflexure_microscope/api/default_extensions/autostorage.py +++ b/openflexure_microscope/api/default_extensions/autostorage.py @@ -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,12 +235,9 @@ 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() + self.extension.check_location() return autostorage_extension_v2.get_locations() @@ -217,8 +245,8 @@ 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,11 @@ 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) -) diff --git a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/__init__.py b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/__init__.py index 49d03097..fd920b86 100644 --- a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/__init__.py +++ b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/__init__.py @@ -1 +1 @@ -from .extension import lst_extension_v2 +from .extension import LSTExtension diff --git a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/extension.py b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/extension.py index c10491c4..3b7de2ea 100644 --- a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/extension.py +++ b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/extension.py @@ -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" -) diff --git a/openflexure_microscope/api/default_extensions/scan.py b/openflexure_microscope/api/default_extensions/scan.py index 6cf5abd8..8caa6c13 100644 --- a/openflexure_microscope/api/default_extensions/scan.py +++ b/openflexure_microscope/api/default_extensions/scan.py @@ -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.scan_extension_v2.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") diff --git a/openflexure_microscope/api/default_extensions/zip_builder.py b/openflexure_microscope/api/default_extensions/zip_builder.py index 93da215d..74444a07 100644 --- a/openflexure_microscope/api/default_extensions/zip_builder.py +++ b/openflexure_microscope/api/default_extensions/zip_builder.py @@ -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/", 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/", endpoint="get_id" -) -zip_extension_v2.add_view(ZipListAPIView, "/get", endpoint="get") - -zip_extension_v2.add_view(ZipBuilderAPIView, "/build", endpoint="build") diff --git a/openflexure_microscope/api/dev_extensions/__init__.py b/openflexure_microscope/api/dev_extensions/__init__.py index 01e565b4..c5a0a811 100644 --- a/openflexure_microscope/api/dev_extensions/__init__.py +++ b/openflexure_microscope/api/dev_extensions/__init__.py @@ -1 +1,3 @@ from .tools import devtools_extension_v2 + +LABTHINGS_EXTENSIONS = [devtools_extension_v2] From f849afd2c2f2121884ee0637c595bb35934cda14 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 30 Nov 2020 16:45:43 +0000 Subject: [PATCH 05/35] Clarified LABTHINGS_EXTENSIONS in docs --- docs/source/extensions/introduction.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/source/extensions/introduction.rst b/docs/source/extensions/introduction.rst index 265cf94d..464b83cd 100644 --- a/docs/source/extensions/introduction.rst +++ b/docs/source/extensions/introduction.rst @@ -16,6 +16,15 @@ Package extensions ------------------ Generally, for adding anything other than very simple functionality, extensions should be written as `package distributions `_. 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. From 18a0eedd5ec7daada336b41b464f6f057abf6efc Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 30 Nov 2020 16:57:54 +0000 Subject: [PATCH 06/35] Fixed tile method reference --- openflexure_microscope/api/default_extensions/scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openflexure_microscope/api/default_extensions/scan.py b/openflexure_microscope/api/default_extensions/scan.py index 8caa6c13..5a2365c4 100644 --- a/openflexure_microscope/api/default_extensions/scan.py +++ b/openflexure_microscope/api/default_extensions/scan.py @@ -398,7 +398,7 @@ class TileScanAPI(ActionView): # Acquire microscope lock with 1s timeout with microscope.lock(timeout=1): # Run scan_extension_v2 - return self.extension.scan_extension_v2.tile( + return self.extension.tile( microscope, basename=args.get("filename"), namemode=args.get("namemode"), From d9d58d03dd06a8dd926c8adf9bed258028a9c5b5 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 30 Nov 2020 16:58:12 +0000 Subject: [PATCH 07/35] Upgraded to LabThings 1.2.0 --- poetry.lock | 28 ++++++++++++---------------- pyproject.toml | 2 +- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5ca93cb1..05785030 100644 --- a/poetry.lock +++ b/poetry.lock @@ -336,28 +336,21 @@ i18n = ["Babel (>=0.8)"] [[package]] name = "labthings" -version = "1.1.6" +version = "1.2.0" 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,<5.0" -apispec_webframeworks = "^0.5.2" -Flask = "^1.1.1" -flask-cors = "^3.0.8" -marshmallow = "^3.4.0" -webargs = "^6.0.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 = "new-extension-manifest" -resolved_reference = "427b12e4f739fc35bad05a25c099c29c3bba173d" - [[package]] name = "lazy-object-proxy" version = "1.4.3" @@ -955,7 +948,7 @@ rpi = ["RPi.GPIO"] [metadata] lock-version = "1.1" python-versions = "^3.7.3" -content-hash = "61ef63ad367aade04025dd0383286671481252c2efba2c068315522b11664206" +content-hash = "faa770c6eb6028fe292d4ad1d81a81afa45f79b4d2794c1ac0086c19be5740df" [metadata.files] alabaster = [ @@ -1108,7 +1101,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.0-py3-none-any.whl", hash = "sha256:3d5997cadb176706f0c079273211b61de267b2914cdae97f0dece46df347f779"}, + {file = "labthings-1.2.0.tar.gz", hash = "sha256:54f6cac8ceb24db9e61306ecc66961d31be735d8dd14c99867e54f33f2e12b76"}, +] 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"}, diff --git a/pyproject.toml b/pyproject.toml index 332b4692..0f1e4b75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ pyyaml = "^5.3.1" 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 = {git = "https://github.com/labthings/python-labthings", branch="new-extension-manifest", develop = true} +labthings = "1.2.0" [tool.poetry.extras] rpi = ["RPi.GPIO"] From d28848412cd4c57eb99bd58a4d8b1103010a22a5 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 30 Nov 2020 16:59:56 +0000 Subject: [PATCH 08/35] Fixed get_locations method reference --- openflexure_microscope/api/default_extensions/autostorage.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openflexure_microscope/api/default_extensions/autostorage.py b/openflexure_microscope/api/default_extensions/autostorage.py index 2847bff0..ab32bf44 100644 --- a/openflexure_microscope/api/default_extensions/autostorage.py +++ b/openflexure_microscope/api/default_extensions/autostorage.py @@ -238,7 +238,7 @@ class AutostorageExtension(BaseExtension): class GetLocationsView(PropertyView): def get(self): self.extension.check_location() - return autostorage_extension_v2.get_locations() + return self.extension.get_locations() class PreferredLocationView(PropertyView): @@ -272,4 +272,3 @@ class PreferredLocationGUIView(View): self.extension.set_preferred_key(new_path_key) return new_path_title - From bc3927759fbb6a60f4848575ae6e5cd29b68cae2 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 30 Nov 2020 17:10:00 +0000 Subject: [PATCH 09/35] Handle missing endpoints --- openflexure_microscope/api/v2/views/captures.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/openflexure_microscope/api/v2/views/captures.py b/openflexure_microscope/api/v2/views/captures.py index 75b8cc51..e3c1fce4 100644 --- a/openflexure_microscope/api/v2/views/captures.py +++ b/openflexure_microscope/api/v2/views/captures.py @@ -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): From e433a897a4be4cc8da37721f18d424696d3f8a6d Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 30 Nov 2020 17:10:32 +0000 Subject: [PATCH 10/35] Only use CompositeLock for microscope lock --- openflexure_microscope/microscope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openflexure_microscope/microscope.py b/openflexure_microscope/microscope.py index e24a60be..b3c29997 100644 --- a/openflexure_microscope/microscope.py +++ b/openflexure_microscope/microscope.py @@ -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 From b8354d3e8e2f48506de40163e5be9611aff59d50 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 30 Nov 2020 17:11:30 +0000 Subject: [PATCH 11/35] Added type information to LABTHINGS_EXTENSIONS --- openflexure_microscope/api/default_extensions/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openflexure_microscope/api/default_extensions/__init__.py b/openflexure_microscope/api/default_extensions/__init__.py index 1c080a93..6cbef3be 100644 --- a/openflexure_microscope/api/default_extensions/__init__.py +++ b/openflexure_microscope/api/default_extensions/__init__.py @@ -1,8 +1,10 @@ import logging import traceback from contextlib import contextmanager +from labthings.extensions import BaseExtension +from typing import List, Type -LABTHINGS_EXTENSIONS = [] +LABTHINGS_EXTENSIONS: List[Type[BaseExtension]] = [] @contextmanager From 8b20975cb9eb5ddeb291b03c6616374886d4a535 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 30 Nov 2020 17:11:37 +0000 Subject: [PATCH 12/35] Formatting --- docs/source/extensions/example_extension/02_adding_views.py | 1 + .../extensions/example_extension/03_marshaling_data.py | 1 + docs/source/extensions/example_extension/04_properties.py | 1 + docs/source/extensions/example_extension/05_actions.py | 1 + docs/source/extensions/example_extension/07_ev_gui.py | 6 +++--- 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/source/extensions/example_extension/02_adding_views.py b/docs/source/extensions/example_extension/02_adding_views.py index 69aa6d6f..be2bb820 100644 --- a/docs/source/extensions/example_extension/02_adding_views.py +++ b/docs/source/extensions/example_extension/02_adding_views.py @@ -2,6 +2,7 @@ from labthings import fields, find_component from labthings.extensions import BaseExtension from labthings.views import View + # Create the extension class class MyExtension(BaseExtension): def __init__(self): diff --git a/docs/source/extensions/example_extension/03_marshaling_data.py b/docs/source/extensions/example_extension/03_marshaling_data.py index 63a73575..2bf270fe 100644 --- a/docs/source/extensions/example_extension/03_marshaling_data.py +++ b/docs/source/extensions/example_extension/03_marshaling_data.py @@ -2,6 +2,7 @@ from labthings import Schema, fields, find_component from labthings.extensions import BaseExtension from labthings.views import View + # Create the extension class class MyExtension(BaseExtension): def __init__(self): diff --git a/docs/source/extensions/example_extension/04_properties.py b/docs/source/extensions/example_extension/04_properties.py index 9fc7862d..6df7069d 100644 --- a/docs/source/extensions/example_extension/04_properties.py +++ b/docs/source/extensions/example_extension/04_properties.py @@ -2,6 +2,7 @@ from labthings import Schema, fields, find_component from labthings.extensions import BaseExtension from labthings.views import PropertyView + # Create the extension class class MyExtension(BaseExtension): def __init__(self): diff --git a/docs/source/extensions/example_extension/05_actions.py b/docs/source/extensions/example_extension/05_actions.py index dcee9ad8..0ac21880 100644 --- a/docs/source/extensions/example_extension/05_actions.py +++ b/docs/source/extensions/example_extension/05_actions.py @@ -5,6 +5,7 @@ from labthings import Schema, fields, find_component from labthings.extensions import BaseExtension from labthings.views import ActionView, PropertyView + # Create the extension class class MyExtension(BaseExtension): def __init__(self): diff --git a/docs/source/extensions/example_extension/07_ev_gui.py b/docs/source/extensions/example_extension/07_ev_gui.py index 0782af73..42b40751 100644 --- a/docs/source/extensions/example_extension/07_ev_gui.py +++ b/docs/source/extensions/example_extension/07_ev_gui.py @@ -4,12 +4,12 @@ from labthings import current_action, fields, find_component, update_action_prog from labthings.extensions import BaseExtension from labthings.views import ActionView -# Used in our timelapse function -from openflexure_microscope.captures.capture_manager import generate_basename - # Used to convert our GUI dictionary into a complete eV extension GUI from openflexure_microscope.api.utilities.gui import build_gui +# Used in our timelapse function +from openflexure_microscope.captures.capture_manager import generate_basename + # Create the extension class class TimelapseExtension(BaseExtension): From 37f92b6cce382b65a2390b0a4aee773d5eb0ead8 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 30 Nov 2020 17:17:14 +0000 Subject: [PATCH 13/35] Linting fixes --- openflexure_microscope/api/default_extensions/__init__.py | 3 ++- openflexure_microscope/microscope.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openflexure_microscope/api/default_extensions/__init__.py b/openflexure_microscope/api/default_extensions/__init__.py index 6cbef3be..46a0805f 100644 --- a/openflexure_microscope/api/default_extensions/__init__.py +++ b/openflexure_microscope/api/default_extensions/__init__.py @@ -1,9 +1,10 @@ import logging import traceback from contextlib import contextmanager -from labthings.extensions import BaseExtension from typing import List, Type +from labthings.extensions import BaseExtension + LABTHINGS_EXTENSIONS: List[Type[BaseExtension]] = [] diff --git a/openflexure_microscope/microscope.py b/openflexure_microscope/microscope.py index b3c29997..f4404af9 100644 --- a/openflexure_microscope/microscope.py +++ b/openflexure_microscope/microscope.py @@ -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 From 249e3014cbdb96c3b5eae7bdc7ef731213c2df3f Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Tue, 1 Dec 2020 15:06:25 +0000 Subject: [PATCH 14/35] Updated to LabThings 1.2.1 --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 05785030..b4c41f6a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -336,7 +336,7 @@ i18n = ["Babel (>=0.8)"] [[package]] name = "labthings" -version = "1.2.0" +version = "1.2.1" description = "Python implementation of LabThings, based on the Flask microframework" category = "main" optional = false @@ -948,7 +948,7 @@ rpi = ["RPi.GPIO"] [metadata] lock-version = "1.1" python-versions = "^3.7.3" -content-hash = "faa770c6eb6028fe292d4ad1d81a81afa45f79b4d2794c1ac0086c19be5740df" +content-hash = "ec95ce987cd855cbcbdc4d7689dcbee7ca1f0154a60f5dddc3cecc02c92ddc75" [metadata.files] alabaster = [ @@ -1102,8 +1102,8 @@ jinja2 = [ {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, ] labthings = [ - {file = "labthings-1.2.0-py3-none-any.whl", hash = "sha256:3d5997cadb176706f0c079273211b61de267b2914cdae97f0dece46df347f779"}, - {file = "labthings-1.2.0.tar.gz", hash = "sha256:54f6cac8ceb24db9e61306ecc66961d31be735d8dd14c99867e54f33f2e12b76"}, + {file = "labthings-1.2.1-py3-none-any.whl", hash = "sha256:0b4bf4b7348cab8638bc9ec6028a48197f201410e352c866d131618a9877acbc"}, + {file = "labthings-1.2.1.tar.gz", hash = "sha256:f5f0fe319ef7aa2697e06f5969eb47dc834e7981c3726eab29472ee4f34c01ea"}, ] lazy-object-proxy = [ {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, diff --git a/pyproject.toml b/pyproject.toml index 0f1e4b75..8b1b33d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ pyyaml = "^5.3.1" 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.0" +labthings = "1.2.1" [tool.poetry.extras] rpi = ["RPi.GPIO"] From bc9b80d11a112549f2b1e27aadbae39e74c09272 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Tue, 1 Dec 2020 17:10:25 +0000 Subject: [PATCH 15/35] Added start_worker and get_frame for compatibility --- openflexure_microscope/camera/base.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openflexure_microscope/camera/base.py b/openflexure_microscope/camera/base.py index 0251b5c2..126d3070 100644 --- a/openflexure_microscope/camera/base.py +++ b/openflexure_microscope/camera/base.py @@ -170,6 +170,20 @@ 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.debug( + "`start_worker` method has been deprecated and is no longer required. Please avoid calling this method." + ) + return True + + def get_frame(self): + """Return the current camera frame.""" + logging.debug( + "`camera.get_frame` method has been deprecated. Please use `camera.stream.getframe()." + ) + return self.stream.getframe() + def __enter__(self): """Create camera on context enter.""" return self From ac667c3e2aa698bb966ba874ccdafc106ec8d172 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Tue, 1 Dec 2020 17:10:47 +0000 Subject: [PATCH 16/35] Fixed types in move_rel method --- openflexure_microscope/stage/sanga.py | 55 +++++++++++++++++---------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/openflexure_microscope/stage/sanga.py b/openflexure_microscope/stage/sanga.py index 2b8e4c92..735d0378 100644 --- a/openflexure_microscope/stage/sanga.py +++ b/openflexure_microscope/stage/sanga.py @@ -30,11 +30,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 @@ -75,13 +73,16 @@ 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, (list, np.ndarray)): return self._backlash + elif isinstance(self._backlash, int): + return np.array([self._backlash] * self.n_axes) else: return np.array([0] * self.n_axes) + @backlash.setter - def backlash(self, blsh): + def backlash(self, blsh) -> Optional[np.ndarray]: logging.debug("Setting backlash to %s", (blsh)) if blsh is None: self._backlash = None @@ -122,11 +123,23 @@ 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,32 +150,32 @@ 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 = ( + + # Create the displacement array + displacement_array: np.ndarray = np.array([ displacement if axis == "x" else 0, displacement if axis == "y" else 0, displacement if axis == "z" else 0, - ) - displacement = move + ]) - 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: + else: + # Convert our displacement tuple/generator into a numpy array + displacement_array = np.array(list(displacement)) + + # 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) From fd42e2e2b652054ae5a68561b3a03fbd5e57c52a Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Tue, 1 Dec 2020 17:13:20 +0000 Subject: [PATCH 17/35] Fixed broken dataset rendering if key exists but is empty --- .../tabContentComponents/galleryContent.vue | 62 ++++++++++--------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/openflexure_microscope/api/static/src/components/tabContentComponents/galleryContent.vue b/openflexure_microscope/api/static/src/components/tabContentComponents/galleryContent.vue index 3ae91b76..c00afafc 100644 --- a/openflexure_microscope/api/static/src/components/tabContentComponents/galleryContent.vue +++ b/openflexure_microscope/api/static/src/components/tabContentComponents/galleryContent.vue @@ -24,7 +24,7 @@ Filter
@@ -159,10 +159,10 @@ export default { captureCard, scanCard, ZipDownloader, - Paginate + Paginate, }, - data: function() { + data: function () { return { captures: [], checkedTags: [], @@ -171,15 +171,15 @@ export default { scanTag: "scan", unwatchStoreFunction: null, maxitems: 10, - page: 1 + page: 1, }; }, computed: { - capturesUri: function() { + capturesUri: function () { return `${this.$store.getters.baseUri}/api/v2/captures`; }, - allTags: function() { + allTags: function () { // Return an array of unique tags across all captures var tags = []; for (var capture of this.captures) { @@ -192,12 +192,16 @@ export default { return tags.sort(); }, - noScanCaptures: function() { + noScanCaptures: function () { // List of captures that are not part of a scan var captures = []; for (var capture of this.captures) { // Add to capture list if matched - if (!capture.dataset) { + if ( + !capture.dataset || // If no dataset key + (capture.dataset.constructor === Object && // Or dataset is an object... + Object.keys(capture.dataset).length === 0) // ...but it's empty + ) { captures.push(capture); } } @@ -205,7 +209,7 @@ export default { return captures; }, - allScans: function() { + allScans: function () { // List of scans as capture-like objects var scans = {}; @@ -248,12 +252,12 @@ export default { return scans; }, - scanList: function() { + scanList: function () { // List of scans, obtained from this.allScans values return Object.values(this.allScans); }, - itemList: function() { + itemList: function () { // Get list of current items to show // If galleryFolder (ie inside a scan folder), show scan captures // Otherwise, show root captures and scan cards @@ -264,12 +268,12 @@ export default { } }, - filteredItems: function() { + filteredItems: function () { // Filter itemList by checkedTags return this.filterCaptures(this.itemList, this.checkedTags); }, - filteredCaptures: function() { + filteredCaptures: function () { var captures = {}; for (var item of this.filteredItems) { @@ -288,19 +292,19 @@ export default { return captures; }, - sortedItems: function() { + sortedItems: function () { // Sort filteredItems using sortCaptures function return this.sortCaptures(this.filteredItems); }, - pagedItems: function() { + pagedItems: function () { let startIndex = (this.page - 1) * this.maxitems; return this.sortedItems.slice(startIndex, startIndex + this.maxitems); }, - numberOfPages: function() { + numberOfPages: function () { return Math.floor(this.sortedItems.length / this.maxitems); - } + }, }, mounted() { @@ -312,13 +316,13 @@ export default { }); }, - created: function() { + created: function () { // Watch for host 'ready', then update status this.unwatchStoreFunction = this.$store.watch( (state, getters) => { return getters.ready; }, - ready => { + (ready) => { if (ready) { // If the connection is now ready, update capture list this.updateCaptures(); @@ -348,20 +352,20 @@ export default { } }, - updateCaptures: function() { + updateCaptures: function () { if (this.$store.state.available) { axios .get(this.capturesUri) - .then(response => { + .then((response) => { this.captures = response.data; }) - .catch(error => { + .catch((error) => { this.modalError(error); // Let mixin handle error }); } }, - filterCaptures: function(list, filterTags) { + filterCaptures: function (list, filterTags) { // Filter a list of captures by an array of tags var result = []; for (var capture of list) { @@ -370,7 +374,7 @@ export default { // Filter by selected tags var tags = capture.tags; - let checker = (arr, target) => target.every(v => arr.includes(v)); + let checker = (arr, target) => target.every((v) => arr.includes(v)); // True if all tags match includeCapture = checker(tags, filterTags); @@ -383,7 +387,7 @@ export default { return result; }, - sortCaptures: function(list) { + sortCaptures: function (list) { // Sort a list of captures by metadata time function compare(a, b) { if (a.time < b.time) return -1; @@ -398,15 +402,15 @@ export default { } }, - galleryBack: function() { + galleryBack: function () { this.galleryFolder = ""; this.page = 1; }, - selectFolder: function(folderID) { + selectFolder: function (folderID) { this.galleryFolder = folderID; - } - } + }, + }, }; From f2a2d880f29f4ed24bfaaf04571f988be76d7140 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Tue, 1 Dec 2020 17:30:53 +0000 Subject: [PATCH 18/35] Stricter runtime type checks --- openflexure_microscope/stage/sanga.py | 37 +++++++++++++++++---------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/openflexure_microscope/stage/sanga.py b/openflexure_microscope/stage/sanga.py index 735d0378..7d941987 100644 --- a/openflexure_microscope/stage/sanga.py +++ b/openflexure_microscope/stage/sanga.py @@ -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 @@ -31,7 +32,7 @@ class SangaStage(BaseStage): self.board = Sangaboard(port, **kwargs) # Initialise backlash storage, used by property setter/getter - self._backlash = None + self._backlash = None self.settle_time = 0.2 # Default move settle time self._position_on_enter = None @@ -58,7 +59,7 @@ class SangaStage(BaseStage): 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, @@ -73,16 +74,17 @@ 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 isinstance(self._backlash, (list, np.ndarray)): + 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) - @backlash.setter - def backlash(self, blsh) -> Optional[np.ndarray]: + def backlash(self, blsh): logging.debug("Setting backlash to %s", (blsh)) if blsh is None: self._backlash = None @@ -105,7 +107,10 @@ class SangaStage(BaseStage): 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, @@ -139,7 +144,7 @@ class SangaStage(BaseStage): 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 @@ -152,15 +157,21 @@ class SangaStage(BaseStage): raise ValueError("axis must be one of x, y, or z") # Create the displacement array - displacement_array: np.ndarray = np.array([ - displacement if axis == "x" else 0, - displacement if axis == "y" else 0, - displacement if axis == "z" else 0, - ]) + displacement_array: np.ndarray = np.array( + [ + displacement if axis == "x" else 0, + displacement if axis == "y" else 0, + displacement if axis == "z" else 0, + ] + ) - else: + 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)}") # Calculate main movement initial_move: np.ndarray = displacement_array From a844efc8c8b68f36abc25531a31c5b9504801f9c Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Tue, 1 Dec 2020 17:39:53 +0000 Subject: [PATCH 19/35] Tweaked deprecation warnings --- openflexure_microscope/camera/base.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openflexure_microscope/camera/base.py b/openflexure_microscope/camera/base.py index 126d3070..da354f45 100644 --- a/openflexure_microscope/camera/base.py +++ b/openflexure_microscope/camera/base.py @@ -172,16 +172,17 @@ class BaseCamera(metaclass=ABCMeta): def start_worker(self, **_) -> bool: """Start the background camera thread if it isn't running yet.""" - logging.debug( + logging.warning( "`start_worker` method has been deprecated and is no longer required. Please avoid calling this method." ) return True def get_frame(self): - """Return the current camera frame.""" - logging.debug( - "`camera.get_frame` method has been deprecated. Please use `camera.stream.getframe()." - ) + """ + Return the current camera frame. + + Just an alias of self.stream.getframe() + """ return self.stream.getframe() def __enter__(self): From 63b633ba444838389e928433746917954e13a42e Mon Sep 17 00:00:00 2001 From: Joel Collins <2579603-jtc42@users.noreply.gitlab.com> Date: Fri, 4 Dec 2020 13:35:11 +0000 Subject: [PATCH 20/35] 2.9 dev numpy types --- README.md | 6 ++ .../api/default_extensions/autofocus.py | 98 +++++++++++-------- .../recalibrate_utils.py | 8 +- .../api/dev_extensions/__init__.py | 4 +- .../api/dev_extensions/tools.py | 21 ++-- openflexure_microscope/camera/base.py | 19 ++-- openflexure_microscope/stage/sanga.py | 2 +- openflexure_microscope/utilities.py | 43 +++++--- poetry.lock | 38 +++++-- pyproject.toml | 7 +- 10 files changed, 156 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index fce94990..c18cdd85 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/openflexure_microscope/api/default_extensions/autofocus.py b/openflexure_microscope/api/default_extensions/autofocus.py index d2c616a5..91fe08fb 100644 --- a/openflexure_microscope/api/default_extensions/autofocus.py +++ b/openflexure_microscope/api/default_extensions/autofocus.py @@ -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"]: @@ -111,7 +119,7 @@ class JPEGSharpnessMonitor: @contextmanager def monitor_sharpness(microscope: Microscope): - m = JPEGSharpnessMonitor(microscope) + m: JPEGSharpnessMonitor = JPEGSharpnessMonitor(microscope) m.start() try: yield m @@ -119,18 +127,18 @@ def monitor_sharpness(microscope: Microscope): m.stop() -def sharpness_sum_lap2(rgb_image: np.ndarray): +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]] ) @@ -161,7 +169,7 @@ class AutofocusExtension(BaseExtension): def measure_sharpness( self, microscope: Microscope, metric_fn: Callable = sharpness_sum_lap2 - ): + ) -> np.float: """Measure the sharpness of the camera's current view.""" if hasattr(microscope.camera, "array") and callable( @@ -177,7 +185,7 @@ class AutofocusExtension(BaseExtension): 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 @@ -185,12 +193,12 @@ class AutofocusExtension(BaseExtension): highest. No interpolation is performed. dz is assumed to be in ascending order (starting at -ve values) """ - camera = microscope.camera - stage = microscope.stage + camera: BaseCamera = microscope.camera + stage: BaseStage = microscope.stage with set_properties(stage, backlash=256), stage.lock, camera.lock: - sharpnesses = [] - positions = [] + sharpnesses: List[np.float] = [] + positions: List[int] = [] # Some cameras may not have annotate_text. Reset if it does if getattr(camera, "annotate_text"): @@ -198,29 +206,33 @@ class AutofocusExtension(BaseExtension): for _ in stage.scan_z(dz, return_to_start=False): if current_action() and current_action().stopped: - return + return [], [] positions.append(stage.position[2]) time.sleep(settle) sharpnesses.append(self.measure_sharpness(microscope, metric_fn)) - newposition = positions[np.argmax(sharpnesses)] + newposition: int = positions[int(np.argmax(sharpnesses))] stage.move_rel((0, 0, newposition - stage.position[2])) return positions, sharpnesses - def move_and_find_focus(self, microscope: Microscope, dz: int): + 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: m.focus_rel(dz) return m.sharpest_z_on_move(0) - def move_and_measure(self, microscope: Microscope, dz: int): + 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): + 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: @@ -231,7 +243,7 @@ class AutofocusExtension(BaseExtension): # 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) + 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) @@ -248,7 +260,7 @@ class AutofocusExtension(BaseExtension): 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. @@ -294,11 +306,15 @@ class AutofocusExtension(BaseExtension): 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 = jz[np.argmax(js)] + 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") @@ -309,17 +325,19 @@ class AutofocusExtension(BaseExtension): # 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 + 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 = np.argmax( - js < current_js + 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] + correction_move: int = best_z + target_z - jz[inow] logging.debug( "Fast autofocus scan: correcting backlash by moving %s steps", (correction_move), diff --git a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/recalibrate_utils.py b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/recalibrate_utils.py index 94d22a90..9f79b1de 100644 --- a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/recalibrate_utils.py +++ b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/recalibrate_utils.py @@ -78,10 +78,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( diff --git a/openflexure_microscope/api/dev_extensions/__init__.py b/openflexure_microscope/api/dev_extensions/__init__.py index c5a0a811..0f098ecf 100644 --- a/openflexure_microscope/api/dev_extensions/__init__.py +++ b/openflexure_microscope/api/dev_extensions/__init__.py @@ -1,3 +1,3 @@ -from .tools import devtools_extension_v2 +from .tools import DevToolsExtension -LABTHINGS_EXTENSIONS = [devtools_extension_v2] +LABTHINGS_EXTENSIONS = [DevToolsExtension] diff --git a/openflexure_microscope/api/dev_extensions/tools.py b/openflexure_microscope/api/dev_extensions/tools.py index 81231bef..f8da0a03 100644 --- a/openflexure_microscope/api/dev_extensions/tools.py +++ b/openflexure_microscope/api/dev_extensions/tools.py @@ -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") diff --git a/openflexure_microscope/camera/base.py b/openflexure_microscope/camera/base.py index 126d3070..7f515d56 100644 --- a/openflexure_microscope/camera/base.py +++ b/openflexure_microscope/camera/base.py @@ -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): @@ -172,16 +174,17 @@ class BaseCamera(metaclass=ABCMeta): def start_worker(self, **_) -> bool: """Start the background camera thread if it isn't running yet.""" - logging.debug( + logging.warning( "`start_worker` method has been deprecated and is no longer required. Please avoid calling this method." ) return True def get_frame(self): - """Return the current camera frame.""" - logging.debug( - "`camera.get_frame` method has been deprecated. Please use `camera.stream.getframe()." - ) + """ + Return the current camera frame. + + Just an alias of self.stream.getframe() + """ return self.stream.getframe() def __enter__(self): diff --git a/openflexure_microscope/stage/sanga.py b/openflexure_microscope/stage/sanga.py index 7d941987..22be38f3 100644 --- a/openflexure_microscope/stage/sanga.py +++ b/openflexure_microscope/stage/sanga.py @@ -101,7 +101,7 @@ 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") diff --git a/openflexure_microscope/utilities.py b/openflexure_microscope/utilities.py index 58d816ca..b0b22e28 100644 --- a/openflexure_microscope/utilities.py +++ b/openflexure_microscope/utilities.py @@ -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) diff --git a/poetry.lock b/poetry.lock index b4c41f6a..ff67e3a7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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,7 +336,7 @@ i18n = ["Babel (>=0.8)"] [[package]] name = "labthings" -version = "1.2.1" +version = "1.2.2" description = "Python implementation of LabThings, based on the Flask microframework" category = "main" optional = false @@ -421,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" @@ -948,7 +967,7 @@ rpi = ["RPi.GPIO"] [metadata] lock-version = "1.1" python-versions = "^3.7.3" -content-hash = "ec95ce987cd855cbcbdc4d7689dcbee7ca1f0154a60f5dddc3cecc02c92ddc75" +content-hash = "2c0faa244099ec3433edfc2cb9321f5de8bd5c0c32e135cae23f91e717cb07d7" [metadata.files] alabaster = [ @@ -1082,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"}, @@ -1102,8 +1121,8 @@ jinja2 = [ {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, ] labthings = [ - {file = "labthings-1.2.1-py3-none-any.whl", hash = "sha256:0b4bf4b7348cab8638bc9ec6028a48197f201410e352c866d131618a9877acbc"}, - {file = "labthings-1.2.1.tar.gz", hash = "sha256:f5f0fe319ef7aa2697e06f5969eb47dc834e7981c3726eab29472ee4f34c01ea"}, + {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"}, @@ -1219,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"}, diff --git a/pyproject.toml b/pyproject.toml index 8b1b33d3..59019852 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,10 +43,11 @@ expiringdict = "^1.2.1" camera-stage-mapping = "0.1.4" picamerax = ">=20.9.1" pyyaml = "^5.3.1" -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.1" +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 From 3c3ecd7f0c490cbf0b7b58d57d039bb039c287d1 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Fri, 4 Dec 2020 13:39:49 +0000 Subject: [PATCH 21/35] Updated dependencies --- .../api/static/package-lock.json | 718 ++++++++---------- .../api/static/package.json | 8 +- 2 files changed, 340 insertions(+), 386 deletions(-) diff --git a/openflexure_microscope/api/static/package-lock.json b/openflexure_microscope/api/static/package-lock.json index 47f04d31..d3107891 100644 --- a/openflexure_microscope/api/static/package-lock.json +++ b/openflexure_microscope/api/static/package-lock.json @@ -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", @@ -2917,27 +2917,17 @@ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "optional": true, - "requires": { - "color-convert": "^2.0.1" - } - }, "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,41 +2957,17 @@ } }, "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": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "optional": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "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": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, - "optional": true - }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3035,35 +3001,6 @@ "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", "dev": true }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "optional": true - }, - "json5": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", - "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", - "dev": true, - "optional": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", - "dev": true, - "optional": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -3083,9 +3020,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": { @@ -3197,16 +3134,6 @@ "ansi-regex": "^5.0.0" } }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "optional": true, - "requires": { - "has-flag": "^4.0.0" - } - }, "terser-webpack-plugin": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz", @@ -3233,31 +3160,6 @@ "psl": "^1.1.28", "punycode": "^2.1.1" } - }, - "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==", - "dev": true, - "optional": true, - "requires": { - "chalk": "^4.1.0", - "hash-sum": "^2.0.0", - "loader-utils": "^2.0.0" - }, - "dependencies": { - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "optional": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - } - } } } }, @@ -3872,34 +3774,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 +4766,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 +4789,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 +5530,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 +5991,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 +6522,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 +8118,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 +9423,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 +10108,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 +10191,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 +10210,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 +10220,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 +10519,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 +10749,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 +12418,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 +12736,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 +12766,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 +13243,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 +13555,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": { @@ -13857,6 +13713,104 @@ } } }, + "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==", + "dev": true, + "optional": true, + "requires": { + "chalk": "^4.1.0", + "hash-sum": "^2.0.0", + "loader-utils": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "optional": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "optional": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "optional": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "optional": true + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "optional": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "optional": true + }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "optional": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "optional": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "optional": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "vue-observe-visibility": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/vue-observe-visibility/-/vue-observe-visibility-0.4.6.tgz", @@ -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": { diff --git a/openflexure_microscope/api/static/package.json b/openflexure_microscope/api/static/package.json index 8e2a5f33..a0d8b116 100644 --- a/openflexure_microscope/api/static/package.json +++ b/openflexure_microscope/api/static/package.json @@ -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": { From 213dec3e5c9df7247405f35dca71bf29b03157a7 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Fri, 4 Dec 2020 14:24:00 +0000 Subject: [PATCH 22/35] Fixed individual captures creating 'undefined' dataset --- .../tabContentComponents/galleryContent.vue | 76 ++++++++++--------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/openflexure_microscope/api/static/src/components/tabContentComponents/galleryContent.vue b/openflexure_microscope/api/static/src/components/tabContentComponents/galleryContent.vue index c00afafc..79a984fe 100644 --- a/openflexure_microscope/api/static/src/components/tabContentComponents/galleryContent.vue +++ b/openflexure_microscope/api/static/src/components/tabContentComponents/galleryContent.vue @@ -24,7 +24,7 @@ Filter
@@ -159,10 +159,10 @@ export default { captureCard, scanCard, ZipDownloader, - Paginate, + Paginate }, - data: function () { + data: function() { return { captures: [], checkedTags: [], @@ -171,15 +171,15 @@ export default { scanTag: "scan", unwatchStoreFunction: null, maxitems: 10, - page: 1, + page: 1 }; }, computed: { - capturesUri: function () { + capturesUri: function() { return `${this.$store.getters.baseUri}/api/v2/captures`; }, - allTags: function () { + allTags: function() { // Return an array of unique tags across all captures var tags = []; for (var capture of this.captures) { @@ -192,16 +192,12 @@ export default { return tags.sort(); }, - noScanCaptures: function () { + noScanCaptures: function() { // List of captures that are not part of a scan var captures = []; for (var capture of this.captures) { // Add to capture list if matched - if ( - !capture.dataset || // If no dataset key - (capture.dataset.constructor === Object && // Or dataset is an object... - Object.keys(capture.dataset).length === 0) // ...but it's empty - ) { + if (!this.isDatasetPopulated(capture.dataset)) { captures.push(capture); } } @@ -209,14 +205,14 @@ export default { return captures; }, - allScans: function () { + allScans: function() { // List of scans as capture-like objects var scans = {}; 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 @@ -252,12 +248,12 @@ export default { return scans; }, - scanList: function () { + scanList: function() { // List of scans, obtained from this.allScans values return Object.values(this.allScans); }, - itemList: function () { + itemList: function() { // Get list of current items to show // If galleryFolder (ie inside a scan folder), show scan captures // Otherwise, show root captures and scan cards @@ -268,12 +264,12 @@ export default { } }, - filteredItems: function () { + filteredItems: function() { // Filter itemList by checkedTags return this.filterCaptures(this.itemList, this.checkedTags); }, - filteredCaptures: function () { + filteredCaptures: function() { var captures = {}; for (var item of this.filteredItems) { @@ -292,19 +288,19 @@ export default { return captures; }, - sortedItems: function () { + sortedItems: function() { // Sort filteredItems using sortCaptures function return this.sortCaptures(this.filteredItems); }, - pagedItems: function () { + pagedItems: function() { let startIndex = (this.page - 1) * this.maxitems; return this.sortedItems.slice(startIndex, startIndex + this.maxitems); }, - numberOfPages: function () { + numberOfPages: function() { return Math.floor(this.sortedItems.length / this.maxitems); - }, + } }, mounted() { @@ -316,13 +312,13 @@ export default { }); }, - created: function () { + created: function() { // Watch for host 'ready', then update status this.unwatchStoreFunction = this.$store.watch( (state, getters) => { return getters.ready; }, - (ready) => { + ready => { if (ready) { // If the connection is now ready, update capture list this.updateCaptures(); @@ -352,20 +348,32 @@ export default { } }, - updateCaptures: function () { + updateCaptures: function() { if (this.$store.state.available) { axios .get(this.capturesUri) - .then((response) => { + .then(response => { this.captures = response.data; }) - .catch((error) => { + .catch(error => { this.modalError(error); // Let mixin handle error }); } }, - filterCaptures: function (list, filterTags) { + 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 = []; for (var capture of list) { @@ -374,7 +382,7 @@ export default { // Filter by selected tags var tags = capture.tags; - let checker = (arr, target) => target.every((v) => arr.includes(v)); + let checker = (arr, target) => target.every(v => arr.includes(v)); // True if all tags match includeCapture = checker(tags, filterTags); @@ -387,7 +395,7 @@ export default { return result; }, - sortCaptures: function (list) { + sortCaptures: function(list) { // Sort a list of captures by metadata time function compare(a, b) { if (a.time < b.time) return -1; @@ -402,15 +410,15 @@ export default { } }, - galleryBack: function () { + galleryBack: function() { this.galleryFolder = ""; this.page = 1; }, - selectFolder: function (folderID) { + selectFolder: function(folderID) { this.galleryFolder = folderID; - }, - }, + } + } }; From 08f653233b71595ceb3a0330ba48dfdbf6ac26cb Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Fri, 4 Dec 2020 15:08:39 +0000 Subject: [PATCH 23/35] Fixed onError propagation --- .../src/components/genericComponents/taskSubmitter.vue | 1 - .../captureComponents/paneCapture.vue | 9 +-------- .../tabContentComponents/galleryComponents/scanCard.vue | 1 - .../galleryComponents/zipDownloader.vue | 6 +----- .../navigateComponents/paneNavigate.vue | 3 +++ .../CSMSettingsComponents/CSMCalibrationSettings.vue | 6 +----- .../cameraCalibrationSettings.vue | 6 +----- .../slideScanComponents/paneSlideScan.vue | 7 ++++++- 8 files changed, 13 insertions(+), 26 deletions(-) diff --git a/openflexure_microscope/api/static/src/components/genericComponents/taskSubmitter.vue b/openflexure_microscope/api/static/src/components/genericComponents/taskSubmitter.vue index cb11e22a..3ae912d4 100644 --- a/openflexure_microscope/api/static/src/components/genericComponents/taskSubmitter.vue +++ b/openflexure_microscope/api/static/src/components/genericComponents/taskSubmitter.vue @@ -198,7 +198,6 @@ export default { if (!error) { error = Error("Unknown error"); } - this.$emit("error", error); this.$emit("finished"); }) diff --git a/openflexure_microscope/api/static/src/components/tabContentComponents/captureComponents/paneCapture.vue b/openflexure_microscope/api/static/src/components/tabContentComponents/captureComponents/paneCapture.vue index a3827938..a368109b 100644 --- a/openflexure_microscope/api/static/src/components/tabContentComponents/captureComponents/paneCapture.vue +++ b/openflexure_microscope/api/static/src/components/tabContentComponents/captureComponents/paneCapture.vue @@ -248,9 +248,8 @@ :submit-data="scanPayload" :submit-label="'Start Scan'" :button-primary="true" - @submit="onScanSubmit" @response="onScanResponse" - @error="onScanError" + @error="modalError" >
@@ -433,14 +432,8 @@ export default { }); }, - onScanSubmit: function() {}, - onScanResponse: function() { this.modalNotify("Finished scan."); - }, - - onScanError: function(error) { - this.modalError(error); } } }; diff --git a/openflexure_microscope/api/static/src/components/tabContentComponents/galleryComponents/scanCard.vue b/openflexure_microscope/api/static/src/components/tabContentComponents/galleryComponents/scanCard.vue index 1c8d0f49..1af496e7 100644 --- a/openflexure_microscope/api/static/src/components/tabContentComponents/galleryComponents/scanCard.vue +++ b/openflexure_microscope/api/static/src/components/tabContentComponents/galleryComponents/scanCard.vue @@ -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"); }); diff --git a/openflexure_microscope/api/static/src/components/tabContentComponents/galleryComponents/zipDownloader.vue b/openflexure_microscope/api/static/src/components/tabContentComponents/galleryComponents/zipDownloader.vue index a0074706..12e01041 100644 --- a/openflexure_microscope/api/static/src/components/tabContentComponents/galleryComponents/zipDownloader.vue +++ b/openflexure_microscope/api/static/src/components/tabContentComponents/galleryComponents/zipDownloader.vue @@ -18,7 +18,7 @@ :submit-label="'Create ZIP'" :submit-data="captureIds" @response="onResponse" - @error="onError" + @error="modalError" >
@@ -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 } } }; diff --git a/openflexure_microscope/api/static/src/components/tabContentComponents/navigateComponents/paneNavigate.vue b/openflexure_microscope/api/static/src/components/tabContentComponents/navigateComponents/paneNavigate.vue index 9d291f16..aff73c4b 100644 --- a/openflexure_microscope/api/static/src/components/tabContentComponents/navigateComponents/paneNavigate.vue +++ b/openflexure_microscope/api/static/src/components/tabContentComponents/navigateComponents/paneNavigate.vue @@ -148,6 +148,7 @@ :submit-on-event="'globalFastAutofocusEvent'" @taskStarted="isAutofocusing = 1" @finished="isAutofocusing = 0" + @error="modalError" > @@ -160,6 +161,7 @@ :button-primary="false" @taskStarted="isAutofocusing = 2" @finished="isAutofocusing = 0" + @error="modalError" > @@ -172,6 +174,7 @@ :button-primary="false" @taskStarted="isAutofocusing = 3" @finished="isAutofocusing = 0" + @error="modalError" > diff --git a/openflexure_microscope/api/static/src/components/tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue b/openflexure_microscope/api/static/src/components/tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue index 40ed3216..a7280c1f 100644 --- a/openflexure_microscope/api/static/src/components/tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue +++ b/openflexure_microscope/api/static/src/components/tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue @@ -12,7 +12,7 @@ :submit-url="recalibrationLinks.calibrate_xy.href" :submit-label="'Auto-Calibrate using camera'" @response="onRecalibrateResponse" - @error="onRecalibrateError" + @error="modalError" > @@ -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 } } }; diff --git a/openflexure_microscope/api/static/src/components/tabContentComponents/settingsComponents/cameraSettingsComponents/cameraCalibrationSettings.vue b/openflexure_microscope/api/static/src/components/tabContentComponents/settingsComponents/cameraSettingsComponents/cameraCalibrationSettings.vue index 5b995952..e6f3c8d8 100644 --- a/openflexure_microscope/api/static/src/components/tabContentComponents/settingsComponents/cameraSettingsComponents/cameraCalibrationSettings.vue +++ b/openflexure_microscope/api/static/src/components/tabContentComponents/settingsComponents/cameraSettingsComponents/cameraCalibrationSettings.vue @@ -11,7 +11,7 @@ :submit-url="recalibrationLinks.recalibrate.href" :submit-label="'Auto-Calibrate'" @response="onRecalibrateResponse" - @error="onRecalibrateError" + @error="modalError" > @@ -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); }, diff --git a/openflexure_microscope/api/static/src/components/tabContentComponents/slideScanComponents/paneSlideScan.vue b/openflexure_microscope/api/static/src/components/tabContentComponents/slideScanComponents/paneSlideScan.vue index 0d3096bf..62aac7b1 100644 --- a/openflexure_microscope/api/static/src/components/tabContentComponents/slideScanComponents/paneSlideScan.vue +++ b/openflexure_microscope/api/static/src/components/tabContentComponents/slideScanComponents/paneSlideScan.vue @@ -95,7 +95,7 @@ submit-label="Start scan" @submit="scanRunning = true" @response="scanRunning = false" - @error="scanRunning = false" + @error="onScanError" >
@@ -293,6 +293,11 @@ export default { pad(tzo % 60) ); } + }, + + onScanError: function(error) { + this.scanRunning = false; + this.modalError(error); } }; From 4d1d0a15c4e7d912e782d87ad465091e2bf5e8e4 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Fri, 4 Dec 2020 15:08:47 +0000 Subject: [PATCH 24/35] Added log level filter --- .../tabContentComponents/loggingContent.vue | 45 +++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/openflexure_microscope/api/static/src/components/tabContentComponents/loggingContent.vue b/openflexure_microscope/api/static/src/components/tabContentComponents/loggingContent.vue index 93b9447f..8bbf45c6 100644 --- a/openflexure_microscope/api/static/src/components/tabContentComponents/loggingContent.vue +++ b/openflexure_microscope/api/static/src/components/tabContentComponents/loggingContent.vue @@ -11,7 +11,11 @@
+ > + +
@@ -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' }" > -

- {{ formatDateTime(item.data.created) }} -

- {{ item.data.levelname }}: {{ item.data.message }} + {{ formatDateTime(item.data.created) }} +
{{ formatMessage(item) }}
From 8eff136cc3ac056bc70f081590569f2fe7a3a3a5 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Fri, 4 Dec 2020 15:09:45 +0000 Subject: [PATCH 25/35] Reduced logging level of some mock camera notices --- openflexure_microscope/camera/mock.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openflexure_microscope/camera/mock.py b/openflexure_microscope/camera/mock.py index dd0eb850..6b9b67ea 100644 --- a/openflexure_microscope/camera/mock.py +++ b/openflexure_microscope/camera/mock.py @@ -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. From 1de6b2a0ab7882145423e24feb668858e77ea06e Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Fri, 4 Dec 2020 15:14:30 +0000 Subject: [PATCH 26/35] Fixed onScanError --- .../slideScanComponents/paneSlideScan.vue | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openflexure_microscope/api/static/src/components/tabContentComponents/slideScanComponents/paneSlideScan.vue b/openflexure_microscope/api/static/src/components/tabContentComponents/slideScanComponents/paneSlideScan.vue index 62aac7b1..616b047c 100644 --- a/openflexure_microscope/api/static/src/components/tabContentComponents/slideScanComponents/paneSlideScan.vue +++ b/openflexure_microscope/api/static/src/components/tabContentComponents/slideScanComponents/paneSlideScan.vue @@ -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; @@ -293,11 +298,6 @@ export default { pad(tzo % 60) ); } - }, - - onScanError: function(error) { - this.scanRunning = false; - this.modalError(error); } }; From 7ba3f447e16b6f4260df7fa85c3edcd4fa434942 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Fri, 4 Dec 2020 15:23:03 +0000 Subject: [PATCH 27/35] Added extra type hints --- .../recalibrate_utils.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/recalibrate_utils.py b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/recalibrate_utils.py index 9f79b1de..0392720e 100644 --- a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/recalibrate_utils.py +++ b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/recalibrate_utils.py @@ -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 @@ -107,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 @@ -118,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( @@ -131,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: @@ -152,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() From 920354537ad889be9e40bb967e1bc0db1135824b Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Fri, 4 Dec 2020 16:05:39 +0000 Subject: [PATCH 28/35] Integrated CSMExtension --- .../api/default_extensions/__init__.py | 4 +- .../camera_stage_mapping.py | 335 ++++++++++++++++++ 2 files changed, 337 insertions(+), 2 deletions(-) create mode 100644 openflexure_microscope/api/default_extensions/camera_stage_mapping.py diff --git a/openflexure_microscope/api/default_extensions/__init__.py b/openflexure_microscope/api/default_extensions/__init__.py index 46a0805f..7dc4529c 100644 --- a/openflexure_microscope/api/default_extensions/__init__.py +++ b/openflexure_microscope/api/default_extensions/__init__.py @@ -44,6 +44,6 @@ with handle_extension_error("lens shading calibration"): LABTHINGS_EXTENSIONS.append(LSTExtension) with handle_extension_error("camera stage mapping"): - from camera_stage_mapping.ofm_extension import csm_extension + from .camera_stage_mapping import CSMExtension - LABTHINGS_EXTENSIONS.append(csm_extension) + LABTHINGS_EXTENSIONS.append(CSMExtension) diff --git a/openflexure_microscope/api/default_extensions/camera_stage_mapping.py b/openflexure_microscope/api/default_extensions/camera_stage_mapping.py new file mode 100644 index 00000000..4d9efaa9 --- /dev/null +++ b/openflexure_microscope/api/default_extensions/camera_stage_mapping.py @@ -0,0 +1,335 @@ +""" +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 List, NamedTuple, 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.paths import data_file_path + +CSM_DATAFILE_NAME = "csm_calibration.json" +CSM_DATAFILE_PATH = data_file_path(CSM_DATAFILE_NAME) + + +class MoveHistory(NamedTuple): + times: List[float] + stage_positions: List[Tuple[int, int, int]] + + +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): + self._move_function = move_function + self._current_position = None + self.clear_history() + + def __call__(self, new_position, *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): + """The history, as a numpy array of times and another of positions""" + times = np.array([t for t, p in self._history]) + positions = np.array([p for t, p in self._history]) + return MoveHistory(times, positions) + + def clear_history(self): + """Reset our history to be an empty list""" + self._history = [] + + +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 = 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 = ["extensions", self.name] + dictionary = 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): + """Retrieve the settings for this extension""" + keys = ["extensions", self.name] + return get_by_path(self.microscope.read_settings(), keys) + + def camera_stage_functions(self): + """Return functions that allow us to interface with the microscope""" + self.microscope.camera.start_worker() # ensure the worker thread is running, so there is an MJPEG stream + + def grab_image(): + jpeg = self.microscope.camera.get_frame() + return np.array(PIL.Image.open(io.BytesIO(jpeg))) + + def get_position(): + return self.microscope.stage.position + + move = self.microscope.stage.move_abs + + def wait(): + time.sleep(0.2) + + return grab_image, get_position, move, wait + + def calibrate_1d(self, direction): + """Move a microscope's stage in 1D, and figure out the relationship with the camera""" + 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) + + result = calibrate_backlash_1d(tracker, move, direction) + result["move_history"] = move.history + return result + + def calibrate_xy(self): + """Move the microscope's stage in X and Y, to calibrate its relationship to the camera""" + logging.info("Calibrating X axis:") + cal_x = self.calibrate_1d(np.array([1, 0, 0])) + logging.info("Calibrating Y axis:") + cal_y = self.calibrate_1d(np.array([0, 1, 0])) + + # Combine X and Y calibrations to make a 2D calibration + cal_xy = image_to_stage_displacement_from_1d([cal_x, cal_y]) + self.update_settings(cal_xy) + + data = { + "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): + """A 2x2 matrix that converts displacement in image coordinates to stage coordinates.""" + try: + settings = self.get_settings() + return settings["image_to_stage_displacement"] + except KeyError as e: + raise ValueError("The microscope has not yet been calibrated.") from e + + def move_in_image_coordinates(self, displacement_in_pixels): + """Move by a given number of pixels on the camera""" + p = np.array(displacement_in_pixels) + relative_move = np.dot(p, 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, **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, displacement_in_pixels, **kwargs + ) + + def closed_loop_scan(self, scan_path, **kwargs): + """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 numpy 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, N, **kwargs): + """Move the microscope in a spiral scan, and return the positions.""" + scan_path = ordered_spiral(0, 0, N, *step_size) + + for _ in self.closed_loop_scan(np.array(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 = np.array(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( + np.array([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( + np.array([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( + np.array([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 {} From 311366c943e9d3ebd8b2b610f627a43eea3e53ac Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Fri, 4 Dec 2020 17:05:40 +0000 Subject: [PATCH 29/35] Added type hints to CSM extension --- .../camera_stage_mapping.py | 114 ++++++++++-------- openflexure_microscope/camera/base.py | 2 +- openflexure_microscope/stage/base.py | 10 +- 3 files changed, 72 insertions(+), 54 deletions(-) diff --git a/openflexure_microscope/api/default_extensions/camera_stage_mapping.py b/openflexure_microscope/api/default_extensions/camera_stage_mapping.py index 4d9efaa9..ca479e97 100644 --- a/openflexure_microscope/api/default_extensions/camera_stage_mapping.py +++ b/openflexure_microscope/api/default_extensions/camera_stage_mapping.py @@ -15,7 +15,7 @@ import json import logging import os import time -from typing import List, NamedTuple, Tuple +from typing import Any, Callable, Dict, List, NamedTuple, Optional, Tuple import numpy as np import PIL @@ -33,15 +33,19 @@ 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[Tuple[int, int, int]] + stage_positions: List[CoordinateType] class LoggingMoveWrapper: @@ -55,12 +59,12 @@ class LoggingMoveWrapper: so we can estimate how long moves will take. """ - def __init__(self, move_function): - self._move_function = move_function - self._current_position = None + 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, *args, **kwargs): + 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) @@ -68,15 +72,15 @@ class LoggingMoveWrapper: self._history.append((time.time(), self._current_position)) @property - def history(self): + def history(self) -> MoveHistory: """The history, as a numpy array of times and another of positions""" - times = np.array([t for t, p in self._history]) - positions = np.array([p for t, p in self._history]) + 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 = [] + self._history: List[Tuple[float, Optional[CoordinateType]]] = [] class CSMExtension(BaseExtension): @@ -108,7 +112,7 @@ class CSMExtension(BaseExtension): GetCalibrationFile, "/get_calibration", endpoint="get_calibration" ) - _microscope = None + _microscope: Optional[Microscope] = None @property def microscope(self): @@ -119,59 +123,64 @@ class CSMExtension(BaseExtension): def update_settings(self, settings): """Update the stored extension settings dictionary""" - keys = ["extensions", self.name] - dictionary = create_from_path(keys) + 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): + def get_settings(self) -> Dict[str, Any]: """Retrieve the settings for this extension""" - keys = ["extensions", self.name] + keys: List[str] = ["extensions", self.name] return get_by_path(self.microscope.read_settings(), keys) - def camera_stage_functions(self): + def camera_stage_functions(self) -> Tuple[Callable, Callable, Callable, Callable]: """Return functions that allow us to interface with the microscope""" - self.microscope.camera.start_worker() # ensure the worker thread is running, so there is an MJPEG stream def grab_image(): - jpeg = self.microscope.camera.get_frame() + jpeg: bytes = self.microscope.camera.get_frame() return np.array(PIL.Image.open(io.BytesIO(jpeg))) - def get_position(): + def get_position() -> CoordinateType: return self.microscope.stage.position - move = self.microscope.stage.move_abs + 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): + 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) - result = calibrate_backlash_1d(tracker, move, direction) + 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): + 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 = self.calibrate_1d(np.array([1, 0, 0])) + cal_x: dict = self.calibrate_1d((1, 0, 0)) logging.info("Calibrating Y axis:") - cal_y = self.calibrate_1d(np.array([0, 1, 0])) + cal_y: dict = self.calibrate_1d((0, 1, 0)) # Combine X and Y calibrations to make a 2D calibration - cal_xy = image_to_stage_displacement_from_1d([cal_x, cal_y]) + cal_xy: dict = image_to_stage_displacement_from_1d([cal_x, cal_y]) self.update_settings(cal_xy) - data = { + data: Dict[str, dict] = { "camera_stage_mapping_calibration": cal_xy, "linear_calibration_x": cal_x, "linear_calibration_y": cal_y, @@ -183,31 +192,38 @@ class CSMExtension(BaseExtension): return data @property - def image_to_stage_displacement_matrix(self): + def image_to_stage_displacement_matrix(self) -> np.ndarray: # 2x2 integer array """A 2x2 matrix that converts displacement in image coordinates to stage coordinates.""" - try: - settings = self.get_settings() - return settings["image_to_stage_displacement"] - except KeyError as e: - raise ValueError("The microscope has not yet been calibrated.") from e + 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): + def move_in_image_coordinates(self, displacement_in_pixels: XYCoordinateType): """Move by a given number of pixels on the camera""" - p = np.array(displacement_in_pixels) - relative_move = np.dot(p, self.image_to_stage_displacement_matrix) + 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, **kwargs): + 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, displacement_in_pixels, **kwargs + tracker, + self.move_in_image_coordinates, + np.array(displacement_in_pixels), + **kwargs ) - def closed_loop_scan(self, scan_path, **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 @@ -219,7 +235,7 @@ class CSMExtension(BaseExtension): for i, pos in self.extension.closed_loop_scan(scan_path): capture_image(f"image_{i}.jpg") - ``scan_path`` should be an Nx2 numpy array defining + ``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 @@ -235,11 +251,13 @@ class CSMExtension(BaseExtension): tracker, self.move_in_image_coordinates, move, np.array(scan_path), **kwargs ) - def test_closed_loop_spiral_scan(self, step_size, N, **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 = ordered_spiral(0, 0, N, *step_size) + scan_path: List[XYCoordinateType] = ordered_spiral(0, 0, N, *step_size) - for _ in self.closed_loop_scan(np.array(scan_path), **kwargs): + for _ in self.closed_loop_scan(scan_path, **kwargs): pass @@ -249,7 +267,7 @@ class Calibrate1DView(ActionView): def post(self, args): """Calibrate one axis of the microscope stage against the camera.""" - direction = np.array(args.get("direction")) + direction: Tuple[float, float, float] = args.get("direction") return self.extension.calibrate_1d(direction) @@ -273,9 +291,7 @@ class MoveInImageCoordinatesView(ActionView): 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( - np.array([args.get("x"), args.get("y")]) - ) + self.extension.move_in_image_coordinates((args.get("x"), args.get("y"))) return self.extension.microscope.state["stage"]["position"] @@ -294,7 +310,7 @@ class ClosedLoopMoveInImageCoordinatesView(ActionView): """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( - np.array([args.get("x"), args.get("y")]) + (args.get("x"), args.get("y")) ) return self.extension.microscope.state["stage"]["position"] @@ -319,7 +335,7 @@ class TestClosedLoopSpiralScanView(ActionView): """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( - np.array([args.get("x"), args.get("y")]), args.get("N") + (args.get("x"), args.get("y")), args.get("N") ) diff --git a/openflexure_microscope/camera/base.py b/openflexure_microscope/camera/base.py index 7f515d56..f67e3e8f 100644 --- a/openflexure_microscope/camera/base.py +++ b/openflexure_microscope/camera/base.py @@ -179,7 +179,7 @@ class BaseCamera(metaclass=ABCMeta): ) return True - def get_frame(self): + def get_frame(self) -> bytes: """ Return the current camera frame. diff --git a/openflexure_microscope/stage/base.py b/openflexure_microscope/stage/base.py index b38d2ee6..026ac642 100644 --- a/openflexure_microscope/stage/base.py +++ b/openflexure_microscope/stage/base.py @@ -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, ): From b6a0a6395ef6df3a5fcbc160a6e25eff7820c6c5 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Fri, 4 Dec 2020 17:09:52 +0000 Subject: [PATCH 30/35] Bumped version number --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 59019852..1f65701a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [ From 58b2967f53fe6a272414a206c041aa393f2046da Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 7 Dec 2020 10:12:30 +0000 Subject: [PATCH 31/35] Fixed autofocus 'PiCameraStreamer' object has no attribute 'annotate_text' error --- openflexure_microscope/api/default_extensions/autofocus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openflexure_microscope/api/default_extensions/autofocus.py b/openflexure_microscope/api/default_extensions/autofocus.py index 91fe08fb..339291ca 100644 --- a/openflexure_microscope/api/default_extensions/autofocus.py +++ b/openflexure_microscope/api/default_extensions/autofocus.py @@ -201,7 +201,7 @@ class AutofocusExtension(BaseExtension): positions: List[int] = [] # Some cameras may not have annotate_text. Reset if it does - if getattr(camera, "annotate_text"): + if getattr(camera, "annotate_text", None): setattr(camera, "annotate_text", "") for _ in stage.scan_z(dz, return_to_start=False): From 599c31534dcfa01d31f8b6133f53f06fa12bc6f5 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 7 Dec 2020 10:37:04 +0000 Subject: [PATCH 32/35] globalUpdateCaptures after scan --- .../tabContentComponents/captureComponents/paneCapture.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openflexure_microscope/api/static/src/components/tabContentComponents/captureComponents/paneCapture.vue b/openflexure_microscope/api/static/src/components/tabContentComponents/captureComponents/paneCapture.vue index a368109b..696ffadf 100644 --- a/openflexure_microscope/api/static/src/components/tabContentComponents/captureComponents/paneCapture.vue +++ b/openflexure_microscope/api/static/src/components/tabContentComponents/captureComponents/paneCapture.vue @@ -434,6 +434,8 @@ export default { onScanResponse: function() { this.modalNotify("Finished scan."); + // Emit signal to update capture list + this.$root.$emit("globalUpdateCaptures"); } } }; From 9d04842c56693f6d5130d5c71927fc40aa84d11b Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 7 Dec 2020 11:12:07 +0000 Subject: [PATCH 33/35] Handle missing this.$refs.textboxKey ref --- .../api/static/src/components/fieldComponents/keyvalList.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openflexure_microscope/api/static/src/components/fieldComponents/keyvalList.vue b/openflexure_microscope/api/static/src/components/fieldComponents/keyvalList.vue index 4d7743a4..32a82f64 100644 --- a/openflexure_microscope/api/static/src/components/fieldComponents/keyvalList.vue +++ b/openflexure_microscope/api/static/src/components/fieldComponents/keyvalList.vue @@ -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) { From c75a7c5401d28404a79dfc201c5d9c9deabd4f88 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 7 Dec 2020 11:13:35 +0000 Subject: [PATCH 34/35] Lockfile update --- .../api/static/package-lock.json | 196 +++++++++--------- 1 file changed, 98 insertions(+), 98 deletions(-) diff --git a/openflexure_microscope/api/static/package-lock.json b/openflexure_microscope/api/static/package-lock.json index d3107891..f8a024be 100644 --- a/openflexure_microscope/api/static/package-lock.json +++ b/openflexure_microscope/api/static/package-lock.json @@ -2917,6 +2917,16 @@ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "optional": true, + "requires": { + "color-convert": "^2.0.1" + } + }, "browserslist": { "version": "4.15.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.15.0.tgz", @@ -2962,12 +2972,36 @@ "integrity": "sha512-8cEsSMwXfx7lWSUMA2s08z9dIgsnR5NAqjXP23stdsU3AUWkCr/rr4s4OFtHXn5XXr6+7kam3QFVoYyXNPdJPA==", "dev": true }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "optional": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "optional": true + }, "electron-to-chromium": { "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": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "optional": true + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3001,6 +3035,35 @@ "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", "dev": true }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "optional": true + }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "optional": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "optional": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -3134,6 +3197,16 @@ "ansi-regex": "^5.0.0" } }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "optional": true, + "requires": { + "has-flag": "^4.0.0" + } + }, "terser-webpack-plugin": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz", @@ -3160,6 +3233,31 @@ "psl": "^1.1.28", "punycode": "^2.1.1" } + }, + "vue-loader-v16": { + "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": { + "chalk": "^4.1.0", + "hash-sum": "^2.0.0", + "loader-utils": "^2.0.0" + }, + "dependencies": { + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "optional": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } } } }, @@ -13713,104 +13811,6 @@ } } }, - "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==", - "dev": true, - "optional": true, - "requires": { - "chalk": "^4.1.0", - "hash-sum": "^2.0.0", - "loader-utils": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "optional": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "optional": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "optional": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "optional": true - }, - "emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, - "optional": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "optional": true - }, - "json5": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", - "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", - "dev": true, - "optional": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", - "dev": true, - "optional": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "optional": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "vue-observe-visibility": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/vue-observe-visibility/-/vue-observe-visibility-0.4.6.tgz", From 9659c45bb4ef8e3ae560771d1c968f59700a4dcc Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 7 Dec 2020 12:44:34 +0000 Subject: [PATCH 35/35] Fixed SangaDeltaStage type annotations --- openflexure_microscope/stage/sanga.py | 128 ++++++++++++++++++-------- 1 file changed, 90 insertions(+), 38 deletions(-) diff --git a/openflexure_microscope/stage/sanga.py b/openflexure_microscope/stage/sanga.py index 22be38f3..ed9f3c54 100644 --- a/openflexure_microscope/stage/sanga.py +++ b/openflexure_microscope/stage/sanga.py @@ -12,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 @@ -55,7 +68,7 @@ class SangaStage(BaseStage): return 3 @property - def position(self): + def position(self) -> Tuple[int, int, int]: return self.board.position @property @@ -120,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, ): @@ -141,10 +154,6 @@ class SangaStage(BaseStage): """ 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 @@ -155,16 +164,10 @@ 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") - - # Create the displacement array - displacement_array: np.ndarray = np.array( - [ - 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 ) - elif isinstance(displacement, np.ndarray): displacement_array = displacement elif isinstance(displacement, (list, tuple, GeneratorType)): @@ -173,6 +176,11 @@ class SangaStage(BaseStage): else: raise TypeError(f"Unsupported displacement type {type(displacement)}") + # 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( @@ -191,7 +199,7 @@ class SangaStage(BaseStage): # 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: @@ -244,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], @@ -263,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], @@ -278,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)