From 0948c9308a8f2ff47d2844169623a360376d341c Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Thu, 31 Jan 2019 17:20:46 +0000 Subject: [PATCH] Went on a PEP-8 rampage --- openflexure_microscope/__init__.py | 2 +- openflexure_microscope/api/app.py | 17 +---- openflexure_microscope/api/exceptions.py | 4 +- openflexure_microscope/api/utilities.py | 9 ++- .../api/v1/blueprints/base.py | 2 +- .../api/v1/blueprints/camera/capture.py | 15 +++-- .../api/v1/blueprints/camera/function.py | 6 +- .../api/v1/blueprints/camera/preview.py | 4 +- .../api/v1/blueprints/plugins.py | 9 +-- .../api/v1/blueprints/stage.py | 8 +-- .../api/v1/blueprints/task.py | 5 +- openflexure_microscope/api/v1/views.py | 2 +- openflexure_microscope/camera/base.py | 62 +++++++++---------- openflexure_microscope/camera/capture.py | 47 +++++++------- openflexure_microscope/config.py | 2 - openflexure_microscope/exceptions.py | 4 +- openflexure_microscope/lock.py | 2 +- openflexure_microscope/microscope.py | 1 - .../plugins/default/autofocus/api.py | 6 +- .../plugins/default/autofocus/focus_utils.py | 11 ++-- .../plugins/default/autofocus/plugin.py | 4 +- .../default/camera_calibration/plugin.py | 20 +++--- .../camera_calibration/recalibrate_utils.py | 56 ++++++++++------- openflexure_microscope/plugins/example/api.py | 6 +- .../plugins/example/plugin.py | 2 +- openflexure_microscope/plugins/loader.py | 15 ++--- .../plugins/testing/plugin.py | 2 +- openflexure_microscope/stage/openflexure.py | 5 +- openflexure_microscope/task.py | 7 +-- openflexure_microscope/utilities.py | 27 ++++---- tests/api_client.py | 3 +- tests/test_api.py | 14 ++--- tests/test_camera.py | 6 +- tests/test_plugins.py | 1 - tests/test_stage.py | 14 +---- utilities/recalibrate.py | 4 +- 36 files changed, 186 insertions(+), 218 deletions(-) diff --git a/openflexure_microscope/__init__.py b/openflexure_microscope/__init__.py index ae62d4e0..f372f2fb 100644 --- a/openflexure_microscope/__init__.py +++ b/openflexure_microscope/__init__.py @@ -5,4 +5,4 @@ from .microscope import Microscope from . import config from . import utilities from . import task -from . import lock \ No newline at end of file +from . import lock diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index 4f293f31..38b96646 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -1,31 +1,16 @@ #!/usr/bin/env python """ 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 ( - Flask, render_template, Response, url_for, - redirect, request, jsonify, send_file, abort, - make_response) - -from flask.views import MethodView -from werkzeug.exceptions import default_exceptions -from serial import SerialException + Flask, render_template) from flask_cors import CORS -from openflexure_microscope.api.utilities import list_routes from openflexure_microscope.api.exceptions import JSONExceptionHandler from openflexure_microscope import Microscope -from openflexure_microscope.exceptions import LockError from openflexure_microscope.camera.pi import StreamingCamera from openflexure_microscope.stage.openflexure import Stage diff --git a/openflexure_microscope/api/exceptions.py b/openflexure_microscope/api/exceptions.py index fd605aca..d0bc489e 100644 --- a/openflexure_microscope/api/exceptions.py +++ b/openflexure_microscope/api/exceptions.py @@ -2,6 +2,7 @@ from flask import jsonify from werkzeug.exceptions import default_exceptions from werkzeug.exceptions import HTTPException + class JSONExceptionHandler(object): def __init__(self, app=None): @@ -24,7 +25,6 @@ class JSONExceptionHandler(object): } return jsonify(response) - def init_app(self, app): self.app = app self.register(HTTPException) @@ -32,4 +32,4 @@ class JSONExceptionHandler(object): self.register(code) def register(self, exception_or_code, handler=None): - self.app.errorhandler(exception_or_code)(handler or self.std_handler) \ No newline at end of file + self.app.errorhandler(exception_or_code)(handler or self.std_handler) diff --git a/openflexure_microscope/api/utilities.py b/openflexure_microscope/api/utilities.py index 95f91853..3a96ede9 100644 --- a/openflexure_microscope/api/utilities.py +++ b/openflexure_microscope/api/utilities.py @@ -1,8 +1,8 @@ import pprint import logging -import copy from werkzeug.exceptions import BadRequest + class JsonPayload: def __init__(self, request): """ @@ -60,10 +60,9 @@ def gen(camera): def get_bool(get_arg): """Convert GET request argument string to a Python bool""" - if ( - get_arg == 'true' or - get_arg == 'True' or - get_arg == '1'): + if (get_arg == 'true' or + get_arg == 'True' or + get_arg == '1'): return True else: return False diff --git a/openflexure_microscope/api/v1/blueprints/base.py b/openflexure_microscope/api/v1/blueprints/base.py index 681fe12d..022015c7 100644 --- a/openflexure_microscope/api/v1/blueprints/base.py +++ b/openflexure_microscope/api/v1/blueprints/base.py @@ -197,4 +197,4 @@ def construct_blueprint(microscope_obj): view_func=ConfigAPI.as_view('config', microscope=microscope_obj) ) - return(blueprint) + return blueprint diff --git a/openflexure_microscope/api/v1/blueprints/camera/capture.py b/openflexure_microscope/api/v1/blueprints/camera/capture.py index 46a5fcad..0ce668ea 100644 --- a/openflexure_microscope/api/v1/blueprints/camera/capture.py +++ b/openflexure_microscope/api/v1/blueprints/camera/capture.py @@ -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.utilities import filter_dict -from flask import Response, Blueprint, jsonify, request, abort, url_for, redirect, send_file - -import logging +from flask import Response, jsonify, request, abort, url_for, redirect, send_file class ListAPI(MicroscopeView): @@ -368,7 +366,7 @@ class MetadataAPI(MicroscopeView): # If no filename is specified, redirect to the capture's currently set 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 data = capture_obj.yaml @@ -377,6 +375,7 @@ class MetadataAPI(MicroscopeView): data, mimetype="text/yaml") + class TagsAPI(MicroscopeView): def get(self, capture_id): """ @@ -402,7 +401,7 @@ class TagsAPI(MicroscopeView): if not capture_obj or not capture_obj.state['available']: 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) @@ -440,7 +439,7 @@ class TagsAPI(MicroscopeView): for tag in data_dict: 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) @@ -477,6 +476,6 @@ class TagsAPI(MicroscopeView): for tag in data_dict: 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) \ No newline at end of file + return jsonify(metadata_tags) diff --git a/openflexure_microscope/api/v1/blueprints/camera/function.py b/openflexure_microscope/api/v1/blueprints/camera/function.py index 5e8553a4..aee019d1 100644 --- a/openflexure_microscope/api/v1/blueprints/camera/function.py +++ b/openflexure_microscope/api/v1/blueprints/camera/function.py @@ -1,9 +1,7 @@ from openflexure_microscope.api.v1.views import MicroscopeView from openflexure_microscope.api.utilities import JsonPayload -from flask import Response, Blueprint, jsonify, request, abort, url_for, redirect, send_file - -import logging +from flask import jsonify, request class ZoomAPI(MicroscopeView): @@ -102,4 +100,4 @@ class OverlayAPI(MicroscopeView): self.microscope.camera.camera.annotate_text = text self.microscope.camera.camera.annotate_text_size = size - return jsonify({'text': text, 'size': size}) \ No newline at end of file + return jsonify({'text': text, 'size': size}) diff --git a/openflexure_microscope/api/v1/blueprints/camera/preview.py b/openflexure_microscope/api/v1/blueprints/camera/preview.py index 284f0018..2150712f 100644 --- a/openflexure_microscope/api/v1/blueprints/camera/preview.py +++ b/openflexure_microscope/api/v1/blueprints/camera/preview.py @@ -1,8 +1,6 @@ from openflexure_microscope.api.v1.views import MicroscopeView -from flask import Response, Blueprint, jsonify, request, abort, url_for, redirect, send_file - -import logging +from flask import jsonify class GPUPreviewAPI(MicroscopeView): diff --git a/openflexure_microscope/api/v1/blueprints/plugins.py b/openflexure_microscope/api/v1/blueprints/plugins.py index 97d9bd39..9aca0f80 100644 --- a/openflexure_microscope/api/v1/blueprints/plugins.py +++ b/openflexure_microscope/api/v1/blueprints/plugins.py @@ -1,11 +1,12 @@ 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__) @@ -56,4 +57,4 @@ def construct_blueprint(microscope_obj, plugin_paths=[], include_default=True): "No valid 'api_views' dictionary found in {}".format(plugin_obj) ) - return(blueprint) + return blueprint diff --git a/openflexure_microscope/api/v1/blueprints/stage.py b/openflexure_microscope/api/v1/blueprints/stage.py index a6a08d38..9cbddd8f 100644 --- a/openflexure_microscope/api/v1/blueprints/stage.py +++ b/openflexure_microscope/api/v1/blueprints/stage.py @@ -2,7 +2,7 @@ from openflexure_microscope.api.utilities import gen, JsonPayload from openflexure_microscope.api.v1.views import MicroscopeView 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 @@ -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) def post(self): @@ -85,7 +85,7 @@ class PositionAPI(MicroscopeView): with self.microscope.stage.lock: 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) @@ -99,4 +99,4 @@ def construct_blueprint(microscope_obj): view_func=PositionAPI.as_view('position', microscope=microscope_obj) ) - return(blueprint) + return blueprint diff --git a/openflexure_microscope/api/v1/blueprints/task.py b/openflexure_microscope/api/v1/blueprints/task.py index 173ca2b2..5ea27980 100644 --- a/openflexure_microscope/api/v1/blueprints/task.py +++ b/openflexure_microscope/api/v1/blueprints/task.py @@ -1,6 +1,7 @@ from openflexure_microscope.api.v1.views import MicroscopeView from flask import jsonify, abort, Blueprint + class TaskListAPI(MicroscopeView): def get(self): @@ -65,6 +66,7 @@ class TaskListAPI(MicroscopeView): return jsonify(data) + class TaskAPI(MicroscopeView): def get(self, task_id): @@ -135,6 +137,7 @@ class TaskAPI(MicroscopeView): return jsonify(data) + def construct_blueprint(microscope_obj): blueprint = Blueprint('task_blueprint', __name__) @@ -149,4 +152,4 @@ def construct_blueprint(microscope_obj): view_func=TaskAPI.as_view('task', microscope=microscope_obj) ) - return(blueprint) + return blueprint diff --git a/openflexure_microscope/api/v1/views.py b/openflexure_microscope/api/v1/views.py index e4d67efc..3c32ad44 100644 --- a/openflexure_microscope/api/v1/views.py +++ b/openflexure_microscope/api/v1/views.py @@ -23,4 +23,4 @@ class MicroscopeViewPlugin(MicroscopeView): self.plugin = plugin - MicroscopeView.__init__(self, microscope=microscope, **kwargs) \ No newline at end of file + MicroscopeView.__init__(self, microscope=microscope, **kwargs) diff --git a/openflexure_microscope/camera/base.py b/openflexure_microscope/camera/base.py index 0fc9438f..86bab7c1 100644 --- a/openflexure_microscope/camera/base.py +++ b/openflexure_microscope/camera/base.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- import time -import io import os import threading -from PIL import Image import datetime import yaml import logging @@ -35,6 +33,18 @@ def generate_basename(): 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): """ 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() -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): """ Base implementation of StreamingCamera. @@ -155,7 +153,7 @@ class BaseCamera(object): # 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.""" timeout_time = time.time() + timeout @@ -176,7 +174,7 @@ class BaseCamera(object): time.sleep(0) 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.""" logging.debug("Stopping worker thread") timeout_time = time.time() + timeout @@ -217,13 +215,13 @@ class BaseCamera(object): """Return the latest recorded video.""" 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 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 entry_by_id(id, self.videos) + return entry_by_id(video_id, self.videos) # MANAGE CAPTURE DATABASE @@ -277,25 +275,24 @@ class BaseCamera(object): def new_image( self, - write_to_file: bool=False, - temporary: bool=True, - filename: str=None, - fmt: str='jpeg', - shunt_others: bool=True): + write_to_file: bool = False, + temporary: bool = True, + filename: str = None, + fmt: str = 'jpeg'): """ Create a new image capture object. Adds to the image list, and shunt all others. Args: 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. fmt (str): Format of the capture. """ # Generate file name if not filename: - filename = generate_basename(self.images) + filename = generate_numbered_basename(self.images) logging.debug(filename) # Create capture object @@ -317,11 +314,10 @@ class BaseCamera(object): def new_video( self, - write_to_file: bool=True, - temporary: bool=False, - filename: str=None, - fmt: str='h264', - shunt_others: bool=True): + write_to_file: bool = True, + temporary: bool = False, + filename: str = None, + fmt: str = 'h264'): """ 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 if not filename: - filename = generate_basename(self.videos) + filename = generate_numbered_basename(self.videos) logging.debug(filename) # Create capture object diff --git a/openflexure_microscope/camera/capture.py b/openflexure_microscope/camera/capture.py index a87b9f4f..10bfae17 100644 --- a/openflexure_microscope/camera/capture.py +++ b/openflexure_microscope/camera/capture.py @@ -15,6 +15,7 @@ thumbnail_size = (60, 60) BASE_CAPTURE_PATH = os.path.join(os.path.expanduser('~'), 'micrographs') TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, 'tmp') + def clear_tmp(): global TEMP_CAPTURE_PATH @@ -24,6 +25,7 @@ def clear_tmp(): os.remove(f) logging.debug("Removed {}".format(f)) + def capture_from_dict(capture_dict): 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. """ + def __init__( self, - write_to_file: bool=False, - temporary: bool=False, - filename: str='', - folder: str='', - fmt: str='', - create_metadata_file: bool=True) -> None: + write_to_file: bool = False, + temporary: bool = False, + filename: str = '', + folder: str = '', + fmt: str = '', + create_metadata_file: bool = True) -> None: """Create a new StreamObject, to manage capture data.""" # Store a nice ID @@ -102,7 +105,9 @@ class CaptureObject(object): def __enter__(self): """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.context_manager = True # Used in metadata @@ -129,7 +134,7 @@ class CaptureObject(object): else: logging.debug("Target for {} set to 'file'".format(self.id)) self.stream = self.file - + # Save initial metadata file if create_metadata_file: self.save_metadata() @@ -170,7 +175,8 @@ class CaptureObject(object): def split_file_path(self, filepath): """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.metadataname = "{}.yaml".format(self.basename) @@ -234,18 +240,10 @@ class CaptureObject(object): @property def metadata(self) -> dict: # Create basic metadata dictionary - d = { - 'id': self.id, - 'filename': self.filename, - 'path': self.file, - 'time': self.timestring, - 'format': self.format, - 'tags': self.tags - } + d = {'id': self.id, 'filename': self.filename, 'path': self.file, 'time': self.timestring, + 'format': self.format, 'tags': self.tags, 'custom': self._metadata} # Add custom metadata to dictionary - d['custom'] = self._metadata - return d @property @@ -261,14 +259,10 @@ class CaptureObject(object): """Return dictionary of StreamObject properties.""" # Create basic state dictionary - d = { - 'locked': self.locked, - 'temporary': self.temporary, - } + d = {'locked': self.locked, 'temporary': self.temporary, 'metadata': self.metadata, + 'metadata_path': self.metadata_file} # Add metadata to state - d['metadata'] = self.metadata - d['metadata_path'] = self.metadata_file # Check bytestream if self.stream_exists: @@ -404,4 +398,5 @@ class CaptureObject(object): if self.temporary: self.delete() -atexit.register(clear_tmp) \ No newline at end of file + +atexit.register(clear_tmp) diff --git a/openflexure_microscope/config.py b/openflexure_microscope/config.py index 5a66a307..7dc3ee19 100644 --- a/openflexure_microscope/config.py +++ b/openflexure_microscope/config.py @@ -36,7 +36,6 @@ def to_map(data, func): Args: data: Input iterable data 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 isinstance(data, abc.Mapping): @@ -58,7 +57,6 @@ def json_map(data, clean_keys=True): Args: data: Input dictionary clean_keys: Modify any keys unsuitable for JSON return - """ # Do not overwrite original data dictionary diff --git a/openflexure_microscope/exceptions.py b/openflexure_microscope/exceptions.py index ee526ae4..a1136933 100644 --- a/openflexure_microscope/exceptions.py +++ b/openflexure_microscope/exceptions.py @@ -1,8 +1,10 @@ from threading import ThreadError + class TaskDeniedException(Exception): pass + class LockError(ThreadError): ERROR_CODES = { 'ACQUIRE_ERROR': "Unable to acquire. Lock in use by another thread.", @@ -22,4 +24,4 @@ class LockError(ThreadError): ThreadError.__init__(self) def __str__(self): - return self.string \ No newline at end of file + return self.string diff --git a/openflexure_microscope/lock.py b/openflexure_microscope/lock.py index 20ee0a74..566ef80e 100644 --- a/openflexure_microscope/lock.py +++ b/openflexure_microscope/lock.py @@ -53,4 +53,4 @@ class CompositeLock(object): def release(self): for lock in self.locks: - lock.release() \ No newline at end of file + lock.release() diff --git a/openflexure_microscope/microscope.py b/openflexure_microscope/microscope.py index 626be21a..db84b0b3 100644 --- a/openflexure_microscope/microscope.py +++ b/openflexure_microscope/microscope.py @@ -3,7 +3,6 @@ Defines a microscope object, binding a camera and stage with basic functionality. """ import logging -import os import numpy as np import uuid diff --git a/openflexure_microscope/plugins/default/autofocus/api.py b/openflexure_microscope/plugins/default/autofocus/api.py index 3f08ff58..7c211051 100644 --- a/openflexure_microscope/plugins/default/autofocus/api.py +++ b/openflexure_microscope/plugins/default/autofocus/api.py @@ -3,21 +3,21 @@ import numpy as np from openflexure_microscope.api.v1.views import MicroscopeViewPlugin from openflexure_microscope.api.utilities import JsonPayload -from flask import request, Response, escape, jsonify +from flask import request, jsonify -import logging class MeasureSharpnessAPI(MicroscopeViewPlugin): def post(self): payload = JsonPayload(request) return jsonify({'sharpness': self.plugin.measure_sharpness()}) + class AutofocusAPI(MicroscopeViewPlugin): def post(self): payload = JsonPayload(request) # 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...") task = self.microscope.task.start(self.plugin.autofocus, dz) diff --git a/openflexure_microscope/plugins/default/autofocus/focus_utils.py b/openflexure_microscope/plugins/default/autofocus/focus_utils.py index d1471627..45fd1483 100644 --- a/openflexure_microscope/plugins/default/autofocus/focus_utils.py +++ b/openflexure_microscope/plugins/default/autofocus/focus_utils.py @@ -1,21 +1,24 @@ import numpy as np from scipy import ndimage + def decimate_to(shape, image): """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))) return image[::int(decimation), ::int(decimation), ...] + def sharpness_sum_lap2(rgb_image): """Return an image sharpness metric: sum(laplacian(image)**")""" - #image_bw=np.mean(decimate_to((1000,1000), rgb_image),2) - image_bw=np.mean(rgb_image, 2) - image_lap=ndimage.filters.laplace(image_bw) + # image_bw=np.mean(decimate_to((1000,1000), rgb_image),2) + image_bw = np.mean(rgb_image, 2) + image_lap = ndimage.filters.laplace(image_bw) return np.mean(image_lap.astype(np.float)**4) + def sharpness_edge(image): """Return a sharpness metric optimised for vertical lines""" gray = np.mean(image.astype(float), 2) n = 20 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]]) diff --git a/openflexure_microscope/plugins/default/autofocus/plugin.py b/openflexure_microscope/plugins/default/autofocus/plugin.py index 1d0ec19c..685dfe0c 100644 --- a/openflexure_microscope/plugins/default/autofocus/plugin.py +++ b/openflexure_microscope/plugins/default/autofocus/plugin.py @@ -34,7 +34,7 @@ class AutofocusPlugin(MicroscopePlugin): positions = [] 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]) time.sleep(settle) sharpnesses.append(self.measure_sharpness(metric_fn)) @@ -46,4 +46,4 @@ class AutofocusPlugin(MicroscopePlugin): def measure_sharpness(self, metric_fn=sharpness_sum_lap2): """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))) diff --git a/openflexure_microscope/plugins/default/camera_calibration/plugin.py b/openflexure_microscope/plugins/default/camera_calibration/plugin.py index 8d7fe673..49a09726 100644 --- a/openflexure_microscope/plugins/default/camera_calibration/plugin.py +++ b/openflexure_microscope/plugins/default/camera_calibration/plugin.py @@ -1,28 +1,26 @@ -import time -import numpy as np - 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.utilities import JsonPayload -from flask import request, Response, escape, jsonify +from flask import request, jsonify import logging from .recalibrate_utils import recalibrate_camera + class RecalibrateAPIView(MicroscopeViewPlugin): def post(self): 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...") 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) + class Plugin(MicroscopePlugin): """ A set of default plugins @@ -45,16 +43,14 @@ class Plugin(MicroscopePlugin): streaming = scamera.state['stream_active'] if streaming: 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 try: - scamera.camera.resolution=(640,480) + scamera.camera.resolution = (640, 480) recalibrate_camera(scamera.camera) finally: - scamera.camera.resolution=old_resolution + scamera.camera.resolution = old_resolution self.microscope.save_config() if streaming: logging.info("Restarting stream after recalibration") scamera.start_stream_recording() - - diff --git a/openflexure_microscope/plugins/default/camera_calibration/recalibrate_utils.py b/openflexure_microscope/plugins/default/camera_calibration/recalibrate_utils.py index 6ecc84b6..95ff46b8 100644 --- a/openflexure_microscope/plugins/default/camera_calibration/recalibrate_utils.py +++ b/openflexure_microscope/plugins/default/camera_calibration/recalibrate_utils.py @@ -3,12 +3,14 @@ from picamera import PiCamera from picamera.array import PiRGBArray, PiBayerArray import time + def rgb_image(camera, resize=None, **kwargs): """Capture an image and return an RGB numpy array""" with PiRGBArray(camera, size=resize) as output: camera.capture(output, format='rgb', resize=resize, **kwargs) return output.array + def flat_lens_shading_table(camera): """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") return np.zeros(camera._lens_shading_table_shape(), dtype=np.uint8) + 32 + def adjust_exposure_to_setpoint(camera, setpoint): """Adjust the camera's exposure time until the maximum pixel value is .""" print("Adjusting shutter speed to hit setpoint {}".format(setpoint), end="") @@ -29,11 +32,12 @@ def adjust_exposure_to_setpoint(camera, setpoint): time.sleep(1) print("done") + def auto_expose_and_freeze_settings(camera): """Freeze the settings after auto-exposing to white illumination""" print("Allowing the camera to auto-expose") - camera.awb_mode="auto" - camera.exposure_mode="auto" + camera.awb_mode = "auto" + camera.exposure_mode = "auto" for i in range(6): print(".", end="") time.sleep(0.5) @@ -54,18 +58,19 @@ def auto_expose_and_freeze_settings(camera): def channels_from_bayer_array(bayer_array): """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) for i, offset in enumerate(bayer_pattern): # 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 + def lst_from_channels(channels): """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 - #lst_resolution = list(np.ceil(full_resolution / 64.0).astype(int)) + 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 = [(r // 64) + 1 for r in full_resolution] # 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)) @@ -73,7 +78,7 @@ def lst_from_channels(channels): for i in range(lens_shading.shape[0]): image_channel = channels[i, :, :] iw, ih = image_channel.shape - ls_channel = lens_shading[i,:,:] + ls_channel = lens_shading[i, :, :] lw, lh = ls_channel.shape # The lens shading table is rounded **up** in size to 1/64th of the size of # the image. Rather than handle edge images separately, I'm just going to @@ -84,19 +89,23 @@ def lst_from_channels(channels): # less computationally efficient! padded_image_channel = np.pad(image_channel, [(0, lw*32 - iw), (0, lh*32 - ih)], - 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)) + 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)) # Next, fill the shading table (except edge pixels). Please excuse the # for loop - I know it's not fast but this code needn't be! - box = 3 # We average together a square of this side length for each pixel. + box = 3 # We average together a square of this side length for each pixel. # NB this isn't quite what 6by9's program does - it averages 3 pixels # horizontally, but not vertically. for dx in np.arange(box) - box//2: 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 # 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: ls_channel /= np.max(ls_channel) # 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 # lens shading - that's 1/lens_shading_table_float as we currently have it. - gains = 32.0/lens_shading # 32 is unity gain - gains[gains > 255] = 255 # clip at 255, maximum gain is 255/32 - gains[gains < 32] = 32 # clip at 32, minimum gain is 1 (is this necessary?) + gains = 32.0/lens_shading # 32 is unity gain + gains[gains > 255] = 255 # clip at 255, maximum gain is 255/32 + gains[gains < 32] = 32 # clip at 32, minimum gain is 1 (is this necessary?) lens_shading_table = gains.astype(np.uint8) - return lens_shading_table[::-1,:,:].copy() + return lens_shading_table[::-1, :, :].copy() + def recalibrate_camera(camera): """Reset the lens shading table and exposure settings. @@ -124,7 +134,7 @@ def recalibrate_camera(camera): ``StreamingCamera``. """ 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: 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. # 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 - # channels, 1/2 for green because there's twize as many green pixels). + # 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 twice as many green pixels). channels = channels_from_bayer_array(raw_image) lens_shading_table = lst_from_channels(channels) - camera.lens_shading_table=lens_shading_table - test = rgb_image(camera) + camera.lens_shading_table = lens_shading_table + _ = rgb_image(camera) # 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) 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) # Ensure the background is bright but not saturated adjust_exposure_to_setpoint(camera, 230) @@ -157,4 +168,3 @@ if __name__ == "__main__": recalibrate_camera(camera) print("Done.") time.sleep(2) - diff --git a/openflexure_microscope/plugins/example/api.py b/openflexure_microscope/plugins/example/api.py index f6f38cf7..0afe6dd2 100644 --- a/openflexure_microscope/plugins/example/api.py +++ b/openflexure_microscope/plugins/example/api.py @@ -4,8 +4,6 @@ from openflexure_microscope.exceptions import TaskDeniedException from flask import request, Response, escape, jsonify, abort -import logging - class IdentifyAPI(MicroscopeViewPlugin): """ @@ -53,6 +51,7 @@ class HelloWorldAPI(MicroscopeViewPlugin): return Response(self.microscope.plugin_string) + class LongRunningAPI(MicroscopeViewPlugin): """ An example API plugin that uses a long-running plugin method. @@ -75,6 +74,7 @@ class LongRunningAPI(MicroscopeViewPlugin): except TaskDeniedException: return abort(409) + class SomeExceptionAPI(MicroscopeViewPlugin): """ An example API plugin that uses a long-running but broken plugin method. @@ -92,4 +92,4 @@ class SomeExceptionAPI(MicroscopeViewPlugin): return jsonify(task.state), 202 except TaskDeniedException: - return abort(409) \ No newline at end of file + return abort(409) diff --git a/openflexure_microscope/plugins/example/plugin.py b/openflexure_microscope/plugins/example/plugin.py index 1afebf09..9c7335ea 100644 --- a/openflexure_microscope/plugins/example/plugin.py +++ b/openflexure_microscope/plugins/example/plugin.py @@ -58,4 +58,4 @@ class Plugin(MicroscopePlugin): print("Long-running task finished! Releasing locks.") - return n_array \ No newline at end of file + return n_array diff --git a/openflexure_microscope/plugins/loader.py b/openflexure_microscope/plugins/loader.py index 565d2089..ef563984 100644 --- a/openflexure_microscope/plugins/loader.py +++ b/openflexure_microscope/plugins/loader.py @@ -4,7 +4,7 @@ import inspect import logging -class bcolors: +class ConColors: HEADER = '\033[95m' OKBLUE = '\033[94m' OKGREEN = '\033[92m' @@ -23,7 +23,7 @@ def module_from_file(plugin_path): # Check if the path is to a file 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 else: @@ -63,6 +63,7 @@ def check_module(module_path): # If all checks pass, return True return True + def name_from_module(plugin_path): path_array = plugin_path.split('.') @@ -100,7 +101,7 @@ def load_plugin_class(plugin_path, plugin_class_name): try: plugin_class = getattr(plugin_module, plugin_class_name) 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 else: return plugin_class, plugin_name @@ -112,7 +113,7 @@ def class_from_map(plugin_map): plugin_arr = plugin_map.split(':') 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 else: return load_plugin_class(*plugin_arr) @@ -155,7 +156,7 @@ class PluginMount(object): plugin_object = plugin_class() 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 # Attach plugin_object to the plugin mount @@ -165,10 +166,10 @@ class PluginMount(object): # Grant plugin access to the hardware 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. diff --git a/openflexure_microscope/plugins/testing/plugin.py b/openflexure_microscope/plugins/testing/plugin.py index 61f98afe..719eb62b 100644 --- a/openflexure_microscope/plugins/testing/plugin.py +++ b/openflexure_microscope/plugins/testing/plugin.py @@ -10,4 +10,4 @@ class Plugin(MicroscopePlugin): """ Tests for access to Microscope.camera, and Microscope.stage """ - return (self.microscope.camera, self.microscope.stage) + return self.microscope.camera, self.microscope.stage diff --git a/openflexure_microscope/stage/openflexure.py b/openflexure_microscope/stage/openflexure.py index 73d5a380..a08755b0 100644 --- a/openflexure_microscope/stage/openflexure.py +++ b/openflexure_microscope/stage/openflexure.py @@ -5,6 +5,7 @@ from openflexure_microscope.lock import StrictLock import logging + # TODO: Implement lock on movement class Stage(OpenFlexureStage): def __init__(self, *args, **kwargs): @@ -16,7 +17,7 @@ class Stage(OpenFlexureStage): try: OpenFlexureStage.__init__(self, *args, **kwargs) - except SerialException as e: + except SerialException: logging.error("No stage found. Aborting stage.") logging.warning("Stage lock can be acquired, but any stage methods will fail and raise exceptions.") @@ -25,4 +26,4 @@ class Stage(OpenFlexureStage): Overrides :py:function:`openflexure_stage.stage.OpenFlexureStage._move_rel_nobacklash` to acquire lock first. """ with self.lock: - OpenFlexureStage._move_rel_nobacklash(self, *args, **kwargs) \ No newline at end of file + OpenFlexureStage._move_rel_nobacklash(self, *args, **kwargs) diff --git a/openflexure_microscope/task.py b/openflexure_microscope/task.py index 3f75f73c..5575c430 100644 --- a/openflexure_microscope/task.py +++ b/openflexure_microscope/task.py @@ -1,14 +1,13 @@ from threading import Thread -from functools import wraps import datetime import logging import traceback -import time import uuid from openflexure_microscope.exceptions import TaskDeniedException from openflexure_microscope.utilities import entry_by_id + class TaskOrchestrator: """ 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. 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 @@ -139,4 +138,4 @@ class Task: self._running = True # Set start command thread = Thread(target=self.run) # Define thread thread.daemon = True # Stop this thread when main thread closes - thread.start() # Start thread \ No newline at end of file + thread.start() # Start thread diff --git a/openflexure_microscope/utilities.py b/openflexure_microscope/utilities.py index ba926b34..07b2426e 100644 --- a/openflexure_microscope/utilities.py +++ b/openflexure_microscope/utilities.py @@ -1,9 +1,9 @@ import copy import operator -from fractions import Fraction from functools import reduce from contextlib import contextmanager + @contextmanager def set_properties(obj, **kwargs): """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(): 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""" # If no base array is given 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 -def filter_dict(dictionary: dict, keys: list): - # Get value by recursively applying getitem - val = reduce(operator.getitem, keys, dictionary) - - # Create new dictionary by running reduce on key, val pairs - out = reduce(lambda x, y: {y: x}, reversed(keys), val) - - return out -def entry_by_id(id: str, object_list: list): +def filter_dict(dictionary: dict, keys: list): + # Get value by recursively applying getitem + val = reduce(operator.getitem, keys, dictionary) + + # Create new dictionary by running reduce on key, val pairs + out = reduce(lambda x, y: {y: x}, reversed(keys), val) + + return out + + +def entry_by_id(entry_id: str, object_list: list): """Return an object from a list, if .id matches id argument.""" found = None for o in object_list: - if o.id == id: + if o.id == entry_id: found = o return found diff --git a/tests/api_client.py b/tests/api_client.py index 160e3d6d..4201b6d3 100644 --- a/tests/api_client.py +++ b/tests/api_client.py @@ -3,6 +3,7 @@ from PIL import Image from io import BytesIO import numpy as np + 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) @@ -100,4 +101,4 @@ class APIconnection: return self.post('/camera/zoom', json=json) def get_zoom(self): - return self.get('/camera/zoom') \ No newline at end of file + return self.get('/camera/zoom') diff --git a/tests/test_api.py b/tests/test_api.py index 4d2681cd..1cbfd09c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,19 +1,11 @@ #!/usr/bin/env python from api_client import APIconnection -import os -import io -import sys -import time import numpy as np -import uuid - -from PIL import Image - import unittest -from pprint import pprint -import logging, sys +import logging +import sys 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)) + class TestStage(unittest.TestCase): def test_stage_config(self): @@ -114,6 +107,7 @@ class TestStage(unittest.TestCase): self.assertTrue(np.array_equal(diff, move)) + if __name__ == '__main__': suites = [ diff --git a/tests/test_camera.py b/tests/test_camera.py index 0390f43f..d923c13d 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -3,17 +3,15 @@ from openflexure_microscope.camera.pi import StreamingCamera, CaptureObject import os import io -import sys import time import numpy as np -import uuid from PIL import Image import unittest -from pprint import pprint -import logging, sys +import logging +import sys logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 5c6eae3f..a59ffb90 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -46,4 +46,3 @@ if __name__ == '__main__': alltests = unittest.TestSuite(suites) result = unittest.TextTestRunner(verbosity=2).run(alltests) - diff --git a/tests/test_stage.py b/tests/test_stage.py index d9fb0497..90b147be 100644 --- a/tests/test_stage.py +++ b/tests/test_stage.py @@ -1,21 +1,13 @@ #!/usr/bin/env python -from openflexure_microscope.camera.pi import StreamingCamera from openflexure_stage import OpenFlexureStage -from openflexure_microscope import Microscope -import os -import io -import sys -import time import numpy as np -import uuid - -from PIL import Image import unittest -from pprint import pprint -import logging, sys +import logging +import sys + logging.basicConfig(stream=sys.stderr, level=logging.INFO) diff --git a/utilities/recalibrate.py b/utilities/recalibrate.py index 6ec27794..0121db31 100644 --- a/utilities/recalibrate.py +++ b/utilities/recalibrate.py @@ -4,9 +4,7 @@ from openflexure_microscope.camera.pi import StreamingCamera from openflexure_microscope import config import numpy as np -import sys import time -import matplotlib.pyplot as plt import os import logging, sys @@ -164,4 +162,4 @@ def generate_lens_shading_table_closed_loop(output_fname="microscopelst.npy", if __name__ == '__main__': - generate_lens_shading_table_closed_loop() \ No newline at end of file + generate_lens_shading_table_closed_loop()