Merge branch 'cleanup' into 'master'
The grand cleanup See merge request openflexure/openflexure-microscope-server!82
This commit is contained in:
commit
5b3d46ff98
60 changed files with 465 additions and 1180 deletions
0
.gitattributes
vendored
0
.gitattributes
vendored
11
.pre-commit-config.yaml
Normal file
11
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 19.10b0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/timothycrosley/isort
|
||||
rev: 5.4.2
|
||||
hooks:
|
||||
- id: isort
|
||||
additional_dependencies: [toml]
|
||||
exclude: ^.*/?setup\.py$
|
||||
20
Dockerfile
20
Dockerfile
|
|
@ -1,20 +0,0 @@
|
|||
FROM python:3.6
|
||||
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
POETRY_VERSION=1.0.3 \
|
||||
POETRY_HOME="/opt/poetry" \
|
||||
POETRY_VIRTUALENVS_IN_PROJECT=true \
|
||||
POETRY_NO_INTERACTION=1
|
||||
|
||||
ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python
|
||||
|
||||
COPY . ./
|
||||
|
||||
RUN poetry install --no-dev --no-interaction
|
||||
|
||||
EXPOSE 5000
|
||||
CMD ["poetry", "run", "python", "-m", "openflexure_microscope.api.app"]
|
||||
11
README.md
11
README.md
|
|
@ -14,6 +14,17 @@ This includes installing the server in a mode better suited for active developme
|
|||
* `poetry install`
|
||||
* `poetry run build_static`
|
||||
|
||||
### Pre-commit hooks
|
||||
|
||||
We make use of pre-commit hooks to run code analysis before committing to the codebase.
|
||||
|
||||
The simplest way to ensure this works is to install pre-commits into your current Python environment:
|
||||
|
||||
* `pip3 install pre-commit`
|
||||
* `pre-commit install`
|
||||
|
||||
To run pre-commit analysis manually, you can run `pre-commit run`.
|
||||
|
||||
### Node installation
|
||||
|
||||
* Note, building the static interface will require a valid Node.js installation
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ sys.modules.update((mod_name, Mock()) for mod_name in mock_imports)
|
|||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = "OpenFlexure Microscope Software"
|
||||
copyright = "2018, Bath Open Instrumentation Group"
|
||||
copyright = "2018, Bath Open Instrumentation Group" # pylint: disable=redefined-builtin
|
||||
author = "Bath Open Instrumentation Group"
|
||||
|
||||
# The short X.Y version
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from labthings import Schema, fields, find_component
|
||||
from labthings.extensions import BaseExtension
|
||||
from labthings.views import PropertyView, View
|
||||
from labthings.views import PropertyView
|
||||
|
||||
## Extension methods
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import io # Used in our capture action
|
|||
from flask import send_file # Used to send images from our server
|
||||
from labthings import Schema, fields, find_component
|
||||
from labthings.extensions import BaseExtension
|
||||
from labthings.views import ActionView, PropertyView, View
|
||||
from labthings.views import ActionView, PropertyView
|
||||
|
||||
## Extension methods
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,8 @@
|
|||
import io # Used in our capture action
|
||||
import time # Used in our timelapse function
|
||||
|
||||
from flask import send_file # Used to send images from our server
|
||||
from labthings import (
|
||||
Schema,
|
||||
current_action,
|
||||
fields,
|
||||
find_component,
|
||||
update_action_progress,
|
||||
)
|
||||
from labthings import current_action, fields, find_component, update_action_progress
|
||||
from labthings.extensions import BaseExtension
|
||||
from labthings.views import ActionView, PropertyView, View
|
||||
from labthings.views import ActionView
|
||||
|
||||
# Used in our timelapse function
|
||||
from openflexure_microscope.captures.capture_manager import generate_basename
|
||||
|
|
|
|||
|
|
@ -1,16 +1,8 @@
|
|||
import io # Used in our capture action
|
||||
import time # Used in our timelapse function
|
||||
|
||||
from flask import send_file # Used to send images from our server
|
||||
from labthings import (
|
||||
Schema,
|
||||
current_action,
|
||||
fields,
|
||||
find_component,
|
||||
update_action_progress,
|
||||
)
|
||||
from labthings import current_action, fields, find_component, update_action_progress
|
||||
from labthings.extensions import BaseExtension
|
||||
from labthings.views import ActionView, PropertyView, View
|
||||
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
|
||||
|
|
|
|||
|
|
@ -24,13 +24,13 @@ import pkg_resources
|
|||
from flask import abort, send_file
|
||||
from flask_cors import CORS, cross_origin
|
||||
from labthings import create_app
|
||||
from labthings.views import View
|
||||
from labthings.extensions import find_extensions
|
||||
from labthings.views import View
|
||||
|
||||
from openflexure_microscope.api.microscope import default_microscope as api_microscope
|
||||
from openflexure_microscope.api.utilities import init_default_extensions, list_routes
|
||||
from openflexure_microscope.api.v2 import views
|
||||
from openflexure_microscope.config import JSONEncoder
|
||||
from openflexure_microscope.json import JSONEncoder
|
||||
from openflexure_microscope.paths import (
|
||||
OPENFLEXURE_EXTENSIONS_PATH,
|
||||
OPENFLEXURE_VAR_PATH,
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class JPEGSharpnessMonitor:
|
|||
raise e
|
||||
if stop < 1:
|
||||
stop = len(jpeg_times)
|
||||
logging.debug("changing stop to {}".format(stop))
|
||||
logging.debug("changing stop to %s", (stop))
|
||||
jpeg_times = jpeg_times[start:stop]
|
||||
jpeg_zs = np.interp(jpeg_times, stage_times, stage_zs)
|
||||
return jpeg_times, jpeg_zs, jpeg_sizes[start:stop]
|
||||
|
|
@ -258,13 +258,12 @@ def fast_up_down_up_autofocus(
|
|||
# Ensure the MJPEG stream has started
|
||||
microscope.camera.start_stream_recording()
|
||||
|
||||
df = dz # TODO: refactor so I actually use dz in the code below!
|
||||
logging.debug("Initial move")
|
||||
if initial_move_up:
|
||||
m.focus_rel(df / 2)
|
||||
m.focus_rel(dz / 2)
|
||||
# move down
|
||||
logging.debug("Move down")
|
||||
i, z = m.focus_rel(-df)
|
||||
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)
|
||||
|
|
@ -285,16 +284,14 @@ def fast_up_down_up_autofocus(
|
|||
inow = np.argmax(
|
||||
js < current_js
|
||||
) # use the curve we recorded to estimate our position
|
||||
# TODO: fancy interpolation stuff
|
||||
|
||||
# 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 {} steps".format(
|
||||
correction_move
|
||||
)
|
||||
"Fast autofocus scan: correcting backlash by moving %s steps",
|
||||
(correction_move),
|
||||
)
|
||||
m.focus_rel(correction_move)
|
||||
return m.data_dict()
|
||||
|
|
|
|||
|
|
@ -63,17 +63,15 @@ def auto_expose_and_freeze_settings(camera):
|
|||
|
||||
logging.info("Freezing the camera settings...")
|
||||
camera.shutter_speed = camera.exposure_speed
|
||||
logging.info("Shutter speed = {}".format(camera.shutter_speed))
|
||||
logging.info("Shutter speed = %s", (camera.shutter_speed))
|
||||
camera.exposure_mode = "off"
|
||||
logging.info("Auto exposure disabled")
|
||||
g = camera.awb_gains
|
||||
camera.awb_mode = "off"
|
||||
camera.awb_gains = g
|
||||
logging.info("Auto white balance disabled, gains are {}".format(g))
|
||||
logging.info("Auto white balance disabled, gains are %s", (g))
|
||||
logging.info(
|
||||
"Analogue gain: {}, Digital gain: {}".format(
|
||||
camera.analog_gain, camera.digital_gain
|
||||
)
|
||||
"Analogue gain: %s, Digital gain: %s", camera.analog_gain, camera.digital_gain
|
||||
)
|
||||
adjust_exposure_to_setpoint(camera, 215)
|
||||
|
||||
|
|
@ -100,7 +98,7 @@ def lst_from_channels(channels):
|
|||
# lst_resolution = list(np.ceil(full_resolution / 64.0).astype(int))
|
||||
lst_resolution = [(r // 64) + 1 for r in full_resolution]
|
||||
# NB the size of the LST is 1/64th of the image, but rounded UP.
|
||||
logging.info("Generating a lens shading table at {}x{}".format(*lst_resolution))
|
||||
logging.info("Generating a lens shading table at %sx%s", *lst_resolution)
|
||||
lens_shading = np.zeros([channels.shape[0]] + lst_resolution, dtype=np.float)
|
||||
for i in range(lens_shading.shape[0]):
|
||||
image_channel = channels[i, :, :]
|
||||
|
|
@ -118,9 +116,12 @@ def lst_from_channels(channels):
|
|||
image_channel, [(0, lw * 32 - iw), (0, lh * 32 - ih)], mode="edge"
|
||||
) # Pad image to the right and bottom
|
||||
logging.info(
|
||||
"Channel shape: {}x{}, shading table shape: {}x{}, after padding {}".format(
|
||||
iw, ih, lw * 32, lh * 32, padded_image_channel.shape
|
||||
)
|
||||
"Channel shape: %sx%s, shading table shape: %sx%s, after padding %s",
|
||||
iw,
|
||||
ih,
|
||||
lw * 32,
|
||||
lh * 32,
|
||||
padded_image_channel.shape,
|
||||
)
|
||||
# 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!
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from labthings import (
|
|||
from labthings.extensions import BaseExtension
|
||||
from labthings.views import ActionView
|
||||
|
||||
from openflexure_microscope.api.v2.views.actions.camera import FullCaptureArgs
|
||||
from openflexure_microscope.captures.capture_manager import generate_basename
|
||||
from openflexure_microscope.devel import abort
|
||||
|
||||
|
|
@ -216,7 +217,7 @@ class ScanExtension(BaseExtension):
|
|||
|
||||
for x_y in line:
|
||||
# Move to new grid position without changing z
|
||||
logging.debug("Moving to step {}".format([x_y[0], x_y[1], next_z]))
|
||||
logging.debug("Moving to step %s", ([x_y[0], x_y[1], next_z]))
|
||||
microscope.stage.move_abs([x_y[0], x_y[1], next_z])
|
||||
# Refocus
|
||||
if autofocus_enabled:
|
||||
|
|
@ -270,7 +271,7 @@ class ScanExtension(BaseExtension):
|
|||
# Make sure we use our current best estimate of focus (i.e. the current position) next point
|
||||
next_z = microscope.stage.position[2]
|
||||
|
||||
logging.debug("Returning to {}".format(initial_position))
|
||||
logging.debug("Returning to %s", (initial_position))
|
||||
microscope.stage.move_abs(initial_position)
|
||||
|
||||
end = time.time()
|
||||
|
|
@ -328,34 +329,29 @@ class ScanExtension(BaseExtension):
|
|||
return
|
||||
|
||||
if i != steps - 1:
|
||||
logging.debug("Moving z by {}".format(step_size))
|
||||
logging.debug("Moving z by %s", (step_size))
|
||||
microscope.stage.move_rel([0, 0, step_size])
|
||||
if return_to_start:
|
||||
logging.debug("Returning to {}".format(initial_position))
|
||||
logging.debug("Returning to %s", (initial_position))
|
||||
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])
|
||||
style = fields.String(missing="raster")
|
||||
autofocus_dz = fields.Integer(missing=50)
|
||||
fast_autofocus = fields.Boolean(missing=False)
|
||||
stride_size = fields.List(
|
||||
fields.Integer, missing=[2000, 1500, 100], example=[2000, 1500, 100]
|
||||
)
|
||||
|
||||
|
||||
class TileScanAPI(ActionView):
|
||||
args = {
|
||||
"filename": fields.String(missing=None, example=None),
|
||||
"namemode": fields.String(missing="coordinates", example="coordinates"),
|
||||
"temporary": fields.Boolean(missing=False),
|
||||
"stride_size": fields.List(
|
||||
fields.Integer, missing=[2000, 1500, 100], example=[2000, 1500, 100]
|
||||
),
|
||||
"grid": fields.List(fields.Integer, missing=[3, 3, 3], example=[3, 3, 3]),
|
||||
"style": fields.String(missing="raster"),
|
||||
"autofocus_dz": fields.Integer(missing=50),
|
||||
"fast_autofocus": fields.Boolean(missing=False),
|
||||
"use_video_port": fields.Boolean(missing=False),
|
||||
"bayer": fields.Boolean(missing=False),
|
||||
"annotations": fields.Dict(missing={}, example={"Foo": "Bar"}),
|
||||
"tags": fields.List(fields.String, missing=[]),
|
||||
"resize": fields.Dict(missing=None), # TODO: Validate keys
|
||||
}
|
||||
args = TileScanArgs()
|
||||
|
||||
# Allow 10 seconds to stop upon DELETE request
|
||||
# Gives fast-autofocus time to finish if it's running
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ from labthings.schema import Schema, pre_dump
|
|||
from labthings.utilities import description_from_view
|
||||
from labthings.views import ActionView, PropertyView, View
|
||||
|
||||
from openflexure_microscope.devel import JsonResponse, request
|
||||
|
||||
|
||||
class ZipObjectSchema(Schema):
|
||||
id = fields.String()
|
||||
|
|
@ -128,12 +126,15 @@ default_zip_manager = ZipManager()
|
|||
|
||||
|
||||
class ZipBuilderAPIView(ActionView):
|
||||
def post(self):
|
||||
ids = list(JsonResponse(request).json)
|
||||
args = fields.List(fields.String(), required=True)
|
||||
|
||||
def post(self, args):
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Return a handle on the autofocus task
|
||||
return default_zip_manager.marshaled_build_zip_from_capture_ids(microscope, ids)
|
||||
return default_zip_manager.marshaled_build_zip_from_capture_ids(
|
||||
microscope, args
|
||||
)
|
||||
|
||||
|
||||
class ZipListAPIView(PropertyView):
|
||||
|
|
|
|||
|
|
@ -184,7 +184,6 @@ export default {
|
|||
this.checkConnection();
|
||||
// Handle guided tour
|
||||
// If the user has already completed or skipped the guided tour
|
||||
// TODO: Only run this if connected to the API
|
||||
var completedTour = this.getLocalStorageObj("completedTour") || false;
|
||||
if (!completedTour) {
|
||||
this.$tours["guidedTour"].start();
|
||||
|
|
@ -207,8 +206,6 @@ export default {
|
|||
);
|
||||
|
||||
// Keyboard shortcuts
|
||||
|
||||
// TODO: Shortcut guide
|
||||
Mousetrap.bind("?", () => {
|
||||
console.log(this.keyboardManual);
|
||||
this.toggleModalElement(this.$refs["keyboardManualModal"]); // Calls the mixin
|
||||
|
|
|
|||
|
|
@ -16,63 +16,27 @@
|
|||
id="switcher-left"
|
||||
class="uk-flex uk-flex-column uk-padding-remove uk-width-auto uk-height-1-1 uk-text-center"
|
||||
>
|
||||
<tabIcon
|
||||
id="view-tab-icon"
|
||||
tab-i-d="view"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
@set-tab="setTab"
|
||||
>
|
||||
<i class="material-icons">visibility</i>
|
||||
</tabIcon>
|
||||
|
||||
<tabIcon
|
||||
id="gallery-tab-icon"
|
||||
tab-i-d="gallery"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
@set-tab="setTab"
|
||||
>
|
||||
<i class="material-icons">photo_library</i>
|
||||
</tabIcon>
|
||||
|
||||
<hr />
|
||||
|
||||
<tabIcon
|
||||
id="navigate-tab-icon"
|
||||
tab-i-d="navigate"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
@set-tab="setTab"
|
||||
>
|
||||
<i class="material-icons">gamepad</i>
|
||||
</tabIcon>
|
||||
<tabIcon
|
||||
id="capture-tab-icon"
|
||||
tab-i-d="capture"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
@set-tab="setTab"
|
||||
>
|
||||
<i class="material-icons">camera_alt</i>
|
||||
</tabIcon>
|
||||
|
||||
<template v-if="$store.state.globalSettings.IHIEnabled">
|
||||
<hr id="extension-tab-divider" />
|
||||
|
||||
<!-- For each top tab -->
|
||||
<template v-for="(item, index) in enabledTopTabs">
|
||||
<!-- Render the tab icon -->
|
||||
<tabIcon
|
||||
id="slidescan-tab-icon"
|
||||
tab-i-d="slidescan"
|
||||
:id="item.id + '-tab-icon'"
|
||||
:key="item.id + '-tab-icon'"
|
||||
:tab-i-d="item.id"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
:class="item.class"
|
||||
@set-tab="setTab"
|
||||
>
|
||||
<i class="material-icons">settings_overscan</i>
|
||||
<i class="material-icons">{{ item.icon }}</i>
|
||||
</tabIcon>
|
||||
<!-- Add a divider if item.divide is true -->
|
||||
<hr v-if="item.divide" :key="'tab-divider-' + index" />
|
||||
</template>
|
||||
|
||||
<hr id="extension-tab-divider" />
|
||||
|
||||
<!-- For each plugin tab -->
|
||||
<tabIcon
|
||||
v-for="plugin in pluginsGuiList"
|
||||
:key="plugin.id"
|
||||
|
|
@ -88,36 +52,23 @@
|
|||
|
||||
<hr id="extension-tab-divider" />
|
||||
|
||||
<tabIcon
|
||||
id="settings-tab-icon"
|
||||
class="uk-margin-auto-top"
|
||||
tab-i-d="settings"
|
||||
:require-connection="false"
|
||||
:current-tab="currentTab"
|
||||
@set-tab="setTab"
|
||||
>
|
||||
<i class="material-icons">settings</i>
|
||||
</tabIcon>
|
||||
|
||||
<tabIcon
|
||||
id="logging-tab-icon"
|
||||
tab-i-d="logging"
|
||||
:require-connection="false"
|
||||
:current-tab="currentTab"
|
||||
@set-tab="setTab"
|
||||
>
|
||||
<i class="material-icons">assignment_late</i>
|
||||
</tabIcon>
|
||||
|
||||
<tabIcon
|
||||
id="about-tab-icon"
|
||||
tab-i-d="about"
|
||||
:require-connection="false"
|
||||
:current-tab="currentTab"
|
||||
@set-tab="setTab"
|
||||
>
|
||||
<i class="material-icons">info</i>
|
||||
</tabIcon>
|
||||
<!-- For each bottom tab -->
|
||||
<template v-for="(item, index) in bottomTabs">
|
||||
<!-- Render the tab icon -->
|
||||
<tabIcon
|
||||
:id="item.id + '-tab-icon'"
|
||||
:key="item.id + '-tab-icon'"
|
||||
:tab-i-d="item.id"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
:class="item.class"
|
||||
@set-tab="setTab"
|
||||
>
|
||||
<i class="material-icons">{{ item.icon }}</i>
|
||||
</tabIcon>
|
||||
<!-- Add a divider if item.divide is true -->
|
||||
<hr v-if="item.divide" :key="'tab-divider-' + index" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -126,69 +77,19 @@
|
|||
id="container-left"
|
||||
class="uk-padding-remove uk-height-1-1 uk-width-expand"
|
||||
>
|
||||
<!-- For each top tab -->
|
||||
<tabContent
|
||||
tab-i-d="view"
|
||||
v-for="item in enabledTopTabs"
|
||||
:id="item.id + '-tab-content'"
|
||||
:key="item.id + '-tab-content'"
|
||||
:tab-i-d="item.id"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
>
|
||||
<viewContent />
|
||||
</tabContent>
|
||||
<tabContent
|
||||
tab-i-d="gallery"
|
||||
:require-connection="false"
|
||||
:current-tab="currentTab"
|
||||
>
|
||||
<galleryContent />
|
||||
</tabContent>
|
||||
<tabContent
|
||||
tab-i-d="navigate"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
>
|
||||
<navigateContent />
|
||||
</tabContent>
|
||||
<tabContent
|
||||
tab-i-d="capture"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
>
|
||||
<captureContent />
|
||||
</tabContent>
|
||||
|
||||
<template v-if="$store.state.globalSettings.IHIEnabled">
|
||||
<tabContent
|
||||
tab-i-d="slidescan"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
>
|
||||
<slideScanContent />
|
||||
</tabContent>
|
||||
</template>
|
||||
|
||||
<tabContent
|
||||
tab-i-d="settings"
|
||||
:require-connection="false"
|
||||
:current-tab="currentTab"
|
||||
>
|
||||
<settingsContent class="section-content" />
|
||||
</tabContent>
|
||||
|
||||
<tabContent
|
||||
tab-i-d="logging"
|
||||
:require-connection="false"
|
||||
:current-tab="currentTab"
|
||||
>
|
||||
<loggingContent />
|
||||
</tabContent>
|
||||
|
||||
<tabContent
|
||||
tab-i-d="about"
|
||||
:require-connection="false"
|
||||
:current-tab="currentTab"
|
||||
>
|
||||
<aboutContent class="section-content" />
|
||||
<component :is="item.component"></component>
|
||||
</tabContent>
|
||||
|
||||
<!-- For each plugin tab -->
|
||||
<tabContent
|
||||
v-for="plugin in pluginsGuiList"
|
||||
:key="plugin.id"
|
||||
|
|
@ -204,6 +105,18 @@
|
|||
@reloadForms="updatePlugins()"
|
||||
/>
|
||||
</tabContent>
|
||||
|
||||
<!-- For each bottom tab -->
|
||||
<tabContent
|
||||
v-for="item in bottomTabs"
|
||||
:id="item.id + '-tab-content'"
|
||||
:key="item.id + '-tab-content'"
|
||||
:tab-i-d="item.id"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
>
|
||||
<component :is="item.component"></component>
|
||||
</tabContent>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -252,14 +165,69 @@ export default {
|
|||
return {
|
||||
plugins: [],
|
||||
currentTab: "view",
|
||||
unwatchStoreFunction: null
|
||||
unwatchStoreFunction: null,
|
||||
topTabs: [
|
||||
{
|
||||
id: "view",
|
||||
icon: "visibility",
|
||||
component: viewContent
|
||||
},
|
||||
{
|
||||
id: "gallery",
|
||||
icon: "photo_library",
|
||||
component: galleryContent,
|
||||
divide: true // Add a divider after this tab icon
|
||||
},
|
||||
{
|
||||
id: "navigate",
|
||||
icon: "gamepad",
|
||||
component: navigateContent
|
||||
},
|
||||
{
|
||||
id: "capture",
|
||||
icon: "camera_alt",
|
||||
component: captureContent,
|
||||
divide: true // Add a divider after this tab icon
|
||||
}
|
||||
],
|
||||
bottomTabs: [
|
||||
{
|
||||
id: "settings",
|
||||
icon: "settings",
|
||||
component: settingsContent,
|
||||
class: "uk-margin-auto-top"
|
||||
},
|
||||
{
|
||||
id: "logging",
|
||||
icon: "assignment_late",
|
||||
component: loggingContent
|
||||
},
|
||||
{
|
||||
id: "about",
|
||||
icon: "info",
|
||||
component: aboutContent
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
enabledTopTabs: function() {
|
||||
var enabledTabs = this.topTabs;
|
||||
if (this.$store.state.globalSettings.IHIEnabled) {
|
||||
enabledTabs.push({
|
||||
id: "slidescan",
|
||||
icon: "settings_overscan",
|
||||
component: slideScanContent
|
||||
});
|
||||
}
|
||||
return enabledTabs;
|
||||
},
|
||||
|
||||
pluginsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
||||
},
|
||||
|
||||
pluginsGuiList: function() {
|
||||
// List of plugin GUIs, obtained from this.plugins values
|
||||
console.log("Recalculating plugins");
|
||||
|
|
@ -271,15 +239,21 @@ export default {
|
|||
}
|
||||
return pluginGuis;
|
||||
},
|
||||
|
||||
tabOrder: function() {
|
||||
// TODO: There must be a better way to do this, somehow reading the order of the HTML elements?
|
||||
var ind = ["view", "gallery", "navigate", "capture", "settings"];
|
||||
var ind = [];
|
||||
for (const tab of this.enabledTopTabs) {
|
||||
ind.push(tab.id);
|
||||
}
|
||||
for (const plugin of this.pluginsGuiList) {
|
||||
ind.push(plugin.id);
|
||||
}
|
||||
ind.push("about");
|
||||
for (const tab of this.bottomTabs) {
|
||||
ind.push(tab.id);
|
||||
}
|
||||
return ind;
|
||||
},
|
||||
|
||||
currentTabIndex: function() {
|
||||
return this.tabOrder.indexOf(this.currentTab);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,6 @@ export default {
|
|||
},
|
||||
|
||||
submitApiUri: function() {
|
||||
// TODO: This could probably be handled more explicitally
|
||||
return this.pluginApiUri + this.route;
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -24,54 +24,6 @@ def blueprint_name_for_module(module_name, api_ver=2, suffix=""):
|
|||
return f"v{api_ver}_{bp_name}_blueprint{suffix}"
|
||||
|
||||
|
||||
class JsonResponse:
|
||||
def __init__(self, request):
|
||||
"""
|
||||
Object to wrap up simple functionality for parsing a JSON response.
|
||||
"""
|
||||
# Try to load as json
|
||||
try:
|
||||
self.json = (
|
||||
request.get_json()
|
||||
) #: dict: Dictionary representation of request JSON
|
||||
# If malformed JSON is passed, make an empty dictionary
|
||||
except BadRequest as e:
|
||||
logging.error(e)
|
||||
self.json = {}
|
||||
|
||||
if self.json is None:
|
||||
self.json = {}
|
||||
|
||||
# Store raw response data
|
||||
self.data = request.get_data() #: str: String representation of request data
|
||||
|
||||
if self.data is None:
|
||||
self.data = ""
|
||||
|
||||
def param(self, key, default=None, convert=None):
|
||||
"""
|
||||
Check if a key exists in a JSON/dictionary payload, and returns it.
|
||||
|
||||
Args:
|
||||
key (str): JSON key to look for
|
||||
default: Value to return if no matching key-value pair is found.
|
||||
convert: Converter function. By passing a type, the value will be converted to that type.
|
||||
**Use with caution!**
|
||||
"""
|
||||
# If no JSON payload exists, make an empty dictionary
|
||||
if not self.json:
|
||||
self.json = {}
|
||||
|
||||
if key in self.json:
|
||||
val = self.json[key]
|
||||
else:
|
||||
val = default
|
||||
|
||||
if convert and (val is not None):
|
||||
val = convert(val)
|
||||
return val
|
||||
|
||||
|
||||
def gen(camera):
|
||||
"""Video streaming generator function."""
|
||||
while True:
|
||||
|
|
@ -121,12 +73,10 @@ def init_default_extensions(extension_dir):
|
|||
default_ext_path = os.path.join(extension_dir, "defaults.py")
|
||||
|
||||
if not os.path.isfile(default_ext_path): # If user extensions file doesn't exist
|
||||
logging.warning(
|
||||
"No extension file found at {}. Creating...".format(extension_dir)
|
||||
)
|
||||
logging.warning("No extension file found at %s. Creating...", (extension_dir))
|
||||
create_file(default_ext_path)
|
||||
|
||||
logging.info("Populating {}...".format(default_ext_path))
|
||||
logging.info("Populating %s...", (default_ext_path))
|
||||
with open(default_ext_path, "w") as outfile:
|
||||
outfile.write(_DEFAULT_EXTENSION_INIT)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,33 +1,39 @@
|
|||
import io
|
||||
import logging
|
||||
|
||||
from flask import abort, send_file
|
||||
from labthings import fields, find_component
|
||||
from flask import send_file
|
||||
from labthings import Schema, fields, find_component
|
||||
from labthings.views import ActionView
|
||||
|
||||
from openflexure_microscope.api.v2.views.captures import CaptureSchema
|
||||
|
||||
|
||||
class CaptureResizeSchema(Schema):
|
||||
width = fields.Integer(example=640, required=True)
|
||||
height = fields.Integer(example=480, required=True)
|
||||
|
||||
|
||||
class BasicCaptureArgs(Schema):
|
||||
use_video_port = fields.Boolean(missing=False)
|
||||
bayer = fields.Boolean(
|
||||
missing=False, description="Include raw bayer data in capture"
|
||||
)
|
||||
resize = fields.Nested(CaptureResizeSchema(), required=False)
|
||||
|
||||
|
||||
class FullCaptureArgs(BasicCaptureArgs):
|
||||
filename = fields.String(example="MyFileName")
|
||||
temporary = fields.Boolean(missing=False, description="Delete capture on shutdown")
|
||||
annotations = fields.Dict(missing={}, example={"Client": "SwaggerUI"})
|
||||
tags = fields.List(fields.String, missing=[], example=["docs"])
|
||||
|
||||
|
||||
class CaptureAPI(ActionView):
|
||||
"""
|
||||
Create a new image capture.
|
||||
"""
|
||||
|
||||
args = {
|
||||
"filename": fields.String(example="MyFileName"),
|
||||
"temporary": fields.Boolean(
|
||||
missing=False, description="Delete capture on shutdown"
|
||||
),
|
||||
"use_video_port": fields.Boolean(missing=False),
|
||||
"bayer": fields.Boolean(
|
||||
missing=False, description="Store raw bayer data in file"
|
||||
),
|
||||
"annotations": fields.Dict(missing={}, example={"Client": "SwaggerUI"}),
|
||||
"tags": fields.List(fields.String, missing=[], example=["docs"]),
|
||||
"resize": fields.Dict(
|
||||
missing=None, example={"width": 640, "height": 480}
|
||||
), # TODO: Validate keys
|
||||
}
|
||||
args = FullCaptureArgs()
|
||||
schema = CaptureSchema()
|
||||
|
||||
def post(self, args):
|
||||
|
|
@ -38,13 +44,10 @@ class CaptureAPI(ActionView):
|
|||
|
||||
resize = args.get("resize", None)
|
||||
if resize:
|
||||
if ("width" in resize) and ("height" in resize):
|
||||
resize = (
|
||||
int(resize["width"]),
|
||||
int(resize["height"]),
|
||||
) # Convert dict to tuple
|
||||
else:
|
||||
abort(404)
|
||||
resize = (
|
||||
int(resize["width"]),
|
||||
int(resize["height"]),
|
||||
) # Convert dict to tuple
|
||||
|
||||
# Explicitally acquire lock (prevents empty files being created if lock is unavailable)
|
||||
with microscope.camera.lock:
|
||||
|
|
@ -64,16 +67,7 @@ class RAMCaptureAPI(ActionView):
|
|||
Take a non-persistant image capture.
|
||||
"""
|
||||
|
||||
args = {
|
||||
"use_video_port": fields.Boolean(missing=True),
|
||||
"bayer": fields.Boolean(
|
||||
missing=False, description="Return with raw bayer data"
|
||||
),
|
||||
"resize": fields.Dict(
|
||||
missing=None, example={"width": 640, "height": 480}
|
||||
), # TODO: Validate keys
|
||||
}
|
||||
|
||||
args = BasicCaptureArgs()
|
||||
responses = {200: {"content_type": "image/jpeg"}}
|
||||
|
||||
def post(self, args):
|
||||
|
|
@ -84,13 +78,10 @@ class RAMCaptureAPI(ActionView):
|
|||
|
||||
resize = args.get("resize", None)
|
||||
if resize:
|
||||
if ("width" in resize) and ("height" in resize):
|
||||
resize = (
|
||||
int(resize["width"]),
|
||||
int(resize["height"]),
|
||||
) # Convert dict to tuple
|
||||
else:
|
||||
abort(404)
|
||||
resize = (
|
||||
int(resize["width"]),
|
||||
int(resize["height"]),
|
||||
) # Convert dict to tuple
|
||||
|
||||
# Open a BytesIO stream to be destroyed once request has returned
|
||||
with microscope.camera.lock, io.BytesIO() as stream:
|
||||
|
|
@ -133,8 +124,6 @@ class GPUPreviewStartAPI(ActionView):
|
|||
window = [int(w) for w in window]
|
||||
|
||||
microscope.camera.start_preview(fullscreen=fullscreen, window=window)
|
||||
|
||||
# TODO: Make schema for microscope state
|
||||
return microscope.state
|
||||
|
||||
|
||||
|
|
@ -145,5 +134,4 @@ class GPUPreviewStopAPI(ActionView):
|
|||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
microscope.camera.stop_preview()
|
||||
# TODO: Make schema for microscope state
|
||||
return microscope.state
|
||||
|
|
|
|||
|
|
@ -25,11 +25,11 @@ class MoveStageAPI(ActionView):
|
|||
# Handle absolute positioning (calculate a relative move from current position and target)
|
||||
if (args.get("absolute")) and (microscope.stage): # Only if stage exists
|
||||
target_position = axes_to_array(args, ["x", "y", "z"])
|
||||
logging.debug("TARGET: {}".format(target_position))
|
||||
logging.debug("TARGET: %s", (target_position))
|
||||
position = [
|
||||
target_position[i] - microscope.stage.position[i] for i in range(3)
|
||||
]
|
||||
logging.debug("DELTA: {}".format(position))
|
||||
logging.debug("DELTA: %s", (position))
|
||||
|
||||
else:
|
||||
# Get coordinates from payload
|
||||
|
|
@ -45,7 +45,6 @@ class MoveStageAPI(ActionView):
|
|||
else:
|
||||
logging.warning("Unable to move. No stage found.")
|
||||
|
||||
# TODO: Make schema for microscope state
|
||||
return microscope.state["stage"]["position"]
|
||||
|
||||
|
||||
|
|
@ -60,5 +59,4 @@ class ZeroStageAPI(ActionView):
|
|||
with microscope.stage.lock(timeout=1):
|
||||
microscope.stage.zero_position()
|
||||
|
||||
# TODO: Make schema for microscope state
|
||||
return microscope.state["stage"]
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import logging
|
||||
|
||||
from flask import abort, redirect, request, send_file, url_for
|
||||
from labthings import Schema, fields, find_component
|
||||
from labthings.marshalling import marshal_with
|
||||
from labthings.marshalling import marshal_with, use_args
|
||||
from labthings.utilities import description_from_view
|
||||
from labthings.views import PropertyView, View
|
||||
from marshmallow import pre_dump
|
||||
|
||||
from openflexure_microscope.api.utilities import JsonResponse, get_bool
|
||||
from openflexure_microscope.api.utilities import get_bool
|
||||
|
||||
# SCHEMAS
|
||||
|
||||
|
||||
class InstrumentSchema(Schema):
|
||||
|
|
@ -23,17 +23,25 @@ class ImageSchema(Schema):
|
|||
format = fields.String()
|
||||
name = fields.String()
|
||||
tags = fields.List(fields.String())
|
||||
annotations = fields.Dict()
|
||||
annotations = fields.Dict(keys=fields.Str(), values=fields.Str())
|
||||
|
||||
|
||||
class CaptureMetadataSchema(Schema):
|
||||
experimenter = fields.Dict() # TODO: Make schema
|
||||
experimenterGroup = fields.Dict() # TODO: Make schema
|
||||
dataset = fields.Dict() # TODO: Make schema
|
||||
# Full dataset dictionary will change depending on the type of
|
||||
# dataset, so we can't make a specific schema in this case.
|
||||
dataset = fields.Dict()
|
||||
# Nested schema for Image data
|
||||
image = fields.Nested(ImageSchema())
|
||||
# Nested schema for instrument data
|
||||
instrument = fields.Nested(InstrumentSchema())
|
||||
|
||||
|
||||
class BasicDatasetSchema(Schema):
|
||||
id = fields.UUID()
|
||||
name = fields.String()
|
||||
type = fields.String()
|
||||
|
||||
|
||||
class CaptureSchema(ImageSchema):
|
||||
"""
|
||||
Schema containing only basic attributes required
|
||||
|
|
@ -41,10 +49,15 @@ class CaptureSchema(ImageSchema):
|
|||
are returned by using FullCaptureSchema
|
||||
"""
|
||||
|
||||
dataset = fields.Dict() # TODO: Make schema
|
||||
# We need dataset information in the capture array
|
||||
# so that client applications can sort data into folders
|
||||
# without the server having to do a tonne of file IO
|
||||
dataset = fields.Nested(BasicDatasetSchema())
|
||||
file = fields.String(
|
||||
data_key="path", description="Path of file on microscope device"
|
||||
)
|
||||
# No need to make a schema for links as we only ever
|
||||
# create the dictionary right here in `generate_links`
|
||||
links = fields.Dict()
|
||||
|
||||
@pre_dump
|
||||
|
|
@ -104,6 +117,9 @@ class FullCaptureSchema(CaptureSchema):
|
|||
metadata = fields.Nested(CaptureMetadataSchema())
|
||||
|
||||
|
||||
# VIEWS
|
||||
|
||||
|
||||
class CaptureList(PropertyView):
|
||||
tags = ["captures"]
|
||||
schema = CaptureSchema(many=True)
|
||||
|
|
@ -203,7 +219,8 @@ class CaptureTags(View):
|
|||
|
||||
return capture_obj.tags
|
||||
|
||||
def put(self, id_):
|
||||
@use_args(fields.List(fields.String(), required=True))
|
||||
def put(self, args, id_):
|
||||
"""
|
||||
Add tags to a single image capture
|
||||
"""
|
||||
|
|
@ -213,17 +230,12 @@ class CaptureTags(View):
|
|||
if not capture_obj:
|
||||
return abort(404) # 404 Not Found
|
||||
|
||||
# TODO: Replace with normal Flask request JSON thing
|
||||
data_dict = JsonResponse(request).json
|
||||
|
||||
if type(data_dict) != list:
|
||||
return abort(400)
|
||||
|
||||
capture_obj.put_tags(data_dict)
|
||||
capture_obj.put_tags(args)
|
||||
|
||||
return capture_obj.tags
|
||||
|
||||
def delete(self, id_):
|
||||
@use_args(fields.List(fields.String(), required=True))
|
||||
def delete(self, args, id_):
|
||||
"""
|
||||
Delete tags from a single image capture
|
||||
"""
|
||||
|
|
@ -233,12 +245,7 @@ class CaptureTags(View):
|
|||
if not capture_obj:
|
||||
return abort(404) # 404 Not Found
|
||||
|
||||
data_dict = JsonResponse(request).json
|
||||
|
||||
if type(data_dict) != list:
|
||||
return abort(400)
|
||||
|
||||
for tag in data_dict:
|
||||
for tag in args:
|
||||
capture_obj.delete_tag(str(tag))
|
||||
|
||||
return capture_obj.tags
|
||||
|
|
@ -259,7 +266,8 @@ class CaptureAnnotations(View):
|
|||
|
||||
return capture_obj.annotations
|
||||
|
||||
def put(self, id_):
|
||||
@use_args(fields.Dict())
|
||||
def put(self, args, id_):
|
||||
"""
|
||||
Update metadata for a single image capture
|
||||
"""
|
||||
|
|
@ -269,12 +277,6 @@ class CaptureAnnotations(View):
|
|||
if not capture_obj:
|
||||
return abort(404) # 404 Not Found
|
||||
|
||||
data_dict = JsonResponse(request).json
|
||||
logging.debug(data_dict)
|
||||
|
||||
if type(data_dict) != dict:
|
||||
return abort(400)
|
||||
|
||||
capture_obj.put_annotations(data_dict)
|
||||
capture_obj.put_annotations(args)
|
||||
|
||||
return capture_obj.annotations
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import logging
|
||||
|
||||
from flask import abort, request
|
||||
from labthings import find_component
|
||||
from flask import abort
|
||||
from labthings import fields, find_component
|
||||
from labthings.marshalling import use_args
|
||||
from labthings.utilities import create_from_path, get_by_path, set_by_path
|
||||
from labthings.views import PropertyView, View
|
||||
|
||||
from openflexure_microscope.api.utilities import JsonResponse
|
||||
|
||||
|
||||
class SettingsProperty(PropertyView):
|
||||
def get(self):
|
||||
|
|
@ -16,17 +15,17 @@ class SettingsProperty(PropertyView):
|
|||
microscope = find_component("org.openflexure.microscope")
|
||||
return microscope.read_settings()
|
||||
|
||||
def put(self):
|
||||
@use_args(fields.Dict())
|
||||
def put(self, args):
|
||||
"""
|
||||
Update current microscope settings, including camera and stage
|
||||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
payload = JsonResponse(request)
|
||||
|
||||
logging.debug("Updating settings from PUT request:")
|
||||
logging.debug(payload.json)
|
||||
logging.debug(args)
|
||||
|
||||
microscope.update_settings(payload.json)
|
||||
microscope.update_settings(args)
|
||||
microscope.save_settings()
|
||||
|
||||
return self.get()
|
||||
|
|
@ -50,16 +49,16 @@ class NestedSettingsProperty(View):
|
|||
|
||||
return value
|
||||
|
||||
def put(self, route):
|
||||
@use_args(fields.Dict())
|
||||
def put(self, args, route):
|
||||
"""
|
||||
Update a nested section of the current microscope settings
|
||||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
keys = route.split("/")
|
||||
payload = JsonResponse(request)
|
||||
|
||||
dictionary = create_from_path(keys)
|
||||
set_by_path(dictionary, keys, payload.json)
|
||||
set_by_path(dictionary, keys, args)
|
||||
|
||||
microscope.update_settings(dictionary)
|
||||
microscope.save_settings()
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import io
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
import io
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from collections import namedtuple
|
||||
|
||||
|
|
@ -113,10 +113,10 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
|
||||
def close(self):
|
||||
"""Close the BaseCamera and all attached StreamObjects."""
|
||||
logging.info("Closing {}".format(self))
|
||||
logging.info("Closing %s", (self))
|
||||
# Stop worker thread
|
||||
self.stop_worker()
|
||||
logging.info("Closed {}".format(self))
|
||||
logging.info("Closed %s", (self))
|
||||
|
||||
# START AND STOP WORKER THREAD
|
||||
|
||||
|
|
|
|||
|
|
@ -156,10 +156,10 @@ class PiCameraStreamer(BaseCamera):
|
|||
for key in PiCameraStreamer.picamera_settings_keys:
|
||||
try:
|
||||
value = getattr(self.camera, key)
|
||||
logging.debug("Reading PiCamera().{}: {}".format(key, value))
|
||||
logging.debug("Reading PiCamera().%s: %s", key, value)
|
||||
conf_dict["picamera"][key] = value
|
||||
except AttributeError:
|
||||
logging.debug("Unable to read PiCamera attribute {}".format(key))
|
||||
logging.debug("Unable to read PiCamera attribute %s", (key))
|
||||
|
||||
# Include a serialised lens shading table
|
||||
if (
|
||||
|
|
@ -243,26 +243,22 @@ class PiCameraStreamer(BaseCamera):
|
|||
# Set exposure mode
|
||||
if "exposure_mode" in settings_dict:
|
||||
logging.debug(
|
||||
"Applying exposure_mode: {}".format(settings_dict["exposure_mode"])
|
||||
"Applying exposure_mode: %s", (settings_dict["exposure_mode"])
|
||||
)
|
||||
self.camera.exposure_mode = settings_dict["exposure_mode"]
|
||||
|
||||
# Apply gains and let them settle
|
||||
if "analog_gain" in settings_dict:
|
||||
logging.debug(
|
||||
"Applying analog_gain: {}".format(settings_dict["analog_gain"])
|
||||
)
|
||||
logging.debug("Applying analog_gain: %s", (settings_dict["analog_gain"]))
|
||||
set_analog_gain(self.camera, float(settings_dict["analog_gain"]))
|
||||
if "digital_gain" in settings_dict:
|
||||
logging.debug(
|
||||
"Applying digital_gain: {}".format(settings_dict["digital_gain"])
|
||||
)
|
||||
logging.debug("Applying digital_gain: %s", (settings_dict["digital_gain"]))
|
||||
set_digital_gain(self.camera, float(settings_dict["digital_gain"]))
|
||||
|
||||
# Apply shutter speed
|
||||
if "shutter_speed" in settings_dict:
|
||||
logging.debug(
|
||||
"Applying shutter_speed: {}".format(settings_dict["shutter_speed"])
|
||||
"Applying shutter_speed: %s", (settings_dict["shutter_speed"])
|
||||
)
|
||||
self.camera.shutter_speed = int(settings_dict["shutter_speed"])
|
||||
|
||||
|
|
@ -272,17 +268,17 @@ class PiCameraStreamer(BaseCamera):
|
|||
if "awb_gains" in settings_dict:
|
||||
logging.debug("Applying awb_mode: off")
|
||||
self.camera.awb_mode = "off"
|
||||
logging.debug("Applying awb_gains: {}".format(settings_dict["awb_gains"]))
|
||||
logging.debug("Applying awb_gains: %s", (settings_dict["awb_gains"]))
|
||||
self.camera.awb_gains = settings_dict["awb_gains"]
|
||||
elif "awb_mode" in settings_dict:
|
||||
logging.debug("Applying awb_mode: {}".format(settings_dict["awb_mode"]))
|
||||
logging.debug("Applying awb_mode: %s", (settings_dict["awb_mode"]))
|
||||
self.camera.awb_mode = settings_dict["awb_mode"]
|
||||
|
||||
# Handle some properties that can be quickly applied
|
||||
batched_keys = ["framerate", "saturation"]
|
||||
for key in batched_keys:
|
||||
if (key in settings_dict) and hasattr(self.camera, key):
|
||||
logging.debug("Applying {}: {}".format(key, settings_dict[key]))
|
||||
logging.debug("Applying %s: %s", key, settings_dict[key])
|
||||
setattr(self.camera, key, settings_dict[key])
|
||||
|
||||
# Final optional pause to settle
|
||||
|
|
@ -307,7 +303,7 @@ class PiCameraStreamer(BaseCamera):
|
|||
for i in range(2):
|
||||
if np.abs(centre[i] - 0.5) + size / 2 > 0.5:
|
||||
centre[i] = 0.5 + (1.0 - size) / 2 * np.sign(centre[i] - 0.5)
|
||||
logging.info("setting zoom, centre {}, size {}".format(centre, size))
|
||||
logging.info("setting zoom, centre %s, size %s", centre, size)
|
||||
new_fov = (centre[0] - size / 2, centre[1] - size / 2, size, size)
|
||||
self.camera.zoom = new_fov
|
||||
|
||||
|
|
@ -329,13 +325,12 @@ class PiCameraStreamer(BaseCamera):
|
|||
self.preview_active = True
|
||||
except picamerax.exc.PiCameraMMALError as e:
|
||||
logging.error(
|
||||
"Suppressed a MMALError in start_preview. Exception: {}".format(e)
|
||||
"Suppressed a MMALError in start_preview. Exception: %s", (e)
|
||||
)
|
||||
except picamerax.exc.PiCameraValueError as e:
|
||||
logging.error(
|
||||
"Suppressed a ValueError exception in start_preview. Exception: {}".format(
|
||||
e
|
||||
)
|
||||
"Suppressed a ValueError exception in start_preview. Exception: %s",
|
||||
(e),
|
||||
)
|
||||
|
||||
def stop_preview(self):
|
||||
|
|
@ -364,7 +359,7 @@ class PiCameraStreamer(BaseCamera):
|
|||
if not self.record_active:
|
||||
|
||||
# Start the camera video recording on port 2
|
||||
logging.info("Recording to {}".format(output))
|
||||
logging.info("Recording to %s", (output))
|
||||
|
||||
self.camera.start_recording(
|
||||
output,
|
||||
|
|
@ -413,12 +408,12 @@ class PiCameraStreamer(BaseCamera):
|
|||
try:
|
||||
self.camera.stop_recording(splitter_port=splitter_port)
|
||||
except picamerax.exc.PiCameraNotRecording:
|
||||
logging.info("Not recording on splitter_port {}".format(splitter_port))
|
||||
logging.info("Not recording on splitter_port %s", (splitter_port))
|
||||
else:
|
||||
logging.info(
|
||||
"Stopped MJPEG stream on port {1}. Switching to {0}.".format(
|
||||
self.image_resolution, splitter_port
|
||||
)
|
||||
"Stopped MJPEG stream on port %s. Switching to %s.",
|
||||
splitter_port,
|
||||
self.image_resolution,
|
||||
)
|
||||
|
||||
# Increase the resolution for taking an image
|
||||
|
|
@ -470,9 +465,9 @@ class PiCameraStreamer(BaseCamera):
|
|||
)
|
||||
else:
|
||||
logging.debug(
|
||||
"Started MJPEG stream at {} on port {}".format(
|
||||
self.stream_resolution, splitter_port
|
||||
)
|
||||
"Started MJPEG stream at %s on port %s",
|
||||
self.stream_resolution,
|
||||
splitter_port,
|
||||
)
|
||||
|
||||
def capture(
|
||||
|
|
@ -501,7 +496,7 @@ class PiCameraStreamer(BaseCamera):
|
|||
output_object (str/BytesIO): Target object.
|
||||
"""
|
||||
with self.lock:
|
||||
logging.info("Capturing to {}".format(output))
|
||||
logging.info("Capturing to %s", (output))
|
||||
|
||||
# Set resolution and stop stream recording if necessary
|
||||
if not use_video_port:
|
||||
|
|
@ -537,7 +532,7 @@ class PiCameraStreamer(BaseCamera):
|
|||
logging.debug("Creating PiRGBArray")
|
||||
with picamerax.array.PiRGBArray(self.camera) as output:
|
||||
|
||||
logging.info("Capturing to {}".format(output))
|
||||
logging.info("Capturing to %s", (output))
|
||||
|
||||
self.camera.capture(output, format="rgb", use_video_port=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -55,26 +55,17 @@ if __name__ == "__main__":
|
|||
# fix the shutter speed
|
||||
cam.shutter_speed = cam.exposure_speed
|
||||
|
||||
logging.info(
|
||||
"Current a/d gains: {}, {}".format(cam.analog_gain, cam.digital_gain)
|
||||
)
|
||||
logging.info("Current a/d gains: %s, %s", cam.analog_gain, cam.digital_gain)
|
||||
|
||||
logging.info("Attempting to set analogue gain to 1")
|
||||
set_analog_gain(cam, 1)
|
||||
logging.info("Attempting to set digital gain to 1")
|
||||
set_digital_gain(cam, 1)
|
||||
# The old code is left in here in case it is a useful example...
|
||||
# ret = mmal.mmal_port_parameter_set_rational(cam._camera.control._port,
|
||||
# MMAL_PARAMETER_DIGITAL_GAIN,
|
||||
# to_rational(1))
|
||||
# print("Return code: {}".format(ret))
|
||||
|
||||
try:
|
||||
while True:
|
||||
logging.info(
|
||||
"Current a/d gains: {}, {}".format(
|
||||
cam.analog_gain, cam.digital_gain
|
||||
)
|
||||
"Current a/d gains: %s, %s", cam.analog_gain, cam.digital_gain
|
||||
)
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
from . import capture, capture_manager
|
||||
from .capture import CaptureObject, THUMBNAIL_SIZE
|
||||
from .capture import THUMBNAIL_SIZE, CaptureObject
|
||||
from .capture_manager import CaptureManager
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import logging
|
|||
import os
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from PIL import Image
|
||||
|
||||
import dateutil.parser
|
||||
from PIL import Image
|
||||
|
||||
from openflexure_microscope.camera import piexif
|
||||
from openflexure_microscope.camera.piexif._exceptions import InvalidImageDataError
|
||||
from openflexure_microscope.config import JSONEncoder
|
||||
from openflexure_microscope.captures import piexif
|
||||
from openflexure_microscope.captures.piexif import InvalidImageDataError
|
||||
from openflexure_microscope.json import JSONEncoder
|
||||
|
||||
EXIF_FORMATS = ["JPG", "JPEG", "TIF", "TIFF"]
|
||||
THUMBNAIL_SIZE = (200, 150)
|
||||
|
|
@ -25,23 +25,23 @@ def make_file_list(directory, formats):
|
|||
glob.glob("{}/**/*.{}".format(directory, fmt.lower()), recursive=True)
|
||||
)
|
||||
|
||||
logging.info("{} capture files found on disk".format(len(files)))
|
||||
logging.info("%s capture files found on disk", (len(files)))
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def build_captures_from_exif(capture_path):
|
||||
logging.debug("Reloading captures from {}...".format(capture_path))
|
||||
logging.debug("Reloading captures from %s...", (capture_path))
|
||||
files = make_file_list(capture_path, EXIF_FORMATS)
|
||||
captures = OrderedDict()
|
||||
|
||||
for f in files:
|
||||
logging.debug("Reloading capture {}...".format(f))
|
||||
logging.debug("Reloading capture %s...", (f))
|
||||
capture = capture_from_path(f)
|
||||
if capture:
|
||||
captures[capture.id] = capture
|
||||
|
||||
logging.info("{} capture files successfully reloaded".format(len(captures)))
|
||||
logging.info("%s capture files successfully reloaded", (len(captures)))
|
||||
|
||||
return captures
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ def capture_from_path(path):
|
|||
capture.sync_basic_metadata()
|
||||
return capture
|
||||
except (InvalidImageDataError, json.decoder.JSONDecodeError):
|
||||
logging.error("Invalid metadata at {}.".format(path))
|
||||
logging.error("Invalid metadata at %s.", (path))
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ class CaptureObject(object):
|
|||
|
||||
# Store a nice ID
|
||||
self.id = uuid.uuid4() #: str: Unique capture ID
|
||||
logging.debug("Created CaptureObject {}".format(self.id))
|
||||
logging.debug("Created CaptureObject %s", (self.id))
|
||||
|
||||
self.time = datetime.datetime.now()
|
||||
|
||||
|
|
@ -316,7 +316,7 @@ class CaptureObject(object):
|
|||
"""
|
||||
|
||||
if self.exists: # If data file exists
|
||||
logging.info("Opening from file {}".format(self.file))
|
||||
logging.info("Opening from file %s", (self.file))
|
||||
with open(self.file, "rb") as f:
|
||||
d = io.BytesIO(f.read()) # Load bytes from file
|
||||
d.seek(0) # Rewind loaded bytestream
|
||||
|
|
@ -364,7 +364,7 @@ class CaptureObject(object):
|
|||
"""If the StreamObject has been saved, delete the file."""
|
||||
|
||||
if os.path.isfile(self.file):
|
||||
logging.info("Deleting file {}".format(self.file))
|
||||
logging.info("Deleting file %s", (self.file))
|
||||
os.remove(self.file)
|
||||
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class CaptureManager:
|
|||
self.close()
|
||||
|
||||
def close(self):
|
||||
logging.info("Closing {}".format(self))
|
||||
logging.info("Closing %s", (self))
|
||||
# Close all StreamObjects
|
||||
for capture_list in [self.images.values(), self.videos.values()]:
|
||||
for stream_object in capture_list:
|
||||
|
|
@ -75,9 +75,9 @@ class CaptureManager:
|
|||
"""
|
||||
|
||||
if os.path.isdir(self.paths["temp"]):
|
||||
logging.info("Clearing {}...".format(self.paths["temp"]))
|
||||
logging.info("Clearing %s...", (self.paths["temp"]))
|
||||
shutil.rmtree(self.paths["temp"])
|
||||
logging.debug("Cleared {}.".format(self.paths["temp"]))
|
||||
logging.debug("Cleared %s.", (self.paths["temp"]))
|
||||
|
||||
def rebuild_captures(self):
|
||||
self.images = build_captures_from_exif(self.paths["default"])
|
||||
|
|
@ -118,25 +118,7 @@ class CaptureManager:
|
|||
|
||||
# CREATING NEW CAPTURES
|
||||
|
||||
def new_image(
|
||||
self,
|
||||
temporary: bool = True,
|
||||
filename: str = None,
|
||||
folder: str = "",
|
||||
fmt: str = "jpeg",
|
||||
):
|
||||
|
||||
"""
|
||||
Create a new image capture object.
|
||||
|
||||
Args:
|
||||
temporary (bool): Should the data be deleted after session ends.
|
||||
Creating the capture with a content manager sets this to true.
|
||||
filename (str): Name of the stored file. Defaults to timestamp.
|
||||
folder (str): Name of the folder in which to store the capture.
|
||||
fmt (str): Format of the capture.
|
||||
"""
|
||||
|
||||
def _new_output(self, temporary, filename, folder, fmt):
|
||||
# Generate file name
|
||||
if not filename:
|
||||
filename = generate_numbered_basename(self.images.values())
|
||||
|
|
@ -156,10 +138,32 @@ class CaptureManager:
|
|||
if temporary:
|
||||
output.put_tags(["temporary"])
|
||||
|
||||
return output
|
||||
|
||||
def new_image(
|
||||
self,
|
||||
temporary: bool = True,
|
||||
filename: str = None,
|
||||
folder: str = "",
|
||||
fmt: str = "jpeg",
|
||||
):
|
||||
|
||||
"""
|
||||
Create a new image capture object.
|
||||
|
||||
Args:
|
||||
temporary (bool): Should the data be deleted after session ends.
|
||||
Creating the capture with a content manager sets this to true.
|
||||
filename (str): Name of the stored file. Defaults to timestamp.
|
||||
folder (str): Name of the folder in which to store the capture.
|
||||
fmt (str): Format of the capture.
|
||||
"""
|
||||
# Create a new output object
|
||||
output = self._new_output(temporary, filename, folder, fmt)
|
||||
|
||||
# Update capture list
|
||||
capture_key = str(output.id)
|
||||
logging.debug("Adding image %s with key %s", output, capture_key)
|
||||
|
||||
self.images[capture_key] = output
|
||||
|
||||
return output
|
||||
|
|
@ -182,26 +186,8 @@ class CaptureManager:
|
|||
folder (str): Name of the folder in which to store the capture.
|
||||
fmt (str): Format of the capture.
|
||||
"""
|
||||
# TODO: Remove the redundancy here
|
||||
|
||||
# Generate file name
|
||||
if not filename:
|
||||
filename = generate_numbered_basename(self.videos.values())
|
||||
logging.debug(filename)
|
||||
filename = "{}.{}".format(filename, fmt)
|
||||
|
||||
# Generate folder
|
||||
base_folder = self.paths["temp"] if temporary else self.paths["default"]
|
||||
folder = os.path.join(base_folder, folder)
|
||||
|
||||
# Generate file path
|
||||
filepath = os.path.join(folder, filename)
|
||||
|
||||
# Create capture object
|
||||
output = CaptureObject(filepath=filepath)
|
||||
# Insert a temporary tag if temporary
|
||||
if temporary:
|
||||
output.put_tags(["temporary"])
|
||||
# Create a new output object
|
||||
output = self._new_output(temporary, filename, folder, fmt)
|
||||
|
||||
# Update capture list
|
||||
capture_key = str(output.id)
|
||||
|
|
|
|||
|
|
@ -3,18 +3,9 @@ import json
|
|||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from fractions import Fraction
|
||||
from uuid import UUID
|
||||
|
||||
import numpy as np
|
||||
from labthings.json import LabThingsJSONEncoder
|
||||
|
||||
from .paths import (
|
||||
CONFIGURATION_FILE_PATH,
|
||||
DEFAULT_CONFIGURATION_FILE_PATH,
|
||||
DEFAULT_SETTINGS_FILE_PATH,
|
||||
SETTINGS_FILE_PATH,
|
||||
)
|
||||
from .json import JSONEncoder
|
||||
from .paths import CONFIGURATION_FILE_PATH, SETTINGS_FILE_PATH
|
||||
|
||||
|
||||
class OpenflexureSettingsFile:
|
||||
|
|
@ -33,7 +24,12 @@ class OpenflexureSettingsFile:
|
|||
self.path = path
|
||||
|
||||
# Initialise basic config file with defaults if it doesn't exist
|
||||
initialise_file(self.path, populate=defaults)
|
||||
initialise_file(
|
||||
self.path,
|
||||
# Populate with default dictionary, or empty JSON if empty
|
||||
populate=json.dumps(defaults, cls=JSONEncoder, indent=2, sort_keys=True)
|
||||
or "{}\n",
|
||||
)
|
||||
|
||||
def load(self) -> dict:
|
||||
"""
|
||||
|
|
@ -78,32 +74,6 @@ class OpenflexureSettingsFile:
|
|||
return settings
|
||||
|
||||
|
||||
class JSONEncoder(LabThingsJSONEncoder):
|
||||
"""
|
||||
A custom JSON encoder, with type conversions for PiCamera fractions, Numpy integers, and Numpy arrays
|
||||
"""
|
||||
|
||||
def default(self, o):
|
||||
if isinstance(o, UUID):
|
||||
return str(o)
|
||||
# PiCamera fractions
|
||||
elif isinstance(o, Fraction):
|
||||
return float(o)
|
||||
# Numpy integers
|
||||
elif isinstance(o, np.integer):
|
||||
return int(o)
|
||||
# Numpy arrays
|
||||
elif isinstance(o, np.ndarray):
|
||||
return o.tolist()
|
||||
# UUIDs
|
||||
elif isinstance(o, UUID):
|
||||
return str(o)
|
||||
else:
|
||||
# call base class implementation which takes care of
|
||||
# raising exceptions for unsupported types
|
||||
return LabThingsJSONEncoder.default(self, o)
|
||||
|
||||
|
||||
# HANDLE BASIC LOADING AND SAVING OF SETTINGS FILES
|
||||
|
||||
|
||||
|
|
@ -116,7 +86,7 @@ def load_json_file(config_path) -> dict:
|
|||
"""
|
||||
config_path = os.path.expanduser(config_path)
|
||||
|
||||
logging.info("Loading {}...".format(config_path))
|
||||
logging.info("Loading %s...", config_path)
|
||||
|
||||
with open(config_path) as config_file:
|
||||
try:
|
||||
|
|
@ -139,7 +109,7 @@ def save_json_file(config_path: str, config_dict: dict):
|
|||
"""
|
||||
config_path = os.path.expanduser(config_path)
|
||||
|
||||
logging.info("Saving {}...".format(config_path))
|
||||
logging.info("Saving %s...", config_path)
|
||||
logging.debug(config_dict)
|
||||
|
||||
with open(config_path, "w") as outfile:
|
||||
|
|
@ -172,33 +142,26 @@ def initialise_file(config_path, populate: str = "{}\n"):
|
|||
"""
|
||||
config_path = os.path.expanduser(config_path)
|
||||
|
||||
logging.debug("Initialising {}".format(config_path))
|
||||
logging.debug("Exists: {}".format(os.path.exists(config_path)))
|
||||
logging.debug("Initialising %s", (config_path))
|
||||
logging.debug("Exists: %s", (os.path.exists(config_path)))
|
||||
|
||||
if not os.path.exists(config_path): # If user config file doesn't exist
|
||||
logging.warning("No config file found at {}. Creating...".format(config_path))
|
||||
logging.warning("No config file found at %s. Creating...", (config_path))
|
||||
create_file(config_path)
|
||||
|
||||
logging.info("Populating {}...".format(config_path))
|
||||
logging.info("Populating %s...", (config_path))
|
||||
with open(config_path, "w") as outfile:
|
||||
outfile.write(populate)
|
||||
|
||||
|
||||
# Load the default settings
|
||||
with open(DEFAULT_SETTINGS_FILE_PATH, "r") as default_settings:
|
||||
DEFAULT_SETTINGS = default_settings.read()
|
||||
|
||||
#: Default user settings object
|
||||
user_settings = OpenflexureSettingsFile(
|
||||
path=SETTINGS_FILE_PATH, defaults=DEFAULT_SETTINGS
|
||||
)
|
||||
|
||||
|
||||
# Load the default configuration
|
||||
with open(DEFAULT_CONFIGURATION_FILE_PATH, "r") as default_configuration:
|
||||
DEFAULT_CONFIGURATION = default_configuration.read()
|
||||
user_settings = OpenflexureSettingsFile(path=SETTINGS_FILE_PATH)
|
||||
|
||||
#: Default user settings object
|
||||
user_configuration = OpenflexureSettingsFile(
|
||||
path=CONFIGURATION_FILE_PATH, defaults=DEFAULT_CONFIGURATION
|
||||
path=CONFIGURATION_FILE_PATH,
|
||||
defaults={
|
||||
"camera": {"type": "PiCamera"},
|
||||
"stage": {"type": "SangaStage", "port": None},
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ from labthings import current_action as current_task
|
|||
from labthings import update_action_data as update_task_data
|
||||
from labthings import update_action_progress as update_task_progress
|
||||
|
||||
from openflexure_microscope.api.utilities import JsonResponse
|
||||
|
||||
__all__ = [
|
||||
"current_task",
|
||||
"update_task_progress",
|
||||
|
|
|
|||
31
openflexure_microscope/json.py
Normal file
31
openflexure_microscope/json.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
from fractions import Fraction
|
||||
from uuid import UUID
|
||||
|
||||
import numpy as np
|
||||
from labthings.json import LabThingsJSONEncoder
|
||||
|
||||
|
||||
class JSONEncoder(LabThingsJSONEncoder):
|
||||
"""
|
||||
A custom JSON encoder, with type conversions for PiCamera fractions, Numpy integers, and Numpy arrays
|
||||
"""
|
||||
|
||||
def default(self, o):
|
||||
if isinstance(o, UUID):
|
||||
return str(o)
|
||||
# PiCamera fractions
|
||||
elif isinstance(o, Fraction):
|
||||
return float(o)
|
||||
# Numpy integers
|
||||
elif isinstance(o, np.integer):
|
||||
return int(o)
|
||||
# Numpy arrays
|
||||
elif isinstance(o, np.ndarray):
|
||||
return o.tolist()
|
||||
# UUIDs
|
||||
elif isinstance(o, UUID):
|
||||
return str(o)
|
||||
else:
|
||||
# call base class implementation which takes care of
|
||||
# raising exceptions for unsupported types
|
||||
return LabThingsJSONEncoder.default(self, o)
|
||||
|
|
@ -10,7 +10,7 @@ import pkg_resources
|
|||
from expiringdict import ExpiringDict
|
||||
|
||||
from openflexure_microscope.camera.mock import MissingCamera
|
||||
from openflexure_microscope.captures import CaptureManager, THUMBNAIL_SIZE
|
||||
from openflexure_microscope.captures import THUMBNAIL_SIZE, CaptureManager
|
||||
from openflexure_microscope.stage.mock import MissingStage
|
||||
from openflexure_microscope.stage.sanga import SangaDeltaStage, SangaStage
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ class Microscope:
|
|||
|
||||
def close(self):
|
||||
"""Shut down the microscope hardware."""
|
||||
logging.info("Closing {}".format(self))
|
||||
logging.info("Closing %s", (self))
|
||||
if self.camera:
|
||||
try:
|
||||
self.camera.close()
|
||||
|
|
@ -83,7 +83,7 @@ class Microscope:
|
|||
except TimeoutError as e:
|
||||
logging.error(e)
|
||||
self.captures.close()
|
||||
logging.info("Closed {}".format(self))
|
||||
logging.info("Closed %s", (self))
|
||||
|
||||
def setup(self, configuration):
|
||||
"""
|
||||
|
|
@ -196,32 +196,34 @@ class Microscope:
|
|||
Applies a settings dictionary to the microscope. Missing parameters will be left untouched.
|
||||
"""
|
||||
with self.lock:
|
||||
logging.debug("Microscope: Applying settings: {}".format(settings))
|
||||
logging.debug("Microscope: Applying settings: %s", (settings))
|
||||
|
||||
# If attached to a camera
|
||||
if ("camera" in settings) and self.camera:
|
||||
self.camera.update_settings(settings.get("camera", {}))
|
||||
self.camera.update_settings(settings.pop("camera", {}))
|
||||
|
||||
# If attached to a stage
|
||||
if ("stage" in settings) and self.stage:
|
||||
self.stage.update_settings(settings.get("stage", {}))
|
||||
self.stage.update_settings(settings.pop("stage", {}))
|
||||
|
||||
# Capture manager
|
||||
self.captures.update_settings(settings.get("captures", {}))
|
||||
self.captures.update_settings(settings.pop("captures", {}))
|
||||
|
||||
# Microscope settings
|
||||
if "id" in settings:
|
||||
self.id = settings["id"]
|
||||
self.id = settings.pop("id")
|
||||
if "name" in settings:
|
||||
self.name = settings["name"]
|
||||
self.name = settings.pop("name")
|
||||
if "fov" in settings:
|
||||
self.fov = settings["fov"]
|
||||
self.fov = settings.pop("fov")
|
||||
|
||||
# Extension settings
|
||||
if "extensions" in settings:
|
||||
self.extension_settings.update(settings["extensions"])
|
||||
self.extension_settings.update(settings.pop("extensions"))
|
||||
|
||||
# TODO: warn if there are settings that we silently ignore
|
||||
# Warn about any superfluous keys
|
||||
for key in settings.keys():
|
||||
logging.warning("Key %s is unused and was ignored", key)
|
||||
|
||||
def read_settings(self, full: bool = True):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"camera": {
|
||||
"type": "PiCamera"
|
||||
},
|
||||
"stage": {
|
||||
"type": "SangaStage",
|
||||
"port": null
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"fov": [
|
||||
4100,
|
||||
3146
|
||||
],
|
||||
"jpeg_quality": 100
|
||||
}
|
||||
|
|
@ -39,17 +39,6 @@ def logs_file_path(filename: str):
|
|||
return os.path.join(logs_dir, filename)
|
||||
|
||||
|
||||
# HANDLE DEFAULTS FILES STORED IN THIS APPLICATION
|
||||
|
||||
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
#: Path of default (first-run) microscope settings
|
||||
DEFAULT_SETTINGS_FILE_PATH = os.path.join(HERE, "microscope_settings.default.json")
|
||||
#: Path of default (first-run) microscope configuration
|
||||
DEFAULT_CONFIGURATION_FILE_PATH = os.path.join(
|
||||
HERE, "microscope_configuration.default.json"
|
||||
)
|
||||
|
||||
# BASE PATHS
|
||||
|
||||
if os.name == "nt":
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import logging
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
import pkg_resources
|
||||
import sys
|
||||
|
||||
from .error_sources import bcolors
|
||||
import pkg_resources
|
||||
|
||||
from openflexure_microscope.paths import (
|
||||
FALLBACK_OPENFLEXURE_VAR_PATH,
|
||||
|
|
@ -13,11 +12,12 @@ from openflexure_microscope.paths import (
|
|||
|
||||
from . import (
|
||||
check_capture_reload,
|
||||
check_settings,
|
||||
check_picamera,
|
||||
check_sangaboard,
|
||||
check_settings,
|
||||
check_system,
|
||||
)
|
||||
from .error_sources import bcolors
|
||||
|
||||
# Paths for suggestions
|
||||
LOGS_PATHS = [
|
||||
|
|
@ -46,9 +46,7 @@ else:
|
|||
logger.setLevel(logging.WARNING)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
spoof = False
|
||||
|
||||
def main():
|
||||
print()
|
||||
print(bcolors.HEADER + "OpenFlexure Rescue" + bcolors.ENDC)
|
||||
print()
|
||||
|
|
@ -88,3 +86,7 @@ if __name__ == "__main__":
|
|||
print()
|
||||
for err in error_sources:
|
||||
print(err.message)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import logging
|
||||
|
||||
from openflexure_microscope.captures.capture import (
|
||||
EXIF_FORMATS,
|
||||
build_captures_from_exif,
|
||||
make_file_list,
|
||||
EXIF_FORMATS,
|
||||
)
|
||||
from openflexure_microscope.captures.capture_manager import BASE_CAPTURE_PATH
|
||||
from openflexure_microscope.config import user_settings
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from .error_sources import ErrorSource
|
||||
|
||||
from openflexure_microscope.config import user_configuration
|
||||
|
||||
from .error_sources import ErrorSource
|
||||
|
||||
|
||||
def main():
|
||||
error_sources = []
|
||||
|
|
|
|||
|
|
@ -1,34 +1,49 @@
|
|||
import json
|
||||
import logging
|
||||
|
||||
from .error_sources import ErrorSource
|
||||
|
||||
ERROR_SOURCES = []
|
||||
|
||||
|
||||
def trace_config_exceptions():
|
||||
error_sources = []
|
||||
|
||||
from openflexure_microscope.paths import (
|
||||
DEFAULT_CONFIGURATION_FILE_PATH,
|
||||
SETTINGS_FILE_PATH,
|
||||
)
|
||||
from openflexure_microscope.paths import CONFIGURATION_FILE_PATH, SETTINGS_FILE_PATH
|
||||
|
||||
try:
|
||||
default_config = json.load(DEFAULT_CONFIGURATION_FILE_PATH)
|
||||
default_config = json.load(CONFIGURATION_FILE_PATH)
|
||||
if not default_config:
|
||||
error_sources.append("default_config_empty")
|
||||
error_sources.append(
|
||||
ErrorSource(
|
||||
"Configuration file is missing or empty. This may occur if the server has never been started."
|
||||
)
|
||||
)
|
||||
except Exception as e: # pylint: disable=W0703
|
||||
logging.error("Error parsing config:")
|
||||
logging.error(e)
|
||||
error_sources.append("default_config_error")
|
||||
error_sources.append(
|
||||
ErrorSource(
|
||||
f"Configuration file is malformed. You can reset to the default configuration by deleting {CONFIGURATION_FILE_PATH}."
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
default_settings = json.load(SETTINGS_FILE_PATH)
|
||||
if not default_settings:
|
||||
error_sources.append("default_settings_empty")
|
||||
error_sources.append(
|
||||
ErrorSource(
|
||||
"Settings file is missing or empty. This may occur if the server has never been started."
|
||||
)
|
||||
)
|
||||
except Exception as e: # pylint: disable=W0703
|
||||
logging.error("Error parsing settings:")
|
||||
logging.error(e)
|
||||
error_sources.append("default_settings_error")
|
||||
error_sources.append(
|
||||
ErrorSource(
|
||||
f"Settings file is malformed. You can reset to the default settings by deleting {SETTINGS_FILE_PATH}."
|
||||
)
|
||||
)
|
||||
|
||||
return error_sources
|
||||
|
||||
|
|
@ -39,7 +54,11 @@ def main():
|
|||
try:
|
||||
from openflexure_microscope import config as _
|
||||
except Exception as e: # pylint: disable=W0703
|
||||
error_sources.append("config_settings_import_error")
|
||||
error_sources.append(
|
||||
ErrorSource(
|
||||
"Error importing configuration submodule. This could be an error in our code."
|
||||
)
|
||||
)
|
||||
logging.error("Error importing config:")
|
||||
logging.error(e)
|
||||
error_sources.extend(trace_config_exceptions())
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from urllib.request import urlopen
|
||||
from urllib.error import URLError
|
||||
from urllib.request import urlopen
|
||||
|
||||
import psutil
|
||||
|
||||
from .error_sources import WarningSource
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ class SangaStage(BaseStage):
|
|||
|
||||
@backlash.setter
|
||||
def backlash(self, blsh):
|
||||
logging.debug("Setting backlash to {}".format(blsh))
|
||||
logging.debug("Setting backlash to %s", (blsh))
|
||||
if blsh is None:
|
||||
self._backlash = None
|
||||
elif isinstance(blsh, Iterable):
|
||||
|
|
@ -262,7 +262,7 @@ class SangaDeltaStage(SangaStage):
|
|||
# Transform into delta coordinates
|
||||
displacement = np.dot(self.Tdv, displacement)
|
||||
|
||||
logging.debug("Delta displacement: {}".format(displacement))
|
||||
logging.debug("Delta displacement: %s", (displacement))
|
||||
|
||||
# Do the move
|
||||
SangaStage.move_rel(self, displacement, axis=None, backlash=backlash)
|
||||
|
|
@ -274,7 +274,7 @@ class SangaDeltaStage(SangaStage):
|
|||
# Transform into delta coordinates
|
||||
final = np.dot(self.Tdv, final)
|
||||
|
||||
logging.debug("Delta final: {}".format(final))
|
||||
logging.debug("Delta final: %s", (final))
|
||||
|
||||
# Do the move
|
||||
SangaStage.move_abs(self, final, **kwargs)
|
||||
|
|
|
|||
36
poetry.lock
generated
36
poetry.lock
generated
|
|
@ -79,7 +79,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>
|
|||
|
||||
[[package]]
|
||||
name = "babel"
|
||||
version = "2.8.1"
|
||||
version = "2.9.0"
|
||||
description = "Internationalization utilities"
|
||||
category = "dev"
|
||||
optional = false
|
||||
|
|
@ -283,7 +283,7 @@ i18n = ["Babel (>=0.8)"]
|
|||
|
||||
[[package]]
|
||||
name = "labthings"
|
||||
version = "1.1.2"
|
||||
version = "1.1.3"
|
||||
description = "Python implementation of LabThings, based on the Flask microframework"
|
||||
category = "main"
|
||||
optional = false
|
||||
|
|
@ -483,7 +483,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
|||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.24.0"
|
||||
version = "2.25.0"
|
||||
description = "Python HTTP for Humans."
|
||||
category = "dev"
|
||||
optional = false
|
||||
|
|
@ -493,7 +493,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
|||
certifi = ">=2017.4.17"
|
||||
chardet = ">=3.0.2,<4"
|
||||
idna = ">=2.5,<3"
|
||||
urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
|
||||
urllib3 = ">=1.21.1,<1.27"
|
||||
|
||||
[package.extras]
|
||||
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
|
||||
|
|
@ -557,7 +557,7 @@ python-versions = "*"
|
|||
|
||||
[[package]]
|
||||
name = "sphinx"
|
||||
version = "3.3.0"
|
||||
version = "3.3.1"
|
||||
description = "Python documentation generator"
|
||||
category = "dev"
|
||||
optional = false
|
||||
|
|
@ -687,7 +687,7 @@ python-versions = "*"
|
|||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.25.11"
|
||||
version = "1.26.1"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
category = "dev"
|
||||
optional = false
|
||||
|
|
@ -752,8 +752,8 @@ rpi = ["RPi.GPIO"]
|
|||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.6"
|
||||
content-hash = "fc03c76cedfd54d4d6be2ced9eb236dfdd1ffb986fdc731f96ee560053f7aa8a"
|
||||
python-versions = "^3.6.1"
|
||||
content-hash = "e0aad912d5cb388d3ff395dc8a517f701ba2cbe4c2169771080a889906e91b41"
|
||||
|
||||
[metadata.files]
|
||||
alabaster = [
|
||||
|
|
@ -781,8 +781,8 @@ attrs = [
|
|||
{file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
|
||||
]
|
||||
babel = [
|
||||
{file = "Babel-2.8.1-py2.py3-none-any.whl", hash = "sha256:be432f50d6c38c705ea45a0c05a4bbb22a70742a93888fbae5e7948da1635d23"},
|
||||
{file = "Babel-2.8.1.tar.gz", hash = "sha256:820c195271534e8a86f564ba9ef2c207f356cfeb7e94d2bdc6b57897c4233837"},
|
||||
{file = "Babel-2.9.0-py2.py3-none-any.whl", hash = "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5"},
|
||||
{file = "Babel-2.9.0.tar.gz", hash = "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05"},
|
||||
]
|
||||
black = [
|
||||
{file = "black-18.9b0-py36-none-any.whl", hash = "sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739"},
|
||||
|
|
@ -855,8 +855,8 @@ jinja2 = [
|
|||
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
|
||||
]
|
||||
labthings = [
|
||||
{file = "labthings-1.1.2-py3-none-any.whl", hash = "sha256:1e17e20c3585dbb2293ca1e75c46c4b9a21ee3aa30b71a2d839e16f40c21f600"},
|
||||
{file = "labthings-1.1.2.tar.gz", hash = "sha256:0d456f11920c4cd84e94afae2e7f005c3ea65346da5523d51aeec17698808156"},
|
||||
{file = "labthings-1.1.3-py3-none-any.whl", hash = "sha256:51f0bc897fcbacbcbb911658d3daafd41a35373bca4ef2156423cdb728fa3d78"},
|
||||
{file = "labthings-1.1.3.tar.gz", hash = "sha256:3d68c638866f1c525a8b5285570c2d4599005d227b928e78088c0a29e6544007"},
|
||||
]
|
||||
lazy-object-proxy = [
|
||||
{file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"},
|
||||
|
|
@ -1058,8 +1058,8 @@ pyyaml = [
|
|||
{file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"},
|
||||
]
|
||||
requests = [
|
||||
{file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"},
|
||||
{file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"},
|
||||
{file = "requests-2.25.0-py2.py3-none-any.whl", hash = "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"},
|
||||
{file = "requests-2.25.0.tar.gz", hash = "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8"},
|
||||
]
|
||||
rope = [
|
||||
{file = "rope-0.14.0-py2-none-any.whl", hash = "sha256:6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969"},
|
||||
|
|
@ -1103,8 +1103,8 @@ snowballstemmer = [
|
|||
{file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"},
|
||||
]
|
||||
sphinx = [
|
||||
{file = "Sphinx-3.3.0-py3-none-any.whl", hash = "sha256:3abdb2c57a65afaaa4f8573cbabd5465078eb6fd282c1e4f87f006875a7ec0c7"},
|
||||
{file = "Sphinx-3.3.0.tar.gz", hash = "sha256:1c21e7c5481a31b531e6cbf59c3292852ccde175b504b00ce2ff0b8f4adc3649"},
|
||||
{file = "Sphinx-3.3.1-py3-none-any.whl", hash = "sha256:d4e59ad4ea55efbb3c05cde3bfc83bfc14f0c95aa95c3d75346fcce186a47960"},
|
||||
{file = "Sphinx-3.3.1.tar.gz", hash = "sha256:1e8d592225447104d1172be415bc2972bd1357e3e12fdc76edf2261105db4300"},
|
||||
]
|
||||
sphinxcontrib-applehelp = [
|
||||
{file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
|
||||
|
|
@ -1162,8 +1162,8 @@ typed-ast = [
|
|||
{file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.25.11-py2.py3-none-any.whl", hash = "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"},
|
||||
{file = "urllib3-1.25.11.tar.gz", hash = "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2"},
|
||||
{file = "urllib3-1.26.1-py2.py3-none-any.whl", hash = "sha256:61ad24434555a42c0439770462df38b47d05d9e8e353d93ec3742900975e3e65"},
|
||||
{file = "urllib3-1.26.1.tar.gz", hash = "sha256:097116a6f16f13482d2a2e56792088b9b2920f4eb6b4f84a2c90555fb673db74"},
|
||||
]
|
||||
webargs = [
|
||||
{file = "webargs-6.1.1-py2.py3-none-any.whl", hash = "sha256:2ead6ce38559152043ab4ada4d389aef6c804b0c169482e7c92b3f498f690b2c"},
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ packages = [
|
|||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.6"
|
||||
python = "^3.6.1"
|
||||
Flask = "^1.0"
|
||||
numpy = "1.19.2" # Exact version so we can guarantee a wheel
|
||||
Pillow = "^7.2.0"
|
||||
|
|
@ -44,13 +44,14 @@ expiringdict = "^1.2.1"
|
|||
camera-stage-mapping = "0.1.4"
|
||||
picamerax = ">=20.9.1"
|
||||
pyyaml = "^5.3.1"
|
||||
labthings = "^1.1.2"
|
||||
labthings = "^1.1.3"
|
||||
|
||||
[tool.poetry.extras]
|
||||
rpi = ["RPi.GPIO"]
|
||||
|
||||
[tool.poetry.scripts]
|
||||
build_static = 'openflexure_microscope.install:build'
|
||||
rescue = 'openflexure_microscope.rescue.auto:main'
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pynpm = "^0.1.2"
|
||||
|
|
@ -71,5 +72,5 @@ ensure_newline_before_comments = true
|
|||
line_length = 88
|
||||
|
||||
[tool.pylint.'MESSAGES CONTROL']
|
||||
disable = "C0330, C0326, W1202, W0511, C, R"
|
||||
max-line-length = 88
|
||||
disable = "fixme,C,R"
|
||||
max-line-length = 88
|
||||
|
|
|
|||
|
|
@ -1,101 +0,0 @@
|
|||
from io import BytesIO
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
from PIL import Image
|
||||
|
||||
|
||||
class APIconnection:
|
||||
def __init__(self, host="localhost", port=5000, api_ver="v1"):
|
||||
self.base = self.build_base(host=host, port=port, api_ver=api_ver)
|
||||
|
||||
def build_base(self, host, port, api_ver):
|
||||
return "http://{}:{}/api/{}".format(host, port, api_ver)
|
||||
|
||||
def uri(self, suffix):
|
||||
return self.base + suffix
|
||||
|
||||
def get(self, route, json=True, timeout=5):
|
||||
r = requests.get(self.uri(route), timeout=timeout)
|
||||
if json:
|
||||
return r.json()
|
||||
else:
|
||||
return r
|
||||
|
||||
def post(self, route, json=None, timeout=5):
|
||||
r = requests.post(self.uri(route), json=json, timeout=timeout)
|
||||
return r.json()
|
||||
|
||||
def delete(self, route, timeout=5):
|
||||
r = requests.delete(self.uri(route), timeout=timeout)
|
||||
return r.json()
|
||||
|
||||
def set_overlay(self, message="", size=50):
|
||||
json = {"text": message, "size": size}
|
||||
return self.post("/camera/overlay", json=json)
|
||||
|
||||
def get_overlay(self):
|
||||
return self.get("/camera/overlay")
|
||||
|
||||
def get_config(self):
|
||||
return self.get("/config")
|
||||
|
||||
def set_config(self, config_dict):
|
||||
return self.post("/config", json=config_dict)
|
||||
|
||||
def get_state(self):
|
||||
return self.get("/state")
|
||||
|
||||
def start_preview(self):
|
||||
return self.post("/camera/preview/start")
|
||||
|
||||
def stop_preview(self):
|
||||
return self.post("/camera/preview/stop")
|
||||
|
||||
def move_by(self, x=0, y=0, z=0):
|
||||
json = {"x": x, "y": y, "z": z}
|
||||
return self.post("/stage/position", json=json)
|
||||
|
||||
def new_capture(self, use_video_port=True, keep_on_disk=False, resize=None):
|
||||
json = {"keep_on_disk": keep_on_disk, "use_video_port": use_video_port}
|
||||
|
||||
if resize:
|
||||
json["size"] = {"width": resize[0], "height": resize[1]}
|
||||
|
||||
return self.post("/camera/capture", json=json)
|
||||
|
||||
def get_capture(self, capture_id):
|
||||
uri_route = "/camera/capture/{}/download".format(capture_id)
|
||||
r = self.get(uri_route, json=False)
|
||||
img = Image.open(BytesIO(r.content))
|
||||
array = np.asarray(img, dtype=np.int32)
|
||||
return array
|
||||
|
||||
def del_capture(self, capture_id):
|
||||
uri_route = "/camera/capture/{}".format(capture_id)
|
||||
return self.delete(uri_route)
|
||||
|
||||
def capture(
|
||||
self,
|
||||
use_video_port=True,
|
||||
keep_on_disk=False,
|
||||
delete_after_use=True,
|
||||
resize=None,
|
||||
):
|
||||
p = self.new_capture(
|
||||
use_video_port=use_video_port, keep_on_disk=keep_on_disk, resize=resize
|
||||
)
|
||||
capture_id = p["metadata"]["id"]
|
||||
img_array = self.get_capture(capture_id)
|
||||
|
||||
if delete_after_use:
|
||||
self.del_capture(capture_id)
|
||||
|
||||
return img_array
|
||||
|
||||
def set_zoom(self, zoom_value=1.0):
|
||||
json = {"zoom_value": zoom_value}
|
||||
return self.post("/camera/zoom", json=json)
|
||||
|
||||
def get_zoom(self):
|
||||
return self.get("/camera/zoom")
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
import logging
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
from api_client import APIconnection
|
||||
|
||||
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
|
||||
|
||||
|
||||
class TestCapture(unittest.TestCase):
|
||||
def test_capture_config(self):
|
||||
connection = APIconnection(host="localhost", port=5000, api_ver="v1")
|
||||
config = connection.get_config()
|
||||
|
||||
expected_keys = ["image_resolution", "stream_resolution", "numpy_resolution"]
|
||||
|
||||
for key in expected_keys:
|
||||
self.assertTrue(key in config)
|
||||
|
||||
def test_capture_videoport(self):
|
||||
connection = APIconnection(host="localhost", port=5000, api_ver="v1")
|
||||
resolution = connection.get_config()["stream_resolution"]
|
||||
|
||||
for resize in [None, (640, 480)]:
|
||||
|
||||
if resize:
|
||||
resolution = resize
|
||||
|
||||
capture_array = connection.capture(
|
||||
use_video_port=True,
|
||||
keep_on_disk=False,
|
||||
delete_after_use=True,
|
||||
resize=resize,
|
||||
)
|
||||
|
||||
self.assertTrue(capture_array.shape == (resolution[1], resolution[0], 3))
|
||||
|
||||
def test_capture_full(self):
|
||||
connection = APIconnection(host="localhost", port=5000, api_ver="v1")
|
||||
resolution = connection.get_config()["image_resolution"]
|
||||
|
||||
for resize in [None, (640, 480)]:
|
||||
|
||||
if resize:
|
||||
resolution = resize
|
||||
|
||||
capture_array = connection.capture(
|
||||
use_video_port=False,
|
||||
keep_on_disk=False,
|
||||
delete_after_use=True,
|
||||
resize=resize,
|
||||
)
|
||||
|
||||
self.assertTrue(capture_array.shape == (resolution[1], resolution[0], 3))
|
||||
|
||||
|
||||
class TestStage(unittest.TestCase):
|
||||
def test_stage_config(self):
|
||||
connection = APIconnection(host="localhost", port=5000, api_ver="v1")
|
||||
config = connection.get_config()
|
||||
|
||||
expected_keys = ["backlash"]
|
||||
|
||||
for key in expected_keys:
|
||||
self.assertTrue(key in config)
|
||||
|
||||
def test_stage_state(self):
|
||||
connection = APIconnection(host="localhost", port=5000, api_ver="v1")
|
||||
state = connection.get_state()
|
||||
|
||||
self.assertTrue("stage" in state)
|
||||
|
||||
expected_keys = ["position"]
|
||||
|
||||
for key in expected_keys:
|
||||
self.assertTrue(key in state["stage"])
|
||||
|
||||
def test_stage_movement(self):
|
||||
connection = APIconnection(host="localhost", port=5000, api_ver="v1")
|
||||
|
||||
move_distance = 500
|
||||
for axis in range(3):
|
||||
for direction in [1, -1]:
|
||||
pos_i_dict = connection.get_state()["stage"]["position"]
|
||||
pos_i = [pos_i_dict["x"], pos_i_dict["y"], pos_i_dict["z"]]
|
||||
|
||||
move = [0, 0, 0]
|
||||
move[axis] = move_distance * direction
|
||||
|
||||
connection.move_by(*move)
|
||||
|
||||
pos_f_dict = connection.get_state()["stage"]["position"]
|
||||
pos_f = [pos_f_dict["x"], pos_f_dict["y"], pos_f_dict["z"]]
|
||||
|
||||
diff = np.subtract(pos_f, pos_i)
|
||||
logging.debug("{} > {}".format(pos_i, pos_f))
|
||||
|
||||
self.assertTrue(np.array_equal(diff, move))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
suites = [
|
||||
unittest.TestLoader().loadTestsFromTestCase(TestCapture),
|
||||
unittest.TestLoader().loadTestsFromTestCase(TestStage),
|
||||
]
|
||||
|
||||
alltests = unittest.TestSuite(suites)
|
||||
|
||||
result = unittest.TextTestRunner(verbosity=2).run(alltests)
|
||||
|
|
@ -1,266 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from openflexure_microscope.camera.pi import CaptureObject, PiCameraStreamer
|
||||
|
||||
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
|
||||
|
||||
|
||||
class TestCaptureMethods(unittest.TestCase):
|
||||
def test_still_capture(self):
|
||||
"""Tests capturing still images to a BytesIO stream."""
|
||||
global camera
|
||||
|
||||
for use_video_port in [True, False]:
|
||||
for resize in [None, (640, 480)]:
|
||||
|
||||
# Wait for camera
|
||||
camera.wait_for_camera()
|
||||
|
||||
# Capture to a context (auto-deletes files when done)
|
||||
with camera.new_image() as output:
|
||||
|
||||
camera.capture(output, use_video_port=use_video_port, resize=resize)
|
||||
|
||||
# Ensure file deletion fails and returns False
|
||||
self.assertFalse(output.delete_file())
|
||||
# Ensure capture not stored to file
|
||||
self.assertFalse(os.path.isfile(output.file))
|
||||
|
||||
# BEFORE DELETE: Ensure StreamObject 'stream' has
|
||||
# a valid BytesIO object and byte string
|
||||
self.assertTrue(isinstance(output.data, io.IOBase))
|
||||
self.assertTrue(isinstance(output.binary, (bytes, bytearray)))
|
||||
|
||||
# Save capture to file
|
||||
output.save_file()
|
||||
# Check file got saved
|
||||
self.assertTrue(os.path.isfile(output.file))
|
||||
|
||||
# Delete file
|
||||
output.delete_file()
|
||||
# Check file got deleted
|
||||
self.assertFalse(os.path.isfile(output.file))
|
||||
|
||||
# AFTER DELETE: Ensure StreamObject 'stream' has
|
||||
# a valid BytesIO object and byte string
|
||||
self.assertTrue(isinstance(output.data, io.IOBase))
|
||||
self.assertTrue(isinstance(output.binary, (bytes, bytearray)))
|
||||
|
||||
# Create a PIL image from stream
|
||||
image = Image.open(output.data)
|
||||
|
||||
# Ensure a valid PIL image was created
|
||||
self.assertTrue(isinstance(image, Image.Image))
|
||||
|
||||
# Calculate expected dimensions
|
||||
if resize:
|
||||
dims = resize
|
||||
else:
|
||||
if use_video_port:
|
||||
dims = camera.stream_resolution
|
||||
else:
|
||||
dims = camera.image_resolution
|
||||
|
||||
# Ensure PIL image size matches expected size
|
||||
print(image.size, dims)
|
||||
self.assertTrue(image.size == dims)
|
||||
|
||||
def test_still_store(self):
|
||||
"""Tests capturing still images to a file on disk."""
|
||||
global camera
|
||||
|
||||
for use_video_port in [True, False]:
|
||||
for resize in [None, (640, 480)]:
|
||||
# Wait for camera
|
||||
camera.wait_for_camera()
|
||||
|
||||
# Capture
|
||||
output = camera.capture(
|
||||
camera.new_image().file,
|
||||
use_video_port=use_video_port,
|
||||
resize=resize,
|
||||
)
|
||||
|
||||
# Check file got saved
|
||||
self.assertTrue(os.path.isfile(output.file))
|
||||
statinfo = os.stat(output.file)
|
||||
self.assertTrue(statinfo.st_size > 0)
|
||||
|
||||
# Ensure StreamObject 'stream' has
|
||||
# a valid BytesIO object and byte string
|
||||
self.assertTrue(isinstance(output.data, io.IOBase))
|
||||
self.assertTrue(isinstance(output.binary, (bytes, bytearray)))
|
||||
|
||||
# Ensure file deletion completes and returns True
|
||||
self.assertTrue(output.delete_file())
|
||||
|
||||
# Check file got deleted
|
||||
self.assertFalse(os.path.isfile(output.file))
|
||||
|
||||
|
||||
class TestUnencodedMethods(unittest.TestCase):
|
||||
def test_yuv_array(self):
|
||||
"""Tests capturing unencoded YUV data to a Numpy array."""
|
||||
global camera
|
||||
|
||||
for use_video_port in [True, False]:
|
||||
for resize in [None, (640, 480)]:
|
||||
|
||||
print("{}, {}, {}".format(id, resize, use_video_port))
|
||||
|
||||
# Wait for camera
|
||||
camera.wait_for_camera()
|
||||
|
||||
# Capture RGB array
|
||||
yuv = camera.yuv(use_video_port=use_video_port, resize=resize)
|
||||
|
||||
# Ensure capture output is a valid numpy array
|
||||
self.assertTrue(isinstance(yuv, np.ndarray))
|
||||
|
||||
# Calculate expected dimensions
|
||||
if resize:
|
||||
dims = resize
|
||||
else:
|
||||
if use_video_port:
|
||||
dims = camera.stream_resolution
|
||||
else:
|
||||
dims = camera.numpy_resolution
|
||||
|
||||
# Ensure array shape matches expected dimensions
|
||||
self.assertTrue(yuv.shape == (dims[1], dims[0], 3))
|
||||
|
||||
def test_rgb_array(self):
|
||||
"""Tests capturing unencoded YUV/RGB data to a Numpy array."""
|
||||
global camera
|
||||
|
||||
for use_video_port in [True, False]:
|
||||
for resize in [None, (640, 480)]:
|
||||
|
||||
print("{}, {}, {}".format(id, resize, use_video_port))
|
||||
|
||||
# Wait for camera
|
||||
camera.wait_for_camera()
|
||||
|
||||
# Capture RGB array
|
||||
rgb = camera.array(use_video_port=use_video_port, resize=resize)
|
||||
|
||||
# Ensure capture output is a valid numpy array
|
||||
self.assertTrue(isinstance(rgb, np.ndarray))
|
||||
|
||||
# Calculate expected dimensions
|
||||
if resize:
|
||||
dims = resize
|
||||
else:
|
||||
if use_video_port:
|
||||
dims = camera.stream_resolution
|
||||
else:
|
||||
dims = camera.numpy_resolution
|
||||
|
||||
# Ensure array shape matches expected dimensions
|
||||
self.assertTrue(rgb.shape == (dims[1], dims[0], 3))
|
||||
|
||||
|
||||
class TestRecordMethods(unittest.TestCase):
|
||||
def test_video_record(self):
|
||||
"""Tests recording videos to BytesIO stream, and to file on disk."""
|
||||
global camera
|
||||
|
||||
# Wait for camera
|
||||
camera.wait_for_camera()
|
||||
|
||||
with camera.new_video() as output:
|
||||
|
||||
# Start recording
|
||||
camera.start_recording(output)
|
||||
|
||||
# Record for 2 seconds
|
||||
time.sleep(2)
|
||||
# Stop recording
|
||||
camera.stop_recording()
|
||||
|
||||
# Check stream
|
||||
self.assertTrue(isinstance(output.data, io.IOBase))
|
||||
self.assertTrue(isinstance(output.binary, (bytes, bytearray)))
|
||||
|
||||
# Check file
|
||||
statinfo = os.stat(output.file)
|
||||
self.assertTrue(statinfo.st_size > 0)
|
||||
|
||||
# Log path
|
||||
temp_path = output.file
|
||||
|
||||
# Check file got deleted on __exit__
|
||||
self.assertFalse(os.path.isfile(temp_path))
|
||||
|
||||
time.sleep(0.25)
|
||||
|
||||
def test_video_store(self):
|
||||
"""Tests recording videos to file on disk, without context manager."""
|
||||
global camera
|
||||
|
||||
# Wait for camera
|
||||
camera.wait_for_camera()
|
||||
|
||||
# Start recording
|
||||
output = camera.start_recording(camera.new_video())
|
||||
|
||||
# Record for 2 seconds
|
||||
time.sleep(2)
|
||||
# Stop recording
|
||||
camera.stop_recording()
|
||||
|
||||
# Check file
|
||||
statinfo = os.stat(output.file)
|
||||
self.assertTrue(statinfo.st_size > 0)
|
||||
|
||||
# Ensure file deletion completes and returns True
|
||||
self.assertTrue(output.delete_file())
|
||||
|
||||
# Check file got deleted
|
||||
self.assertFalse(os.path.isfile(output.file))
|
||||
|
||||
|
||||
class TestThreadStarting(unittest.TestCase):
|
||||
def test_restarting_stream(self):
|
||||
"""Tests that a capture call restarts the camera worker thread."""
|
||||
global camera
|
||||
|
||||
# Wait for camera
|
||||
camera.wait_for_camera()
|
||||
|
||||
# Force-stop the camera worker thread (should return True)
|
||||
self.assertTrue(camera.stop_worker())
|
||||
|
||||
# Check Pi camera has been disconnected
|
||||
self.assertTrue(camera.camera)
|
||||
|
||||
# Restart worker thread
|
||||
self.assertTrue(camera.start_worker())
|
||||
|
||||
self.assertTrue(camera.camera)
|
||||
self.assertTrue(camera.thread)
|
||||
self.assertIsInstance(camera.stream, io.IOBase)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with PiCameraStreamer() as camera:
|
||||
|
||||
suites = [
|
||||
unittest.TestLoader().loadTestsFromTestCase(TestCaptureMethods),
|
||||
unittest.TestLoader().loadTestsFromTestCase(TestUnencodedMethods),
|
||||
unittest.TestLoader().loadTestsFromTestCase(TestThreadStarting),
|
||||
unittest.TestLoader().loadTestsFromTestCase(TestRecordMethods),
|
||||
]
|
||||
|
||||
alltests = unittest.TestSuite(suites)
|
||||
|
||||
result = unittest.TextTestRunner(verbosity=2).run(alltests)
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
import atexit
|
||||
import logging
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from openflexure_stage import OpenFlexureStage
|
||||
|
||||
from openflexure_microscope import Microscope, config
|
||||
from openflexure_microscope.camera.pi import PiCameraStreamer
|
||||
|
||||
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
|
||||
|
||||
|
||||
class TestPluginMethods(unittest.TestCase):
|
||||
def test_plugin_load(self):
|
||||
plugin_arr = microscope.plugins.plugins
|
||||
|
||||
plugin_names = [plugin[0] for plugin in plugin_arr]
|
||||
|
||||
self.assertTrue("testing" in plugin_names)
|
||||
|
||||
def test_camera_access(self):
|
||||
identify = microscope.plugins.testing.identify()
|
||||
self.assertTrue(identify[0] is microscope.camera)
|
||||
|
||||
def test_stage_access(self):
|
||||
identify = microscope.plugins.testing.identify()
|
||||
self.assertTrue(identify[1] is microscope.stage)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
with Microscope() as microscope:
|
||||
|
||||
microscope.attach(PiCameraStreamer(), OpenFlexureStage())
|
||||
|
||||
microscope.plugins.attach("openflexure_microscope.plugins.testing:Plugin")
|
||||
|
||||
suites = [unittest.TestLoader().loadTestsFromTestCase(TestPluginMethods)]
|
||||
|
||||
alltests = unittest.TestSuite(suites)
|
||||
|
||||
result = unittest.TextTestRunner(verbosity=2).run(alltests)
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
import logging
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
from openflexure_stage import OpenFlexureStage
|
||||
|
||||
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
|
||||
|
||||
|
||||
class TestMicroscope(unittest.TestCase):
|
||||
def test_movement(self):
|
||||
move_distance = 500
|
||||
for axis in range(3):
|
||||
for direction in [1, -1]:
|
||||
pos_i = stage.position
|
||||
logging.debug(pos_i)
|
||||
|
||||
logging.info(
|
||||
"Moving axis {} by {}".format(axis, move_distance * direction)
|
||||
)
|
||||
move = [0, 0, 0]
|
||||
move[axis] = move_distance * direction
|
||||
|
||||
stage.move_rel(move)
|
||||
|
||||
pos_f = stage.position
|
||||
diff = np.subtract(pos_f, pos_i)
|
||||
logging.debug("{} > {}".format(pos_i, pos_f))
|
||||
|
||||
self.assertTrue(np.array_equal(diff, move))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with OpenFlexureStage("/dev/ttyUSB0") as stage:
|
||||
|
||||
suites = [unittest.TestLoader().loadTestsFromTestCase(TestMicroscope)]
|
||||
|
||||
alltests = unittest.TestSuite(suites)
|
||||
|
||||
result = unittest.TextTestRunner(verbosity=2).run(alltests)
|
||||
Loading…
Add table
Add a link
Reference in a new issue