Went on a PEP-8 rampage

This commit is contained in:
Joel Collins 2019-01-31 17:20:46 +00:00
parent f99ad30fb6
commit 0948c9308a
36 changed files with 186 additions and 218 deletions

View file

@ -1,31 +1,16 @@
#!/usr/bin/env python #!/usr/bin/env python
""" """
TODO: Implement API route to cleanly shut down server TODO: Implement API route to cleanly shut down server
TODO: Implement plugin API routes somehow
""" """
import numpy as np
from importlib import import_module
import time
import datetime
import os
from flask import ( from flask import (
Flask, render_template, Response, url_for, Flask, render_template)
redirect, request, jsonify, send_file, abort,
make_response)
from flask.views import MethodView
from werkzeug.exceptions import default_exceptions
from serial import SerialException
from flask_cors import CORS from flask_cors import CORS
from openflexure_microscope.api.utilities import list_routes
from openflexure_microscope.api.exceptions import JSONExceptionHandler from openflexure_microscope.api.exceptions import JSONExceptionHandler
from openflexure_microscope import Microscope from openflexure_microscope import Microscope
from openflexure_microscope.exceptions import LockError
from openflexure_microscope.camera.pi import StreamingCamera from openflexure_microscope.camera.pi import StreamingCamera
from openflexure_microscope.stage.openflexure import Stage from openflexure_microscope.stage.openflexure import Stage

View file

@ -2,6 +2,7 @@ from flask import jsonify
from werkzeug.exceptions import default_exceptions from werkzeug.exceptions import default_exceptions
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
class JSONExceptionHandler(object): class JSONExceptionHandler(object):
def __init__(self, app=None): def __init__(self, app=None):
@ -24,7 +25,6 @@ class JSONExceptionHandler(object):
} }
return jsonify(response) return jsonify(response)
def init_app(self, app): def init_app(self, app):
self.app = app self.app = app
self.register(HTTPException) self.register(HTTPException)

View file

@ -1,8 +1,8 @@
import pprint import pprint
import logging import logging
import copy
from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest
class JsonPayload: class JsonPayload:
def __init__(self, request): def __init__(self, request):
""" """
@ -60,10 +60,9 @@ def gen(camera):
def get_bool(get_arg): def get_bool(get_arg):
"""Convert GET request argument string to a Python bool""" """Convert GET request argument string to a Python bool"""
if ( if (get_arg == 'true' or
get_arg == 'true' or get_arg == 'True' or
get_arg == 'True' or get_arg == '1'):
get_arg == '1'):
return True return True
else: else:
return False return False

View file

@ -197,4 +197,4 @@ def construct_blueprint(microscope_obj):
view_func=ConfigAPI.as_view('config', microscope=microscope_obj) view_func=ConfigAPI.as_view('config', microscope=microscope_obj)
) )
return(blueprint) return blueprint

View file

@ -2,9 +2,7 @@ from openflexure_microscope.api.utilities import gen, get_bool, JsonPayload
from openflexure_microscope.api.v1.views import MicroscopeView from openflexure_microscope.api.v1.views import MicroscopeView
from openflexure_microscope.utilities import filter_dict from openflexure_microscope.utilities import filter_dict
from flask import Response, Blueprint, jsonify, request, abort, url_for, redirect, send_file from flask import Response, jsonify, request, abort, url_for, redirect, send_file
import logging
class ListAPI(MicroscopeView): class ListAPI(MicroscopeView):
@ -368,7 +366,7 @@ class MetadataAPI(MicroscopeView):
# If no filename is specified, redirect to the capture's currently set filename # If no filename is specified, redirect to the capture's currently set filename
if not filename: if not filename:
return redirect(url_for('capture_download', capture_id=capture_id, filename=capture_obj.metadataname, as_attachment=as_attachment), code=307) return redirect(url_for('capture_download', capture_id=capture_id, filename=capture_obj.metadataname), code=307)
# Download the metadata using the requested filename # Download the metadata using the requested filename
data = capture_obj.yaml data = capture_obj.yaml
@ -377,6 +375,7 @@ class MetadataAPI(MicroscopeView):
data, data,
mimetype="text/yaml") mimetype="text/yaml")
class TagsAPI(MicroscopeView): class TagsAPI(MicroscopeView):
def get(self, capture_id): def get(self, capture_id):
""" """
@ -402,7 +401,7 @@ class TagsAPI(MicroscopeView):
if not capture_obj or not capture_obj.state['available']: if not capture_obj or not capture_obj.state['available']:
return abort(404) # 404 Not Found return abort(404) # 404 Not Found
metadata_tags = filter_dict(capture_obj.state, ('metadata', 'tags')) metadata_tags = filter_dict(capture_obj.state, ['metadata', 'tags'])
return jsonify(metadata_tags) return jsonify(metadata_tags)
@ -440,7 +439,7 @@ class TagsAPI(MicroscopeView):
for tag in data_dict: for tag in data_dict:
capture_obj.put_tag(str(tag)) capture_obj.put_tag(str(tag))
metadata_tags = filter_dict(capture_obj.state, ('metadata', 'tags')) metadata_tags = filter_dict(capture_obj.state, ['metadata', 'tags'])
return jsonify(metadata_tags) return jsonify(metadata_tags)
@ -477,6 +476,6 @@ class TagsAPI(MicroscopeView):
for tag in data_dict: for tag in data_dict:
capture_obj.delete_tag(str(tag)) capture_obj.delete_tag(str(tag))
metadata_tags = filter_dict(capture_obj.state, ('metadata', 'tags')) metadata_tags = filter_dict(capture_obj.state, ['metadata', 'tags'])
return jsonify(metadata_tags) return jsonify(metadata_tags)

View file

@ -1,9 +1,7 @@
from openflexure_microscope.api.v1.views import MicroscopeView from openflexure_microscope.api.v1.views import MicroscopeView
from openflexure_microscope.api.utilities import JsonPayload from openflexure_microscope.api.utilities import JsonPayload
from flask import Response, Blueprint, jsonify, request, abort, url_for, redirect, send_file from flask import jsonify, request
import logging
class ZoomAPI(MicroscopeView): class ZoomAPI(MicroscopeView):

View file

@ -1,8 +1,6 @@
from openflexure_microscope.api.v1.views import MicroscopeView from openflexure_microscope.api.v1.views import MicroscopeView
from flask import Response, Blueprint, jsonify, request, abort, url_for, redirect, send_file from flask import jsonify
import logging
class GPUPreviewAPI(MicroscopeView): class GPUPreviewAPI(MicroscopeView):

View file

@ -1,11 +1,12 @@
from openflexure_microscope.api.v1.views import MicroscopeViewPlugin from openflexure_microscope.api.v1.views import MicroscopeViewPlugin
from flask import Response, Blueprint, jsonify from flask import Blueprint
import logging, warnings import logging
import warnings
def construct_blueprint(microscope_obj, plugin_paths=[], include_default=True): def construct_blueprint(microscope_obj):
blueprint = Blueprint('plugin_blueprint', __name__) blueprint = Blueprint('plugin_blueprint', __name__)
@ -56,4 +57,4 @@ def construct_blueprint(microscope_obj, plugin_paths=[], include_default=True):
"No valid 'api_views' dictionary found in {}".format(plugin_obj) "No valid 'api_views' dictionary found in {}".format(plugin_obj)
) )
return(blueprint) return blueprint

View file

@ -2,7 +2,7 @@ from openflexure_microscope.api.utilities import gen, JsonPayload
from openflexure_microscope.api.v1.views import MicroscopeView from openflexure_microscope.api.v1.views import MicroscopeView
from openflexure_microscope.utilities import axes_to_array, filter_dict from openflexure_microscope.utilities import axes_to_array, filter_dict
from flask import Response, Blueprint, jsonify, request from flask import Blueprint, jsonify, request
import logging import logging
@ -42,7 +42,7 @@ class PositionAPI(MicroscopeView):
} }
""" """
out = filter_dict(self.microscope.state, ('stage', 'position')) out = filter_dict(self.microscope.state, ['stage', 'position'])
return jsonify(out) return jsonify(out)
def post(self): def post(self):
@ -85,7 +85,7 @@ class PositionAPI(MicroscopeView):
with self.microscope.stage.lock: with self.microscope.stage.lock:
self.microscope.stage.move_rel(position) self.microscope.stage.move_rel(position)
out = filter_dict(self.microscope.state, ('stage', 'position')) out = filter_dict(self.microscope.state, ['stage', 'position'])
return jsonify(out) return jsonify(out)
@ -99,4 +99,4 @@ def construct_blueprint(microscope_obj):
view_func=PositionAPI.as_view('position', microscope=microscope_obj) view_func=PositionAPI.as_view('position', microscope=microscope_obj)
) )
return(blueprint) return blueprint

View file

@ -1,6 +1,7 @@
from openflexure_microscope.api.v1.views import MicroscopeView from openflexure_microscope.api.v1.views import MicroscopeView
from flask import jsonify, abort, Blueprint from flask import jsonify, abort, Blueprint
class TaskListAPI(MicroscopeView): class TaskListAPI(MicroscopeView):
def get(self): def get(self):
@ -65,6 +66,7 @@ class TaskListAPI(MicroscopeView):
return jsonify(data) return jsonify(data)
class TaskAPI(MicroscopeView): class TaskAPI(MicroscopeView):
def get(self, task_id): def get(self, task_id):
@ -135,6 +137,7 @@ class TaskAPI(MicroscopeView):
return jsonify(data) return jsonify(data)
def construct_blueprint(microscope_obj): def construct_blueprint(microscope_obj):
blueprint = Blueprint('task_blueprint', __name__) blueprint = Blueprint('task_blueprint', __name__)
@ -149,4 +152,4 @@ def construct_blueprint(microscope_obj):
view_func=TaskAPI.as_view('task', microscope=microscope_obj) view_func=TaskAPI.as_view('task', microscope=microscope_obj)
) )
return(blueprint) return blueprint

View file

@ -1,9 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import time import time
import io
import os import os
import threading import threading
from PIL import Image
import datetime import datetime
import yaml import yaml
import logging import logging
@ -35,6 +33,18 @@ def generate_basename():
return datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") return datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
def generate_numbered_basename(obj_list: list) -> str:
initial_basename = generate_basename()
basename = initial_basename
# Handle clashing
iterator = 1
while basename in [obj.basename for obj in obj_list]:
basename = initial_basename + "_{}".format(iterator)
iterator += 1
return basename
class CameraEvent(object): class CameraEvent(object):
""" """
A frame-signaller object used by any instances or subclasses of BaseCamera. A frame-signaller object used by any instances or subclasses of BaseCamera.
@ -79,18 +89,6 @@ class CameraEvent(object):
self.events[get_ident()][0].clear() self.events[get_ident()][0].clear()
def generate_basename(obj_list: list) -> str:
initial_basename = generate_basename()
basename = initial_basename
# Handle clashing
iterator = 1
while basename in [obj.basename for obj in obj_list]:
basename = initial_basename + "_{}".format(iterator)
iterator += 1
return basename
class BaseCamera(object): class BaseCamera(object):
""" """
Base implementation of StreamingCamera. Base implementation of StreamingCamera.
@ -155,7 +153,7 @@ class BaseCamera(object):
# START AND STOP WORKER THREAD # START AND STOP WORKER THREAD
def start_worker(self, timeout: int=5) -> bool: def start_worker(self, timeout: int = 5) -> bool:
"""Start the background camera thread if it isn't running yet.""" """Start the background camera thread if it isn't running yet."""
timeout_time = time.time() + timeout timeout_time = time.time() + timeout
@ -176,7 +174,7 @@ class BaseCamera(object):
time.sleep(0) time.sleep(0)
return True return True
def stop_worker(self, timeout: int=5) -> bool: def stop_worker(self, timeout: int = 5) -> bool:
"""Flag worker thread for stop. Waits for thread close or timeout.""" """Flag worker thread for stop. Waits for thread close or timeout."""
logging.debug("Stopping worker thread") logging.debug("Stopping worker thread")
timeout_time = time.time() + timeout timeout_time = time.time() + timeout
@ -217,13 +215,13 @@ class BaseCamera(object):
"""Return the latest recorded video.""" """Return the latest recorded video."""
return last_entry(self.videos) return last_entry(self.videos)
def image_from_id(self, id): def image_from_id(self, image_id):
"""Return an image StreamObject with a matching ID.""" """Return an image StreamObject with a matching ID."""
return entry_by_id(id, self.images) return entry_by_id(image_id, self.images)
def video_from_id(self, id): def video_from_id(self, video_id):
"""Return a video StreamObject with a matching ID.""" """Return a video StreamObject with a matching ID."""
return entry_by_id(id, self.videos) return entry_by_id(video_id, self.videos)
# MANAGE CAPTURE DATABASE # MANAGE CAPTURE DATABASE
@ -277,25 +275,24 @@ class BaseCamera(object):
def new_image( def new_image(
self, self,
write_to_file: bool=False, write_to_file: bool = False,
temporary: bool=True, temporary: bool = True,
filename: str=None, filename: str = None,
fmt: str='jpeg', fmt: str = 'jpeg'):
shunt_others: bool=True):
""" """
Create a new image capture object. Adds to the image list, and shunt all others. Create a new image capture object. Adds to the image list, and shunt all others.
Args: Args:
write_to_file (bool): Should the StreamObject write to a file, or an in-memory byte stream. write_to_file (bool): Should the StreamObject write to a file, or an in-memory byte stream.
keep_on_disk (bool): Should the data be deleted after session ends. Creating the capture with a content manager sets this to true. 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. filename (str): Name of the stored file. Defaults to timestamp.
fmt (str): Format of the capture. fmt (str): Format of the capture.
""" """
# Generate file name # Generate file name
if not filename: if not filename:
filename = generate_basename(self.images) filename = generate_numbered_basename(self.images)
logging.debug(filename) logging.debug(filename)
# Create capture object # Create capture object
@ -317,11 +314,10 @@ class BaseCamera(object):
def new_video( def new_video(
self, self,
write_to_file: bool=True, write_to_file: bool = True,
temporary: bool=False, temporary: bool = False,
filename: str=None, filename: str = None,
fmt: str='h264', fmt: str = 'h264'):
shunt_others: bool=True):
""" """
Create a new video capture object. Adds to the image list, and shunt all others. Create a new video capture object. Adds to the image list, and shunt all others.
@ -335,7 +331,7 @@ class BaseCamera(object):
# Generate file name # Generate file name
if not filename: if not filename:
filename = generate_basename(self.videos) filename = generate_numbered_basename(self.videos)
logging.debug(filename) logging.debug(filename)
# Create capture object # Create capture object

View file

@ -15,6 +15,7 @@ thumbnail_size = (60, 60)
BASE_CAPTURE_PATH = os.path.join(os.path.expanduser('~'), 'micrographs') BASE_CAPTURE_PATH = os.path.join(os.path.expanduser('~'), 'micrographs')
TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, 'tmp') TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, 'tmp')
def clear_tmp(): def clear_tmp():
global TEMP_CAPTURE_PATH global TEMP_CAPTURE_PATH
@ -24,6 +25,7 @@ def clear_tmp():
os.remove(f) os.remove(f)
logging.debug("Removed {}".format(f)) logging.debug("Removed {}".format(f))
def capture_from_dict(capture_dict): def capture_from_dict(capture_dict):
capture = CaptureObject(create_metadata_file=False) # Create a placeholder capture capture = CaptureObject(create_metadata_file=False) # Create a placeholder capture
@ -48,14 +50,15 @@ class CaptureObject(object):
Note: Captures cannot be stored in a lower-level directory than BASE_CAPTURE_PATH. Note: Captures cannot be stored in a lower-level directory than BASE_CAPTURE_PATH.
""" """
def __init__( def __init__(
self, self,
write_to_file: bool=False, write_to_file: bool = False,
temporary: bool=False, temporary: bool = False,
filename: str='', filename: str = '',
folder: str='', folder: str = '',
fmt: str='', fmt: str = '',
create_metadata_file: bool=True) -> None: create_metadata_file: bool = True) -> None:
"""Create a new StreamObject, to manage capture data.""" """Create a new StreamObject, to manage capture data."""
# Store a nice ID # Store a nice ID
@ -102,7 +105,9 @@ class CaptureObject(object):
def __enter__(self): def __enter__(self):
"""Create StreamObject in context, to auto-clean disk data.""" """Create StreamObject in context, to auto-clean disk data."""
logging.debug("Entering context for {}. Stored files will be cleaned up automatically regardless of location.".format(self.id)) logging.debug(
"Entering context for {}. Stored files will be cleaned up automatically regardless of location.".format(
self.id))
self.temporary = True # Flag file to be removed on close. self.temporary = True # Flag file to be removed on close.
self.context_manager = True # Used in metadata self.context_manager = True # Used in metadata
@ -170,7 +175,8 @@ class CaptureObject(object):
def split_file_path(self, filepath): def split_file_path(self, filepath):
"""Takes a full file path, and splits it into separated class properties.""" """Takes a full file path, and splits it into separated class properties."""
self.filefolder, self.filename = os.path.split(filepath) # Split the full file path into a folder and a filename self.filefolder, self.filename = os.path.split(
filepath) # Split the full file path into a folder and a filename
self.basename = os.path.splitext(self.filename)[0] # Split the filename out from it's file extension self.basename = os.path.splitext(self.filename)[0] # Split the filename out from it's file extension
self.metadataname = "{}.yaml".format(self.basename) self.metadataname = "{}.yaml".format(self.basename)
@ -234,18 +240,10 @@ class CaptureObject(object):
@property @property
def metadata(self) -> dict: def metadata(self) -> dict:
# Create basic metadata dictionary # Create basic metadata dictionary
d = { d = {'id': self.id, 'filename': self.filename, 'path': self.file, 'time': self.timestring,
'id': self.id, 'format': self.format, 'tags': self.tags, 'custom': self._metadata}
'filename': self.filename,
'path': self.file,
'time': self.timestring,
'format': self.format,
'tags': self.tags
}
# Add custom metadata to dictionary # Add custom metadata to dictionary
d['custom'] = self._metadata
return d return d
@property @property
@ -261,14 +259,10 @@ class CaptureObject(object):
"""Return dictionary of StreamObject properties.""" """Return dictionary of StreamObject properties."""
# Create basic state dictionary # Create basic state dictionary
d = { d = {'locked': self.locked, 'temporary': self.temporary, 'metadata': self.metadata,
'locked': self.locked, 'metadata_path': self.metadata_file}
'temporary': self.temporary,
}
# Add metadata to state # Add metadata to state
d['metadata'] = self.metadata
d['metadata_path'] = self.metadata_file
# Check bytestream # Check bytestream
if self.stream_exists: if self.stream_exists:
@ -404,4 +398,5 @@ class CaptureObject(object):
if self.temporary: if self.temporary:
self.delete() self.delete()
atexit.register(clear_tmp) atexit.register(clear_tmp)

View file

@ -36,7 +36,6 @@ def to_map(data, func):
Args: Args:
data: Input iterable data data: Input iterable data
func: Function to apply to all non-iterable values func: Function to apply to all non-iterable values
excluded_keys: Any dictionary keys to exclude from the returned data
""" """
# If the object is a dictionary # If the object is a dictionary
if isinstance(data, abc.Mapping): if isinstance(data, abc.Mapping):
@ -58,7 +57,6 @@ def json_map(data, clean_keys=True):
Args: Args:
data: Input dictionary data: Input dictionary
clean_keys: Modify any keys unsuitable for JSON return clean_keys: Modify any keys unsuitable for JSON return
""" """
# Do not overwrite original data dictionary # Do not overwrite original data dictionary

View file

@ -1,8 +1,10 @@
from threading import ThreadError from threading import ThreadError
class TaskDeniedException(Exception): class TaskDeniedException(Exception):
pass pass
class LockError(ThreadError): class LockError(ThreadError):
ERROR_CODES = { ERROR_CODES = {
'ACQUIRE_ERROR': "Unable to acquire. Lock in use by another thread.", 'ACQUIRE_ERROR': "Unable to acquire. Lock in use by another thread.",

View file

@ -3,7 +3,6 @@
Defines a microscope object, binding a camera and stage with basic functionality. Defines a microscope object, binding a camera and stage with basic functionality.
""" """
import logging import logging
import os
import numpy as np import numpy as np
import uuid import uuid

View file

@ -3,21 +3,21 @@ import numpy as np
from openflexure_microscope.api.v1.views import MicroscopeViewPlugin from openflexure_microscope.api.v1.views import MicroscopeViewPlugin
from openflexure_microscope.api.utilities import JsonPayload from openflexure_microscope.api.utilities import JsonPayload
from flask import request, Response, escape, jsonify from flask import request, jsonify
import logging
class MeasureSharpnessAPI(MicroscopeViewPlugin): class MeasureSharpnessAPI(MicroscopeViewPlugin):
def post(self): def post(self):
payload = JsonPayload(request) payload = JsonPayload(request)
return jsonify({'sharpness': self.plugin.measure_sharpness()}) return jsonify({'sharpness': self.plugin.measure_sharpness()})
class AutofocusAPI(MicroscopeViewPlugin): class AutofocusAPI(MicroscopeViewPlugin):
def post(self): def post(self):
payload = JsonPayload(request) payload = JsonPayload(request)
# Figure out the range of z values to use # Figure out the range of z values to use
dz = payload.param("dz", default=np.linspace(-300,300,7), convert=np.array) dz = payload.param("dz", default=np.linspace(-300, 300, 7), convert=np.array)
print("Running autofocus...") print("Running autofocus...")
task = self.microscope.task.start(self.plugin.autofocus, dz) task = self.microscope.task.start(self.plugin.autofocus, dz)

View file

@ -1,21 +1,24 @@
import numpy as np import numpy as np
from scipy import ndimage from scipy import ndimage
def decimate_to(shape, image): def decimate_to(shape, image):
"""Decimate an image to reduce its size if it's too big.""" """Decimate an image to reduce its size if it's too big."""
decimation = np.max(np.ceil(np.array(image.shape, dtype=np.float)[:len(shape)]/np.array(shape))) decimation = np.max(np.ceil(np.array(image.shape, dtype=np.float)[:len(shape)]/np.array(shape)))
return image[::int(decimation), ::int(decimation), ...] return image[::int(decimation), ::int(decimation), ...]
def sharpness_sum_lap2(rgb_image): def sharpness_sum_lap2(rgb_image):
"""Return an image sharpness metric: sum(laplacian(image)**")""" """Return an image sharpness metric: sum(laplacian(image)**")"""
#image_bw=np.mean(decimate_to((1000,1000), rgb_image),2) # image_bw=np.mean(decimate_to((1000,1000), rgb_image),2)
image_bw=np.mean(rgb_image, 2) image_bw = np.mean(rgb_image, 2)
image_lap=ndimage.filters.laplace(image_bw) image_lap = ndimage.filters.laplace(image_bw)
return np.mean(image_lap.astype(np.float)**4) return np.mean(image_lap.astype(np.float)**4)
def sharpness_edge(image): def sharpness_edge(image):
"""Return a sharpness metric optimised for vertical lines""" """Return a sharpness metric optimised for vertical lines"""
gray = np.mean(image.astype(float), 2) gray = np.mean(image.astype(float), 2)
n = 20 n = 20
edge = np.array([[-1]*n + [1]*n]) edge = np.array([[-1]*n + [1]*n])
return np.sum([np.sum(ndimage.filters.convolve(gray,W)**2) for W in [edge, edge.T]]) return np.sum([np.sum(ndimage.filters.convolve(gray, W)**2) for W in [edge, edge.T]])

View file

@ -34,7 +34,7 @@ class AutofocusPlugin(MicroscopePlugin):
positions = [] positions = []
camera.annotate_text = "" camera.annotate_text = ""
for i in stage.scan_z(dz, return_to_start=False): for _ in stage.scan_z(dz, return_to_start=False):
positions.append(stage.position[2]) positions.append(stage.position[2])
time.sleep(settle) time.sleep(settle)
sharpnesses.append(self.measure_sharpness(metric_fn)) sharpnesses.append(self.measure_sharpness(metric_fn))
@ -46,4 +46,4 @@ class AutofocusPlugin(MicroscopePlugin):
def measure_sharpness(self, metric_fn=sharpness_sum_lap2): def measure_sharpness(self, metric_fn=sharpness_sum_lap2):
"""Measure the sharpness of the camera's current view.""" """Measure the sharpness of the camera's current view."""
return metric_fn(self.microscope.camera.array(use_video_port=True, resize=(640,480))) return metric_fn(self.microscope.camera.array(use_video_port=True, resize=(640, 480)))

View file

@ -1,28 +1,26 @@
import time
import numpy as np
from openflexure_microscope.plugins import MicroscopePlugin from openflexure_microscope.plugins import MicroscopePlugin
from openflexure_microscope.utilities import set_properties
from openflexure_microscope.api.v1.views import MicroscopeViewPlugin from openflexure_microscope.api.v1.views import MicroscopeViewPlugin
from openflexure_microscope.api.utilities import JsonPayload from openflexure_microscope.api.utilities import JsonPayload
from flask import request, Response, escape, jsonify from flask import request, jsonify
import logging import logging
from .recalibrate_utils import recalibrate_camera from .recalibrate_utils import recalibrate_camera
class RecalibrateAPIView(MicroscopeViewPlugin): class RecalibrateAPIView(MicroscopeViewPlugin):
def post(self): def post(self):
payload = JsonPayload(request) payload = JsonPayload(request)
# Figure out the range of z values to use # TODO: Figure out the range of z values to use
print("Starting microscope recalibration...") print("Starting microscope recalibration...")
task = self.microscope.task.start(self.plugin.recalibrate) task = self.microscope.task.start(self.plugin.recalibrate)
# return a handle on the autofocus task # Return a handle on the autofocus task
return jsonify(task.state, 202) return jsonify(task.state, 202)
class Plugin(MicroscopePlugin): class Plugin(MicroscopePlugin):
""" """
A set of default plugins A set of default plugins
@ -45,16 +43,14 @@ class Plugin(MicroscopePlugin):
streaming = scamera.state['stream_active'] streaming = scamera.state['stream_active']
if streaming: if streaming:
logging.info("Stopping stream before recalibration") logging.info("Stopping stream before recalibration")
scamera.stop_stream_recording(resolution=(640,480)) scamera.stop_stream_recording(resolution=(640, 480))
old_resolution = scamera.camera.resolution old_resolution = scamera.camera.resolution
try: try:
scamera.camera.resolution=(640,480) scamera.camera.resolution = (640, 480)
recalibrate_camera(scamera.camera) recalibrate_camera(scamera.camera)
finally: finally:
scamera.camera.resolution=old_resolution scamera.camera.resolution = old_resolution
self.microscope.save_config() self.microscope.save_config()
if streaming: if streaming:
logging.info("Restarting stream after recalibration") logging.info("Restarting stream after recalibration")
scamera.start_stream_recording() scamera.start_stream_recording()

View file

@ -3,12 +3,14 @@ from picamera import PiCamera
from picamera.array import PiRGBArray, PiBayerArray from picamera.array import PiRGBArray, PiBayerArray
import time import time
def rgb_image(camera, resize=None, **kwargs): def rgb_image(camera, resize=None, **kwargs):
"""Capture an image and return an RGB numpy array""" """Capture an image and return an RGB numpy array"""
with PiRGBArray(camera, size=resize) as output: with PiRGBArray(camera, size=resize) as output:
camera.capture(output, format='rgb', resize=resize, **kwargs) camera.capture(output, format='rgb', resize=resize, **kwargs)
return output.array return output.array
def flat_lens_shading_table(camera): def flat_lens_shading_table(camera):
"""Return a flat (i.e. unity gain) lens shading table. """Return a flat (i.e. unity gain) lens shading table.
@ -20,6 +22,7 @@ def flat_lens_shading_table(camera):
raise ImportError("This program requires the forked picamera library with lens shading support") raise ImportError("This program requires the forked picamera library with lens shading support")
return np.zeros(camera._lens_shading_table_shape(), dtype=np.uint8) + 32 return np.zeros(camera._lens_shading_table_shape(), dtype=np.uint8) + 32
def adjust_exposure_to_setpoint(camera, setpoint): def adjust_exposure_to_setpoint(camera, setpoint):
"""Adjust the camera's exposure time until the maximum pixel value is <setpoint>.""" """Adjust the camera's exposure time until the maximum pixel value is <setpoint>."""
print("Adjusting shutter speed to hit setpoint {}".format(setpoint), end="") print("Adjusting shutter speed to hit setpoint {}".format(setpoint), end="")
@ -29,11 +32,12 @@ def adjust_exposure_to_setpoint(camera, setpoint):
time.sleep(1) time.sleep(1)
print("done") print("done")
def auto_expose_and_freeze_settings(camera): def auto_expose_and_freeze_settings(camera):
"""Freeze the settings after auto-exposing to white illumination""" """Freeze the settings after auto-exposing to white illumination"""
print("Allowing the camera to auto-expose") print("Allowing the camera to auto-expose")
camera.awb_mode="auto" camera.awb_mode = "auto"
camera.exposure_mode="auto" camera.exposure_mode = "auto"
for i in range(6): for i in range(6):
print(".", end="") print(".", end="")
time.sleep(0.5) time.sleep(0.5)
@ -54,18 +58,19 @@ def auto_expose_and_freeze_settings(camera):
def channels_from_bayer_array(bayer_array): def channels_from_bayer_array(bayer_array):
"""Given the 'array' from a PiBayerArray, return the 4 channels.""" """Given the 'array' from a PiBayerArray, return the 4 channels."""
bayer_pattern = [(i//2, i%2) for i in range(4)] bayer_pattern = [(i//2, i % 2) for i in range(4)]
channels = np.zeros((4, bayer_array.shape[0]//2, bayer_array.shape[1]//2), dtype=bayer_array.dtype) channels = np.zeros((4, bayer_array.shape[0]//2, bayer_array.shape[1]//2), dtype=bayer_array.dtype)
for i, offset in enumerate(bayer_pattern): for i, offset in enumerate(bayer_pattern):
# We simplify life by dealing with only one channel at a time. # We simplify life by dealing with only one channel at a time.
channels[i, :, :] = np.sum(bayer_array[offset[0]::2, offset[1]::2, :], axis=2) channels[i, :, :] = np.sum(bayer_array[offset[0]::2, offset[1]::2, :], axis=2)
return channels return channels
def lst_from_channels(channels): def lst_from_channels(channels):
"""Given the 4 Bayer colour channels from a white image, generate a LST.""" """Given the 4 Bayer colour channels from a white image, generate a LST."""
full_resolution = np.array(channels.shape[1:]) * 2 # channels have been binned full_resolution = np.array(channels.shape[1:]) * 2 # channels have been binned
#lst_resolution = list(np.ceil(full_resolution / 64.0).astype(int)) # lst_resolution = list(np.ceil(full_resolution / 64.0).astype(int))
lst_resolution = [(r // 64) + 1 for r in full_resolution] 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. # NB the size of the LST is 1/64th of the image, but rounded UP.
print("Generating a lens shading table at {}x{}".format(*lst_resolution)) print("Generating a lens shading table at {}x{}".format(*lst_resolution))
@ -73,7 +78,7 @@ def lst_from_channels(channels):
for i in range(lens_shading.shape[0]): for i in range(lens_shading.shape[0]):
image_channel = channels[i, :, :] image_channel = channels[i, :, :]
iw, ih = image_channel.shape iw, ih = image_channel.shape
ls_channel = lens_shading[i,:,:] ls_channel = lens_shading[i, :, :]
lw, lh = ls_channel.shape lw, lh = ls_channel.shape
# The lens shading table is rounded **up** in size to 1/64th of the size of # The lens shading table is rounded **up** in size to 1/64th of the size of
# the image. Rather than handle edge images separately, I'm just going to # the image. Rather than handle edge images separately, I'm just going to
@ -84,19 +89,23 @@ def lst_from_channels(channels):
# less computationally efficient! # less computationally efficient!
padded_image_channel = np.pad(image_channel, padded_image_channel = np.pad(image_channel,
[(0, lw*32 - iw), (0, lh*32 - ih)], [(0, lw*32 - iw), (0, lh*32 - ih)],
mode="edge") # Pad image to the right and bottom mode="edge") # Pad image to the right and bottom
print("Channel shape: {}x{}, shading table shape: {}x{}, after padding {}".format(iw,ih,lw*32,lh*32,padded_image_channel.shape)) print("Channel shape: {}x{}, shading table shape: {}x{}, after padding {}".format(iw,
ih,
lw*32,
lh*32,
padded_image_channel.shape))
# Next, fill the shading table (except edge pixels). Please excuse the # Next, fill the shading table (except edge pixels). Please excuse the
# for loop - I know it's not fast but this code needn't be! # for loop - I know it's not fast but this code needn't be!
box = 3 # We average together a square of this side length for each pixel. box = 3 # We average together a square of this side length for each pixel.
# NB this isn't quite what 6by9's program does - it averages 3 pixels # NB this isn't quite what 6by9's program does - it averages 3 pixels
# horizontally, but not vertically. # horizontally, but not vertically.
for dx in np.arange(box) - box//2: for dx in np.arange(box) - box//2:
for dy in np.arange(box) - box//2: for dy in np.arange(box) - box//2:
ls_channel[:,:] += padded_image_channel[16+dx::32,16+dy::32] - 64 ls_channel[:, :] += padded_image_channel[16+dx::32, 16+dy::32] - 64
ls_channel /= box**2 ls_channel /= box**2
# The original C code written by 6by9 normalises to the central 64 pixels in each channel. # The original C code written by 6by9 normalises to the central 64 pixels in each channel.
#ls_channel /= np.mean(image_channel[iw//2-4:iw//2+4, ih//2-4:ih//2+4]) # ls_channel /= np.mean(image_channel[iw//2-4:iw//2+4, ih//2-4:ih//2+4])
# I have had better results just normalising to the maximum: # I have had better results just normalising to the maximum:
ls_channel /= np.max(ls_channel) ls_channel /= np.max(ls_channel)
# NB the central pixel should now be *approximately* 1.0 (may not be exactly # NB the central pixel should now be *approximately* 1.0 (may not be exactly
@ -107,11 +116,12 @@ def lst_from_channels(channels):
# What we actually want to calculate is the gains needed to compensate for the # What we actually want to calculate is the gains needed to compensate for the
# lens shading - that's 1/lens_shading_table_float as we currently have it. # lens shading - that's 1/lens_shading_table_float as we currently have it.
gains = 32.0/lens_shading # 32 is unity gain gains = 32.0/lens_shading # 32 is unity gain
gains[gains > 255] = 255 # clip at 255, maximum gain is 255/32 gains[gains > 255] = 255 # clip at 255, maximum gain is 255/32
gains[gains < 32] = 32 # clip at 32, minimum gain is 1 (is this necessary?) gains[gains < 32] = 32 # clip at 32, minimum gain is 1 (is this necessary?)
lens_shading_table = gains.astype(np.uint8) lens_shading_table = gains.astype(np.uint8)
return lens_shading_table[::-1,:,:].copy() return lens_shading_table[::-1, :, :].copy()
def recalibrate_camera(camera): def recalibrate_camera(camera):
"""Reset the lens shading table and exposure settings. """Reset the lens shading table and exposure settings.
@ -124,7 +134,7 @@ def recalibrate_camera(camera):
``StreamingCamera``. ``StreamingCamera``.
""" """
camera.lens_shading_table = flat_lens_shading_table(camera) camera.lens_shading_table = flat_lens_shading_table(camera)
discarded = rgb_image(camera) # for some reason the camera won't work unless I do this! _ = rgb_image(camera) # for some reason the camera won't work unless I do this!
with PiBayerArray(camera) as a: with PiBayerArray(camera) as a:
camera.capture(a, format="jpeg", bayer=True) camera.capture(a, format="jpeg", bayer=True)
@ -132,18 +142,19 @@ def recalibrate_camera(camera):
# Now we need to calculate a lens shading table that would make this flat. # Now we need to calculate a lens shading table that would make this flat.
# raw_image is a 3D array, with full resolution and 3 colour channels. No # raw_image is a 3D array, with full resolution and 3 colour channels. No
# demosaicing has been done, so 2/3 of the values are zero (3/4 for R and B # de-mosaicing has been done, so 2/3 of the values are zero (3/4 for R and B
# channels, 1/2 for green because there's twize as many green pixels). # channels, 1/2 for green because there's twice as many green pixels).
channels = channels_from_bayer_array(raw_image) channels = channels_from_bayer_array(raw_image)
lens_shading_table = lst_from_channels(channels) lens_shading_table = lst_from_channels(channels)
camera.lens_shading_table=lens_shading_table camera.lens_shading_table = lens_shading_table
test = rgb_image(camera) _ = rgb_image(camera)
# Fix the AWB gains so the image is neutral # Fix the AWB gains so the image is neutral
channel_means = np.mean(np.mean(rgb_image(camera), axis=0, dtype=np.float), axis=0) channel_means = np.mean(np.mean(rgb_image(camera), axis=0, dtype=np.float), axis=0)
old_gains = camera.awb_gains old_gains = camera.awb_gains
camera.awb_gains = (channel_means[1]/channel_means[0] * old_gains[0], channel_means[1]/channel_means[2]*old_gains[1]) camera.awb_gains = (channel_means[1]/channel_means[0] * old_gains[0],
channel_means[1]/channel_means[2]*old_gains[1])
time.sleep(1) time.sleep(1)
# Ensure the background is bright but not saturated # Ensure the background is bright but not saturated
adjust_exposure_to_setpoint(camera, 230) adjust_exposure_to_setpoint(camera, 230)
@ -157,4 +168,3 @@ if __name__ == "__main__":
recalibrate_camera(camera) recalibrate_camera(camera)
print("Done.") print("Done.")
time.sleep(2) time.sleep(2)

View file

@ -4,8 +4,6 @@ from openflexure_microscope.exceptions import TaskDeniedException
from flask import request, Response, escape, jsonify, abort from flask import request, Response, escape, jsonify, abort
import logging
class IdentifyAPI(MicroscopeViewPlugin): class IdentifyAPI(MicroscopeViewPlugin):
""" """
@ -53,6 +51,7 @@ class HelloWorldAPI(MicroscopeViewPlugin):
return Response(self.microscope.plugin_string) return Response(self.microscope.plugin_string)
class LongRunningAPI(MicroscopeViewPlugin): class LongRunningAPI(MicroscopeViewPlugin):
""" """
An example API plugin that uses a long-running plugin method. An example API plugin that uses a long-running plugin method.
@ -75,6 +74,7 @@ class LongRunningAPI(MicroscopeViewPlugin):
except TaskDeniedException: except TaskDeniedException:
return abort(409) return abort(409)
class SomeExceptionAPI(MicroscopeViewPlugin): class SomeExceptionAPI(MicroscopeViewPlugin):
""" """
An example API plugin that uses a long-running but broken plugin method. An example API plugin that uses a long-running but broken plugin method.

View file

@ -4,7 +4,7 @@ import inspect
import logging import logging
class bcolors: class ConColors:
HEADER = '\033[95m' HEADER = '\033[95m'
OKBLUE = '\033[94m' OKBLUE = '\033[94m'
OKGREEN = '\033[92m' OKGREEN = '\033[92m'
@ -23,7 +23,7 @@ def module_from_file(plugin_path):
# Check if the path is to a file # Check if the path is to a file
if not os.path.isfile(plugin_path): if not os.path.isfile(plugin_path):
logging.warning(bcolors.FAIL + "No valid plugin found at {}.".format(plugin_path) + bcolors.ENDC) logging.warning(ConColors.FAIL + "No valid plugin found at {}.".format(plugin_path) + ConColors.ENDC)
return None, None, None return None, None, None
else: else:
@ -63,6 +63,7 @@ def check_module(module_path):
# If all checks pass, return True # If all checks pass, return True
return True return True
def name_from_module(plugin_path): def name_from_module(plugin_path):
path_array = plugin_path.split('.') path_array = plugin_path.split('.')
@ -100,7 +101,7 @@ def load_plugin_class(plugin_path, plugin_class_name):
try: try:
plugin_class = getattr(plugin_module, plugin_class_name) plugin_class = getattr(plugin_module, plugin_class_name)
except AttributeError: except AttributeError:
logging.warning(bcolors.FAIL + "Class {} does not exist in plugin {}. Skipping.".format(plugin_class_name, plugin_path) + bcolors.ENDC) logging.warning(ConColors.FAIL + "Class {} does not exist in plugin {}. Skipping.".format(plugin_class_name, plugin_path) + ConColors.ENDC)
return None, None return None, None
else: else:
return plugin_class, plugin_name return plugin_class, plugin_name
@ -112,7 +113,7 @@ def class_from_map(plugin_map):
plugin_arr = plugin_map.split(':') plugin_arr = plugin_map.split(':')
if not len(plugin_arr) == 2: if not len(plugin_arr) == 2:
logging.warning(bcolors.WARNING + "Malformed plugin map {}. Skipping.".format(plugin_map) + bcolors.ENDC) logging.warning(ConColors.WARNING + "Malformed plugin map {}. Skipping.".format(plugin_map) + ConColors.ENDC)
return None, None return None, None
else: else:
return load_plugin_class(*plugin_arr) return load_plugin_class(*plugin_arr)
@ -155,7 +156,7 @@ class PluginMount(object):
plugin_object = plugin_class() plugin_object = plugin_class()
if hasattr(self, plugin_name): # If a plugin with the same name is already attached. if hasattr(self, plugin_name): # If a plugin with the same name is already attached.
logging.warning(bcolors.WARNING + "A plugin named {} has already been loaded. Skipping {}.".format(plugin_name, plugin_map) + bcolors.ENDC) logging.warning(ConColors.WARNING + "A plugin named {} has already been loaded. Skipping {}.".format(plugin_name, plugin_map) + ConColors.ENDC)
elif isinstance(plugin_object, MicroscopePlugin): # If plugin_object is an instance of MicroscopePlugin elif isinstance(plugin_object, MicroscopePlugin): # If plugin_object is an instance of MicroscopePlugin
# Attach plugin_object to the plugin mount # Attach plugin_object to the plugin mount
@ -165,10 +166,10 @@ class PluginMount(object):
# Grant plugin access to the hardware # Grant plugin access to the hardware
plugin_object.microscope = self.parent plugin_object.microscope = self.parent
logging.info(bcolors.OKGREEN + "Plugin {} loaded as {}.".format(plugin_map, plugin_name) + bcolors.ENDC) logging.info(ConColors.OKGREEN + "Plugin {} loaded as {}.".format(plugin_map, plugin_name) + ConColors.ENDC)
class MicroscopePlugin(): class MicroscopePlugin:
""" """
Parent class for all microscope plugins. Parent class for all microscope plugins.

View file

@ -10,4 +10,4 @@ class Plugin(MicroscopePlugin):
""" """
Tests for access to Microscope.camera, and Microscope.stage Tests for access to Microscope.camera, and Microscope.stage
""" """
return (self.microscope.camera, self.microscope.stage) return self.microscope.camera, self.microscope.stage

View file

@ -5,6 +5,7 @@ from openflexure_microscope.lock import StrictLock
import logging import logging
# TODO: Implement lock on movement # TODO: Implement lock on movement
class Stage(OpenFlexureStage): class Stage(OpenFlexureStage):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -16,7 +17,7 @@ class Stage(OpenFlexureStage):
try: try:
OpenFlexureStage.__init__(self, *args, **kwargs) OpenFlexureStage.__init__(self, *args, **kwargs)
except SerialException as e: except SerialException:
logging.error("No stage found. Aborting stage.") logging.error("No stage found. Aborting stage.")
logging.warning("Stage lock can be acquired, but any stage methods will fail and raise exceptions.") logging.warning("Stage lock can be acquired, but any stage methods will fail and raise exceptions.")

View file

@ -1,14 +1,13 @@
from threading import Thread from threading import Thread
from functools import wraps
import datetime import datetime
import logging import logging
import traceback import traceback
import time
import uuid import uuid
from openflexure_microscope.exceptions import TaskDeniedException from openflexure_microscope.exceptions import TaskDeniedException
from openflexure_microscope.utilities import entry_by_id from openflexure_microscope.utilities import entry_by_id
class TaskOrchestrator: class TaskOrchestrator:
""" """
Class responsible for spawning threaded tasks, and storing their returns. Class responsible for spawning threaded tasks, and storing their returns.
@ -76,7 +75,7 @@ class TaskOrchestrator:
Method returning bool describing if a particular task is allowed to run. Method returning bool describing if a particular task is allowed to run.
Currently always allows tasks to start, as locks determine access to hardware. Currently always allows tasks to start, as locks determine access to hardware.
""" """
#return not any([task._running for task in self.tasks]) # return not any([task._running for task in self.tasks])
return True return True

View file

@ -1,9 +1,9 @@
import copy import copy
import operator import operator
from fractions import Fraction
from functools import reduce from functools import reduce
from contextlib import contextmanager from contextlib import contextmanager
@contextmanager @contextmanager
def set_properties(obj, **kwargs): def set_properties(obj, **kwargs):
"""A context manager to set, then reset, certain properties of an object. """A context manager to set, then reset, certain properties of an object.
@ -25,7 +25,8 @@ def set_properties(obj, **kwargs):
for k, v in saved_properties.items(): for k, v in saved_properties.items():
setattr(obj, k, v) setattr(obj, k, v)
def axes_to_array(coordinate_dictionary, axis_keys=['x', 'y', 'z'], base_array=None):
def axes_to_array(coordinate_dictionary, axis_keys=('x', 'y', 'z'), base_array=None):
"""Takes key-value pairs of a JSON value, and maps onto an array""" """Takes key-value pairs of a JSON value, and maps onto an array"""
# If no base array is given # If no base array is given
if not base_array: if not base_array:
@ -42,19 +43,21 @@ def axes_to_array(coordinate_dictionary, axis_keys=['x', 'y', 'z'], base_array=N
return base_array return base_array
def filter_dict(dictionary: dict, keys: list): def filter_dict(dictionary: dict, keys: list):
# Get value by recursively applying getitem # Get value by recursively applying getitem
val = reduce(operator.getitem, keys, dictionary) val = reduce(operator.getitem, keys, dictionary)
# Create new dictionary by running reduce on key, val pairs # Create new dictionary by running reduce on key, val pairs
out = reduce(lambda x, y: {y: x}, reversed(keys), val) out = reduce(lambda x, y: {y: x}, reversed(keys), val)
return out return out
def entry_by_id(id: str, object_list: list):
def entry_by_id(entry_id: str, object_list: list):
"""Return an object from a list, if <object>.id matches id argument.""" """Return an object from a list, if <object>.id matches id argument."""
found = None found = None
for o in object_list: for o in object_list:
if o.id == id: if o.id == entry_id:
found = o found = o
return found return found

View file

@ -3,6 +3,7 @@ from PIL import Image
from io import BytesIO from io import BytesIO
import numpy as np import numpy as np
class APIconnection: class APIconnection:
def __init__(self, host="localhost", port=5000, api_ver="v1"): def __init__(self, host="localhost", port=5000, api_ver="v1"):
self.base = self.build_base(host=host, port=port, api_ver=api_ver) self.base = self.build_base(host=host, port=port, api_ver=api_ver)

View file

@ -1,19 +1,11 @@
#!/usr/bin/env python #!/usr/bin/env python
from api_client import APIconnection from api_client import APIconnection
import os
import io
import sys
import time
import numpy as np import numpy as np
import uuid
from PIL import Image
import unittest import unittest
from pprint import pprint
import logging, sys import logging
import sys
logging.basicConfig(stream=sys.stderr, level=logging.INFO) logging.basicConfig(stream=sys.stderr, level=logging.INFO)
@ -66,6 +58,7 @@ class TestCapture(unittest.TestCase):
self.assertTrue(capture_array.shape == (resolution[1], resolution[0], 3)) self.assertTrue(capture_array.shape == (resolution[1], resolution[0], 3))
class TestStage(unittest.TestCase): class TestStage(unittest.TestCase):
def test_stage_config(self): def test_stage_config(self):
@ -114,6 +107,7 @@ class TestStage(unittest.TestCase):
self.assertTrue(np.array_equal(diff, move)) self.assertTrue(np.array_equal(diff, move))
if __name__ == '__main__': if __name__ == '__main__':
suites = [ suites = [

View file

@ -3,17 +3,15 @@ from openflexure_microscope.camera.pi import StreamingCamera, CaptureObject
import os import os
import io import io
import sys
import time import time
import numpy as np import numpy as np
import uuid
from PIL import Image from PIL import Image
import unittest import unittest
from pprint import pprint
import logging, sys import logging
import sys
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)

View file

@ -46,4 +46,3 @@ if __name__ == '__main__':
alltests = unittest.TestSuite(suites) alltests = unittest.TestSuite(suites)
result = unittest.TextTestRunner(verbosity=2).run(alltests) result = unittest.TextTestRunner(verbosity=2).run(alltests)

View file

@ -1,21 +1,13 @@
#!/usr/bin/env python #!/usr/bin/env python
from openflexure_microscope.camera.pi import StreamingCamera
from openflexure_stage import OpenFlexureStage from openflexure_stage import OpenFlexureStage
from openflexure_microscope import Microscope
import os
import io
import sys
import time
import numpy as np import numpy as np
import uuid
from PIL import Image
import unittest import unittest
from pprint import pprint
import logging, sys import logging
import sys
logging.basicConfig(stream=sys.stderr, level=logging.INFO) logging.basicConfig(stream=sys.stderr, level=logging.INFO)

View file

@ -4,9 +4,7 @@ from openflexure_microscope.camera.pi import StreamingCamera
from openflexure_microscope import config from openflexure_microscope import config
import numpy as np import numpy as np
import sys
import time import time
import matplotlib.pyplot as plt
import os import os
import logging, sys import logging, sys