Merge branch 'cleanup' into 'master'

The grand cleanup

See merge request openflexure/openflexure-microscope-server!82
This commit is contained in:
Joel Collins 2020-11-13 13:42:56 +00:00
commit 5b3d46ff98
60 changed files with 465 additions and 1180 deletions

0
.gitattributes vendored
View file

11
.pre-commit-config.yaml Normal file
View 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$

View file

@ -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"]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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()

View file

@ -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!

View file

@ -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

View file

@ -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):

View file

@ -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

View file

@ -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);
}

View file

@ -131,7 +131,6 @@ export default {
},
submitApiUri: function() {
// TODO: This could probably be handled more explicitally
return this.pluginApiUri + this.route;
}
},

View file

@ -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)

View file

@ -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

View file

@ -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"]

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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},
},
)

View file

@ -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",

View 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)

View file

@ -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):
"""

View file

@ -1,9 +0,0 @@
{
"camera": {
"type": "PiCamera"
},
"stage": {
"type": "SangaStage",
"port": null
}
}

View file

@ -1,7 +0,0 @@
{
"fov": [
4100,
3146
],
"jpeg_quality": 100
}

View file

@ -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":

View file

@ -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()

View file

@ -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

View file

@ -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 = []

View file

@ -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())

View file

@ -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

View file

@ -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
View file

@ -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"},

View file

@ -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

View file

@ -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")

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)