From 5966ce29be76e203ac2b6797c25a9c5b9e9bf5cb Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Sun, 15 Sep 2019 14:17:52 +0100 Subject: [PATCH] Blackened everything --- openflexure_microscope/__init__.py | 2 +- openflexure_microscope/api/__init__.py | 2 +- openflexure_microscope/api/app.py | 51 +- openflexure_microscope/api/exceptions.py | 7 +- openflexure_microscope/api/utilities.py | 20 +- .../api/v1/blueprints/base.py | 17 +- .../api/v1/blueprints/camera/__init__.py | 63 +- .../api/v1/blueprints/camera/capture.py | 157 +++-- .../api/v1/blueprints/camera/function.py | 16 +- .../api/v1/blueprints/camera/preview.py | 3 +- .../api/v1/blueprints/plugins.py | 50 +- .../api/v1/blueprints/stage.py | 23 +- .../api/v1/blueprints/task.py | 19 +- openflexure_microscope/api/v1/views.py | 2 + openflexure_microscope/camera/base.py | 74 ++- openflexure_microscope/camera/capture.py | 78 ++- openflexure_microscope/camera/pi.py | 254 ++++---- .../camera/piexif/__init__.py | 3 +- .../camera/piexif/_common.py | 25 +- openflexure_microscope/camera/piexif/_dump.py | 120 ++-- openflexure_microscope/camera/piexif/_exif.py | 613 +++++++++--------- .../camera/piexif/_insert.py | 7 +- openflexure_microscope/camera/piexif/_load.py | 262 +++++--- .../camera/piexif/_remove.py | 5 +- .../camera/piexif/_transplant.py | 6 +- openflexure_microscope/camera/piexif/_webp.py | 76 ++- .../camera/piexif/helper.py | 50 +- .../camera/set_picamera_gain.py | 18 +- openflexure_microscope/config.py | 1 - openflexure_microscope/exceptions.py | 4 +- openflexure_microscope/lock.py | 6 +- openflexure_microscope/plugins/__init__.py | 18 +- .../plugins/default/autofocus/__init__.py | 4 +- .../plugins/default/autofocus/api.py | 21 +- .../plugins/default/autofocus/focus_utils.py | 39 +- .../plugins/default/autofocus/plugin.py | 45 +- .../default/camera_calibration/__init__.py | 2 +- .../default/camera_calibration/plugin.py | 18 +- .../camera_calibration/recalibrate_utils.py | 74 ++- .../plugins/default/scan/__init__.py | 2 +- .../plugins/default/scan/api.py | 42 +- .../plugins/default/scan/plugin.py | 194 +++--- .../plugins/example/__init__.py | 4 +- openflexure_microscope/plugins/example/api.py | 13 +- .../plugins/example/plugin.py | 14 +- openflexure_microscope/plugins/loader.py | 85 ++- .../plugins/testing/form_example/__init__.py | 2 +- .../plugins/testing/form_example/api.py | 47 +- .../plugins/testing/form_example/plugin.py | 29 +- openflexure_microscope/stage/__init__.py | 2 +- openflexure_microscope/stage/base.py | 1 + openflexure_microscope/stage/mock.py | 27 +- openflexure_microscope/stage/sanga.py | 55 +- .../extensible_serial_instrument.py | 307 ++++++--- .../stage/sangaboard/sangaboard.py | 227 ++++--- openflexure_microscope/task.py | 28 +- openflexure_microscope/utilities.py | 18 +- 57 files changed, 1938 insertions(+), 1414 deletions(-) diff --git a/openflexure_microscope/__init__.py b/openflexure_microscope/__init__.py index 8cd70322..e1ee2c30 100644 --- a/openflexure_microscope/__init__.py +++ b/openflexure_microscope/__init__.py @@ -1,4 +1,4 @@ -__all__ = ['Microscope', 'config', 'task', 'lock', 'utilities'] +__all__ = ["Microscope", "config", "task", "lock", "utilities"] __version__ = "0.1.0" from .microscope import Microscope diff --git a/openflexure_microscope/api/__init__.py b/openflexure_microscope/api/__init__.py index daa8c0a6..05039c1d 100644 --- a/openflexure_microscope/api/__init__.py +++ b/openflexure_microscope/api/__init__.py @@ -1,3 +1,3 @@ -__all__ = ['utilities'] +__all__ = ["utilities"] from . import utilities diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index cbdfeb71..bf01a0b1 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -6,8 +6,7 @@ import logging import sys import os -from flask import ( - Flask, jsonify, send_file) +from flask import Flask, jsonify, send_file from serial import SerialException from datetime import datetime @@ -39,7 +38,7 @@ from openflexure_microscope.stage.mock import MockStage # Handle logging is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "") -DEFAULT_LOGFILE = os.path.join(USER_CONFIG_DIR, 'openflexure_microscope.log') +DEFAULT_LOGFILE = os.path.join(USER_CONFIG_DIR, "openflexure_microscope.log") if (__name__ == "__main__") or (not is_gunicorn): # If imported, but not by gunicorn @@ -48,18 +47,15 @@ if (__name__ == "__main__") or (not is_gunicorn): else: # Direct standard Python logging to file and console root = logging.getLogger() - error_formatter = logging.Formatter("[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s") - - rotating_logfile = logging.handlers.RotatingFileHandler( - DEFAULT_LOGFILE, - maxBytes=1000000, - backupCount=7 + error_formatter = logging.Formatter( + "[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s" ) - error_handlers = [ - rotating_logfile, - logging.StreamHandler() - ] + rotating_logfile = logging.handlers.RotatingFileHandler( + DEFAULT_LOGFILE, maxBytes=1000000, backupCount=7 + ) + + error_handlers = [rotating_logfile, logging.StreamHandler()] for handler in error_handlers: handler.setFormatter(error_formatter) @@ -88,7 +84,7 @@ def uri(suffix, api_version, base=None): app = Flask(__name__) app.url_map.strict_slashes = False -CORS(app, resources=r'/api/*') +CORS(app, resources=r"/api/*") # Make errors more API friendly handler = JSONExceptionHandler(app) @@ -121,10 +117,7 @@ def attach_microscope(): # Attach devices to microscope logging.debug("Attaching devices to microscope...") - api_microscope.attach( - api_camera, - api_stage - ) + api_microscope.attach(api_camera, api_stage) # Restore loaded capture array to camera object logging.debug("Restoring captures...") @@ -140,26 +133,26 @@ def attach_microscope(): # Base routes base_blueprint = blueprints.base.construct_blueprint(api_microscope) -app.register_blueprint(base_blueprint, url_prefix=uri('', 'v1')) +app.register_blueprint(base_blueprint, url_prefix=uri("", "v1")) # Stage routes stage_blueprint = blueprints.stage.construct_blueprint(api_microscope) -app.register_blueprint(stage_blueprint, url_prefix=uri('/stage', 'v1')) +app.register_blueprint(stage_blueprint, url_prefix=uri("/stage", "v1")) # Camera routes camera_blueprint = blueprints.camera.construct_blueprint(api_microscope) -app.register_blueprint(camera_blueprint, url_prefix=uri('/camera', 'v1')) +app.register_blueprint(camera_blueprint, url_prefix=uri("/camera", "v1")) # Plugin routes plugin_blueprint = blueprints.plugins.construct_blueprint(api_microscope) -app.register_blueprint(plugin_blueprint, url_prefix=uri('/plugin', 'v1')) +app.register_blueprint(plugin_blueprint, url_prefix=uri("/plugin", "v1")) # Task routes task_blueprint = blueprints.task.construct_blueprint(api_microscope) -app.register_blueprint(task_blueprint, url_prefix=uri('/task', 'v1')) +app.register_blueprint(task_blueprint, url_prefix=uri("/task", "v1")) -@app.route('/routes') +@app.route("/routes") def routes(): """ List of all connected API routes @@ -173,7 +166,7 @@ def routes(): return jsonify(list_routes(app)) -@app.route('/log') +@app.route("/log") def err_log(): """ Most recent 1mb of log output @@ -186,9 +179,9 @@ def err_log(): """ timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") return send_file( - DEFAULT_LOGFILE, - as_attachment=True, - attachment_filename='openflexure_microscope_{}.log'.format(timestamp) + DEFAULT_LOGFILE, + as_attachment=True, + attachment_filename="openflexure_microscope_{}.log".format(timestamp), ) @@ -219,4 +212,4 @@ def cleanup(): atexit.register(cleanup) if __name__ == "__main__": - app.run(host='0.0.0.0', port="5000", threaded=True, debug=True, use_reloader=False) + app.run(host="0.0.0.0", port="5000", threaded=True, debug=True, use_reloader=False) diff --git a/openflexure_microscope/api/exceptions.py b/openflexure_microscope/api/exceptions.py index ccda5087..624d5dba 100644 --- a/openflexure_microscope/api/exceptions.py +++ b/openflexure_microscope/api/exceptions.py @@ -17,17 +17,14 @@ class JSONExceptionHandler(object): def std_handler(self, error): if isinstance(error, HTTPException): message = error.description - elif hasattr(error, 'message'): + elif hasattr(error, "message"): message = error.message else: message = str(error) status_code = error.code if isinstance(error, HTTPException) else 500 - response = { - 'status_code': status_code, - 'message': escape(message) - } + response = {"status_code": status_code, "message": escape(message)} return jsonify(response), status_code def init_app(self, app): diff --git a/openflexure_microscope/api/utilities.py b/openflexure_microscope/api/utilities.py index e014f171..60d490db 100644 --- a/openflexure_microscope/api/utilities.py +++ b/openflexure_microscope/api/utilities.py @@ -10,7 +10,9 @@ class JsonResponse: """ # Try to load as json try: - self.json = request.get_json() #: dict: Dictionary representation of request JSON + self.json = ( + request.get_json() + ) #: dict: Dictionary representation of request JSON # If malformed JSON is passed, make an empty dictionary except BadRequest as e: logging.error(e) @@ -55,15 +57,12 @@ def gen(camera): # the obtained frame is a jpeg frame = camera.get_frame() - yield (b'--frame\r\n' - b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') + yield (b"--frame\r\n" b"Content-Type: image/jpeg\r\n\r\n" + frame + b"\r\n") 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 @@ -80,10 +79,7 @@ def list_routes(app): endpoint = rule.endpoint methods = list(rule.methods) url = url_for(rule.endpoint, **options) - line = { - 'endpoint': endpoint, - 'methods': methods - } + line = {"endpoint": endpoint, "methods": methods} output[url] = line - - return output \ No newline at end of file + + return output diff --git a/openflexure_microscope/api/v1/blueprints/base.py b/openflexure_microscope/api/v1/blueprints/base.py index 74e1cc32..81c64ca5 100644 --- a/openflexure_microscope/api/v1/blueprints/base.py +++ b/openflexure_microscope/api/v1/blueprints/base.py @@ -6,7 +6,6 @@ import logging class StreamAPI(MicroscopeView): - def get(self): """ Real-time MJPEG stream from the microscope camera @@ -22,11 +21,11 @@ class StreamAPI(MicroscopeView): return Response( gen(self.microscope.camera), - mimetype='multipart/x-mixed-replace; boundary=frame') + mimetype="multipart/x-mixed-replace; boundary=frame", + ) class StateAPI(MicroscopeView): - def get(self): """ JSON representation of the microscope object. @@ -77,7 +76,6 @@ class StateAPI(MicroscopeView): class ConfigAPI(MicroscopeView): - def get(self): """ JSON representation of the microscope config. @@ -216,21 +214,18 @@ class ConfigAPI(MicroscopeView): def construct_blueprint(microscope_obj): - blueprint = Blueprint('base_blueprint', __name__) + blueprint = Blueprint("base_blueprint", __name__) blueprint.add_url_rule( - '/stream', - view_func=StreamAPI.as_view('stream', microscope=microscope_obj) + "/stream", view_func=StreamAPI.as_view("stream", microscope=microscope_obj) ) blueprint.add_url_rule( - '/state', - view_func=StateAPI.as_view('state', microscope=microscope_obj) + "/state", view_func=StateAPI.as_view("state", microscope=microscope_obj) ) blueprint.add_url_rule( - '/config', - view_func=ConfigAPI.as_view('config', microscope=microscope_obj) + "/config", view_func=ConfigAPI.as_view("config", microscope=microscope_obj) ) return blueprint diff --git a/openflexure_microscope/api/v1/blueprints/camera/__init__.py b/openflexure_microscope/api/v1/blueprints/camera/__init__.py index f21d321e..ab475937 100644 --- a/openflexure_microscope/api/v1/blueprints/camera/__init__.py +++ b/openflexure_microscope/api/v1/blueprints/camera/__init__.py @@ -7,51 +7,70 @@ from . import capture, record, preview, function def construct_blueprint(microscope_obj): - blueprint = Blueprint('camera_blueprint', __name__) + blueprint = Blueprint("camera_blueprint", __name__) # Metadata routes blueprint.add_url_rule( - '/capture//metadata/', - view_func=capture.MetadataAPI.as_view('metadata_download', microscope=microscope_obj)) + "/capture//metadata/", + view_func=capture.MetadataAPI.as_view( + "metadata_download", microscope=microscope_obj + ), + ) blueprint.add_url_rule( - '/capture//metadata', - view_func=capture.MetadataRedirectAPI.as_view('metadata_download_redirect', microscope=microscope_obj)) + "/capture//metadata", + view_func=capture.MetadataRedirectAPI.as_view( + "metadata_download_redirect", microscope=microscope_obj + ), + ) # Tag routes blueprint.add_url_rule( - '/capture//tags', - view_func=capture.TagsAPI.as_view('capture_tags', microscope=microscope_obj)) + "/capture//tags", + view_func=capture.TagsAPI.as_view("capture_tags", microscope=microscope_obj), + ) # Capture routes blueprint.add_url_rule( - '/capture//download/', - view_func=capture.DownloadAPI.as_view('capture_download', microscope=microscope_obj)) + "/capture//download/", + view_func=capture.DownloadAPI.as_view( + "capture_download", microscope=microscope_obj + ), + ) blueprint.add_url_rule( - '/capture//download', - view_func=capture.DownloadRedirectAPI.as_view('capture_download_redirect', microscope=microscope_obj)) + "/capture//download", + view_func=capture.DownloadRedirectAPI.as_view( + "capture_download_redirect", microscope=microscope_obj + ), + ) blueprint.add_url_rule( - '/capture//', - view_func=capture.CaptureAPI.as_view('capture', microscope=microscope_obj)) + "/capture//", + view_func=capture.CaptureAPI.as_view("capture", microscope=microscope_obj), + ) blueprint.add_url_rule( - '/capture/', - view_func=capture.ListAPI.as_view('capture_list', microscope=microscope_obj)) + "/capture/", + view_func=capture.ListAPI.as_view("capture_list", microscope=microscope_obj), + ) # Preview routes blueprint.add_url_rule( - '/preview/', - view_func=preview.GPUPreviewAPI.as_view('gpu_preview', microscope=microscope_obj)) + "/preview/", + view_func=preview.GPUPreviewAPI.as_view( + "gpu_preview", microscope=microscope_obj + ), + ) # Function routes blueprint.add_url_rule( - '/overlay', - view_func=function.OverlayAPI.as_view('overlay', microscope=microscope_obj)) + "/overlay", + view_func=function.OverlayAPI.as_view("overlay", microscope=microscope_obj), + ) blueprint.add_url_rule( - '/zoom', - view_func=function.ZoomAPI.as_view('zoom', microscope=microscope_obj)) + "/zoom", view_func=function.ZoomAPI.as_view("zoom", microscope=microscope_obj) + ) - return(blueprint) \ No newline at end of file + return blueprint diff --git a/openflexure_microscope/api/v1/blueprints/camera/capture.py b/openflexure_microscope/api/v1/blueprints/camera/capture.py index 51bd6b27..9e5ac730 100644 --- a/openflexure_microscope/api/v1/blueprints/camera/capture.py +++ b/openflexure_microscope/api/v1/blueprints/camera/capture.py @@ -6,7 +6,6 @@ from flask import Response, jsonify, request, abort, url_for, redirect, send_fil class ListAPI(MicroscopeView): - def get(self): """ Get list of image captures. @@ -30,12 +29,16 @@ class ListAPI(MicroscopeView): :status 200: capture found :status 404: no capture found with that id """ - include_unavailable = get_bool(request.args.get('include_unavailable')) + include_unavailable = get_bool(request.args.get("include_unavailable")) if include_unavailable: captures = [image.state for image in self.microscope.camera.images] else: - captures = [image.state for image in self.microscope.camera.images if image.state['available']] + captures = [ + image.state + for image in self.microscope.camera.images + if image.state["available"] + ] return jsonify(captures) @@ -100,38 +103,40 @@ class ListAPI(MicroscopeView): """ payload = JsonResponse(request) - filename = payload.param('filename') - temporary = payload.param('temporary', default=False, convert=bool) - use_video_port = payload.param('use_video_port', default=False, convert=bool) - bayer = payload.param('bayer', default=True, convert=bool) - metadata = payload.param('metadata', default={}, convert=dict) - tags = payload.param('tags', default=[], convert=list) + filename = payload.param("filename") + temporary = payload.param("temporary", default=False, convert=bool) + use_video_port = payload.param("use_video_port", default=False, convert=bool) + bayer = payload.param("bayer", default=True, convert=bool) + metadata = payload.param("metadata", default={}, convert=dict) + tags = payload.param("tags", default=[], convert=list) - resize = payload.param('size', default=None) + resize = payload.param("size", default=None) if resize: - if ('width' in resize) and ('height' in resize): - resize = (int(resize['width']), int(resize['height'])) # Convert dict to tuple + if ("width" in resize) and ("height" in resize): + resize = ( + int(resize["width"]), + int(resize["height"]), + ) # Convert dict to tuple else: abort(404) # Explicitally acquire lock (prevents empty files being created if lock is unavailable) with self.microscope.camera.lock: output = self.microscope.camera.new_image( - write_to_file=True, - temporary=temporary, - filename=filename) + write_to_file=True, temporary=temporary, filename=filename + ) self.microscope.camera.capture( - output, - use_video_port=use_video_port, - resize=resize, - bayer=bayer) + output, use_video_port=use_video_port, resize=resize, bayer=bayer + ) - metadata.update({ - 'position': self.microscope.state['stage']['position'], - 'microscope_id': self.microscope.id, - 'microscope_name': self.microscope.name - }) + metadata.update( + { + "position": self.microscope.state["stage"]["position"], + "microscope_id": self.microscope.id, + "microscope_name": self.microscope.name, + } + ) output.put_metadata(metadata) output.put_tags(tags) @@ -140,7 +145,6 @@ class ListAPI(MicroscopeView): class CaptureAPI(MicroscopeView): - def get(self, capture_id): """ Get JSON representation of a capture @@ -200,14 +204,15 @@ class CaptureAPI(MicroscopeView): # Add API routes to returned state uri_dict = { - 'uri': {'state': '{}'.format(url_for('.capture', capture_id=capture_obj.id))} + "uri": { + "state": "{}".format(url_for(".capture", capture_id=capture_obj.id)) + } } # If available, also add download link - if capture_metadata['available']: - uri_dict['uri']['download'] = '{}download/{}'.format( - url_for('.capture', capture_id=capture_obj.id), - capture_obj.filename + if capture_metadata["available"]: + uri_dict["uri"]["download"] = "{}download/{}".format( + url_for(".capture", capture_id=capture_obj.id), capture_obj.filename ) capture_metadata.update(uri_dict) @@ -257,7 +262,7 @@ class CaptureAPI(MicroscopeView): if not capture_obj: return abort(404) # 404 Not Found - + data_dict = JsonResponse(request).json capture_obj.put_metadata(data_dict) @@ -299,17 +304,20 @@ class DownloadRedirectAPI(MicroscopeView): """ capture_obj = self.microscope.camera.image_from_id(capture_id) - 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 - thumbnail = get_bool(request.args.get('thumbnail')) + thumbnail = get_bool(request.args.get("thumbnail")) - return redirect(url_for( - '.capture_download', - capture_id=capture_id, - filename=capture_obj.filename, - thumbnail=thumbnail - ), code=307) + return redirect( + url_for( + ".capture_download", + capture_id=capture_id, + filename=capture_obj.filename, + thumbnail=thumbnail, + ), + code=307, + ) class DownloadAPI(MicroscopeView): @@ -317,19 +325,22 @@ class DownloadAPI(MicroscopeView): capture_obj = self.microscope.camera.image_from_id(capture_id) - 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 - thumbnail = get_bool(request.args.get('thumbnail')) + thumbnail = get_bool(request.args.get("thumbnail")) # 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.filename, - thumbnail=thumbnail - ), code=307) + return redirect( + url_for( + "capture_download", + capture_id=capture_id, + filename=capture_obj.filename, + thumbnail=thumbnail, + ), + code=307, + ) # Download the image data using the requested filename if thumbnail: @@ -337,9 +348,7 @@ class DownloadAPI(MicroscopeView): else: img = capture_obj.data - return send_file( - img, - mimetype='image/jpeg') + return send_file(img, mimetype="image/jpeg") class MetadataRedirectAPI(MicroscopeView): @@ -374,14 +383,17 @@ class MetadataRedirectAPI(MicroscopeView): """ capture_obj = self.microscope.camera.image_from_id(capture_id) - 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 redirect(url_for( - '.metadata_download', - capture_id=capture_id, - filename=capture_obj.metadataname - ), code=307) + return redirect( + url_for( + ".metadata_download", + capture_id=capture_id, + filename=capture_obj.metadataname, + ), + code=307, + ) class MetadataAPI(MicroscopeView): @@ -389,23 +401,24 @@ class MetadataAPI(MicroscopeView): capture_obj = self.microscope.camera.image_from_id(capture_id) - 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 # 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 - ), 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 - return Response( - data, - mimetype="text/yaml") + return Response(data, mimetype="text/yaml") class TagsAPI(MicroscopeView): @@ -430,13 +443,13 @@ class TagsAPI(MicroscopeView): """ capture_obj = self.microscope.camera.image_from_id(capture_id) - 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 - metadata_tags = filter_dict(capture_obj.state, ['metadata', 'tags']) + metadata_tags = filter_dict(capture_obj.state, ["metadata", "tags"]) return jsonify(metadata_tags) - + def put(self, capture_id): """ Add tags to the capture @@ -460,9 +473,9 @@ class TagsAPI(MicroscopeView): capture_obj = self.microscope.camera.image_from_id(capture_id) - 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 - + data_dict = JsonResponse(request).json if type(data_dict) != list: @@ -470,7 +483,7 @@ class TagsAPI(MicroscopeView): capture_obj.put_tags(data_dict) - metadata_tags = filter_dict(capture_obj.state, ['metadata', 'tags']) + metadata_tags = filter_dict(capture_obj.state, ["metadata", "tags"]) return jsonify(metadata_tags) @@ -507,6 +520,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) diff --git a/openflexure_microscope/api/v1/blueprints/camera/function.py b/openflexure_microscope/api/v1/blueprints/camera/function.py index 2720a5a7..15235409 100644 --- a/openflexure_microscope/api/v1/blueprints/camera/function.py +++ b/openflexure_microscope/api/v1/blueprints/camera/function.py @@ -5,7 +5,6 @@ from flask import jsonify, request class ZoomAPI(MicroscopeView): - def get(self): """ Get the current zoom value @@ -17,9 +16,9 @@ class ZoomAPI(MicroscopeView): :
/', - view_func=TaskAPI.as_view('task', microscope=microscope_obj) + "//", view_func=TaskAPI.as_view("task", microscope=microscope_obj) ) return blueprint diff --git a/openflexure_microscope/api/v1/views.py b/openflexure_microscope/api/v1/views.py index 3c32ad44..1ec58243 100644 --- a/openflexure_microscope/api/v1/views.py +++ b/openflexure_microscope/api/v1/views.py @@ -6,6 +6,7 @@ class MicroscopeView(MethodView): Create a generic MethodView with a globally available microscope object passed as an argument. """ + def __init__(self, microscope, **kwargs): self.microscope = microscope @@ -19,6 +20,7 @@ class MicroscopeViewPlugin(MicroscopeView): microscope object passed as an argument, and a plugin reference stored in 'self'. Initially None. """ + def __init__(self, microscope, plugin=None, **kwargs): self.plugin = plugin diff --git a/openflexure_microscope/camera/base.py b/openflexure_microscope/camera/base.py index 1ea62010..1a2c3625 100644 --- a/openflexure_microscope/camera/base.py +++ b/openflexure_microscope/camera/base.py @@ -56,6 +56,7 @@ class CameraEvent(object): An event-like class that signals all active clients when a new frame is available. """ + def __init__(self): self.events = {} @@ -112,9 +113,10 @@ class BaseCamera(metaclass=ABCMeta): images (list): List of image capture objects videos (list): List of video capture objects """ + def __init__(self): self.thread = None - self.camera = None + self.camera = None self.lock = StrictLock(timeout=1) @@ -128,11 +130,11 @@ class BaseCamera(metaclass=ABCMeta): self.state = {} self.paths = { - 'image': BASE_CAPTURE_PATH, - 'video': BASE_CAPTURE_PATH, - 'image_tmp': TEMP_CAPTURE_PATH, - 'video_tpm': TEMP_CAPTURE_PATH - } + "image": BASE_CAPTURE_PATH, + "video": BASE_CAPTURE_PATH, + "image_tmp": TEMP_CAPTURE_PATH, + "video_tpm": TEMP_CAPTURE_PATH, + } # Capture data self.images = [] @@ -176,7 +178,7 @@ class BaseCamera(metaclass=ABCMeta): self.last_access = time.time() self.stop = False - if not self.state['stream_active']: + if not self.state["stream_active"]: # start background frame thread self.thread = threading.Thread(target=self._thread) self.thread.daemon = True @@ -196,12 +198,12 @@ class BaseCamera(metaclass=ABCMeta): logging.debug("Stopping worker thread") timeout_time = time.time() + timeout - if self.state['stream_active']: + if self.state["stream_active"]: self.stop = True self.thread.join() # Wait for stream thread to exit logging.debug("Waiting for stream thread to exit.") - while self.state['stream_active']: + while self.state["stream_active"]: if time.time() > timeout_time: logging.debug("Timeout waiting for worker thread close.") raise TimeoutError("Timeout waiting for worker thread close.") @@ -249,12 +251,13 @@ class BaseCamera(metaclass=ABCMeta): # CREATING NEW CAPTURES def new_image( - self, - write_to_file: bool = True, - temporary: bool = True, - filename: str = None, - folder: str = "", - fmt: str = 'jpeg'): + self, + write_to_file: bool = True, + temporary: bool = True, + filename: str = None, + folder: str = "", + fmt: str = "jpeg", + ): """ Create a new image capture object. Adds to the image list, and shunt all others. @@ -275,7 +278,7 @@ class BaseCamera(metaclass=ABCMeta): filename = "{}.{}".format(filename, fmt) # Generate folder - base_folder = self.paths['image_tmp'] if temporary else self.paths['image'] + base_folder = self.paths["image_tmp"] if temporary else self.paths["image"] folder = os.path.join(base_folder, folder) # Generate file path @@ -283,9 +286,8 @@ class BaseCamera(metaclass=ABCMeta): # Create capture object output = CaptureObject( - write_to_file=write_to_file, - temporary=temporary, - filepath=filepath) + write_to_file=write_to_file, temporary=temporary, filepath=filepath + ) # Update capture list shunt_captures(self.images) @@ -294,12 +296,13 @@ class BaseCamera(metaclass=ABCMeta): return output def new_video( - self, - write_to_file: bool = True, - temporary: bool = False, - filename: str = None, - folder: str = "", - fmt: str = 'h264'): + self, + write_to_file: bool = True, + temporary: bool = False, + filename: str = None, + folder: str = "", + fmt: str = "h264", + ): """ Create a new video capture object. Adds to the image list, and shunt all others. @@ -320,7 +323,7 @@ class BaseCamera(metaclass=ABCMeta): filename = "{}.{}".format(filename, fmt) # Generate folder - base_folder = self.paths['video_tmp'] if temporary else self.paths['video'] + base_folder = self.paths["video_tmp"] if temporary else self.paths["video"] folder = os.path.join(base_folder, folder) # Generate file path @@ -328,9 +331,8 @@ class BaseCamera(metaclass=ABCMeta): # Create capture object output = CaptureObject( - write_to_file=write_to_file, - temporary=temporary, - filepath=filepath) + write_to_file=write_to_file, temporary=temporary, filepath=filepath + ) # Update capture list shunt_captures(self.videos) @@ -345,7 +347,7 @@ class BaseCamera(metaclass=ABCMeta): self.frames_iterator = self.frames() logging.debug("Entering worker thread.") - self.state['stream_active'] = True + self.state["stream_active"] = True for frame in self.frames_iterator: self.frame = frame @@ -354,9 +356,13 @@ class BaseCamera(metaclass=ABCMeta): # Handle timeout if ( - self.stream_timeout_enabled and # If using timeout - (time.time() - self.last_access > self.stream_timeout) and # And timeout time - not self.state['preview_active'] # And GPU preview is not active + self.stream_timeout_enabled + and ( # If using timeout + time.time() - self.last_access > self.stream_timeout + ) + and not self.state[ # And timeout time + "preview_active" + ] # And GPU preview is not active ): self.frames_iterator.close() break @@ -372,4 +378,4 @@ class BaseCamera(metaclass=ABCMeta): logging.debug("BaseCamera worker thread exiting...") # Set stream_activate state - self.state['stream_active'] = False + self.state["stream_active"] = False diff --git a/openflexure_microscope/camera/capture.py b/openflexure_microscope/camera/capture.py index ea789088..f4d8cf9a 100644 --- a/openflexure_microscope/camera/capture.py +++ b/openflexure_microscope/camera/capture.py @@ -18,12 +18,12 @@ Attributes: TEMP_CAPTURE_PATH (str): Base path to store all temporary captures (automatically emptied) """ -PIL_FORMATS = ['JPG', 'JPEG', 'PNG', 'TIF', 'TIFF'] -EXIF_FORMATS = ['JPG', 'JPEG', 'TIF', 'TIFF'] +PIL_FORMATS = ["JPG", "JPEG", "PNG", "TIF", "TIFF"] +EXIF_FORMATS = ["JPG", "JPEG", "TIF", "TIFF"] THUMBNAIL_SIZE = (200, 150) -BASE_CAPTURE_PATH = os.path.join(os.path.expanduser('~'), 'micrographs') -TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, 'tmp') +BASE_CAPTURE_PATH = os.path.join(os.path.expanduser("~"), "micrographs") +TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, "tmp") # TODO: Move these methods to a camera utilities module? @@ -50,8 +50,8 @@ def pull_usercomment_dict(filepath): except InvalidImageDataError: logging.error("Invalid data at {}. Skipping.".format(filepath)) return None - if 'Exif' in exif_dict and 37510 in exif_dict['Exif']: - return yaml.load(exif_dict['Exif'][37510].decode()) + if "Exif" in exif_dict and 37510 in exif_dict["Exif"]: + return yaml.load(exif_dict["Exif"][37510].decode()) else: return None @@ -59,7 +59,9 @@ def pull_usercomment_dict(filepath): def make_file_list(directory, formats): files = [] for fmt in formats: - files.extend(glob.glob('{}/**/*.{}'.format(directory, fmt.lower()), recursive=True)) + files.extend( + glob.glob("{}/**/*.{}".format(directory, fmt.lower()), recursive=True) + ) logging.info("{} capture files found on disk".format(len(files))) @@ -98,21 +100,19 @@ def capture_from_exif(path, exif_dict): """ # Create a placeholder capture - capture = CaptureObject( - filepath=path - ) + capture = CaptureObject(filepath=path) # Build file path information capture.split_file_path(capture.file) # Populate capture parameters - capture.id = exif_dict['id'] - - capture.timestring = exif_dict['time'] - capture.format = exif_dict['format'] + capture.id = exif_dict["id"] - capture._metadata = exif_dict['custom'] - capture.tags = exif_dict['tags'] + capture.timestring = exif_dict["time"] + capture.format = exif_dict["format"] + + capture._metadata = exif_dict["custom"] + capture.tags = exif_dict["tags"] return capture @@ -138,10 +138,8 @@ class CaptureObject(object): """ def __init__( - self, - write_to_file: bool = False, - temporary: bool = False, - filepath: str = '') -> None: + self, write_to_file: bool = False, temporary: bool = False, filepath: str = "" + ) -> None: """Create a new StreamObject, to manage capture data.""" # Store a nice ID @@ -184,7 +182,9 @@ class CaptureObject(object): """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)) + self.id + ) + ) self.temporary = True # Flag file to be removed on close. self.context_manager = True # Used in metadata @@ -203,12 +203,14 @@ class CaptureObject(object): Construct a full file path, based on filename, folder, and file format. Defaults to UUID. """ - global TEMP_CAPTURE_PATH, BASE_CAPTURE_PATH + global TEMP_CAPTURE_PATH, BASE_CAPTURE_PATH if self.temporary: base_dir = TEMP_CAPTURE_PATH else: base_dir = BASE_CAPTURE_PATH - return os.path.join(base_dir, self.filename) # Full file name by joining given folder to given name + return os.path.join( + base_dir, self.filename + ) # Full file name by joining given folder to given name def split_file_path(self, filepath): """ @@ -221,7 +223,7 @@ class CaptureObject(object): self.filefolder, self.filename = os.path.split(filepath) # Split the filename out from it's file extension self.basename = os.path.splitext(self.filename)[0] - self.format = self.filename.split('.')[-1] + self.format = self.filename.split(".")[-1] # Create folder and file if not os.path.exists(self.filefolder): @@ -298,7 +300,7 @@ class CaptureObject(object): # Serialize metadata metadata_string = yaml.safe_dump(self.metadata) # Insert metadata into exif_dict - exif_dict['Exif'][piexif.ExifIFD.UserComment] = metadata_string.encode() + exif_dict["Exif"][piexif.ExifIFD.UserComment] = metadata_string.encode() # Convert new exif dict to exif bytes exif_bytes = piexif.dump(exif_dict) # Insert exif into file @@ -310,8 +312,14 @@ class CaptureObject(object): Create basic metadata dictionary from basic capture data, and any added custom metadata and tags. """ - d = {'id': self.id, 'filename': self.filename, 'time': self.timestring, - 'format': self.format, 'tags': self.tags, 'custom': self._metadata} + d = { + "id": self.id, + "filename": self.filename, + "time": self.timestring, + "format": self.format, + "tags": self.tags, + "custom": self._metadata, + } # Add custom metadata to dictionary return d @@ -339,19 +347,19 @@ class CaptureObject(object): """ # Create basic state dictionary - d = {'path': self.file, 'temporary': self.temporary, 'metadata': self.metadata} + d = {"path": self.file, "temporary": self.temporary, "metadata": self.metadata} # Check bytestream if self.stream_exists: - d['bytestream'] = True + d["bytestream"] = True else: - d['bytestream'] = False + d["bytestream"] = False # Combined availability of data if self.exists: - d['available'] = True + d["available"] = True else: - d['available'] = False + d["available"] = False return d @@ -373,7 +381,7 @@ class CaptureObject(object): else: # If data bytestream is empty if self.file_exists: # If data file exists logging.info("Opening from file {}".format(self.file)) - with open(self.file, 'rb') as f: + with open(self.file, "rb") as f: d = io.BytesIO(f.read()) # Load bytes from file d.seek(0) # Rewind loaded bytestream # Create a copy of the bytestream bytes @@ -413,7 +421,7 @@ class CaptureObject(object): def load_file(self) -> bool: """Load data stored on disk to the in-memory bytestream.""" if self.file_exists: # If data file exists - with open(self.file, 'rb') as f: + with open(self.file, "rb") as f: self.bytestream = io.BytesIO(f.read()) # Load bytes from file self.bytestream.seek(0) # Rewind data bytes again return True @@ -423,7 +431,7 @@ class CaptureObject(object): def save_file(self) -> bool: """Write the StreamObjects bytestream to a file.""" if self.stream_exists: # If there's a bytestream to save - with open(self.file, 'ab') as f: # Load file as bytes + with open(self.file, "ab") as f: # Load file as bytes logging.debug("Writing bytestream to file {}".format(self.file)) f.seek(0, 0) # Seek to the start of the file f.write(self.binary) # Write data bytes to file diff --git a/openflexure_microscope/camera/pi.py b/openflexure_microscope/camera/pi.py index cf2d70c6..f7e1acac 100644 --- a/openflexure_microscope/camera/pi.py +++ b/openflexure_microscope/camera/pi.py @@ -36,6 +36,7 @@ import picamera.array from typing import Tuple from .base import BaseCamera, CaptureObject + # Richard's fix gain from .set_picamera_gain import set_analog_gain, set_digital_gain @@ -43,30 +44,31 @@ from .set_picamera_gain import set_analog_gain, set_digital_gain # MAIN CLASS class PiCameraStreamer(BaseCamera): """Raspberry Pi camera implementation of PiCameraStreamer.""" + picamera_settings_keys = [ - 'exposure_mode', - 'analog_gain', - 'digital_gain', - 'shutter_speed', - 'awb_gains', - 'awb_mode', - 'framerate', - 'saturation', - 'lens_shading_table' + "exposure_mode", + "analog_gain", + "digital_gain", + "shutter_speed", + "awb_gains", + "awb_mode", + "framerate", + "saturation", + "lens_shading_table", ] def __init__(self): # Run BaseCamera init BaseCamera.__init__(self) # Attach to Pi camera - self.camera = picamera.PiCamera() #: :py:class:`picamera.PiCamera`: Picamera object + self.camera = ( + picamera.PiCamera() + ) #: :py:class:`picamera.PiCamera`: Picamera object # Store state of PiCameraStreamer - self.state.update({ - 'stream_active': False, - 'record_active': False, - 'preview_active': False, - }) + self.state.update( + {"stream_active": False, "record_active": False, "preview_active": False} + ) # Reset variable states self.set_zoom(1.0) @@ -101,11 +103,11 @@ class PiCameraStreamer(BaseCamera): """ conf_dict = { - 'stream_resolution': self.stream_resolution, - 'image_resolution': self.image_resolution, - 'numpy_resolution': self.numpy_resolution, - 'jpeg_quality': self.jpeg_quality, - 'picamera_settings': {}, + "stream_resolution": self.stream_resolution, + "image_resolution": self.image_resolution, + "numpy_resolution": self.numpy_resolution, + "jpeg_quality": self.jpeg_quality, + "picamera_settings": {}, } # PiCamera parameters @@ -113,7 +115,7 @@ class PiCameraStreamer(BaseCamera): try: value = getattr(self.camera, key) logging.debug("Reading PiCamera().{}: {}".format(key, value)) - conf_dict['picamera_settings'][key] = value + conf_dict["picamera_settings"][key] = value except AttributeError: logging.debug("Unable to read PiCamera attribute {}".format(key)) @@ -138,21 +140,23 @@ class PiCameraStreamer(BaseCamera): with self.lock: # Apply valid config params to Picamera object - if not self.state['record_active']: # If not recording a video + if not self.state["record_active"]: # If not recording a video # Pause stream while changing settings - if self.state['stream_active']: # If stream is active + if self.state["stream_active"]: # If stream is active logging.info("Pausing stream to update config.") self.stop_stream_recording() # Pause stream paused_stream = True # Remember to unpause stream when done # PiCamera parameters - if 'picamera_settings' in config: # If new settings are given - self.apply_picamera_settings(config['picamera_settings'], pause_for_effect=True) + if "picamera_settings" in config: # If new settings are given + self.apply_picamera_settings( + config["picamera_settings"], pause_for_effect=True + ) # PiCameraStreamer parameters for key, value in config.items(): # For each provided setting - if (key != 'picamera_settings') and hasattr(self, key): + if (key != "picamera_settings") and hasattr(self, key): setattr(self, key, value) # If stream was paused to update config, unpause @@ -162,67 +166,84 @@ class PiCameraStreamer(BaseCamera): else: raise Exception( - "Cannot update camera config while recording is active.") + "Cannot update camera config while recording is active." + ) - def apply_picamera_settings(self, settings_dict: dict, pause_for_effect: bool=True): + def apply_picamera_settings( + self, settings_dict: dict, pause_for_effect: bool = True + ): # Set exposure mode - if 'exposure_mode' in settings_dict: - logging.debug("Applying exposure_mode: {}".format(settings_dict['exposure_mode'])) - self.camera.exposure_mode = settings_dict['exposure_mode'] + if "exposure_mode" in settings_dict: + logging.debug( + "Applying exposure_mode: {}".format(settings_dict["exposure_mode"]) + ) + self.camera.exposure_mode = settings_dict["exposure_mode"] # Apply gains and let them settle - if 'analog_gain' in settings_dict: - logging.debug("Applying analog_gain: {}".format(settings_dict['analog_gain'])) - set_analog_gain(self.camera, float(settings_dict['analog_gain'])) - if 'digital_gain' in settings_dict: - logging.debug("Applying digital_gain: {}".format(settings_dict['digital_gain'])) - set_digital_gain(self.camera, float(settings_dict['digital_gain'])) - + if "analog_gain" in settings_dict: + logging.debug( + "Applying analog_gain: {}".format(settings_dict["analog_gain"]) + ) + set_analog_gain(self.camera, float(settings_dict["analog_gain"])) + if "digital_gain" in settings_dict: + logging.debug( + "Applying digital_gain: {}".format(settings_dict["digital_gain"]) + ) + set_digital_gain(self.camera, float(settings_dict["digital_gain"])) + # Apply shutter speed - if 'shutter_speed' in settings_dict: - logging.debug("Applying shutter_speed: {}".format(settings_dict['shutter_speed'])) - self.camera.shutter_speed = int(settings_dict['shutter_speed']) + if "shutter_speed" in settings_dict: + logging.debug( + "Applying shutter_speed: {}".format(settings_dict["shutter_speed"]) + ) + self.camera.shutter_speed = int(settings_dict["shutter_speed"]) time.sleep(0.2) # Let gains settle # Handle AWB in a half-smart way - if 'awb_gains' in settings_dict: + if "awb_gains" in settings_dict: logging.debug("Applying awb_mode: off") - self.camera.awb_mode = 'off' - logging.debug("Applying awb_gains: {}".format(settings_dict['awb_gains'])) - self.camera.awb_gains = settings_dict['awb_gains'] - elif 'awb_mode' in settings_dict: - logging.debug("Applying awb_mode: {}".format(settings_dict['awb_mode'])) - self.camera.awb_mode = settings_dict['awb_mode'] + self.camera.awb_mode = "off" + logging.debug("Applying awb_gains: {}".format(settings_dict["awb_gains"])) + self.camera.awb_gains = settings_dict["awb_gains"] + elif "awb_mode" in settings_dict: + logging.debug("Applying awb_mode: {}".format(settings_dict["awb_mode"])) + self.camera.awb_mode = settings_dict["awb_mode"] # Handle some properties that can be quickly applied - batched_keys = ['framerate', 'saturation'] + batched_keys = ["framerate", "saturation"] for key in batched_keys: if (key in settings_dict) and hasattr(self.camera, key): logging.debug("Applying {}: {}".format(key, settings_dict[key])) setattr(self.camera, key, settings_dict[key]) # Handle lens shading if camera supports it - if ('lens_shading_table' in settings_dict) and hasattr(self.camera, 'lens_shading_table'): - logging.debug("Applying lens_shading_table: {}".format(settings_dict['lens_shading_table'])) - self.camera.lens_shading_table = settings_dict['lens_shading_table'] + if ("lens_shading_table" in settings_dict) and hasattr( + self.camera, "lens_shading_table" + ): + logging.debug( + "Applying lens_shading_table: {}".format( + settings_dict["lens_shading_table"] + ) + ) + self.camera.lens_shading_table = settings_dict["lens_shading_table"] # Final optional pause to settle if pause_for_effect: time.sleep(0.2) - def set_zoom(self, zoom_value: float = 1.) -> None: + def set_zoom(self, zoom_value: float = 1.0) -> None: """ Change the camera zoom, handling re-centering and scaling. """ with self.lock: - self.state['zoom_value'] = float(zoom_value) - if self.state['zoom_value'] < 1: - self.state['zoom_value'] = 1 + self.state["zoom_value"] = float(zoom_value) + if self.state["zoom_value"] < 1: + self.state["zoom_value"] = 1 # Richard's code for zooming ! fov = self.camera.zoom centre = np.array([fov[0] + fov[2] / 2.0, fov[1] + fov[3] / 2.0]) - size = 1.0 / self.state['zoom_value'] + size = 1.0 / self.state["zoom_value"] # If the new zoom value would be invalid, move the centre to # keep it within the camera's sensor (this is only relevant # when zooming out, if the FoV is not centred on (0.5, 0.5) @@ -252,22 +273,24 @@ class PiCameraStreamer(BaseCamera): self.camera.preview.window = window if fullscreen: self.camera.preview.fullscreen = fullscreen - self.state['preview_active'] = True + self.state["preview_active"] = True except picamera.exc.PiCameraMMALError as e: - logging.error("Suppressed a MMALError in start_preview. Exception: {}".format(e)) + logging.error( + "Suppressed a MMALError in start_preview. Exception: {}".format(e) + ) except picamera.exc.PiCameraValueError as e: - logging.error("Suppressed a ValueError exception in start_preview. Exception: {}".format(e)) + logging.error( + "Suppressed a ValueError exception in start_preview. Exception: {}".format( + e + ) + ) def stop_preview(self): """Stop the on board GPU camera preview.""" self.camera.stop_preview() - self.state['preview_active'] = False + self.state["preview_active"] = False - def start_recording( - self, - output, - fmt: str = 'h264', - quality: int = 15): + def start_recording(self, output, fmt: str = "h264", quality: int = 15): """Start recording. Start a new video recording, writing to a output object. @@ -283,7 +306,7 @@ class PiCameraStreamer(BaseCamera): """ with self.lock: # Start recording method only if a current recording is not running - if not self.state['record_active']: + if not self.state["record_active"]: # If output is a StreamObject if isinstance(output, CaptureObject): @@ -300,17 +323,19 @@ class PiCameraStreamer(BaseCamera): format=fmt, splitter_port=2, resize=self.stream_resolution, - quality=quality) + quality=quality, + ) # Update state dictionary - self.state['record_active'] = True + self.state["record_active"] = True return output else: logging.error( "Cannot start a new recording\ - until the current recording has stopped.") + until the current recording has stopped." + ) return None def stop_recording(self): @@ -322,12 +347,11 @@ class PiCameraStreamer(BaseCamera): logging.info("Recording stopped") # Update state dictionary - self.state['record_active'] = False + self.state["record_active"] = False def stop_stream_recording( - self, - splitter_port: int = 1, - resolution: Tuple[int, int] = None) -> None: + self, splitter_port: int = 1, resolution: Tuple[int, int] = None + ) -> None: """ Sets the camera resolution to the still-image resolution, and stops recording if the stream is active. @@ -346,16 +370,21 @@ class PiCameraStreamer(BaseCamera): except picamera.exc.PiCameraNotRecording: logging.info("Not recording on splitter_port {}".format(splitter_port)) else: - logging.info("Stopped MJPEG stream on port {1}. Switching to {0}.".format(resolution, splitter_port)) + logging.info( + "Stopped MJPEG stream on port {1}. Switching to {0}.".format( + resolution, splitter_port + ) + ) # Increase the resolution for taking an image - time.sleep(0.2) # Sprinkled a sleep to prevent camera getting confused by rapid commands + time.sleep( + 0.2 + ) # Sprinkled a sleep to prevent camera getting confused by rapid commands self.camera.resolution = resolution def start_stream_recording( - self, - splitter_port: int = 1, - resolution: Tuple[int, int] = None) -> None: + self, splitter_port: int = 1, resolution: Tuple[int, int] = None + ) -> None: """ Sets the camera resolution to the video/stream resolution, and starts recording if the stream should be active. @@ -366,45 +395,57 @@ class PiCameraStreamer(BaseCamera): """ with self.lock: # If stream object was destroyed - if not hasattr(self, 'stream'): + if not hasattr(self, "stream"): self.stream = io.BytesIO() # Create a stream object # If no explicit resolution is passed if not resolution: - resolution = self.stream_resolution # Default to video recording resolution + resolution = ( + self.stream_resolution + ) # Default to video recording resolution # Reduce the resolution for video streaming try: self.camera._check_recording_stopped() except picamera.exc.PiCameraRuntimeError: - logging.info("Error while changing resolution: Recording already running.") + logging.info( + "Error while changing resolution: Recording already running." + ) else: self.camera.resolution = resolution # If the stream should be active - if self.state['stream_active']: + if self.state["stream_active"]: try: # Start recording on stream port self.camera.start_recording( self.stream, - format='mjpeg', + format="mjpeg", quality=self.jpeg_quality, - bitrate=-1, # RWB: disable bitrate control + bitrate=-1, # RWB: disable bitrate control # (bitrate control makes JPEG size less good as a focus # metric) - splitter_port=splitter_port) + splitter_port=splitter_port, + ) except picamera.exc.PiCameraAlreadyRecording: - logging.info("Error while starting preview: Recording already running.") + logging.info( + "Error while starting preview: Recording already running." + ) else: - logging.debug("Started MJPEG stream at {} on port {}".format(resolution, splitter_port)) + logging.debug( + "Started MJPEG stream at {} on port {}".format( + resolution, splitter_port + ) + ) def capture( - self, - output, - fmt: str = 'jpeg', - use_video_port: bool = False, - resize: Tuple[int, int] = None, - bayer: bool = True): + self, + output, + fmt: str = "jpeg", + use_video_port: bool = False, + resize: Tuple[int, int] = None, + bayer: bool = True, + ): """ Capture a still image to a StreamObject. @@ -439,7 +480,8 @@ class PiCameraStreamer(BaseCamera): quality=100, resize=resize, bayer=(not use_video_port) and bayer, - use_video_port=use_video_port) + use_video_port=use_video_port, + ) # Set resolution and start stream recording if necessary if not use_video_port: @@ -448,9 +490,8 @@ class PiCameraStreamer(BaseCamera): return output def yuv( - self, - use_video_port: bool = True, - resize: Tuple[int, int] = None) -> np.ndarray: + self, use_video_port: bool = True, resize: Tuple[int, int] = None + ) -> np.ndarray: """Capture an uncompressed still YUV image to a Numpy array. Args: @@ -477,10 +518,8 @@ class PiCameraStreamer(BaseCamera): logging.info("Capturing to {}".format(output)) self.camera.capture( - output, - resize=size, - format='yuv', - use_video_port=use_video_port) + output, resize=size, format="yuv", use_video_port=use_video_port + ) if not use_video_port: self.start_stream_recording() @@ -488,9 +527,8 @@ class PiCameraStreamer(BaseCamera): return output.array def array( - self, - use_video_port: bool = True, - resize: Tuple[int, int] = None) -> np.ndarray: + self, use_video_port: bool = True, resize: Tuple[int, int] = None + ) -> np.ndarray: """Capture an uncompressed still RGB image to a Numpy array. Args: @@ -517,10 +555,8 @@ class PiCameraStreamer(BaseCamera): logging.info("Capturing to {}".format(output)) self.camera.capture( - output, - resize=size, - format='rgb', - use_video_port=use_video_port) + output, resize=size, format="rgb", use_video_port=use_video_port + ) # Resume stream self.start_stream_recording() diff --git a/openflexure_microscope/camera/piexif/__init__.py b/openflexure_microscope/camera/piexif/__init__.py index 95cc5de1..95954ecb 100644 --- a/openflexure_microscope/camera/piexif/__init__.py +++ b/openflexure_microscope/camera/piexif/__init__.py @@ -7,5 +7,4 @@ from ._exif import * from ._exceptions import * - -VERSION = '1.1.2' +VERSION = "1.1.2" diff --git a/openflexure_microscope/camera/piexif/_common.py b/openflexure_microscope/camera/piexif/_common.py index 28e45871..2661a1ee 100644 --- a/openflexure_microscope/camera/piexif/_common.py +++ b/openflexure_microscope/camera/piexif/_common.py @@ -12,20 +12,21 @@ def split_into_segments(data): head = 2 segments = [b"\xff\xd8"] while 1: - if data[head: head + 2] == b"\xff\xda": + if data[head : head + 2] == b"\xff\xda": segments.append(data[head:]) break else: - length = struct.unpack(">H", data[head + 2: head + 4])[0] + length = struct.unpack(">H", data[head + 2 : head + 4])[0] endPoint = head + length + 2 - seg = data[head: endPoint] + seg = data[head:endPoint] segments.append(seg) head = endPoint - if (head >= len(data)): + if head >= len(data): raise InvalidImageDataError("Wrong JPEG data.") return segments + def read_exif_from_file(filename): """Slices JPEG meta data into a list from JPEG binary data. """ @@ -39,11 +40,11 @@ def read_exif_from_file(filename): HEAD_LENGTH = 4 exif = None while 1: - length = struct.unpack(">H", head[2: 4])[0] + length = struct.unpack(">H", head[2:4])[0] if head[:2] == b"\xff\xe1": segment_data = f.read(length - 2) - if segment_data[:4] != b'Exif': + if segment_data[:4] != b"Exif": head = f.read(HEAD_LENGTH) continue exif = head + segment_data @@ -57,6 +58,7 @@ def read_exif_from_file(filename): f.close() return exif + def get_exif_seg(segments): """Returns Exif from JPEG meta data list """ @@ -69,9 +71,11 @@ def get_exif_seg(segments): def merge_segments(segments, exif=b""): """Merges Exif with APP0 and APP1 manipulations. """ - if segments[1][0:2] == b"\xff\xe0" and \ - segments[2][0:2] == b"\xff\xe1" and \ - segments[2][4:10] == b"Exif\x00\x00": + if ( + segments[1][0:2] == b"\xff\xe0" + and segments[2][0:2] == b"\xff\xe1" + and segments[2][4:10] == b"Exif\x00\x00" + ): if exif: segments[2] = exif segments.pop(1) @@ -82,8 +86,7 @@ def merge_segments(segments, exif=b""): elif segments[1][0:2] == b"\xff\xe0": if exif: segments[1] = exif - elif segments[1][0:2] == b"\xff\xe1" and \ - segments[1][4:10] == b"Exif\x00\x00": + elif segments[1][0:2] == b"\xff\xe1" and segments[1][4:10] == b"Exif\x00\x00": if exif: segments[1] = exif elif exif is None: diff --git a/openflexure_microscope/camera/piexif/_dump.py b/openflexure_microscope/camera/piexif/_dump.py index 4617a69a..a32f4936 100644 --- a/openflexure_microscope/camera/piexif/_dump.py +++ b/openflexure_microscope/camera/piexif/_dump.py @@ -31,16 +31,20 @@ def dump(exif_dict_original): else: zeroth_ifd = {} - if (("Exif" in exif_dict) and len(exif_dict["Exif"]) or - ("Interop" in exif_dict) and len(exif_dict["Interop"]) ): + if ( + ("Exif" in exif_dict) + and len(exif_dict["Exif"]) + or ("Interop" in exif_dict) + and len(exif_dict["Interop"]) + ): zeroth_ifd[ImageIFD.ExifTag] = 1 exif_is = True exif_ifd = exif_dict["Exif"] if ("Interop" in exif_dict) and len(exif_dict["Interop"]): - exif_ifd[ExifIFD. InteroperabilityTag] = 1 + exif_ifd[ExifIFD.InteroperabilityTag] = 1 interop_is = True interop_ifd = exif_dict["Interop"] - elif ExifIFD. InteroperabilityTag in exif_ifd: + elif ExifIFD.InteroperabilityTag in exif_ifd: exif_ifd.pop(ExifIFD.InteroperabilityTag) elif ImageIFD.ExifTag in zeroth_ifd: zeroth_ifd.pop(ImageIFD.ExifTag) @@ -52,17 +56,20 @@ def dump(exif_dict_original): elif ImageIFD.GPSTag in zeroth_ifd: zeroth_ifd.pop(ImageIFD.GPSTag) - if (("1st" in exif_dict) and - ("thumbnail" in exif_dict) and - (exif_dict["thumbnail"] is not None)): + if ( + ("1st" in exif_dict) + and ("thumbnail" in exif_dict) + and (exif_dict["thumbnail"] is not None) + ): first_is = True exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] = 1 exif_dict["1st"][ImageIFD.JPEGInterchangeFormatLength] = 1 first_ifd = exif_dict["1st"] zeroth_set = _dict_to_bytes(zeroth_ifd, "0th", 0) - zeroth_length = (len(zeroth_set[0]) + exif_is * 12 + gps_is * 12 + 4 + - len(zeroth_set[1])) + zeroth_length = ( + len(zeroth_set[0]) + exif_is * 12 + gps_is * 12 + 4 + len(zeroth_set[1]) + ) if exif_is: exif_set = _dict_to_bytes(exif_ifd, "Exif", zeroth_length) @@ -115,8 +122,7 @@ def dump(exif_dict_original): else: gps_pointer = b"" if interop_is: - pointer_value = (TIFF_HEADER_LENGTH + - zeroth_length + exif_length + gps_length) + pointer_value = TIFF_HEADER_LENGTH + zeroth_length + exif_length + gps_length pointer_str = struct.pack(">I", pointer_value) key = ExifIFD.InteroperabilityTag key_str = struct.pack(">H", key) @@ -126,33 +132,46 @@ def dump(exif_dict_original): else: interop_pointer = b"" if first_is: - pointer_value = (TIFF_HEADER_LENGTH + zeroth_length + - exif_length + gps_length + interop_length) + pointer_value = ( + TIFF_HEADER_LENGTH + + zeroth_length + + exif_length + + gps_length + + interop_length + ) first_ifd_pointer = struct.pack(">L", pointer_value) - thumbnail_pointer = (pointer_value + len(first_set[0]) + 24 + - 4 + len(first_set[1])) - thumbnail_p_bytes = (b"\x02\x01\x00\x04\x00\x00\x00\x01" + - struct.pack(">L", thumbnail_pointer)) - thumbnail_length_bytes = (b"\x02\x02\x00\x04\x00\x00\x00\x01" + - struct.pack(">L", len(thumbnail))) - first_bytes = (first_set[0] + thumbnail_p_bytes + - thumbnail_length_bytes + b"\x00\x00\x00\x00" + - first_set[1] + thumbnail) + thumbnail_pointer = ( + pointer_value + len(first_set[0]) + 24 + 4 + len(first_set[1]) + ) + thumbnail_p_bytes = b"\x02\x01\x00\x04\x00\x00\x00\x01" + struct.pack( + ">L", thumbnail_pointer + ) + thumbnail_length_bytes = b"\x02\x02\x00\x04\x00\x00\x00\x01" + struct.pack( + ">L", len(thumbnail) + ) + first_bytes = ( + first_set[0] + + thumbnail_p_bytes + + thumbnail_length_bytes + + b"\x00\x00\x00\x00" + + first_set[1] + + thumbnail + ) else: first_ifd_pointer = b"\x00\x00\x00\x00" - zeroth_bytes = (zeroth_set[0] + exif_pointer + gps_pointer + - first_ifd_pointer + zeroth_set[1]) + zeroth_bytes = ( + zeroth_set[0] + exif_pointer + gps_pointer + first_ifd_pointer + zeroth_set[1] + ) if exif_is: exif_bytes = exif_set[0] + interop_pointer + exif_set[1] - return (header + zeroth_bytes + exif_bytes + gps_bytes + - interop_bytes + first_bytes) + return header + zeroth_bytes + exif_bytes + gps_bytes + interop_bytes + first_bytes def _get_thumbnail(jpeg): segments = split_into_segments(jpeg) - while (b"\xff\xe0" <= segments[1][0:2] <= b"\xff\xef"): + while b"\xff\xe0" <= segments[1][0:2] <= b"\xff\xef": segments.pop(1) thumbnail = b"".join(segments) return thumbnail @@ -161,24 +180,31 @@ def _get_thumbnail(jpeg): def _pack_byte(*args): return struct.pack("B" * len(args), *args) + def _pack_signed_byte(*args): return struct.pack("b" * len(args), *args) + def _pack_short(*args): return struct.pack(">" + "H" * len(args), *args) + def _pack_signed_short(*args): return struct.pack(">" + "h" * len(args), *args) + def _pack_long(*args): return struct.pack(">" + "L" * len(args), *args) + def _pack_slong(*args): return struct.pack(">" + "l" * len(args), *args) + def _pack_float(*args): return struct.pack(">" + "f" * len(args), *args) + def _pack_double(*args): return struct.pack(">" + "d" * len(args), *args) @@ -190,16 +216,14 @@ def _value_to_bytes(raw_value, value_type, offset): if value_type == TYPES.Byte: length = len(raw_value) if length <= 4: - value_str = (_pack_byte(*raw_value) + - b"\x00" * (4 - length)) + value_str = _pack_byte(*raw_value) + b"\x00" * (4 - length) else: value_str = struct.pack(">I", offset) four_bytes_over = _pack_byte(*raw_value) elif value_type == TYPES.Short: length = len(raw_value) if length <= 2: - value_str = (_pack_short(*raw_value) + - b"\x00\x00" * (2 - length)) + value_str = _pack_short(*raw_value) + b"\x00\x00" * (2 - length) else: value_str = struct.pack(">I", offset) four_bytes_over = _pack_short(*raw_value) @@ -241,8 +265,7 @@ def _value_to_bytes(raw_value, value_type, offset): new_value = b"" for n, val in enumerate(raw_value): num, den = val - new_value += (struct.pack(">L", num) + - struct.pack(">L", den)) + new_value += struct.pack(">L", num) + struct.pack(">L", den) value_str = struct.pack(">I", offset) four_bytes_over = new_value elif value_type == TYPES.SRational: @@ -255,8 +278,7 @@ def _value_to_bytes(raw_value, value_type, offset): new_value = b"" for n, val in enumerate(raw_value): num, den = val - new_value += (struct.pack(">l", num) + - struct.pack(">l", den)) + new_value += struct.pack(">l", num) + struct.pack(">l", den) value_str = struct.pack(">I", offset) four_bytes_over = new_value elif value_type == TYPES.Undefined: @@ -272,19 +294,17 @@ def _value_to_bytes(raw_value, value_type, offset): value_str = raw_value + b"\x00" * (4 - length) except TypeError: raise ValueError("Got invalid type to convert.") - elif value_type == TYPES.SByte: # Signed Byte + elif value_type == TYPES.SByte: # Signed Byte length = len(raw_value) if length <= 4: - value_str = (_pack_signed_byte(*raw_value) + - b"\x00" * (4 - length)) + value_str = _pack_signed_byte(*raw_value) + b"\x00" * (4 - length) else: value_str = struct.pack(">I", offset) four_bytes_over = _pack_signed_byte(*raw_value) - elif value_type == TYPES.SShort: # Signed Short + elif value_type == TYPES.SShort: # Signed Short length = len(raw_value) if length <= 2: - value_str = (_pack_signed_short(*raw_value) + - b"\x00\x00" * (2 - length)) + value_str = _pack_signed_short(*raw_value) + b"\x00\x00" * (2 - length) else: value_str = struct.pack(">I", offset) four_bytes_over = _pack_signed_short(*raw_value) @@ -295,7 +315,7 @@ def _value_to_bytes(raw_value, value_type, offset): else: value_str = struct.pack(">I", offset) four_bytes_over = _pack_float(*raw_value) - elif value_type == TYPES.DFloat: # Double + elif value_type == TYPES.DFloat: # Double length = len(raw_value) value_str = struct.pack(">I", offset) four_bytes_over = _pack_double(*raw_value) @@ -303,6 +323,7 @@ def _value_to_bytes(raw_value, value_type, offset): length_str = struct.pack(">I", length) return length_str, value_str, four_bytes_over + def _dict_to_bytes(ifd_dict, ifd, ifd_offset): tag_count = len(ifd_dict) entry_header = struct.pack(">H", tag_count) @@ -318,7 +339,10 @@ def _dict_to_bytes(ifd_dict, ifd, ifd_offset): continue elif (ifd == "Exif") and (key == ExifIFD.InteroperabilityTag): continue - elif (ifd == "1st") and (key in (ImageIFD.JPEGInterchangeFormat, ImageIFD.JPEGInterchangeFormatLength)): + elif (ifd == "1st") and ( + key + in (ImageIFD.JPEGInterchangeFormat, ImageIFD.JPEGInterchangeFormatLength) + ): continue raw_value = ifd_dict[key] @@ -332,13 +356,13 @@ def _dict_to_bytes(ifd_dict, ifd, ifd_offset): offset = TIFF_HEADER_LENGTH + entries_length + ifd_offset + len(values) try: - length_str, value_str, four_bytes_over = _value_to_bytes(raw_value, - value_type, - offset) + length_str, value_str, four_bytes_over = _value_to_bytes( + raw_value, value_type, offset + ) except ValueError: raise ValueError( - '"dump" got wrong type of exif value.\n' + - '{0} in {1} IFD. Got as {2}.'.format(key, ifd, type(ifd_dict[key])) + '"dump" got wrong type of exif value.\n' + + "{0} in {1} IFD. Got as {2}.".format(key, ifd, type(ifd_dict[key])) ) entries += key_str + type_str + length_str + value_str diff --git a/openflexure_microscope/camera/piexif/_exif.py b/openflexure_microscope/camera/piexif/_exif.py index abdf0bc4..3c25003c 100644 --- a/openflexure_microscope/camera/piexif/_exif.py +++ b/openflexure_microscope/camera/piexif/_exif.py @@ -14,314 +14,322 @@ class TYPES: TAGS = { - 'Image': {11: {'name': 'ProcessingSoftware', 'type': TYPES.Ascii}, - 254: {'name': 'NewSubfileType', 'type': TYPES.Long}, - 255: {'name': 'SubfileType', 'type': TYPES.Short}, - 256: {'name': 'ImageWidth', 'type': TYPES.Long}, - 257: {'name': 'ImageLength', 'type': TYPES.Long}, - 258: {'name': 'BitsPerSample', 'type': TYPES.Short}, - 259: {'name': 'Compression', 'type': TYPES.Short}, - 262: {'name': 'PhotometricInterpretation', 'type': TYPES.Short}, - 263: {'name': 'Threshholding', 'type': TYPES.Short}, - 264: {'name': 'CellWidth', 'type': TYPES.Short}, - 265: {'name': 'CellLength', 'type': TYPES.Short}, - 266: {'name': 'FillOrder', 'type': TYPES.Short}, - 269: {'name': 'DocumentName', 'type': TYPES.Ascii}, - 270: {'name': 'ImageDescription', 'type': TYPES.Ascii}, - 271: {'name': 'Make', 'type': TYPES.Ascii}, - 272: {'name': 'Model', 'type': TYPES.Ascii}, - 273: {'name': 'StripOffsets', 'type': TYPES.Long}, - 274: {'name': 'Orientation', 'type': TYPES.Short}, - 277: {'name': 'SamplesPerPixel', 'type': TYPES.Short}, - 278: {'name': 'RowsPerStrip', 'type': TYPES.Long}, - 279: {'name': 'StripByteCounts', 'type': TYPES.Long}, - 282: {'name': 'XResolution', 'type': TYPES.Rational}, - 283: {'name': 'YResolution', 'type': TYPES.Rational}, - 284: {'name': 'PlanarConfiguration', 'type': TYPES.Short}, - 290: {'name': 'GrayResponseUnit', 'type': TYPES.Short}, - 291: {'name': 'GrayResponseCurve', 'type': TYPES.Short}, - 292: {'name': 'T4Options', 'type': TYPES.Long}, - 293: {'name': 'T6Options', 'type': TYPES.Long}, - 296: {'name': 'ResolutionUnit', 'type': TYPES.Short}, - 301: {'name': 'TransferFunction', 'type': TYPES.Short}, - 305: {'name': 'Software', 'type': TYPES.Ascii}, - 306: {'name': 'DateTime', 'type': TYPES.Ascii}, - 315: {'name': 'Artist', 'type': TYPES.Ascii}, - 316: {'name': 'HostComputer', 'type': TYPES.Ascii}, - 317: {'name': 'Predictor', 'type': TYPES.Short}, - 318: {'name': 'WhitePoint', 'type': TYPES.Rational}, - 319: {'name': 'PrimaryChromaticities', 'type': TYPES.Rational}, - 320: {'name': 'ColorMap', 'type': TYPES.Short}, - 321: {'name': 'HalftoneHints', 'type': TYPES.Short}, - 322: {'name': 'TileWidth', 'type': TYPES.Short}, - 323: {'name': 'TileLength', 'type': TYPES.Short}, - 324: {'name': 'TileOffsets', 'type': TYPES.Short}, - 325: {'name': 'TileByteCounts', 'type': TYPES.Short}, - 330: {'name': 'SubIFDs', 'type': TYPES.Long}, - 332: {'name': 'InkSet', 'type': TYPES.Short}, - 333: {'name': 'InkNames', 'type': TYPES.Ascii}, - 334: {'name': 'NumberOfInks', 'type': TYPES.Short}, - 336: {'name': 'DotRange', 'type': TYPES.Byte}, - 337: {'name': 'TargetPrinter', 'type': TYPES.Ascii}, - 338: {'name': 'ExtraSamples', 'type': TYPES.Short}, - 339: {'name': 'SampleFormat', 'type': TYPES.Short}, - 340: {'name': 'SMinSampleValue', 'type': TYPES.Short}, - 341: {'name': 'SMaxSampleValue', 'type': TYPES.Short}, - 342: {'name': 'TransferRange', 'type': TYPES.Short}, - 343: {'name': 'ClipPath', 'type': TYPES.Byte}, - 344: {'name': 'XClipPathUnits', 'type': TYPES.Long}, - 345: {'name': 'YClipPathUnits', 'type': TYPES.Long}, - 346: {'name': 'Indexed', 'type': TYPES.Short}, - 347: {'name': 'JPEGTables', 'type': TYPES.Undefined}, - 351: {'name': 'OPIProxy', 'type': TYPES.Short}, - 512: {'name': 'JPEGProc', 'type': TYPES.Long}, - 513: {'name': 'JPEGInterchangeFormat', 'type': TYPES.Long}, - 514: {'name': 'JPEGInterchangeFormatLength', 'type': TYPES.Long}, - 515: {'name': 'JPEGRestartInterval', 'type': TYPES.Short}, - 517: {'name': 'JPEGLosslessPredictors', 'type': TYPES.Short}, - 518: {'name': 'JPEGPointTransforms', 'type': TYPES.Short}, - 519: {'name': 'JPEGQTables', 'type': TYPES.Long}, - 520: {'name': 'JPEGDCTables', 'type': TYPES.Long}, - 521: {'name': 'JPEGACTables', 'type': TYPES.Long}, - 529: {'name': 'YCbCrCoefficients', 'type': TYPES.Rational}, - 530: {'name': 'YCbCrSubSampling', 'type': TYPES.Short}, - 531: {'name': 'YCbCrPositioning', 'type': TYPES.Short}, - 532: {'name': 'ReferenceBlackWhite', 'type': TYPES.Rational}, - 700: {'name': 'XMLPacket', 'type': TYPES.Byte}, - 18246: {'name': 'Rating', 'type': TYPES.Short}, - 18249: {'name': 'RatingPercent', 'type': TYPES.Short}, - 32781: {'name': 'ImageID', 'type': TYPES.Ascii}, - 33421: {'name': 'CFARepeatPatternDim', 'type': TYPES.Short}, - 33422: {'name': 'CFAPattern', 'type': TYPES.Byte}, - 33423: {'name': 'BatteryLevel', 'type': TYPES.Rational}, - 33432: {'name': 'Copyright', 'type': TYPES.Ascii}, - 33434: {'name': 'ExposureTime', 'type': TYPES.Rational}, - 34377: {'name': 'ImageResources', 'type': TYPES.Byte}, - 34665: {'name': 'ExifTag', 'type': TYPES.Long}, - 34675: {'name': 'InterColorProfile', 'type': TYPES.Undefined}, - 34853: {'name': 'GPSTag', 'type': TYPES.Long}, - 34857: {'name': 'Interlace', 'type': TYPES.Short}, - 34858: {'name': 'TimeZoneOffset', 'type': TYPES.Long}, - 34859: {'name': 'SelfTimerMode', 'type': TYPES.Short}, - 37387: {'name': 'FlashEnergy', 'type': TYPES.Rational}, - 37388: {'name': 'SpatialFrequencyResponse', 'type': TYPES.Undefined}, - 37389: {'name': 'Noise', 'type': TYPES.Undefined}, - 37390: {'name': 'FocalPlaneXResolution', 'type': TYPES.Rational}, - 37391: {'name': 'FocalPlaneYResolution', 'type': TYPES.Rational}, - 37392: {'name': 'FocalPlaneResolutionUnit', 'type': TYPES.Short}, - 37393: {'name': 'ImageNumber', 'type': TYPES.Long}, - 37394: {'name': 'SecurityClassification', 'type': TYPES.Ascii}, - 37395: {'name': 'ImageHistory', 'type': TYPES.Ascii}, - 37397: {'name': 'ExposureIndex', 'type': TYPES.Rational}, - 37398: {'name': 'TIFFEPStandardID', 'type': TYPES.Byte}, - 37399: {'name': 'SensingMethod', 'type': TYPES.Short}, - 40091: {'name': 'XPTitle', 'type': TYPES.Byte}, - 40092: {'name': 'XPComment', 'type': TYPES.Byte}, - 40093: {'name': 'XPAuthor', 'type': TYPES.Byte}, - 40094: {'name': 'XPKeywords', 'type': TYPES.Byte}, - 40095: {'name': 'XPSubject', 'type': TYPES.Byte}, - 50341: {'name': 'PrintImageMatching', 'type': TYPES.Undefined}, - 50706: {'name': 'DNGVersion', 'type': TYPES.Byte}, - 50707: {'name': 'DNGBackwardVersion', 'type': TYPES.Byte}, - 50708: {'name': 'UniqueCameraModel', 'type': TYPES.Ascii}, - 50709: {'name': 'LocalizedCameraModel', 'type': TYPES.Byte}, - 50710: {'name': 'CFAPlaneColor', 'type': TYPES.Byte}, - 50711: {'name': 'CFALayout', 'type': TYPES.Short}, - 50712: {'name': 'LinearizationTable', 'type': TYPES.Short}, - 50713: {'name': 'BlackLevelRepeatDim', 'type': TYPES.Short}, - 50714: {'name': 'BlackLevel', 'type': TYPES.Rational}, - 50715: {'name': 'BlackLevelDeltaH', 'type': TYPES.SRational}, - 50716: {'name': 'BlackLevelDeltaV', 'type': TYPES.SRational}, - 50717: {'name': 'WhiteLevel', 'type': TYPES.Short}, - 50718: {'name': 'DefaultScale', 'type': TYPES.Rational}, - 50719: {'name': 'DefaultCropOrigin', 'type': TYPES.Short}, - 50720: {'name': 'DefaultCropSize', 'type': TYPES.Short}, - 50721: {'name': 'ColorMatrix1', 'type': TYPES.SRational}, - 50722: {'name': 'ColorMatrix2', 'type': TYPES.SRational}, - 50723: {'name': 'CameraCalibration1', 'type': TYPES.SRational}, - 50724: {'name': 'CameraCalibration2', 'type': TYPES.SRational}, - 50725: {'name': 'ReductionMatrix1', 'type': TYPES.SRational}, - 50726: {'name': 'ReductionMatrix2', 'type': TYPES.SRational}, - 50727: {'name': 'AnalogBalance', 'type': TYPES.Rational}, - 50728: {'name': 'AsShotNeutral', 'type': TYPES.Short}, - 50729: {'name': 'AsShotWhiteXY', 'type': TYPES.Rational}, - 50730: {'name': 'BaselineExposure', 'type': TYPES.SRational}, - 50731: {'name': 'BaselineNoise', 'type': TYPES.Rational}, - 50732: {'name': 'BaselineSharpness', 'type': TYPES.Rational}, - 50733: {'name': 'BayerGreenSplit', 'type': TYPES.Long}, - 50734: {'name': 'LinearResponseLimit', 'type': TYPES.Rational}, - 50735: {'name': 'CameraSerialNumber', 'type': TYPES.Ascii}, - 50736: {'name': 'LensInfo', 'type': TYPES.Rational}, - 50737: {'name': 'ChromaBlurRadius', 'type': TYPES.Rational}, - 50738: {'name': 'AntiAliasStrength', 'type': TYPES.Rational}, - 50739: {'name': 'ShadowScale', 'type': TYPES.SRational}, - 50740: {'name': 'DNGPrivateData', 'type': TYPES.Byte}, - 50741: {'name': 'MakerNoteSafety', 'type': TYPES.Short}, - 50778: {'name': 'CalibrationIlluminant1', 'type': TYPES.Short}, - 50779: {'name': 'CalibrationIlluminant2', 'type': TYPES.Short}, - 50780: {'name': 'BestQualityScale', 'type': TYPES.Rational}, - 50781: {'name': 'RawDataUniqueID', 'type': TYPES.Byte}, - 50827: {'name': 'OriginalRawFileName', 'type': TYPES.Byte}, - 50828: {'name': 'OriginalRawFileData', 'type': TYPES.Undefined}, - 50829: {'name': 'ActiveArea', 'type': TYPES.Short}, - 50830: {'name': 'MaskedAreas', 'type': TYPES.Short}, - 50831: {'name': 'AsShotICCProfile', 'type': TYPES.Undefined}, - 50832: {'name': 'AsShotPreProfileMatrix', 'type': TYPES.SRational}, - 50833: {'name': 'CurrentICCProfile', 'type': TYPES.Undefined}, - 50834: {'name': 'CurrentPreProfileMatrix', 'type': TYPES.SRational}, - 50879: {'name': 'ColorimetricReference', 'type': TYPES.Short}, - 50931: {'name': 'CameraCalibrationSignature', 'type': TYPES.Byte}, - 50932: {'name': 'ProfileCalibrationSignature', 'type': TYPES.Byte}, - 50934: {'name': 'AsShotProfileName', 'type': TYPES.Byte}, - 50935: {'name': 'NoiseReductionApplied', 'type': TYPES.Rational}, - 50936: {'name': 'ProfileName', 'type': TYPES.Byte}, - 50937: {'name': 'ProfileHueSatMapDims', 'type': TYPES.Long}, - 50938: {'name': 'ProfileHueSatMapData1', 'type': TYPES.Float}, - 50939: {'name': 'ProfileHueSatMapData2', 'type': TYPES.Float}, - 50940: {'name': 'ProfileToneCurve', 'type': TYPES.Float}, - 50941: {'name': 'ProfileEmbedPolicy', 'type': TYPES.Long}, - 50942: {'name': 'ProfileCopyright', 'type': TYPES.Byte}, - 50964: {'name': 'ForwardMatrix1', 'type': TYPES.SRational}, - 50965: {'name': 'ForwardMatrix2', 'type': TYPES.SRational}, - 50966: {'name': 'PreviewApplicationName', 'type': TYPES.Byte}, - 50967: {'name': 'PreviewApplicationVersion', 'type': TYPES.Byte}, - 50968: {'name': 'PreviewSettingsName', 'type': TYPES.Byte}, - 50969: {'name': 'PreviewSettingsDigest', 'type': TYPES.Byte}, - 50970: {'name': 'PreviewColorSpace', 'type': TYPES.Long}, - 50971: {'name': 'PreviewDateTime', 'type': TYPES.Ascii}, - 50972: {'name': 'RawImageDigest', 'type': TYPES.Undefined}, - 50973: {'name': 'OriginalRawFileDigest', 'type': TYPES.Undefined}, - 50974: {'name': 'SubTileBlockSize', 'type': TYPES.Long}, - 50975: {'name': 'RowInterleaveFactor', 'type': TYPES.Long}, - 50981: {'name': 'ProfileLookTableDims', 'type': TYPES.Long}, - 50982: {'name': 'ProfileLookTableData', 'type': TYPES.Float}, - 51008: {'name': 'OpcodeList1', 'type': TYPES.Undefined}, - 51009: {'name': 'OpcodeList2', 'type': TYPES.Undefined}, - 51022: {'name': 'OpcodeList3', 'type': TYPES.Undefined}, - 60606: {'name': 'ZZZTestSlong1', 'type': TYPES.SLong}, - 60607: {'name': 'ZZZTestSlong2', 'type': TYPES.SLong}, - 60608: {'name': 'ZZZTestSByte', 'type': TYPES.SByte}, - 60609: {'name': 'ZZZTestSShort', 'type': TYPES.SShort}, - 60610: {'name': 'ZZZTestDFloat', 'type': TYPES.DFloat},}, - 'Exif': {33434: {'name': 'ExposureTime', 'type': TYPES.Rational}, - 33437: {'name': 'FNumber', 'type': TYPES.Rational}, - 34850: {'name': 'ExposureProgram', 'type': TYPES.Short}, - 34852: {'name': 'SpectralSensitivity', 'type': TYPES.Ascii}, - 34855: {'name': 'ISOSpeedRatings', 'type': TYPES.Short}, - 34856: {'name': 'OECF', 'type': TYPES.Undefined}, - 34864: {'name': 'SensitivityType', 'type': TYPES.Short}, - 34865: {'name': 'StandardOutputSensitivity', 'type': TYPES.Long}, - 34866: {'name': 'RecommendedExposureIndex', 'type': TYPES.Long}, - 34867: {'name': 'ISOSpeed', 'type': TYPES.Long}, - 34868: {'name': 'ISOSpeedLatitudeyyy', 'type': TYPES.Long}, - 34869: {'name': 'ISOSpeedLatitudezzz', 'type': TYPES.Long}, - 36864: {'name': 'ExifVersion', 'type': TYPES.Undefined}, - 36867: {'name': 'DateTimeOriginal', 'type': TYPES.Ascii}, - 36868: {'name': 'DateTimeDigitized', 'type': TYPES.Ascii}, - 36880: {'name': 'OffsetTime', 'type': TYPES.Ascii}, - 36881: {'name': 'OffsetTimeOriginal', 'type': TYPES.Ascii}, - 36882: {'name': 'OffsetTimeDigitized', 'type': TYPES.Ascii}, - 37121: {'name': 'ComponentsConfiguration', 'type': TYPES.Undefined}, - 37122: {'name': 'CompressedBitsPerPixel', 'type': TYPES.Rational}, - 37377: {'name': 'ShutterSpeedValue', 'type': TYPES.SRational}, - 37378: {'name': 'ApertureValue', 'type': TYPES.Rational}, - 37379: {'name': 'BrightnessValue', 'type': TYPES.SRational}, - 37380: {'name': 'ExposureBiasValue', 'type': TYPES.SRational}, - 37381: {'name': 'MaxApertureValue', 'type': TYPES.Rational}, - 37382: {'name': 'SubjectDistance', 'type': TYPES.Rational}, - 37383: {'name': 'MeteringMode', 'type': TYPES.Short}, - 37384: {'name': 'LightSource', 'type': TYPES.Short}, - 37385: {'name': 'Flash', 'type': TYPES.Short}, - 37386: {'name': 'FocalLength', 'type': TYPES.Rational}, - 37396: {'name': 'SubjectArea', 'type': TYPES.Short}, - 37500: {'name': 'MakerNote', 'type': TYPES.Undefined}, - 37510: {'name': 'UserComment', 'type': TYPES.Undefined}, - 37520: {'name': 'SubSecTime', 'type': TYPES.Ascii}, - 37521: {'name': 'SubSecTimeOriginal', 'type': TYPES.Ascii}, - 37522: {'name': 'SubSecTimeDigitized', 'type': TYPES.Ascii}, - 37888: {'name': 'Temperature', 'type': TYPES.SRational}, - 37889: {'name': 'Humidity', 'type': TYPES.Rational}, - 37890: {'name': 'Pressure', 'type': TYPES.Rational}, - 37891: {'name': 'WaterDepth', 'type': TYPES.SRational}, - 37892: {'name': 'Acceleration', 'type': TYPES.Rational}, - 37893: {'name': 'CameraElevationAngle', 'type': TYPES.SRational}, - 40960: {'name': 'FlashpixVersion', 'type': TYPES.Undefined}, - 40961: {'name': 'ColorSpace', 'type': TYPES.Short}, - 40962: {'name': 'PixelXDimension', 'type': TYPES.Long}, - 40963: {'name': 'PixelYDimension', 'type': TYPES.Long}, - 40964: {'name': 'RelatedSoundFile', 'type': TYPES.Ascii}, - 40965: {'name': 'InteroperabilityTag', 'type': TYPES.Long}, - 41483: {'name': 'FlashEnergy', 'type': TYPES.Rational}, - 41484: {'name': 'SpatialFrequencyResponse', 'type': TYPES.Undefined}, - 41486: {'name': 'FocalPlaneXResolution', 'type': TYPES.Rational}, - 41487: {'name': 'FocalPlaneYResolution', 'type': TYPES.Rational}, - 41488: {'name': 'FocalPlaneResolutionUnit', 'type': TYPES.Short}, - 41492: {'name': 'SubjectLocation', 'type': TYPES.Short}, - 41493: {'name': 'ExposureIndex', 'type': TYPES.Rational}, - 41495: {'name': 'SensingMethod', 'type': TYPES.Short}, - 41728: {'name': 'FileSource', 'type': TYPES.Undefined}, - 41729: {'name': 'SceneType', 'type': TYPES.Undefined}, - 41730: {'name': 'CFAPattern', 'type': TYPES.Undefined}, - 41985: {'name': 'CustomRendered', 'type': TYPES.Short}, - 41986: {'name': 'ExposureMode', 'type': TYPES.Short}, - 41987: {'name': 'WhiteBalance', 'type': TYPES.Short}, - 41988: {'name': 'DigitalZoomRatio', 'type': TYPES.Rational}, - 41989: {'name': 'FocalLengthIn35mmFilm', 'type': TYPES.Short}, - 41990: {'name': 'SceneCaptureType', 'type': TYPES.Short}, - 41991: {'name': 'GainControl', 'type': TYPES.Short}, - 41992: {'name': 'Contrast', 'type': TYPES.Short}, - 41993: {'name': 'Saturation', 'type': TYPES.Short}, - 41994: {'name': 'Sharpness', 'type': TYPES.Short}, - 41995: {'name': 'DeviceSettingDescription', 'type': TYPES.Undefined}, - 41996: {'name': 'SubjectDistanceRange', 'type': TYPES.Short}, - 42016: {'name': 'ImageUniqueID', 'type': TYPES.Ascii}, - 42032: {'name': 'CameraOwnerName', 'type': TYPES.Ascii}, - 42033: {'name': 'BodySerialNumber', 'type': TYPES.Ascii}, - 42034: {'name': 'LensSpecification', 'type': TYPES.Rational}, - 42035: {'name': 'LensMake', 'type': TYPES.Ascii}, - 42036: {'name': 'LensModel', 'type': TYPES.Ascii}, - 42037: {'name': 'LensSerialNumber', 'type': TYPES.Ascii}, - 42240: {'name': 'Gamma', 'type': TYPES.Rational}}, - 'GPS': {0: {'name': 'GPSVersionID', 'type': TYPES.Byte}, - 1: {'name': 'GPSLatitudeRef', 'type': TYPES.Ascii}, - 2: {'name': 'GPSLatitude', 'type': TYPES.Rational}, - 3: {'name': 'GPSLongitudeRef', 'type': TYPES.Ascii}, - 4: {'name': 'GPSLongitude', 'type': TYPES.Rational}, - 5: {'name': 'GPSAltitudeRef', 'type': TYPES.Byte}, - 6: {'name': 'GPSAltitude', 'type': TYPES.Rational}, - 7: {'name': 'GPSTimeStamp', 'type': TYPES.Rational}, - 8: {'name': 'GPSSatellites', 'type': TYPES.Ascii}, - 9: {'name': 'GPSStatus', 'type': TYPES.Ascii}, - 10: {'name': 'GPSMeasureMode', 'type': TYPES.Ascii}, - 11: {'name': 'GPSDOP', 'type': TYPES.Rational}, - 12: {'name': 'GPSSpeedRef', 'type': TYPES.Ascii}, - 13: {'name': 'GPSSpeed', 'type': TYPES.Rational}, - 14: {'name': 'GPSTrackRef', 'type': TYPES.Ascii}, - 15: {'name': 'GPSTrack', 'type': TYPES.Rational}, - 16: {'name': 'GPSImgDirectionRef', 'type': TYPES.Ascii}, - 17: {'name': 'GPSImgDirection', 'type': TYPES.Rational}, - 18: {'name': 'GPSMapDatum', 'type': TYPES.Ascii}, - 19: {'name': 'GPSDestLatitudeRef', 'type': TYPES.Ascii}, - 20: {'name': 'GPSDestLatitude', 'type': TYPES.Rational}, - 21: {'name': 'GPSDestLongitudeRef', 'type': TYPES.Ascii}, - 22: {'name': 'GPSDestLongitude', 'type': TYPES.Rational}, - 23: {'name': 'GPSDestBearingRef', 'type': TYPES.Ascii}, - 24: {'name': 'GPSDestBearing', 'type': TYPES.Rational}, - 25: {'name': 'GPSDestDistanceRef', 'type': TYPES.Ascii}, - 26: {'name': 'GPSDestDistance', 'type': TYPES.Rational}, - 27: {'name': 'GPSProcessingMethod', 'type': TYPES.Undefined}, - 28: {'name': 'GPSAreaInformation', 'type': TYPES.Undefined}, - 29: {'name': 'GPSDateStamp', 'type': TYPES.Ascii}, - 30: {'name': 'GPSDifferential', 'type': TYPES.Short}, - 31: {'name': 'GPSHPositioningError', 'type': TYPES.Rational}}, - 'Interop': {1: {'name': 'InteroperabilityIndex', 'type': TYPES.Ascii}}, + "Image": { + 11: {"name": "ProcessingSoftware", "type": TYPES.Ascii}, + 254: {"name": "NewSubfileType", "type": TYPES.Long}, + 255: {"name": "SubfileType", "type": TYPES.Short}, + 256: {"name": "ImageWidth", "type": TYPES.Long}, + 257: {"name": "ImageLength", "type": TYPES.Long}, + 258: {"name": "BitsPerSample", "type": TYPES.Short}, + 259: {"name": "Compression", "type": TYPES.Short}, + 262: {"name": "PhotometricInterpretation", "type": TYPES.Short}, + 263: {"name": "Threshholding", "type": TYPES.Short}, + 264: {"name": "CellWidth", "type": TYPES.Short}, + 265: {"name": "CellLength", "type": TYPES.Short}, + 266: {"name": "FillOrder", "type": TYPES.Short}, + 269: {"name": "DocumentName", "type": TYPES.Ascii}, + 270: {"name": "ImageDescription", "type": TYPES.Ascii}, + 271: {"name": "Make", "type": TYPES.Ascii}, + 272: {"name": "Model", "type": TYPES.Ascii}, + 273: {"name": "StripOffsets", "type": TYPES.Long}, + 274: {"name": "Orientation", "type": TYPES.Short}, + 277: {"name": "SamplesPerPixel", "type": TYPES.Short}, + 278: {"name": "RowsPerStrip", "type": TYPES.Long}, + 279: {"name": "StripByteCounts", "type": TYPES.Long}, + 282: {"name": "XResolution", "type": TYPES.Rational}, + 283: {"name": "YResolution", "type": TYPES.Rational}, + 284: {"name": "PlanarConfiguration", "type": TYPES.Short}, + 290: {"name": "GrayResponseUnit", "type": TYPES.Short}, + 291: {"name": "GrayResponseCurve", "type": TYPES.Short}, + 292: {"name": "T4Options", "type": TYPES.Long}, + 293: {"name": "T6Options", "type": TYPES.Long}, + 296: {"name": "ResolutionUnit", "type": TYPES.Short}, + 301: {"name": "TransferFunction", "type": TYPES.Short}, + 305: {"name": "Software", "type": TYPES.Ascii}, + 306: {"name": "DateTime", "type": TYPES.Ascii}, + 315: {"name": "Artist", "type": TYPES.Ascii}, + 316: {"name": "HostComputer", "type": TYPES.Ascii}, + 317: {"name": "Predictor", "type": TYPES.Short}, + 318: {"name": "WhitePoint", "type": TYPES.Rational}, + 319: {"name": "PrimaryChromaticities", "type": TYPES.Rational}, + 320: {"name": "ColorMap", "type": TYPES.Short}, + 321: {"name": "HalftoneHints", "type": TYPES.Short}, + 322: {"name": "TileWidth", "type": TYPES.Short}, + 323: {"name": "TileLength", "type": TYPES.Short}, + 324: {"name": "TileOffsets", "type": TYPES.Short}, + 325: {"name": "TileByteCounts", "type": TYPES.Short}, + 330: {"name": "SubIFDs", "type": TYPES.Long}, + 332: {"name": "InkSet", "type": TYPES.Short}, + 333: {"name": "InkNames", "type": TYPES.Ascii}, + 334: {"name": "NumberOfInks", "type": TYPES.Short}, + 336: {"name": "DotRange", "type": TYPES.Byte}, + 337: {"name": "TargetPrinter", "type": TYPES.Ascii}, + 338: {"name": "ExtraSamples", "type": TYPES.Short}, + 339: {"name": "SampleFormat", "type": TYPES.Short}, + 340: {"name": "SMinSampleValue", "type": TYPES.Short}, + 341: {"name": "SMaxSampleValue", "type": TYPES.Short}, + 342: {"name": "TransferRange", "type": TYPES.Short}, + 343: {"name": "ClipPath", "type": TYPES.Byte}, + 344: {"name": "XClipPathUnits", "type": TYPES.Long}, + 345: {"name": "YClipPathUnits", "type": TYPES.Long}, + 346: {"name": "Indexed", "type": TYPES.Short}, + 347: {"name": "JPEGTables", "type": TYPES.Undefined}, + 351: {"name": "OPIProxy", "type": TYPES.Short}, + 512: {"name": "JPEGProc", "type": TYPES.Long}, + 513: {"name": "JPEGInterchangeFormat", "type": TYPES.Long}, + 514: {"name": "JPEGInterchangeFormatLength", "type": TYPES.Long}, + 515: {"name": "JPEGRestartInterval", "type": TYPES.Short}, + 517: {"name": "JPEGLosslessPredictors", "type": TYPES.Short}, + 518: {"name": "JPEGPointTransforms", "type": TYPES.Short}, + 519: {"name": "JPEGQTables", "type": TYPES.Long}, + 520: {"name": "JPEGDCTables", "type": TYPES.Long}, + 521: {"name": "JPEGACTables", "type": TYPES.Long}, + 529: {"name": "YCbCrCoefficients", "type": TYPES.Rational}, + 530: {"name": "YCbCrSubSampling", "type": TYPES.Short}, + 531: {"name": "YCbCrPositioning", "type": TYPES.Short}, + 532: {"name": "ReferenceBlackWhite", "type": TYPES.Rational}, + 700: {"name": "XMLPacket", "type": TYPES.Byte}, + 18246: {"name": "Rating", "type": TYPES.Short}, + 18249: {"name": "RatingPercent", "type": TYPES.Short}, + 32781: {"name": "ImageID", "type": TYPES.Ascii}, + 33421: {"name": "CFARepeatPatternDim", "type": TYPES.Short}, + 33422: {"name": "CFAPattern", "type": TYPES.Byte}, + 33423: {"name": "BatteryLevel", "type": TYPES.Rational}, + 33432: {"name": "Copyright", "type": TYPES.Ascii}, + 33434: {"name": "ExposureTime", "type": TYPES.Rational}, + 34377: {"name": "ImageResources", "type": TYPES.Byte}, + 34665: {"name": "ExifTag", "type": TYPES.Long}, + 34675: {"name": "InterColorProfile", "type": TYPES.Undefined}, + 34853: {"name": "GPSTag", "type": TYPES.Long}, + 34857: {"name": "Interlace", "type": TYPES.Short}, + 34858: {"name": "TimeZoneOffset", "type": TYPES.Long}, + 34859: {"name": "SelfTimerMode", "type": TYPES.Short}, + 37387: {"name": "FlashEnergy", "type": TYPES.Rational}, + 37388: {"name": "SpatialFrequencyResponse", "type": TYPES.Undefined}, + 37389: {"name": "Noise", "type": TYPES.Undefined}, + 37390: {"name": "FocalPlaneXResolution", "type": TYPES.Rational}, + 37391: {"name": "FocalPlaneYResolution", "type": TYPES.Rational}, + 37392: {"name": "FocalPlaneResolutionUnit", "type": TYPES.Short}, + 37393: {"name": "ImageNumber", "type": TYPES.Long}, + 37394: {"name": "SecurityClassification", "type": TYPES.Ascii}, + 37395: {"name": "ImageHistory", "type": TYPES.Ascii}, + 37397: {"name": "ExposureIndex", "type": TYPES.Rational}, + 37398: {"name": "TIFFEPStandardID", "type": TYPES.Byte}, + 37399: {"name": "SensingMethod", "type": TYPES.Short}, + 40091: {"name": "XPTitle", "type": TYPES.Byte}, + 40092: {"name": "XPComment", "type": TYPES.Byte}, + 40093: {"name": "XPAuthor", "type": TYPES.Byte}, + 40094: {"name": "XPKeywords", "type": TYPES.Byte}, + 40095: {"name": "XPSubject", "type": TYPES.Byte}, + 50341: {"name": "PrintImageMatching", "type": TYPES.Undefined}, + 50706: {"name": "DNGVersion", "type": TYPES.Byte}, + 50707: {"name": "DNGBackwardVersion", "type": TYPES.Byte}, + 50708: {"name": "UniqueCameraModel", "type": TYPES.Ascii}, + 50709: {"name": "LocalizedCameraModel", "type": TYPES.Byte}, + 50710: {"name": "CFAPlaneColor", "type": TYPES.Byte}, + 50711: {"name": "CFALayout", "type": TYPES.Short}, + 50712: {"name": "LinearizationTable", "type": TYPES.Short}, + 50713: {"name": "BlackLevelRepeatDim", "type": TYPES.Short}, + 50714: {"name": "BlackLevel", "type": TYPES.Rational}, + 50715: {"name": "BlackLevelDeltaH", "type": TYPES.SRational}, + 50716: {"name": "BlackLevelDeltaV", "type": TYPES.SRational}, + 50717: {"name": "WhiteLevel", "type": TYPES.Short}, + 50718: {"name": "DefaultScale", "type": TYPES.Rational}, + 50719: {"name": "DefaultCropOrigin", "type": TYPES.Short}, + 50720: {"name": "DefaultCropSize", "type": TYPES.Short}, + 50721: {"name": "ColorMatrix1", "type": TYPES.SRational}, + 50722: {"name": "ColorMatrix2", "type": TYPES.SRational}, + 50723: {"name": "CameraCalibration1", "type": TYPES.SRational}, + 50724: {"name": "CameraCalibration2", "type": TYPES.SRational}, + 50725: {"name": "ReductionMatrix1", "type": TYPES.SRational}, + 50726: {"name": "ReductionMatrix2", "type": TYPES.SRational}, + 50727: {"name": "AnalogBalance", "type": TYPES.Rational}, + 50728: {"name": "AsShotNeutral", "type": TYPES.Short}, + 50729: {"name": "AsShotWhiteXY", "type": TYPES.Rational}, + 50730: {"name": "BaselineExposure", "type": TYPES.SRational}, + 50731: {"name": "BaselineNoise", "type": TYPES.Rational}, + 50732: {"name": "BaselineSharpness", "type": TYPES.Rational}, + 50733: {"name": "BayerGreenSplit", "type": TYPES.Long}, + 50734: {"name": "LinearResponseLimit", "type": TYPES.Rational}, + 50735: {"name": "CameraSerialNumber", "type": TYPES.Ascii}, + 50736: {"name": "LensInfo", "type": TYPES.Rational}, + 50737: {"name": "ChromaBlurRadius", "type": TYPES.Rational}, + 50738: {"name": "AntiAliasStrength", "type": TYPES.Rational}, + 50739: {"name": "ShadowScale", "type": TYPES.SRational}, + 50740: {"name": "DNGPrivateData", "type": TYPES.Byte}, + 50741: {"name": "MakerNoteSafety", "type": TYPES.Short}, + 50778: {"name": "CalibrationIlluminant1", "type": TYPES.Short}, + 50779: {"name": "CalibrationIlluminant2", "type": TYPES.Short}, + 50780: {"name": "BestQualityScale", "type": TYPES.Rational}, + 50781: {"name": "RawDataUniqueID", "type": TYPES.Byte}, + 50827: {"name": "OriginalRawFileName", "type": TYPES.Byte}, + 50828: {"name": "OriginalRawFileData", "type": TYPES.Undefined}, + 50829: {"name": "ActiveArea", "type": TYPES.Short}, + 50830: {"name": "MaskedAreas", "type": TYPES.Short}, + 50831: {"name": "AsShotICCProfile", "type": TYPES.Undefined}, + 50832: {"name": "AsShotPreProfileMatrix", "type": TYPES.SRational}, + 50833: {"name": "CurrentICCProfile", "type": TYPES.Undefined}, + 50834: {"name": "CurrentPreProfileMatrix", "type": TYPES.SRational}, + 50879: {"name": "ColorimetricReference", "type": TYPES.Short}, + 50931: {"name": "CameraCalibrationSignature", "type": TYPES.Byte}, + 50932: {"name": "ProfileCalibrationSignature", "type": TYPES.Byte}, + 50934: {"name": "AsShotProfileName", "type": TYPES.Byte}, + 50935: {"name": "NoiseReductionApplied", "type": TYPES.Rational}, + 50936: {"name": "ProfileName", "type": TYPES.Byte}, + 50937: {"name": "ProfileHueSatMapDims", "type": TYPES.Long}, + 50938: {"name": "ProfileHueSatMapData1", "type": TYPES.Float}, + 50939: {"name": "ProfileHueSatMapData2", "type": TYPES.Float}, + 50940: {"name": "ProfileToneCurve", "type": TYPES.Float}, + 50941: {"name": "ProfileEmbedPolicy", "type": TYPES.Long}, + 50942: {"name": "ProfileCopyright", "type": TYPES.Byte}, + 50964: {"name": "ForwardMatrix1", "type": TYPES.SRational}, + 50965: {"name": "ForwardMatrix2", "type": TYPES.SRational}, + 50966: {"name": "PreviewApplicationName", "type": TYPES.Byte}, + 50967: {"name": "PreviewApplicationVersion", "type": TYPES.Byte}, + 50968: {"name": "PreviewSettingsName", "type": TYPES.Byte}, + 50969: {"name": "PreviewSettingsDigest", "type": TYPES.Byte}, + 50970: {"name": "PreviewColorSpace", "type": TYPES.Long}, + 50971: {"name": "PreviewDateTime", "type": TYPES.Ascii}, + 50972: {"name": "RawImageDigest", "type": TYPES.Undefined}, + 50973: {"name": "OriginalRawFileDigest", "type": TYPES.Undefined}, + 50974: {"name": "SubTileBlockSize", "type": TYPES.Long}, + 50975: {"name": "RowInterleaveFactor", "type": TYPES.Long}, + 50981: {"name": "ProfileLookTableDims", "type": TYPES.Long}, + 50982: {"name": "ProfileLookTableData", "type": TYPES.Float}, + 51008: {"name": "OpcodeList1", "type": TYPES.Undefined}, + 51009: {"name": "OpcodeList2", "type": TYPES.Undefined}, + 51022: {"name": "OpcodeList3", "type": TYPES.Undefined}, + 60606: {"name": "ZZZTestSlong1", "type": TYPES.SLong}, + 60607: {"name": "ZZZTestSlong2", "type": TYPES.SLong}, + 60608: {"name": "ZZZTestSByte", "type": TYPES.SByte}, + 60609: {"name": "ZZZTestSShort", "type": TYPES.SShort}, + 60610: {"name": "ZZZTestDFloat", "type": TYPES.DFloat}, + }, + "Exif": { + 33434: {"name": "ExposureTime", "type": TYPES.Rational}, + 33437: {"name": "FNumber", "type": TYPES.Rational}, + 34850: {"name": "ExposureProgram", "type": TYPES.Short}, + 34852: {"name": "SpectralSensitivity", "type": TYPES.Ascii}, + 34855: {"name": "ISOSpeedRatings", "type": TYPES.Short}, + 34856: {"name": "OECF", "type": TYPES.Undefined}, + 34864: {"name": "SensitivityType", "type": TYPES.Short}, + 34865: {"name": "StandardOutputSensitivity", "type": TYPES.Long}, + 34866: {"name": "RecommendedExposureIndex", "type": TYPES.Long}, + 34867: {"name": "ISOSpeed", "type": TYPES.Long}, + 34868: {"name": "ISOSpeedLatitudeyyy", "type": TYPES.Long}, + 34869: {"name": "ISOSpeedLatitudezzz", "type": TYPES.Long}, + 36864: {"name": "ExifVersion", "type": TYPES.Undefined}, + 36867: {"name": "DateTimeOriginal", "type": TYPES.Ascii}, + 36868: {"name": "DateTimeDigitized", "type": TYPES.Ascii}, + 36880: {"name": "OffsetTime", "type": TYPES.Ascii}, + 36881: {"name": "OffsetTimeOriginal", "type": TYPES.Ascii}, + 36882: {"name": "OffsetTimeDigitized", "type": TYPES.Ascii}, + 37121: {"name": "ComponentsConfiguration", "type": TYPES.Undefined}, + 37122: {"name": "CompressedBitsPerPixel", "type": TYPES.Rational}, + 37377: {"name": "ShutterSpeedValue", "type": TYPES.SRational}, + 37378: {"name": "ApertureValue", "type": TYPES.Rational}, + 37379: {"name": "BrightnessValue", "type": TYPES.SRational}, + 37380: {"name": "ExposureBiasValue", "type": TYPES.SRational}, + 37381: {"name": "MaxApertureValue", "type": TYPES.Rational}, + 37382: {"name": "SubjectDistance", "type": TYPES.Rational}, + 37383: {"name": "MeteringMode", "type": TYPES.Short}, + 37384: {"name": "LightSource", "type": TYPES.Short}, + 37385: {"name": "Flash", "type": TYPES.Short}, + 37386: {"name": "FocalLength", "type": TYPES.Rational}, + 37396: {"name": "SubjectArea", "type": TYPES.Short}, + 37500: {"name": "MakerNote", "type": TYPES.Undefined}, + 37510: {"name": "UserComment", "type": TYPES.Undefined}, + 37520: {"name": "SubSecTime", "type": TYPES.Ascii}, + 37521: {"name": "SubSecTimeOriginal", "type": TYPES.Ascii}, + 37522: {"name": "SubSecTimeDigitized", "type": TYPES.Ascii}, + 37888: {"name": "Temperature", "type": TYPES.SRational}, + 37889: {"name": "Humidity", "type": TYPES.Rational}, + 37890: {"name": "Pressure", "type": TYPES.Rational}, + 37891: {"name": "WaterDepth", "type": TYPES.SRational}, + 37892: {"name": "Acceleration", "type": TYPES.Rational}, + 37893: {"name": "CameraElevationAngle", "type": TYPES.SRational}, + 40960: {"name": "FlashpixVersion", "type": TYPES.Undefined}, + 40961: {"name": "ColorSpace", "type": TYPES.Short}, + 40962: {"name": "PixelXDimension", "type": TYPES.Long}, + 40963: {"name": "PixelYDimension", "type": TYPES.Long}, + 40964: {"name": "RelatedSoundFile", "type": TYPES.Ascii}, + 40965: {"name": "InteroperabilityTag", "type": TYPES.Long}, + 41483: {"name": "FlashEnergy", "type": TYPES.Rational}, + 41484: {"name": "SpatialFrequencyResponse", "type": TYPES.Undefined}, + 41486: {"name": "FocalPlaneXResolution", "type": TYPES.Rational}, + 41487: {"name": "FocalPlaneYResolution", "type": TYPES.Rational}, + 41488: {"name": "FocalPlaneResolutionUnit", "type": TYPES.Short}, + 41492: {"name": "SubjectLocation", "type": TYPES.Short}, + 41493: {"name": "ExposureIndex", "type": TYPES.Rational}, + 41495: {"name": "SensingMethod", "type": TYPES.Short}, + 41728: {"name": "FileSource", "type": TYPES.Undefined}, + 41729: {"name": "SceneType", "type": TYPES.Undefined}, + 41730: {"name": "CFAPattern", "type": TYPES.Undefined}, + 41985: {"name": "CustomRendered", "type": TYPES.Short}, + 41986: {"name": "ExposureMode", "type": TYPES.Short}, + 41987: {"name": "WhiteBalance", "type": TYPES.Short}, + 41988: {"name": "DigitalZoomRatio", "type": TYPES.Rational}, + 41989: {"name": "FocalLengthIn35mmFilm", "type": TYPES.Short}, + 41990: {"name": "SceneCaptureType", "type": TYPES.Short}, + 41991: {"name": "GainControl", "type": TYPES.Short}, + 41992: {"name": "Contrast", "type": TYPES.Short}, + 41993: {"name": "Saturation", "type": TYPES.Short}, + 41994: {"name": "Sharpness", "type": TYPES.Short}, + 41995: {"name": "DeviceSettingDescription", "type": TYPES.Undefined}, + 41996: {"name": "SubjectDistanceRange", "type": TYPES.Short}, + 42016: {"name": "ImageUniqueID", "type": TYPES.Ascii}, + 42032: {"name": "CameraOwnerName", "type": TYPES.Ascii}, + 42033: {"name": "BodySerialNumber", "type": TYPES.Ascii}, + 42034: {"name": "LensSpecification", "type": TYPES.Rational}, + 42035: {"name": "LensMake", "type": TYPES.Ascii}, + 42036: {"name": "LensModel", "type": TYPES.Ascii}, + 42037: {"name": "LensSerialNumber", "type": TYPES.Ascii}, + 42240: {"name": "Gamma", "type": TYPES.Rational}, + }, + "GPS": { + 0: {"name": "GPSVersionID", "type": TYPES.Byte}, + 1: {"name": "GPSLatitudeRef", "type": TYPES.Ascii}, + 2: {"name": "GPSLatitude", "type": TYPES.Rational}, + 3: {"name": "GPSLongitudeRef", "type": TYPES.Ascii}, + 4: {"name": "GPSLongitude", "type": TYPES.Rational}, + 5: {"name": "GPSAltitudeRef", "type": TYPES.Byte}, + 6: {"name": "GPSAltitude", "type": TYPES.Rational}, + 7: {"name": "GPSTimeStamp", "type": TYPES.Rational}, + 8: {"name": "GPSSatellites", "type": TYPES.Ascii}, + 9: {"name": "GPSStatus", "type": TYPES.Ascii}, + 10: {"name": "GPSMeasureMode", "type": TYPES.Ascii}, + 11: {"name": "GPSDOP", "type": TYPES.Rational}, + 12: {"name": "GPSSpeedRef", "type": TYPES.Ascii}, + 13: {"name": "GPSSpeed", "type": TYPES.Rational}, + 14: {"name": "GPSTrackRef", "type": TYPES.Ascii}, + 15: {"name": "GPSTrack", "type": TYPES.Rational}, + 16: {"name": "GPSImgDirectionRef", "type": TYPES.Ascii}, + 17: {"name": "GPSImgDirection", "type": TYPES.Rational}, + 18: {"name": "GPSMapDatum", "type": TYPES.Ascii}, + 19: {"name": "GPSDestLatitudeRef", "type": TYPES.Ascii}, + 20: {"name": "GPSDestLatitude", "type": TYPES.Rational}, + 21: {"name": "GPSDestLongitudeRef", "type": TYPES.Ascii}, + 22: {"name": "GPSDestLongitude", "type": TYPES.Rational}, + 23: {"name": "GPSDestBearingRef", "type": TYPES.Ascii}, + 24: {"name": "GPSDestBearing", "type": TYPES.Rational}, + 25: {"name": "GPSDestDistanceRef", "type": TYPES.Ascii}, + 26: {"name": "GPSDestDistance", "type": TYPES.Rational}, + 27: {"name": "GPSProcessingMethod", "type": TYPES.Undefined}, + 28: {"name": "GPSAreaInformation", "type": TYPES.Undefined}, + 29: {"name": "GPSDateStamp", "type": TYPES.Ascii}, + 30: {"name": "GPSDifferential", "type": TYPES.Short}, + 31: {"name": "GPSHPositioningError", "type": TYPES.Rational}, + }, + "Interop": {1: {"name": "InteroperabilityIndex", "type": TYPES.Ascii}}, } TAGS["0th"] = TAGS["Image"] TAGS["1st"] = TAGS["Image"] + class ImageIFD: """Exif tag number reference - 0th IFD""" + ProcessingSoftware = 11 NewSubfileType = 254 SubfileType = 255 @@ -516,6 +524,7 @@ class ImageIFD: class ExifIFD: """Exif tag number reference - Exif IFD""" + ExposureTime = 33434 FNumber = 33437 ExposureProgram = 34850 @@ -599,6 +608,7 @@ class ExifIFD: class GPSIFD: """Exif tag number reference - GPS IFD""" + GPSVersionID = 0 GPSLatitudeRef = 1 GPSLatitude = 2 @@ -635,4 +645,5 @@ class GPSIFD: class InteropIFD: """Exif tag number reference - Interoperability IFD""" + InteroperabilityIndex = 1 diff --git a/openflexure_microscope/camera/piexif/_insert.py b/openflexure_microscope/camera/piexif/_insert.py index 2f328653..4330eb66 100644 --- a/openflexure_microscope/camera/piexif/_insert.py +++ b/openflexure_microscope/camera/piexif/_insert.py @@ -6,6 +6,7 @@ from ._common import * from ._exceptions import InvalidImageDataError from . import _webp + def insert(exif, image, new_file=None): """ py:function:: piexif.insert(exif_bytes, filename) @@ -20,7 +21,7 @@ def insert(exif, image, new_file=None): output_file = False # Prevents "UnicodeWarning: Unicode equal comparison failed" warnings on Python 2 - maybe_image = sys.version_info >= (3,0,0) or isinstance(image, str) + maybe_image = sys.version_info >= (3, 0, 0) or isinstance(image, str) if maybe_image and image[0:2] == b"\xff\xd8": image_data = image @@ -29,7 +30,7 @@ def insert(exif, image, new_file=None): image_data = image file_type = "webp" else: - with open(image, 'rb') as f: + with open(image, "rb") as f: image_data = f.read() if image_data[0:2] == b"\xff\xd8": file_type = "jpeg" @@ -57,4 +58,4 @@ def insert(exif, image, new_file=None): with open(image, "wb+") as f: f.write(new_data) else: - raise ValueError("Give a 3rd argument to 'insert' to output file") \ No newline at end of file + raise ValueError("Give a 3rd argument to 'insert' to output file") diff --git a/openflexure_microscope/camera/piexif/_load.py b/openflexure_microscope/camera/piexif/_load.py index f7d6e31a..c4143beb 100644 --- a/openflexure_microscope/camera/piexif/_load.py +++ b/openflexure_microscope/camera/piexif/_load.py @@ -19,12 +19,14 @@ def load(input_data, key_is_name=False): :return: Exif data({"0th":dict, "Exif":dict, "GPS":dict, "Interop":dict, "1st":dict, "thumbnail":bytes}) :rtype: dict """ - exif_dict = {"0th":{}, - "Exif":{}, - "GPS":{}, - "Interop":{}, - "1st":{}, - "thumbnail":None} + exif_dict = { + "0th": {}, + "Exif": {}, + "GPS": {}, + "Interop": {}, + "1st": {}, + "thumbnail": None, + } exifReader = _ExifReader(input_data) if exifReader.tiftag is None: return exif_dict @@ -34,8 +36,7 @@ def load(input_data, key_is_name=False): else: exifReader.endian_mark = ">" - pointer = struct.unpack(exifReader.endian_mark + "L", - exifReader.tiftag[4:8])[0] + pointer = struct.unpack(exifReader.endian_mark + "L", exifReader.tiftag[4:8])[0] exif_dict["0th"] = exifReader.get_ifd_dict(pointer, "0th") first_ifd_pointer = exif_dict["0th"].pop("first_ifd_pointer") if ImageIFD.ExifTag in exif_dict["0th"]: @@ -48,14 +49,19 @@ def load(input_data, key_is_name=False): pointer = exif_dict["Exif"][ExifIFD.InteroperabilityTag] exif_dict["Interop"] = exifReader.get_ifd_dict(pointer, "Interop") if first_ifd_pointer != b"\x00\x00\x00\x00": - pointer = struct.unpack(exifReader.endian_mark + "L", - first_ifd_pointer)[0] + pointer = struct.unpack(exifReader.endian_mark + "L", first_ifd_pointer)[0] exif_dict["1st"] = exifReader.get_ifd_dict(pointer, "1st") - if (ImageIFD.JPEGInterchangeFormat in exif_dict["1st"] and - ImageIFD.JPEGInterchangeFormatLength in exif_dict["1st"]): - end = (exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] + - exif_dict["1st"][ImageIFD.JPEGInterchangeFormatLength]) - thumb = exifReader.tiftag[exif_dict["1st"][ImageIFD.JPEGInterchangeFormat]:end] + if ( + ImageIFD.JPEGInterchangeFormat in exif_dict["1st"] + and ImageIFD.JPEGInterchangeFormatLength in exif_dict["1st"] + ): + end = ( + exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] + + exif_dict["1st"][ImageIFD.JPEGInterchangeFormatLength] + ) + thumb = exifReader.tiftag[ + exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] : end + ] exif_dict["thumbnail"] = thumb if key_is_name: @@ -66,7 +72,7 @@ def load(input_data, key_is_name=False): class _ExifReader(object): def __init__(self, data): # Prevents "UnicodeWarning: Unicode equal comparison failed" warnings on Python 2 - maybe_image = sys.version_info >= (3,0,0) or isinstance(data, str) + maybe_image = sys.version_info >= (3, 0, 0) or isinstance(data, str) if maybe_image and data[0:2] == b"\xff\xd8": # JPEG segments = split_into_segments(data) @@ -82,7 +88,7 @@ class _ExifReader(object): elif maybe_image and data[0:4] == b"Exif": # Exif self.tiftag = data[6:] else: - with open(data, 'rb') as f: + with open(data, "rb") as f: magic_number = f.read(2) if magic_number == b"\xff\xd8": # JPEG app1 = read_exif_from_file(data) @@ -91,13 +97,13 @@ class _ExifReader(object): else: self.tiftag = None elif magic_number in (b"\x49\x49", b"\x4d\x4d"): # TIFF - with open(data, 'rb') as f: + with open(data, "rb") as f: self.tiftag = f.read() else: - with open(data, 'rb') as f: + with open(data, "rb") as f: header = f.read(12) - if header[0:4] == b"RIFF"and header[8:12] == b"WEBP": - with open(data, 'rb') as f: + if header[0:4] == b"RIFF" and header[8:12] == b"WEBP": + with open(data, "rb") as f: file_data = f.read() self.tiftag = _webp.get_exif(file_data) else: @@ -105,8 +111,9 @@ class _ExifReader(object): def get_ifd_dict(self, pointer, ifd_name, read_unknown=False): ifd_dict = {} - tag_count = struct.unpack(self.endian_mark + "H", - self.tiftag[pointer: pointer+2])[0] + tag_count = struct.unpack( + self.endian_mark + "H", self.tiftag[pointer : pointer + 2] + )[0] offset = pointer + 2 if ifd_name in ["0th", "1st"]: t = "Image" @@ -115,26 +122,28 @@ class _ExifReader(object): p_and_value = [] for x in range(tag_count): pointer = offset + 12 * x - tag = struct.unpack(self.endian_mark + "H", - self.tiftag[pointer: pointer+2])[0] - value_type = struct.unpack(self.endian_mark + "H", - self.tiftag[pointer + 2: pointer + 4])[0] - value_num = struct.unpack(self.endian_mark + "L", - self.tiftag[pointer + 4: pointer + 8] - )[0] - value = self.tiftag[pointer+8: pointer+12] + tag = struct.unpack( + self.endian_mark + "H", self.tiftag[pointer : pointer + 2] + )[0] + value_type = struct.unpack( + self.endian_mark + "H", self.tiftag[pointer + 2 : pointer + 4] + )[0] + value_num = struct.unpack( + self.endian_mark + "L", self.tiftag[pointer + 4 : pointer + 8] + )[0] + value = self.tiftag[pointer + 8 : pointer + 12] p_and_value.append((pointer, value_type, value_num, value)) v_set = (value_type, value_num, value, tag) if tag in TAGS[t]: ifd_dict[tag] = self.convert_value(v_set) elif read_unknown: ifd_dict[tag] = (v_set[0], v_set[1], v_set[2], self.tiftag) - #else: + # else: # pass if ifd_name == "0th": pointer = offset + 12 * tag_count - ifd_dict["first_ifd_pointer"] = self.tiftag[pointer:pointer + 4] + ifd_dict["first_ifd_pointer"] = self.tiftag[pointer : pointer + 4] return ifd_dict def convert_value(self, val): @@ -143,114 +152,148 @@ class _ExifReader(object): length = val[1] value = val[2] - if t == TYPES.Byte: # BYTE + if t == TYPES.Byte: # BYTE if length > 4: pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = struct.unpack("B" * length, - self.tiftag[pointer: pointer + length]) + data = struct.unpack( + "B" * length, self.tiftag[pointer : pointer + length] + ) else: data = struct.unpack("B" * length, value[0:length]) - elif t == TYPES.Ascii: # ASCII + elif t == TYPES.Ascii: # ASCII if length > 4: pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = self.tiftag[pointer: pointer+length - 1] + data = self.tiftag[pointer : pointer + length - 1] else: - data = value[0: length - 1] - elif t == TYPES.Short: # SHORT + data = value[0 : length - 1] + elif t == TYPES.Short: # SHORT if length > 2: pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = struct.unpack(self.endian_mark + "H" * length, - self.tiftag[pointer: pointer+length*2]) + data = struct.unpack( + self.endian_mark + "H" * length, + self.tiftag[pointer : pointer + length * 2], + ) else: - data = struct.unpack(self.endian_mark + "H" * length, - value[0:length * 2]) - elif t == TYPES.Long: # LONG + data = struct.unpack( + self.endian_mark + "H" * length, value[0 : length * 2] + ) + elif t == TYPES.Long: # LONG if length > 1: pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = struct.unpack(self.endian_mark + "L" * length, - self.tiftag[pointer: pointer+length*4]) + data = struct.unpack( + self.endian_mark + "L" * length, + self.tiftag[pointer : pointer + length * 4], + ) else: - data = struct.unpack(self.endian_mark + "L" * length, - value) - elif t == TYPES.Rational: # RATIONAL + data = struct.unpack(self.endian_mark + "L" * length, value) + elif t == TYPES.Rational: # RATIONAL pointer = struct.unpack(self.endian_mark + "L", value)[0] if length > 1: data = tuple( - (struct.unpack(self.endian_mark + "L", - self.tiftag[pointer + x * 8: - pointer + 4 + x * 8])[0], - struct.unpack(self.endian_mark + "L", - self.tiftag[pointer + 4 + x * 8: - pointer + 8 + x * 8])[0]) + ( + struct.unpack( + self.endian_mark + "L", + self.tiftag[pointer + x * 8 : pointer + 4 + x * 8], + )[0], + struct.unpack( + self.endian_mark + "L", + self.tiftag[pointer + 4 + x * 8 : pointer + 8 + x * 8], + )[0], + ) for x in range(length) ) else: - data = (struct.unpack(self.endian_mark + "L", - self.tiftag[pointer: pointer + 4])[0], - struct.unpack(self.endian_mark + "L", - self.tiftag[pointer + 4: pointer + 8] - )[0]) - elif t == TYPES.SByte: # SIGNED BYTES + data = ( + struct.unpack( + self.endian_mark + "L", self.tiftag[pointer : pointer + 4] + )[0], + struct.unpack( + self.endian_mark + "L", self.tiftag[pointer + 4 : pointer + 8] + )[0], + ) + elif t == TYPES.SByte: # SIGNED BYTES if length > 4: pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = struct.unpack("b" * length, - self.tiftag[pointer: pointer + length]) + data = struct.unpack( + "b" * length, self.tiftag[pointer : pointer + length] + ) else: data = struct.unpack("b" * length, value[0:length]) - elif t == TYPES.Undefined: # UNDEFINED BYTES + elif t == TYPES.Undefined: # UNDEFINED BYTES if length > 4: pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = self.tiftag[pointer: pointer+length] + data = self.tiftag[pointer : pointer + length] else: data = value[0:length] - elif t == TYPES.SShort: # SIGNED SHORT + elif t == TYPES.SShort: # SIGNED SHORT if length > 2: pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = struct.unpack(self.endian_mark + "h" * length, - self.tiftag[pointer: pointer+length*2]) + data = struct.unpack( + self.endian_mark + "h" * length, + self.tiftag[pointer : pointer + length * 2], + ) else: - data = struct.unpack(self.endian_mark + "h" * length, - value[0:length * 2]) - elif t == TYPES.SLong: # SLONG + data = struct.unpack( + self.endian_mark + "h" * length, value[0 : length * 2] + ) + elif t == TYPES.SLong: # SLONG if length > 1: pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = struct.unpack(self.endian_mark + "l" * length, - self.tiftag[pointer: pointer+length*4]) + data = struct.unpack( + self.endian_mark + "l" * length, + self.tiftag[pointer : pointer + length * 4], + ) else: - data = struct.unpack(self.endian_mark + "l" * length, - value) - elif t == TYPES.SRational: # SRATIONAL + data = struct.unpack(self.endian_mark + "l" * length, value) + elif t == TYPES.SRational: # SRATIONAL pointer = struct.unpack(self.endian_mark + "L", value)[0] if length > 1: data = tuple( - (struct.unpack(self.endian_mark + "l", - self.tiftag[pointer + x * 8: pointer + 4 + x * 8])[0], - struct.unpack(self.endian_mark + "l", - self.tiftag[pointer + 4 + x * 8: pointer + 8 + x * 8])[0]) - for x in range(length) + ( + struct.unpack( + self.endian_mark + "l", + self.tiftag[pointer + x * 8 : pointer + 4 + x * 8], + )[0], + struct.unpack( + self.endian_mark + "l", + self.tiftag[pointer + 4 + x * 8 : pointer + 8 + x * 8], + )[0], + ) + for x in range(length) ) else: - data = (struct.unpack(self.endian_mark + "l", - self.tiftag[pointer: pointer + 4])[0], - struct.unpack(self.endian_mark + "l", - self.tiftag[pointer + 4: pointer + 8] - )[0]) - elif t == TYPES.Float: # FLOAT + data = ( + struct.unpack( + self.endian_mark + "l", self.tiftag[pointer : pointer + 4] + )[0], + struct.unpack( + self.endian_mark + "l", self.tiftag[pointer + 4 : pointer + 8] + )[0], + ) + elif t == TYPES.Float: # FLOAT if length > 1: pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = struct.unpack(self.endian_mark + "f" * length, - self.tiftag[pointer: pointer+length*4]) + data = struct.unpack( + self.endian_mark + "f" * length, + self.tiftag[pointer : pointer + length * 4], + ) else: - data = struct.unpack(self.endian_mark + "f" * length, - value) - elif t == TYPES.DFloat: # DOUBLE + data = struct.unpack(self.endian_mark + "f" * length, value) + elif t == TYPES.DFloat: # DOUBLE pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = struct.unpack(self.endian_mark + "d" * length, - self.tiftag[pointer: pointer+length*8]) + data = struct.unpack( + self.endian_mark + "d" * length, + self.tiftag[pointer : pointer + length * 8], + ) else: - raise ValueError("Exif might be wrong. Got incorrect value " + - "type to decode.\n" + - "tag: " + str(val[3]) + "\ntype: " + str(t)) + raise ValueError( + "Exif might be wrong. Got incorrect value " + + "type to decode.\n" + + "tag: " + + str(val[3]) + + "\ntype: " + + str(t) + ) if isinstance(data, tuple) and (len(data) == 1): return data[0] @@ -260,11 +303,20 @@ class _ExifReader(object): def _get_key_name_dict(exif_dict): new_dict = { - "0th":{TAGS["Image"][n]["name"]:value for n, value in exif_dict["0th"].items()}, - "Exif":{TAGS["Exif"][n]["name"]:value for n, value in exif_dict["Exif"].items()}, - "1st":{TAGS["Image"][n]["name"]:value for n, value in exif_dict["1st"].items()}, - "GPS":{TAGS["GPS"][n]["name"]:value for n, value in exif_dict["GPS"].items()}, - "Interop":{TAGS["Interop"][n]["name"]:value for n, value in exif_dict["Interop"].items()}, - "thumbnail":exif_dict["thumbnail"], + "0th": { + TAGS["Image"][n]["name"]: value for n, value in exif_dict["0th"].items() + }, + "Exif": { + TAGS["Exif"][n]["name"]: value for n, value in exif_dict["Exif"].items() + }, + "1st": { + TAGS["Image"][n]["name"]: value for n, value in exif_dict["1st"].items() + }, + "GPS": {TAGS["GPS"][n]["name"]: value for n, value in exif_dict["GPS"].items()}, + "Interop": { + TAGS["Interop"][n]["name"]: value + for n, value in exif_dict["Interop"].items() + }, + "thumbnail": exif_dict["thumbnail"], } - return new_dict \ No newline at end of file + return new_dict diff --git a/openflexure_microscope/camera/piexif/_remove.py b/openflexure_microscope/camera/piexif/_remove.py index e89cda42..c55fa101 100644 --- a/openflexure_microscope/camera/piexif/_remove.py +++ b/openflexure_microscope/camera/piexif/_remove.py @@ -3,6 +3,7 @@ import io from ._common import * from . import _webp + def remove(src, new_file=None): """ py:function:: piexif.remove(filename) @@ -19,7 +20,7 @@ def remove(src, new_file=None): src_data = src file_type = "webp" else: - with open(src, 'rb') as f: + with open(src, "rb") as f: src_data = f.read() output_is_file = True if src_data[0:2] == b"\xff\xd8": @@ -53,4 +54,4 @@ def remove(src, new_file=None): with open(src, "wb+") as f: f.write(new_data) else: - raise ValueError("Give a second argument to 'remove' to output file") \ No newline at end of file + raise ValueError("Give a second argument to 'remove' to output file") diff --git a/openflexure_microscope/camera/piexif/_transplant.py b/openflexure_microscope/camera/piexif/_transplant.py index 0abf7f11..8f2a0a4f 100644 --- a/openflexure_microscope/camera/piexif/_transplant.py +++ b/openflexure_microscope/camera/piexif/_transplant.py @@ -15,7 +15,7 @@ def transplant(exif_src, image, new_file=None): if exif_src[0:2] == b"\xff\xd8": src_data = exif_src else: - with open(exif_src, 'rb') as f: + with open(exif_src, "rb") as f: src_data = f.read() segments = split_into_segments(src_data) exif = get_exif_seg(segments) @@ -26,7 +26,7 @@ def transplant(exif_src, image, new_file=None): if image[0:2] == b"\xff\xd8": image_data = image else: - with open(image, 'rb') as f: + with open(image, "rb") as f: image_data = f.read() output_file = True segments = split_into_segments(image_data) @@ -42,4 +42,4 @@ def transplant(exif_src, image, new_file=None): with open(image, "wb+") as f: f.write(new_data) else: - raise ValueError("Give a 3rd argument to 'transplant' to output file") \ No newline at end of file + raise ValueError("Give a 3rd argument to 'transplant' to output file") diff --git a/openflexure_microscope/camera/piexif/_webp.py b/openflexure_microscope/camera/piexif/_webp.py index f9cc42ea..10649edf 100644 --- a/openflexure_microscope/camera/piexif/_webp.py +++ b/openflexure_microscope/camera/piexif/_webp.py @@ -1,4 +1,3 @@ - import struct @@ -18,26 +17,33 @@ def split(data): chunks = [] while pointer + CHUNK_FOURCC_LENGTH + LENGTH_BYTES_LENGTH < file_size: - fourcc = data[pointer:pointer + CHUNK_FOURCC_LENGTH] + fourcc = data[pointer : pointer + CHUNK_FOURCC_LENGTH] pointer += CHUNK_FOURCC_LENGTH - chunk_length_bytes = data[pointer:pointer + LENGTH_BYTES_LENGTH] + chunk_length_bytes = data[pointer : pointer + LENGTH_BYTES_LENGTH] chunk_length = struct.unpack("> 5-1 & ord(b"\x01") + flag = ord(chunk_data[4:5]) >> 5 - 1 & ord(b"\x01") contains = 1 * flag return contains + def _get_size_from_vp8L(chunk): b1 = chunk["data"][1:2] b2 = chunk["data"][2:3] @@ -79,11 +90,14 @@ def _get_size_from_vp8L(chunk): width_minus_one = (ord(b2) & ord(b"\x3F")) << 8 | ord(b1) width = width_minus_one + 1 - height_minus_one = (ord(b4) & ord(b"\x0F")) << 10 | ord(b3) << 2 | (ord(b2) & ord(b"\xC0")) >> 6 + height_minus_one = ( + (ord(b4) & ord(b"\x0F")) << 10 | ord(b3) << 2 | (ord(b2) & ord(b"\xC0")) >> 6 + ) height = height_minus_one + 1 return (width, height) + def _get_size_from_anmf(chunk): width_minus_one_bytes = chunk["data"][6:9] + b"\x00" width_minus_one = struct.unpack(" self.timeout - + def keep_alive(self): import time + self.kept_alive = time.time() - + def start(self): "Start monitoring sharpness by looking at JPEG size" self.background_thread = threading.Thread(target=self._measure_jpegs) self.background_thread.start() return self - + def stop(self): "Stop the background thread" self.stop_event.set() self.background_thread.join() - + def _measure_jpegs(self): "Function that runs in a background thread to record sharpness" logging.info("Starting sharpness measurement in background thread") @@ -59,7 +62,6 @@ class JPEGSharpnessMonitor(): """Return the size of a frame from the MJPEG stream""" return len(self.camera.get_frame()) - def focus_rel(self, dz, backlash=False, **kwargs): self.keep_alive() self.stage_times.append(time.time()) @@ -69,7 +71,7 @@ class JPEGSharpnessMonitor(): self.stage_positions.append(self.stage.position) i = len(self.stage_positions) - 2 return i, self.stage_positions[-1][2] - + def move_data(self, istart, istop=None): "Extract sharpness as a function of (interpolated) z" global np, logging @@ -92,18 +94,21 @@ class JPEGSharpnessMonitor(): """Return the z position of the sharpest image on a given move""" jt, jz, js = self.move_data(index) return jz[np.argmax(js)] - + def data_dict(self): """Return the gathered data as a single convenient dictionary""" data = {} - for k in ['jpeg_times', 'jpeg_sizes', 'stage_times', 'stage_positions']: + for k in ["jpeg_times", "jpeg_sizes", "stage_times", "stage_positions"]: data[k] = getattr(self, k) return data + 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), ...] + 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): @@ -111,12 +116,14 @@ def sharpness_sum_lap2(rgb_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) - return np.mean(image_lap.astype(np.float)**4) + 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]]) + edge = np.array([[-1] * n + [1] * n]) + 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 9dcd8b30..2cba84bb 100644 --- a/openflexure_microscope/plugins/default/autofocus/plugin.py +++ b/openflexure_microscope/plugins/default/autofocus/plugin.py @@ -10,15 +10,16 @@ from .api import MeasureSharpnessAPI, AutofocusAPI, FastAutofocusAPI from openflexure_microscope.devel import MicroscopePlugin + class AutofocusPlugin(MicroscopePlugin): """ Basic autofocus plugin """ api_views = { - '/measure_sharpness': MeasureSharpnessAPI, - '/autofocus': AutofocusAPI, - '/fast_autofocus': FastAutofocusAPI, + "/measure_sharpness": MeasureSharpnessAPI, + "/autofocus": AutofocusAPI, + "/fast_autofocus": FastAutofocusAPI, } ### SLOW AUTOFOCUS @@ -70,21 +71,24 @@ class AutofocusPlugin(MicroscopePlugin): m.focus_rel(dz) return m.sharpest_z_on_move(0) - def fast_autofocus(self, dz=2000, backlash=None): """Perform a down-up-down-up autofocus""" with self.monitor_sharpness() as m: - i, z = m.focus_rel(-dz/2) + i, z = m.focus_rel(-dz / 2) i, z = m.focus_rel(dz) fz = m.sharpest_z_on_move(i) if backlash is None: - i, z = m.focus_rel(-dz) # move all the way to the start so it's consistent + i, z = m.focus_rel( + -dz + ) # move all the way to the start so it's consistent else: i, z = m.focus_rel(fz - z - backlash) m.focus_rel(fz - z) return m.data_dict() - def fast_up_down_up_autofocus(self, dz=2000, target_z=0, initial_move_up=True, mini_backlash=150): + def fast_up_down_up_autofocus( + self, dz=2000, target_z=0, initial_move_up=True, mini_backlash=150 + ): """Autofocus by measuring on the way down, and moving back up with feedback. This autofocus method is very efficient, as it only passes the peak once. @@ -124,32 +128,41 @@ class AutofocusPlugin(MicroscopePlugin): # Ensure the MJPEG stream has started self.microscope.camera.start_stream_recording() - df = dz #TODO: refactor so I actually use dz in the code below! + df = dz # TODO: refactor so I actually use dz in the code below! if initial_move_up: - m.focus_rel(df/2) + m.focus_rel(df / 2) # move down i, z = m.focus_rel(-df) # now inspect where the sharpest point is, and estimate the sharpness # (JPEG size) that we should find at the start of the Z stack jt, jz, js = m.move_data(i) best_z = jz[np.argmax(js)] - target_s = np.interp([best_z+target_z], jz[::-1], js[::-1]) #NB jz is decreasing + target_s = np.interp( + [best_z + target_z], jz[::-1], js[::-1] + ) # NB jz is decreasing # now move to the start of the z stack - i, z = m.focus_rel(best_z + target_z - z + mini_backlash) # takes us to the start of the stack + i, z = m.focus_rel( + best_z + target_z - z + mini_backlash + ) # takes us to the start of the stack # We've deliberately undershot - figure out how much further we should move based on the curve current_js = m.jpeg_size() - imax = np.argmax(js) # we want to crop out just the bit below the peak - js = js[imax:] # NB z is in DECREASING order + imax = np.argmax(js) # we want to crop out just the bit below the peak + js = js[imax:] # NB z is in DECREASING order jz = jz[imax:] - inow = np.argmax(js < current_js) # use the curve we recorded to estimate our position + inow = np.argmax( + js < current_js + ) # use the curve we recorded to estimate our position # TODO: fancy interpolation stuff # So, the Z position corresponding to our current sharpness value is zs[inow] # That means we should move forwards, by best_z - zs[inow] correction_move = best_z + target_z - jz[inow] - logging.debug("Fast autofocus scan: correcting backlash by moving {} steps".format(correction_move)) + logging.debug( + "Fast autofocus scan: correcting backlash by moving {} steps".format( + correction_move + ) + ) m.focus_rel(correction_move) return m.data_dict() - diff --git a/openflexure_microscope/plugins/default/camera_calibration/__init__.py b/openflexure_microscope/plugins/default/camera_calibration/__init__.py index 0f02ba77..0a7ba613 100644 --- a/openflexure_microscope/plugins/default/camera_calibration/__init__.py +++ b/openflexure_microscope/plugins/default/camera_calibration/__init__.py @@ -1,2 +1,2 @@ -__all__ = ['Plugin'] +__all__ = ["Plugin"] from .plugin import Plugin diff --git a/openflexure_microscope/plugins/default/camera_calibration/plugin.py b/openflexure_microscope/plugins/default/camera_calibration/plugin.py index c8c45023..b4ded10a 100644 --- a/openflexure_microscope/plugins/default/camera_calibration/plugin.py +++ b/openflexure_microscope/plugins/default/camera_calibration/plugin.py @@ -1,4 +1,10 @@ -from openflexure_microscope.devel import MicroscopePlugin, MicroscopeViewPlugin, JsonResponse, request, jsonify +from openflexure_microscope.devel import ( + MicroscopePlugin, + MicroscopeViewPlugin, + JsonResponse, + request, + jsonify, +) import logging @@ -19,9 +25,7 @@ class Plugin(MicroscopePlugin): A set of default plugins """ - api_views = { - '/recalibrate': RecalibrateAPIView, - } + api_views = {"/recalibrate": RecalibrateAPIView} def recalibrate(self): """Reset the camera's settings. @@ -32,8 +36,10 @@ class Plugin(MicroscopePlugin): """ scamera = self.microscope.camera with scamera.lock: - assert not scamera.state['record_active'], "Can't recalibrate while recording!" - streaming = scamera.state['stream_active'] + assert not scamera.state[ + "record_active" + ], "Can't recalibrate while recording!" + streaming = scamera.state["stream_active"] if streaming: logging.info("Stopping stream before recalibration") scamera.stop_stream_recording(resolution=(640, 480)) diff --git a/openflexure_microscope/plugins/default/camera_calibration/recalibrate_utils.py b/openflexure_microscope/plugins/default/camera_calibration/recalibrate_utils.py index 4940a77a..5dae166d 100644 --- a/openflexure_microscope/plugins/default/camera_calibration/recalibrate_utils.py +++ b/openflexure_microscope/plugins/default/camera_calibration/recalibrate_utils.py @@ -4,10 +4,11 @@ import time from picamera import PiCamera from picamera.array import PiRGBArray, PiBayerArray + 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) + camera.capture(output, format="rgb", resize=resize, **kwargs) return output.array @@ -19,7 +20,9 @@ def flat_lens_shading_table(camera): library (with lens shading table support) it will raise an error. """ if not hasattr(PiCamera, "lens_shading_table"): - 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 @@ -28,7 +31,9 @@ def adjust_exposure_to_setpoint(camera, setpoint): print("Adjusting shutter speed to hit setpoint {}".format(setpoint), end="") for i in range(3): print(".", end="") - camera.shutter_speed = int(camera.shutter_speed * setpoint / np.max(rgb_image(camera))) + camera.shutter_speed = int( + camera.shutter_speed * setpoint / np.max(rgb_image(camera)) + ) time.sleep(1) print("done") @@ -38,7 +43,9 @@ def auto_expose_and_freeze_settings(camera): print("Allowing the camera to auto-expose") camera.awb_mode = "auto" camera.exposure_mode = "auto" - camera.iso = 0 # This is important, if it's on a fixed ISO, gain might not set properly. + camera.iso = ( + 0 + ) # This is important, if it's on a fixed ISO, gain might not set properly. for i in range(6): print(".", end="") time.sleep(0.5) @@ -53,17 +60,26 @@ def auto_expose_and_freeze_settings(camera): camera.awb_mode = "off" camera.awb_gains = g print("Auto white balance disabled, gains are {}".format(g)) - print("Analogue gain: {}, Digital gain: {}".format(camera.analog_gain, camera.digital_gain)) + print( + "Analogue gain: {}, Digital gain: {}".format( + camera.analog_gain, camera.digital_gain + ) + ) adjust_exposure_to_setpoint(camera, 215) 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)] - channels = np.zeros((4, bayer_array.shape[0]//2, bayer_array.shape[1]//2), dtype=bayer_array.dtype) + 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 @@ -86,25 +102,27 @@ def lst_from_channels(channels): # pad the image by copying edge pixels, so that it is exactly 32 times the # size of the lens shading table (NB 32 not 64 because each channel is only # half the size of the full image - remember the Bayer pattern... This - # should give results very close to 6by9's solution, albeit considerably + # should give results very close to 6by9's solution, albeit considerably # 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)) + 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 + ) + ) # 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. # 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 /= box**2 + 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 /= 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]) # I have had better results just normalising to the maximum: @@ -114,10 +132,10 @@ def lst_from_channels(channels): # For most sensible lenses I'd expect that 1.0 is the maximum value. # NB ls_channel should be a "view" of the whole lens shading array, so we don't # need to update the big array here. - - # 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. - 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 < 32] = 32 # clip at 32, minimum gain is 1 (is this necessary?) lens_shading_table = gains.astype(np.uint8) @@ -145,7 +163,7 @@ def recalibrate_camera(camera): # raw_image is a 3D array, with full resolution and 3 colour channels. No # 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) + channels = channels_from_bayer_array(raw_image) lens_shading_table = lst_from_channels(channels) camera.lens_shading_table = lens_shading_table @@ -154,8 +172,10 @@ def recalibrate_camera(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) diff --git a/openflexure_microscope/plugins/default/scan/__init__.py b/openflexure_microscope/plugins/default/scan/__init__.py index cfdd7ee4..ef2437f5 100644 --- a/openflexure_microscope/plugins/default/scan/__init__.py +++ b/openflexure_microscope/plugins/default/scan/__init__.py @@ -1,2 +1,2 @@ -__all__ = ['ScanPlugin'] +__all__ = ["ScanPlugin"] from .plugin import ScanPlugin diff --git a/openflexure_microscope/plugins/default/scan/api.py b/openflexure_microscope/plugins/default/scan/api.py index 71ff8681..7d338a14 100644 --- a/openflexure_microscope/plugins/default/scan/api.py +++ b/openflexure_microscope/plugins/default/scan/api.py @@ -1,36 +1,46 @@ -from openflexure_microscope.devel import MicroscopeViewPlugin, JsonResponse, request, jsonify, abort +from openflexure_microscope.devel import ( + MicroscopeViewPlugin, + JsonResponse, + request, + jsonify, + abort, +) import logging + class TileScanAPI(MicroscopeViewPlugin): def post(self): payload = JsonResponse(request) # Get params - filename = payload.param('filename') - temporary = payload.param('temporary', default=False, convert=bool) + filename = payload.param("filename") + temporary = payload.param("temporary", default=False, convert=bool) - step_size = payload.param('step_size', default=[2000, 1500, 100], convert=list) + step_size = payload.param("step_size", default=[2000, 1500, 100], convert=list) step_size = [int(i) for i in step_size] - grid = payload.param('grid', default=[3, 3, 5], convert=list) + grid = payload.param("grid", default=[3, 3, 5], convert=list) grid = [int(i) for i in grid] - style = payload.param('style', default='raster', convert=str) - autofocus_dz = payload.param('autofocus_dz', default=50, convert=int) - fast_autofocus = payload.param('fast_autofocus', default=False, convert=bool) + style = payload.param("style", default="raster", convert=str) + autofocus_dz = payload.param("autofocus_dz", default=50, convert=int) + fast_autofocus = payload.param("fast_autofocus", default=False, convert=bool) - use_video_port = payload.param('use_video_port', default=True, convert=bool) - resize = payload.param('size', default=None) + use_video_port = payload.param("use_video_port", default=True, convert=bool) + resize = payload.param("size", default=None) if resize: - if ('width' in resize) and ('height' in resize): - resize = (int(resize['width']), int(resize['height'])) # Convert dict to tuple + if ("width" in resize) and ("height" in resize): + resize = ( + int(resize["width"]), + int(resize["height"]), + ) # Convert dict to tuple else: abort(404) - bayer = payload.param('bayer', default=False, convert=bool) - metadata = payload.param('metadata', default={}, convert=dict) - tags = payload.param('tags', default=[], convert=list) + bayer = payload.param("bayer", default=False, convert=bool) + metadata = payload.param("metadata", default={}, convert=dict) + tags = payload.param("tags", default=[], convert=list) logging.info("Running tile scan...") task = self.microscope.task.start( @@ -46,7 +56,7 @@ class TileScanAPI(MicroscopeViewPlugin): bayer=bayer, fast_autofocus=fast_autofocus, metadata=metadata, - tags=tags + tags=tags, ) # return a handle on the autofocus task diff --git a/openflexure_microscope/plugins/default/scan/plugin.py b/openflexure_microscope/plugins/default/scan/plugin.py index d7f5bceb..42c23bcb 100644 --- a/openflexure_microscope/plugins/default/scan/plugin.py +++ b/openflexure_microscope/plugins/default/scan/plugin.py @@ -11,29 +11,31 @@ from openflexure_microscope.devel import MicroscopePlugin from .api import TileScanAPI -def construct_grid(initial, step_sizes, n_steps, style='raster'): + +def construct_grid(initial, step_sizes, n_steps, style="raster"): """ Given an initial position, step sizes, and number of steps, construct a 2-dimensional list of scan x-y positions. """ arr = [] - for i in range(n_steps[0]): # x axis + for i in range(n_steps[0]): # x axis arr.append([]) - for j in range(n_steps[1]): # y axis + for j in range(n_steps[1]): # y axis # Create a coordinate array - coord = [initial[ax] + [i, j][ax]*step_sizes[ax] for ax in range(2)] + coord = [initial[ax] + [i, j][ax] * step_sizes[ax] for ax in range(2)] # Append coordinate array to position grid arr[i].append(tuple(coord)) # Style modifiers - if style == 'snake': + if style == "snake": for i, line in enumerate(arr): if i % 2 != 0: line.reverse() return arr + def flatten_grid(grid): """ Convert a 3D list of scan positions into a flat list @@ -43,24 +45,25 @@ def flatten_grid(grid): grid = list(itertools.chain(*grid)) return grid + class ScanPlugin(MicroscopePlugin): """ Stack and tile plugin """ - api_views = { - '/tile': TileScanAPI, - } + api_views = {"/tile": TileScanAPI} - def capture(self, - basename, - scan_id, - temporary: bool = False, - use_video_port: bool = False, - resize: Tuple[int, int] = None, - bayer: bool = False, - metadata: dict = {}, - tags: list = []): + def capture( + self, + basename, + scan_id, + temporary: bool = False, + use_video_port: bool = False, + resize: Tuple[int, int] = None, + bayer: bool = False, + metadata: dict = {}, + tags: list = [], + ): # Construct a tile filename filename = "{}_{}_{}_{}".format(basename, *self.microscope.stage.position) @@ -68,47 +71,46 @@ class ScanPlugin(MicroscopePlugin): # Create output object output = self.microscope.camera.new_image( - write_to_file=True, - temporary=temporary, - filename=filename, - folder=folder) + write_to_file=True, temporary=temporary, filename=filename, folder=folder + ) # Capture self.microscope.camera.capture( - output, - use_video_port=use_video_port, - resize=resize, - bayer=bayer) + output, use_video_port=use_video_port, resize=resize, bayer=bayer + ) # Affix metadata - if 'scan' not in tags: - tags.append('scan') + if "scan" not in tags: + tags.append("scan") - metadata.update({ - 'position': self.microscope.state['stage']['position'], - 'scan_id': scan_id, - 'basename': basename, - 'microscope_id': self.microscope.id, - 'microscope_name': self.microscope.name - }) + metadata.update( + { + "position": self.microscope.state["stage"]["position"], + "scan_id": scan_id, + "basename": basename, + "microscope_id": self.microscope.id, + "microscope_name": self.microscope.name, + } + ) output.put_metadata(metadata) output.put_tags(tags) def tile( - self, - basename: str = None, - temporary: bool = False, - step_size: int = [2000, 1500, 100], - grid: list = [3, 3, 5], - style='raster', - autofocus_dz: int = 50, - use_video_port: bool = False, - resize: Tuple[int, int] = None, - bayer: bool = False, - fast_autofocus = False, - metadata: dict = {}, - tags: list = []): + self, + basename: str = None, + temporary: bool = False, + step_size: int = [2000, 1500, 100], + grid: list = [3, 3, 5], + style="raster", + autofocus_dz: int = 50, + use_video_port: bool = False, + resize: Tuple[int, int] = None, + bayer: bool = False, + fast_autofocus=False, + metadata: dict = {}, + tags: list = [], + ): # Generate a basename if none given if not basename: @@ -121,42 +123,47 @@ class ScanPlugin(MicroscopePlugin): initial_position = self.microscope.stage.position # Add scan metadata - if not 'time' in metadata: - metadata['time'] = generate_basename() + if not "time" in metadata: + metadata["time"] = generate_basename() # Check if autofocus is enabled - if autofocus_dz and hasattr(self.microscope.plugin, 'default_autofocus'): + if autofocus_dz and hasattr(self.microscope.plugin, "default_autofocus"): autofocus_enabled = True else: autofocus_enabled = False - if fast_autofocus and not hasattr(self.microscope.plugin.default_autofocus, 'monitor_sharpness'): - logging.error("Can't use fast autofocus in the scan - the default plugin doesn't support monitor_sharpness; maybe it is too old?") + if fast_autofocus and not hasattr( + self.microscope.plugin.default_autofocus, "monitor_sharpness" + ): + logging.error( + "Can't use fast autofocus in the scan - the default plugin doesn't support monitor_sharpness; maybe it is too old?" + ) fast_autofocus = False - z_stack_dz = grid[2] * step_size[2] if grid[2] > 1 else 0 # shorthand for Z stack range + z_stack_dz = ( + grid[2] * step_size[2] if grid[2] > 1 else 0 + ) # shorthand for Z stack range # Construct an x-y grid (worry about z later) x_y_grid = construct_grid( - initial_position, - step_size[:2], - grid[:2], - style=style + initial_position, step_size[:2], grid[:2], style=style ) # Keep the initial Z position the same as our current position next_z = initial_position[2] - if fast_autofocus: # If fast autofocus is enabled, make - next_z += autofocus_dz/2 # sure we start from the top of the range - initial_z = next_z # Save this value for use in raster scans + if fast_autofocus: # If fast autofocus is enabled, make + next_z += autofocus_dz / 2 # sure we start from the top of the range + initial_z = next_z # Save this value for use in raster scans # Now step through each point in the x-y coordinate array for line in x_y_grid: # If rastering, rather than snake (or eventually spiral) # Return focus to initial position - if style == 'raster': + if style == "raster": next_z = initial_z logging.debug("Returning to initial z position") - self.microscope.stage.move_abs([line[0][0], line[0][1], next_z]) #RWB: I think this line is redundant + self.microscope.stage.move_abs( + [line[0][0], line[0][1], next_z] + ) # RWB: I think this line is redundant for x_y in line: # Move to new grid position without changing z @@ -166,20 +173,21 @@ class ScanPlugin(MicroscopePlugin): if autofocus_enabled: if fast_autofocus: self.microscope.plugin.default_autofocus.fast_up_down_up_autofocus( - dz=autofocus_dz, - target_z=-z_stack_dz/2.0, # Finish below the focus - initial_move_up=False, # We're already at the top of the scan - ) - #TODO: save the focus data for future reference? Use it for diagnostics? + dz=autofocus_dz, + target_z=-z_stack_dz / 2.0, # Finish below the focus + initial_move_up=False, # We're already at the top of the scan + ) + # TODO: save the focus data for future reference? Use it for diagnostics? else: logging.debug("Running autofocus") self.microscope.plugin.default_autofocus.autofocus( - range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz)) + range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz) + ) logging.debug("Finished autofocus") time.sleep(1) # TODO: Remove # If we're not doing a z-stack, just capture - if (grid[2] <= 1): + if grid[2] <= 1: self.capture( basename, scan_id, @@ -188,7 +196,7 @@ class ScanPlugin(MicroscopePlugin): resize=resize, bayer=bayer, metadata=metadata, - tags=tags + tags=tags, ) else: logging.debug("Entering z-stack") @@ -198,38 +206,43 @@ class ScanPlugin(MicroscopePlugin): scan_id=scan_id, step_size=step_size[2], steps=grid[2], - center=not fast_autofocus, # fast_autofocus does this for us! + center=not fast_autofocus, # fast_autofocus does this for us! return_to_start=not fast_autofocus, use_video_port=use_video_port, resize=resize, bayer=bayer, metadata=metadata, - tags=tags + tags=tags, ) # Make sure we use our current best estimate of focus (i.e. the current position) next point next_z = self.microscope.stage.position[2] if fast_autofocus: - next_z += autofocus_dz/2 # Fast autofocus requires us to start at the top of the range + next_z += ( + autofocus_dz / 2 + ) # Fast autofocus requires us to start at the top of the range if grid[2] > 1: - next_z -= int(grid[2]/2.0*step_size[2]) # Z stacking means we're higher up to start with + next_z -= int( + grid[2] / 2.0 * step_size[2] + ) # Z stacking means we're higher up to start with logging.debug("Returning to {}".format(initial_position)) self.microscope.stage.move_abs(initial_position) def stack( - self, - basename: str = None, - temporary: bool = False, - scan_id: str = None, - step_size: int = 100, - steps: int = 5, - center: bool = True, - return_to_start: bool = True, - use_video_port: bool = False, - resize: Tuple[int, int] = None, - bayer: bool = False, - metadata: dict = {}, - tags: list = []): + self, + basename: str = None, + temporary: bool = False, + scan_id: str = None, + step_size: int = 100, + steps: int = 5, + center: bool = True, + return_to_start: bool = True, + use_video_port: bool = False, + resize: Tuple[int, int] = None, + bayer: bool = False, + metadata: dict = {}, + tags: list = [], + ): # Generate a basename if none given if not basename: @@ -240,13 +253,12 @@ class ScanPlugin(MicroscopePlugin): scan_id = uuid.uuid4().hex # Add scan metadata - if not 'time' in metadata: - metadata['time'] = generate_basename() + if not "time" in metadata: + metadata["time"] = generate_basename() # Store initial position initial_position = self.microscope.stage.position - with self.microscope.lock: # Move to center scan if center: @@ -264,7 +276,7 @@ class ScanPlugin(MicroscopePlugin): resize=resize, bayer=bayer, metadata=metadata, - tags=tags + tags=tags, ) if i != steps - 1: diff --git a/openflexure_microscope/plugins/example/__init__.py b/openflexure_microscope/plugins/example/__init__.py index b1d5b636..ce9672da 100644 --- a/openflexure_microscope/plugins/example/__init__.py +++ b/openflexure_microscope/plugins/example/__init__.py @@ -1,4 +1,4 @@ -__all__ = ['Plugin', 'HelloWorldAPI', 'IdentifyAPI'] +__all__ = ["Plugin", "HelloWorldAPI", "IdentifyAPI"] from .plugin import Plugin -from .api import HelloWorldAPI, IdentifyAPI \ No newline at end of file +from .api import HelloWorldAPI, IdentifyAPI diff --git a/openflexure_microscope/plugins/example/api.py b/openflexure_microscope/plugins/example/api.py index f8d65266..e68f21e8 100644 --- a/openflexure_microscope/plugins/example/api.py +++ b/openflexure_microscope/plugins/example/api.py @@ -9,11 +9,14 @@ class IdentifyAPI(MicroscopeViewPlugin): """ A simple example API plugin, attached through the main microscope plugin. """ + def get(self): """ Method to call when an HTTP GET request is made. """ - data = self.plugin.identify() # Call a method from our plugin, using the MicroscopeViewPlugin.plugin shortcut + data = ( + self.plugin.identify() + ) # Call a method from our plugin, using the MicroscopeViewPlugin.plugin shortcut return Response(escape(data)) @@ -28,7 +31,7 @@ class HelloWorldAPI(MicroscopeViewPlugin): """ # If the microscope does not already contain our plugin_string attribute - if not hasattr(self.microscope, 'plugin_string'): + if not hasattr(self.microscope, "plugin_string"): # Make a string, using the MicroscopeViewPlugin.plugin shortcut self.microscope.plugin_string = self.plugin.hello_world() @@ -43,7 +46,7 @@ class HelloWorldAPI(MicroscopeViewPlugin): payload = JsonResponse(request) # Extract a value from the JSON key 'plugin_string', and convert to a string. If no value is given, default to empty. - new_plugin_string = payload.param('plugin_string', default='', convert=str) + new_plugin_string = payload.param("plugin_string", default="", convert=str) if new_plugin_string: # If not None or empty # Set microscope attribute to the specified string @@ -56,6 +59,7 @@ class LongRunningAPI(MicroscopeViewPlugin): """ An example API plugin that uses a long-running plugin method. """ + def post(self): """ Method to call when an HTTP POST request is made. @@ -64,7 +68,7 @@ class LongRunningAPI(MicroscopeViewPlugin): payload = JsonResponse(request) # Extract a values from the JSON payload. - time_to_run = payload.param('time', default=10, convert=int) + time_to_run = payload.param("time", default=10, convert=int) # Attach the long-running method as a microscope task try: @@ -79,6 +83,7 @@ class SomeExceptionAPI(MicroscopeViewPlugin): """ An example API plugin that uses a long-running but broken plugin method. """ + def post(self): """ Method to call when an HTTP POST request is made. diff --git a/openflexure_microscope/plugins/example/plugin.py b/openflexure_microscope/plugins/example/plugin.py index 9c7335ea..c61cb16e 100644 --- a/openflexure_microscope/plugins/example/plugin.py +++ b/openflexure_microscope/plugins/example/plugin.py @@ -11,10 +11,10 @@ class Plugin(MicroscopePlugin): """ api_views = { - '/identify': IdentifyAPI, - '/hello': HelloWorldAPI, - '/long_running': LongRunningAPI, - '/some_exception': SomeExceptionAPI, + "/identify": IdentifyAPI, + "/hello": HelloWorldAPI, + "/long_running": LongRunningAPI, + "/some_exception": SomeExceptionAPI, } def identify(self): @@ -22,7 +22,9 @@ class Plugin(MicroscopePlugin): Demonstrate access to Microscope.camera, and Microscope.stage """ - response = "My parent camera is {}, and my parent stage is {}.".format(self.microscope.camera, self.microscope.stage) + response = "My parent camera is {}, and my parent stage is {}.".format( + self.microscope.camera, self.microscope.stage + ) print(response) return response @@ -39,7 +41,7 @@ class Plugin(MicroscopePlugin): """ with self.microscope.lock: time.sleep(3) - result = 15./0 + result = 15.0 / 0 return result diff --git a/openflexure_microscope/plugins/loader.py b/openflexure_microscope/plugins/loader.py index 26a5f991..98acdef2 100644 --- a/openflexure_microscope/plugins/loader.py +++ b/openflexure_microscope/plugins/loader.py @@ -5,14 +5,14 @@ import logging class ConColors: - HEADER = '\033[95m' - OKBLUE = '\033[94m' - OKGREEN = '\033[92m' - WARNING = '\033[93m' - FAIL = '\033[91m' - ENDC = '\033[0m' - BOLD = '\033[1m' - UNDERLINE = '\033[4m' + HEADER = "\033[95m" + OKBLUE = "\033[94m" + OKGREEN = "\033[92m" + WARNING = "\033[93m" + FAIL = "\033[91m" + ENDC = "\033[0m" + BOLD = "\033[1m" + UNDERLINE = "\033[4m" def module_from_file(plugin_path): @@ -23,7 +23,11 @@ def module_from_file(plugin_path): # Check if the path is to a file if not os.path.isfile(plugin_path): - logging.warning(ConColors.FAIL + "No valid plugin found at {}.".format(plugin_path) + ConColors.ENDC) + logging.warning( + ConColors.FAIL + + "No valid plugin found at {}.".format(plugin_path) + + ConColors.ENDC + ) return None, None, None else: @@ -35,6 +39,7 @@ def module_from_file(plugin_path): return plugin_spec, plugin_module, plugin_name + def check_module(module_path): """ Used to check if a module exists, without ever importing it. @@ -65,13 +70,13 @@ def check_module(module_path): def name_from_module(plugin_path): - path_array = plugin_path.split('.') + path_array = plugin_path.split(".") # Truncate namespace of default plugins - if path_array[0:2] == ['openflexure_microscope', 'plugins']: + if path_array[0:2] == ["openflexure_microscope", "plugins"]: path_array = path_array[2:] - - return '/'.join(path_array) + + return "/".join(path_array) def load_plugin_module(plugin_path): @@ -104,7 +109,13 @@ def load_plugin_class(plugin_path, plugin_class_name): try: plugin_class = getattr(plugin_module, plugin_class_name) except AttributeError: - logging.warning(ConColors.FAIL + "Class {} does not exist in plugin {}. Skipping.".format(plugin_class_name, plugin_path) + ConColors.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 @@ -113,10 +124,14 @@ def load_plugin_class(plugin_path, plugin_class_name): def class_from_map(plugin_map): - plugin_arr = plugin_map.split(':') + plugin_arr = plugin_map.split(":") if not len(plugin_arr) == 2: - logging.warning(ConColors.WARNING + "Malformed plugin map {}. Skipping.".format(plugin_map) + ConColors.ENDC) + logging.warning( + ConColors.WARNING + + "Malformed plugin map {}. Skipping.".format(plugin_map) + + ConColors.ENDC + ) return None, None else: return load_plugin_class(*plugin_arr) @@ -129,6 +144,7 @@ class PluginMount(object): Args: parent (:py:class:`openflexure_microscope.microscope.Microscope`): The parent Microscope object to attach to. """ + def __init__(self, parent): self.parent = parent self.plugins = [] # List of plugin objects @@ -141,13 +157,17 @@ class PluginMount(object): @property def members(self): - ignores = ['state', 'members', 'attach'] + ignores = ["state", "members", "attach"] plugin_array = [] for obj_name in dir(self): - if not obj_name in ignores and not obj_name[:2] == '__': + if not obj_name in ignores and not obj_name[:2] == "__": obj = getattr(self, obj_name) if isinstance(obj, MicroscopePlugin): - plugin_members = [member for member in inspect.getmembers(obj) if not member[0][:2] == '__'] + plugin_members = [ + member + for member in inspect.getmembers(obj) + if not member[0][:2] == "__" + ] plugin_info = (obj_name, plugin_members) plugin_array.append(plugin_info) return plugin_array @@ -168,10 +188,20 @@ class PluginMount(object): if plugin_class and plugin_name: plugin_object = plugin_class() - if hasattr(self, plugin_name): # If a plugin with the same name is already attached. - logging.warning(ConColors.WARNING + "A plugin named {} has already been loaded. Skipping {}.".format(plugin_name, plugin_map) + ConColors.ENDC) + if hasattr( + self, plugin_name + ): # If a plugin with the same name is already attached. + 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 setattr(self, pythonsafe_plugin_name, plugin_object) self.plugins.append((plugin_name, plugin_object)) @@ -179,11 +209,16 @@ class PluginMount(object): # Grant plugin access to the hardware plugin_object.microscope = self.parent - logging.info(ConColors.OKGREEN + "Plugin {} loaded as {}.".format(plugin_map, plugin_name) + ConColors.ENDC) + logging.info( + ConColors.OKGREEN + + "Plugin {} loaded as {}.".format(plugin_map, plugin_name) + + ConColors.ENDC + ) else: logging.error(f"Error loading plugin {plugin_map}. Moving on.") + class MicroscopePlugin: """ Parent class for all microscope plugins. @@ -195,4 +230,6 @@ class MicroscopePlugin: api_views = {} # Initially empty dictionary of API views associated with the plugin def __init__(self): - self.microscope = None #: :py:class:`openflexure_microscope.microscope.Microscope`: Microscope object + self.microscope = ( + None + ) #: :py:class:`openflexure_microscope.microscope.Microscope`: Microscope object diff --git a/openflexure_microscope/plugins/testing/form_example/__init__.py b/openflexure_microscope/plugins/testing/form_example/__init__.py index 3e9a8c4d..97549f5d 100644 --- a/openflexure_microscope/plugins/testing/form_example/__init__.py +++ b/openflexure_microscope/plugins/testing/form_example/__init__.py @@ -1,2 +1,2 @@ from .plugin import ExamplePlugin -from . import api \ No newline at end of file +from . import api diff --git a/openflexure_microscope/plugins/testing/form_example/api.py b/openflexure_microscope/plugins/testing/form_example/api.py index 302c4757..c05b3c4d 100644 --- a/openflexure_microscope/plugins/testing/form_example/api.py +++ b/openflexure_microscope/plugins/testing/form_example/api.py @@ -1,7 +1,16 @@ -from openflexure_microscope.devel import MicroscopeViewPlugin, TaskDeniedException, JsonResponse, Response, request, escape, jsonify +from openflexure_microscope.devel import ( + MicroscopeViewPlugin, + TaskDeniedException, + JsonResponse, + Response, + request, + escape, + jsonify, +) import logging + class DoAPI(MicroscopeViewPlugin): """ A simple example API plugin @@ -15,27 +24,21 @@ class DoAPI(MicroscopeViewPlugin): payload = JsonResponse(request) # Extract a values from the JSON payload. - val_int = payload.param('val_int', default=None, convert=int) - val_str = payload.param('val_str', default=None, convert=str) - val_radio = payload.param('val_radio', default=None) - val_check = payload.param('val_check', default=None) - val_select = payload.param('val_select', default=None) - val_disposable = payload.param('val_disposable', default=None) + val_int = payload.param("val_int", default=None, convert=int) + val_str = payload.param("val_str", default=None, convert=str) + val_radio = payload.param("val_radio", default=None) + val_check = payload.param("val_check", default=None) + val_select = payload.param("val_select", default=None) + val_disposable = payload.param("val_disposable", default=None) self.plugin.set_values( - val_int, - val_str, - val_radio, - val_check, - val_select, - val_disposable + val_int, val_str, val_radio, val_check, val_select, val_disposable ) print(self.plugin.get_values_dict()) - return jsonify({ - 'response': 'completed' - }) + return jsonify({"response": "completed"}) + class TaskAPI(MicroscopeViewPlugin): """ @@ -43,19 +46,19 @@ class TaskAPI(MicroscopeViewPlugin): """ def get(self): - return jsonify({ - 'run_time': self.plugin.run_time - }) + return jsonify({"run_time": self.plugin.run_time}) def post(self): # Get payload JSON payload = JsonResponse(request) # Extract a values from the JSON payload. - val_int = payload.param('run_time', default=5, convert=int) + val_int = payload.param("run_time", default=5, convert=int) logging.info("Running task...") - task = self.microscope.task.start(self.plugin.generate_random_numbers_for_a_while, val_int) + task = self.microscope.task.start( + self.plugin.generate_random_numbers_for_a_while, val_int + ) # return a handle on the autofocus task - return jsonify(task.state), 202 \ No newline at end of file + return jsonify(task.state), 202 diff --git a/openflexure_microscope/plugins/testing/form_example/plugin.py b/openflexure_microscope/plugins/testing/form_example/plugin.py index 97b1a6c7..8781e6ad 100644 --- a/openflexure_microscope/plugins/testing/form_example/plugin.py +++ b/openflexure_microscope/plugins/testing/form_example/plugin.py @@ -9,19 +9,18 @@ from .api import DoAPI, TaskAPI HERE = os.path.dirname(os.path.realpath(__file__)) FORM_PATH = os.path.join(HERE, "forms.json") + class ExamplePlugin(MicroscopePlugin): """ An example plugin using a comprehensive form """ + global FORM_PATH - with open(FORM_PATH, 'r') as sc: + with open(FORM_PATH, "r") as sc: api_form = json.load(sc) - api_views = { - '/do': DoAPI, - '/task': TaskAPI - } + api_views = {"/do": DoAPI, "/task": TaskAPI} def __init__(self): self.val_int = 10 @@ -33,7 +32,9 @@ class ExamplePlugin(MicroscopePlugin): self.run_time = 5 - def set_values(self, val_int, val_str, val_radio, val_check, val_select, val_disposable): + def set_values( + self, val_int, val_str, val_radio, val_check, val_select, val_disposable + ): """ Demonstrate a plugin with form """ @@ -54,12 +55,12 @@ class ExamplePlugin(MicroscopePlugin): def get_values_dict(self): return { - 'val_int': self.val_int, - 'val_str': self.val_str, - 'val_radio': self.val_radio, - 'val_check': self.val_check, - 'val_select': self.val_select, - 'val_unused': self.val_unused + "val_int": self.val_int, + "val_str": self.val_str, + "val_radio": self.val_radio, + "val_check": self.val_check, + "val_select": self.val_select, + "val_unused": self.val_unused, } def generate_random_numbers_for_a_while(self, run_time: int): @@ -68,5 +69,5 @@ class ExamplePlugin(MicroscopePlugin): for _ in range(run_time): vals.append(random.random()) time.sleep(1) - - return vals \ No newline at end of file + + return vals diff --git a/openflexure_microscope/stage/__init__.py b/openflexure_microscope/stage/__init__.py index 46cd36a8..b159fe5f 100644 --- a/openflexure_microscope/stage/__init__.py +++ b/openflexure_microscope/stage/__init__.py @@ -1,3 +1,3 @@ -__all__ = ['base', 'mock', 'sanga'] +__all__ = ["base", "mock", "sanga"] from . import base, mock, sanga diff --git a/openflexure_microscope/stage/base.py b/openflexure_microscope/stage/base.py index 13d23be9..55ddbfba 100644 --- a/openflexure_microscope/stage/base.py +++ b/openflexure_microscope/stage/base.py @@ -8,6 +8,7 @@ class BaseStage(metaclass=ABCMeta): lock (:py:class:`openflexure_microscope.lock.StrictLock`): Strict lock controlling thread access to camera hardware """ + def __init__(self): self.lock = StrictLock(timeout=5) diff --git a/openflexure_microscope/stage/mock.py b/openflexure_microscope/stage/mock.py index 0c2ddfec..75f671e4 100644 --- a/openflexure_microscope/stage/mock.py +++ b/openflexure_microscope/stage/mock.py @@ -4,6 +4,7 @@ from openflexure_microscope.utilities import axes_to_array from collections.abc import Iterable import numpy as np + class MockStage(BaseStage): def __init__(self, port=None, **kwargs): BaseStage.__init__(self) @@ -15,13 +16,13 @@ class MockStage(BaseStage): def state(self): """The general state dictionary of the board.""" state = { - 'position': { - 'x': self.position[0], - 'y': self.position[1], - 'z': self.position[2], + "position": { + "x": self.position[0], + "y": self.position[1], + "z": self.position[2], }, - 'board': None, - 'version': '0' + "board": None, + "version": "0", } return state @@ -29,21 +30,15 @@ class MockStage(BaseStage): """Update settings from a config dictionary""" # Set backlash. Expects a dictionary with axis labels - if 'backlash' in config: + if "backlash" in config: # Construct backlash array - backlash = axes_to_array(config['backlash'], ['x', 'y', 'z'], [0, 0, 0]) + backlash = axes_to_array(config["backlash"], ["x", "y", "z"], [0, 0, 0]) self.backlash = backlash def read_config(self) -> dict: """Return the current settings as a dictionary""" blsh = self.backlash.tolist() - config = { - 'backlash': { - 'x': blsh[0], - 'y': blsh[1], - 'z': blsh[2], - } - } + config = {"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]}} return config @property @@ -69,7 +64,7 @@ class MockStage(BaseStage): assert len(blsh) == self.n_axes self._backlash = np.array(blsh) else: - self._backlash = np.array([int(blsh)]*self.n_axes, dtype=np.int) + self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int) def move_rel(self, displacement, axis=None, backlash=True): pass diff --git a/openflexure_microscope/stage/sanga.py b/openflexure_microscope/stage/sanga.py index 9e4c25ec..4359d2b0 100644 --- a/openflexure_microscope/stage/sanga.py +++ b/openflexure_microscope/stage/sanga.py @@ -18,26 +18,29 @@ class SangaStage(BaseStage): board (:py:class:`openflexure_microscope.stage.sangaboard.Sangaboard`): Parent Sangaboard object. _backlash (list): 3-element (element-per-axis) list of backlash compensation in steps. """ + def __init__(self, port=None, **kwargs): """Class managing serial communications with the motors for an Openflexure stage""" BaseStage.__init__(self) self.board = Sangaboard(port, **kwargs) - self._backlash = None # Initialise backlash storage, used by property setter/getter - self.axis_names = ['x', 'y', 'z'] # Assume all sangaboards are 3 axis + self._backlash = ( + None + ) # Initialise backlash storage, used by property setter/getter + self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis @property def state(self): """The general state dictionary of the board.""" state = { - 'position': { - 'x': self.position[0], - 'y': self.position[1], - 'z': self.position[2], + "position": { + "x": self.position[0], + "y": self.position[1], + "z": self.position[2], }, - 'board': self.board.board, - 'firmware': self.board.firmware + "board": self.board.board, + "firmware": self.board.firmware, } return state @@ -80,27 +83,21 @@ class SangaStage(BaseStage): assert len(blsh) == self.n_axes self._backlash = np.array(blsh) else: - self._backlash = np.array([int(blsh)]*self.n_axes, dtype=np.int) + self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int) def apply_config(self, config: dict): """Update settings from a config dictionary""" # Set backlash. Expects a dictionary with axis labels - if 'backlash' in config: + if "backlash" in config: # Construct backlash array - backlash = axes_to_array(config['backlash'], ['x', 'y', 'z'], [0, 0, 0]) + backlash = axes_to_array(config["backlash"], ["x", "y", "z"], [0, 0, 0]) self.backlash = backlash def read_config(self) -> dict: """Return the current settings as a dictionary""" blsh = self.backlash.tolist() - config = { - 'backlash': { - 'x': blsh[0], - 'y': blsh[1], - 'z': blsh[2], - } - } + config = {"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]}} return config @@ -117,7 +114,9 @@ class SangaStage(BaseStage): if axis is not None: # backlash correction is easier if we're always in 3D # so this code just converts single-axis moves into all-axis moves. - assert axis in self.axis_names, "axis must be one of {}".format(self.axis_names) + assert axis in self.axis_names, "axis must be one of {}".format( + self.axis_names + ) move = np.zeros(self.n_axes, dtype=np.int) move[np.argmax(np.array(self.axis_names) == axis)] = int(displacement) displacement = move @@ -134,7 +133,7 @@ class SangaStage(BaseStage): initial_move -= np.where( self.backlash * displacement < 0, self.backlash, - np.zeros(self.n_axes, dtype=self.backlash.dtype) + np.zeros(self.n_axes, dtype=self.backlash.dtype), ) self.board.move_rel(initial_move) if np.any(displacement - initial_move != 0): @@ -160,7 +159,9 @@ class SangaStage(BaseStage): """ starting_position = self.position rel_positions = np.array(rel_positions) - assert rel_positions.shape[1] == 3, ValueError("Positions should be 3 elements long.") + assert rel_positions.shape[1] == 3, ValueError( + "Positions should be 3 elements long." + ) try: self.move_rel(rel_positions[0], backlash=backlash) yield 0 @@ -186,7 +187,7 @@ class SangaStage(BaseStage): def close(self): """Cleanly close communication with the stage""" - if hasattr(self, 'board'): + if hasattr(self, "board"): self.board.close() # Methods specific to Sangaboard @@ -205,12 +206,16 @@ class SangaStage(BaseStage): need to worry about that here. """ if type is not None: - print("An exception occurred inside a with block, resetting position \ - to its value at the start of the with block") + print( + "An exception occurred inside a with block, resetting position \ + to its value at the start of the with block" + ) try: time.sleep(0.5) self.move_abs(self._position_on_enter) except Exception as e: - print("A further exception occurred when resetting position: {}".format(e)) + print( + "A further exception occurred when resetting position: {}".format(e) + ) print("Move completed, raising exception...") raise value # Propagate the exception diff --git a/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py b/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py index c4fc45ce..efbf3f6c 100644 --- a/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py +++ b/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py @@ -29,6 +29,7 @@ import time import logging import warnings + class ExtensibleSerialInstrument(object): """ An instrument that communicates by sending strings back and forth over serial @@ -49,8 +50,13 @@ class ExtensibleSerialInstrument(object): blocked for a long time. The lock is reentrant so there's no issue with acquiring it twice. """ - termination_character = "\n" #: All messages to or from the instrument end with this character. - termination_line = None #: If multi-line responses are recieved, they must end with this string + + termination_character = ( + "\n" + ) #: All messages to or from the instrument end with this character. + termination_line = ( + None + ) #: If multi-line responses are recieved, they must end with this string ignore_echo = False port_settings = {} @@ -61,7 +67,7 @@ class ExtensibleSerialInstrument(object): logging.info("Updating ESI port settings") self.port_settings.update(kwargs) logging.info("Opening ESI connection to port {}".format(port)) - self.open(port, False) # Eventually this shouldn't rely on init... + self.open(port, False) # Eventually this shouldn't rely on init... logging.info("Opened ESI connection to port {}".format(port)) def open(self, port=None, quiet=True): @@ -71,17 +77,22 @@ class ExtensibleSerialInstrument(object): then we don't warn when ports are opened multiple times. """ with self.communications_lock: - if hasattr(self,'_ser') and self._ser.isOpen(): - if not quiet: logging.warning("Attempted to open an already-open port!") + if hasattr(self, "_ser") and self._ser.isOpen(): + if not quiet: + logging.warning("Attempted to open an already-open port!") return - if port is None: - port=self.find_port() - assert port is not None, "We don't have a serial port to open, meaning you didn't specify a valid port. Are you sure the instrument is connected?" - self._ser = serial.Serial(port,**self.port_settings) - #the block above wraps the serial IO layer with a text IO layer - #this allows us to read/write in neat lines. NB the buffer size must - #be set to 1 byte for maximum responsiveness. - assert self.test_communications(), "The instrument doesn't seem to be responding. Did you specify the right port?" + if port is None: + port = self.find_port() + assert ( + port is not None + ), "We don't have a serial port to open, meaning you didn't specify a valid port. Are you sure the instrument is connected?" + self._ser = serial.Serial(port, **self.port_settings) + # the block above wraps the serial IO layer with a text IO layer + # this allows us to read/write in neat lines. NB the buffer size must + # be set to 1 byte for maximum responsiveness. + assert ( + self.test_communications() + ), "The instrument doesn't seem to be responding. Did you specify the right port?" def close(self): """Cleanly close the device. Includes proper logging statements.""" @@ -95,11 +106,12 @@ class ExtensibleSerialInstrument(object): def __del__(self): """Emergency close the device. Try to avoid having to use this.""" - if hasattr(self, '_ser') and self._ser.isOpen(): + if hasattr(self, "_ser") and self._ser.isOpen(): with self.communications_lock: print( - "Closing an open serial communication has been triggered by garbage collection!\n"\ - "Please close this device more sensibly in future.") + "Closing an open serial communication has been triggered by garbage collection!\n" + "Please close this device more sensibly in future." + ) try: self._ser.close() except Exception as e: @@ -111,30 +123,39 @@ class ExtensibleSerialInstrument(object): def __exit__(self, type, value, traceback): """Cleanly close down the instrument at end of a with block.""" self.close() - - def write(self,query_string): + + def write(self, query_string): """Write a string to the serial port""" with self.communications_lock: - assert self._ser.isOpen(), "Attempted to write to the serial port before it was opened. Perhaps you need to call the 'open' method first?" - #TODO: Check if this code is needed and if not kill it -# try: -# if self._ser.outWaiting()>0: self._ser.flushOutput() #ensure there's nothing waiting -# except AttributeError: -# if self._ser.out_waiting>0: self._ser.flushOutput() #ensure there's nothing waiting - data=query_string+self.termination_character - data=data.encode() + assert ( + self._ser.isOpen() + ), "Attempted to write to the serial port before it was opened. Perhaps you need to call the 'open' method first?" + # TODO: Check if this code is needed and if not kill it + # try: + # if self._ser.outWaiting()>0: self._ser.flushOutput() #ensure there's nothing waiting + # except AttributeError: + # if self._ser.out_waiting>0: self._ser.flushOutput() #ensure there's nothing waiting + data = query_string + self.termination_character + data = data.encode() self._ser.write(data) def flush_input_buffer(self): """Make sure there's nothing waiting to be read, and clear the buffer if there is.""" with self.communications_lock: - if self._ser.inWaiting()>0: self._ser.flushInput() + if self._ser.inWaiting() > 0: + self._ser.flushInput() + def readline(self, timeout=None): """Read one line from the serial port.""" with self.communications_lock: - return self._ser.readline().decode('utf8').replace(self.termination_character,"\n") + return ( + self._ser.readline() + .decode("utf8") + .replace(self.termination_character, "\n") + ) _communications_lock = None + @property def communications_lock(self): """A lock object used to protect access to the communications bus""" @@ -143,7 +164,7 @@ class ExtensibleSerialInstrument(object): if self._communications_lock is None: self._communications_lock = threading.RLock() return self._communications_lock - + def read_multiline(self, termination_line=None, timeout=None): """Read one line from the underlying bus. Must be overriden. @@ -152,15 +173,19 @@ class ExtensibleSerialInstrument(object): with self.communications_lock: if termination_line is None: termination_line = self.termination_line - assert isinstance(termination_line, str), "If you perform a multiline query, you must specify a termination line either through the termination_line keyword argument or the termination_line property of the NPSerialInstrument." + assert isinstance( + termination_line, str + ), "If you perform a multiline query, you must specify a termination line either through the termination_line keyword argument or the termination_line property of the NPSerialInstrument." response = "" last_line = "dummy" - while termination_line not in last_line and len(last_line) > 0: #read until we get the termination line. + while ( + termination_line not in last_line and len(last_line) > 0 + ): # read until we get the termination line. last_line = self.readline(timeout) response += last_line return response - - def query(self,queryString,multiline=False,termination_line=None,timeout=None): + + def query(self, queryString, multiline=False, termination_line=None, timeout=None): """ Write a string to the stage controller and return its response. @@ -170,22 +195,31 @@ class ExtensibleSerialInstrument(object): with self.communications_lock: self.flush_input_buffer() self.write(queryString) - if self.ignore_echo == True: # Needs Implementing for a multiline read! + if self.ignore_echo == True: # Needs Implementing for a multiline read! first_line = self.readline(timeout).strip() if first_line == queryString: return self.readline(timeout).strip() else: - logging.info('This command did not echo!!!') + logging.info("This command did not echo!!!") return first_line - + if termination_line is not None: multiline = True if multiline: return self.read_multiline(termination_line) else: - return self.readline(timeout).strip() #question: should we strip the final newline? - - def parsed_query(self, query_string, response_string=r"%d", re_flags=0, parse_function=None, **kwargs): + return self.readline( + timeout + ).strip() # question: should we strip the final newline? + + def parsed_query( + self, + query_string, + response_string=r"%d", + re_flags=0, + parse_function=None, + **kwargs + ): """ Perform a query, returning a parsed form of the response. @@ -204,46 +238,81 @@ class ExtensibleSerialInstrument(object): """ response_regex = response_string - noop = lambda x: x #placeholder null parse function - placeholders = [ #tuples of (regex matching placeholder, regex to replace it with, parse function) + noop = lambda x: x # placeholder null parse function + placeholders = [ # tuples of (regex matching placeholder, regex to replace it with, parse function) (r"%c", r".", noop), - (r"%(\d+)c", r".{\1}", noop), #TODO support %cn where n is a number of chars + ( + r"%(\d+)c", + r".{\1}", + noop, + ), # TODO support %cn where n is a number of chars (r"%d", r"[-+]?\\d+", int), (r"%[eEfg]", r"[-+]?(?:\\d+(?:\.\\d*)?|\.\\d+)(?:[eE][-+]?\\d+)?", float), - (r"%i", r"[-+]?(?:0[xX][\\dA-Fa-f]+|0[0-7]*|\\d+)", lambda x: int(x, 0)), #0=autodetect base - (r"%o", r"[-+]?[0-7]+", lambda x: int(x, 8)), #8 means octal + ( + r"%i", + r"[-+]?(?:0[xX][\\dA-Fa-f]+|0[0-7]*|\\d+)", + lambda x: int(x, 0), + ), # 0=autodetect base + (r"%o", r"[-+]?[0-7]+", lambda x: int(x, 8)), # 8 means octal (r"%s", r"\\s+", noop), (r"%u", r"\\d+", int), - (r"%[xX]", r"[-+]?(?:0[xX])?[\\dA-Fa-f]+", lambda x: int(x, 16)), #16 forces hexadecimal + ( + r"%[xX]", + r"[-+]?(?:0[xX])?[\\dA-Fa-f]+", + lambda x: int(x, 16), + ), # 16 forces hexadecimal ] matched_placeholders = [] for placeholder, regex, parse_fun in placeholders: - response_regex = re.sub(placeholder, '('+regex+')', response_regex) #substitute regex for placeholder - matched_placeholders.extend([(parse_fun, m.start()) for m in re.finditer(placeholder, response_string)]) #save the positions of the placeholders + response_regex = re.sub( + placeholder, "(" + regex + ")", response_regex + ) # substitute regex for placeholder + matched_placeholders.extend( + [ + (parse_fun, m.start()) + for m in re.finditer(placeholder, response_string) + ] + ) # save the positions of the placeholders if parse_function is None: - parse_function = [f for f, s in sorted(matched_placeholders, key=lambda m: m[1])] #order parse functions by their occurrence in the original string - if not hasattr(parse_function,'__iter__'): - parse_function = [parse_function] #make sure it's a list. - - reply = self.query(query_string, **kwargs) #do the query - #if match this could be because another response entered the buffer between write and read. Sleep for short while then - #check if something is now in the buffer, while the buffer is not empty repeat regex - waited=False - res=re.search(response_regex, reply, flags=re_flags) + parse_function = [ + f for f, s in sorted(matched_placeholders, key=lambda m: m[1]) + ] # order parse functions by their occurrence in the original string + if not hasattr(parse_function, "__iter__"): + parse_function = [parse_function] # make sure it's a list. + + reply = self.query(query_string, **kwargs) # do the query + # if match this could be because another response entered the buffer between write and read. Sleep for short while then + # check if something is now in the buffer, while the buffer is not empty repeat regex + waited = False + res = re.search(response_regex, reply, flags=re_flags) while res is None: if not waited: - time.sleep(.1) - waited=True + time.sleep(0.1) + waited = True original_reply = reply if self._ser.inWaiting(): reply = self.readline().strip() - res=re.search(response_regex, reply, flags=re_flags) + res = re.search(response_regex, reply, flags=re_flags) if res is not None: - warnings.warn("Query suceeded after initially receieving unmatched response ('%s') to '%s'. Match pattern /%s/ (generated regex /%s/)"%(original_reply, query_string, response_string, response_regex), RuntimeWarning) + warnings.warn( + "Query suceeded after initially receieving unmatched response ('%s') to '%s'. Match pattern /%s/ (generated regex /%s/)" + % ( + original_reply, + query_string, + response_string, + response_regex, + ), + RuntimeWarning, + ) else: - raise ValueError("Stage response to '%s' ('%s') wasn't matched by /%s/ (generated regex /%s/)" % (query_string, original_reply, response_string, response_regex)) + raise ValueError( + "Stage response to '%s' ('%s') wasn't matched by /%s/ (generated regex /%s/)" + % (query_string, original_reply, response_string, response_regex) + ) try: - parsed_result= [f(g) for f, g in zip(parse_function, res.groups())] #try to apply each parse function to its argument + parsed_result = [ + f(g) for f, g in zip(parse_function, res.groups()) + ] # try to apply each parse function to its argument if len(parsed_result) == 1: return parsed_result[0] else: @@ -251,10 +320,15 @@ class ExtensibleSerialInstrument(object): except ValueError: logging.info("Matched Groups: {}".format(res.groups())) logging.info("Parsing Functions {}:".format(parse_function)) - raise ValueError("Stage response to %s ('%s') couldn't be parsed by the supplied function" % (query_string, reply)) + raise ValueError( + "Stage response to %s ('%s') couldn't be parsed by the supplied function" + % (query_string, reply) + ) + def int_query(self, query_string, **kwargs): """Perform a query and return the result(s) as integer(s) (see parsedQuery)""" return self.parsed_query(query_string, "%d", **kwargs) + def float_query(self, query_string, **kwargs): """Perform a query and return the result(s) as float(s) (see parsedQuery)""" return self.parsed_query(query_string, "%f", **kwargs) @@ -273,7 +347,13 @@ class ExtensibleSerialInstrument(object): if our instrument is there.""" with self.communications_lock: success = False - for port_name, _, _ in serial.tools.list_ports.comports(): #loop through serial ports, apparently 256 is the limit?! + for ( + port_name, + _, + _, + ) in ( + serial.tools.list_ports.comports() + ): # loop through serial ports, apparently 256 is the limit?! try: logging.info("Trying port {}".format(port_name)) self.open(port_name) @@ -285,14 +365,15 @@ class ExtensibleSerialInstrument(object): try: self.close() except: - pass #we don't care if there's an error closing the port... + pass # we don't care if there's an error closing the port... if success: - break #again, make sure this happens *after* closing the port + break # again, make sure this happens *after* closing the port if success: return port_name else: return None + class OptionalModule(object): """This allows a `ExtensibleSerialInstrument` to have optional features. @@ -300,32 +381,42 @@ class OptionalModule(object): for interfacing with optional modules which may or may not be included with the serial instrument, and can be added or removed at run-time. """ - - def __init__(self,available,parent=None,module_type="Undefined",model="Generic"): - assert type(available) is bool, 'Option module availablity should be a boolean not a {}'.format(type(available)) - self._available=available - self._parent=parent - assert type(module_type) is str, 'Option module type should be a string not a {}'.format(type(module_typ)) - self.module_type=module_type + + def __init__( + self, available, parent=None, module_type="Undefined", model="Generic" + ): + assert ( + type(available) is bool + ), "Option module availablity should be a boolean not a {}".format( + type(available) + ) + self._available = available + self._parent = parent + assert ( + type(module_type) is str + ), "Option module type should be a string not a {}".format(type(module_typ)) + self.module_type = module_type if available: - assert type(model) is str, 'Option module type should be a string not a {}'.format(type(model)) - self.model=model + assert ( + type(model) is str + ), "Option module type should be a string not a {}".format(type(model)) + self.model = model else: - self.model=None - + self.model = None + @property def available(self): return self._available - + def confirm_available(self): """Check if module is available, no return, will raise exception if not available!""" - assert self._available, "No \"{}\" supported on firmware".format(self.module_type) - + assert self._available, 'No "{}" supported on firmware'.format(self.module_type) + def describe(self): """Consistently spaced desciption for listing modules""" - return self.module_type+" "*(25-len(self.module_type))+"- "+self.model - - + return self.module_type + " " * (25 - len(self.module_type)) + "- " + self.model + + class QueriedProperty(object): """A Property interface that reads and writes from the instrument on the bus. @@ -359,8 +450,18 @@ class QueriedProperty(object): :ack_writes: set to "readline" to discard a line of input after writing. """ - def __init__(self, get_cmd=None, set_cmd=None, validate=None, valrange=None, - fdel=None, doc=None, response_string=None, ack_writes="no"): + + def __init__( + self, + get_cmd=None, + set_cmd=None, + validate=None, + valrange=None, + fdel=None, + doc=None, + response_string=None, + ack_writes="no", + ): self.response_string = response_string self.get_cmd = get_cmd self.set_cmd = set_cmd @@ -369,48 +470,54 @@ class QueriedProperty(object): self.fdel = fdel self.ack_writes = ack_writes self.__doc__ = doc - # TODO: standardise the return (single value only vs parsed result), consider bool def __get__(self, obj, objtype=None): - if issubclass(type(obj),OptionalModule): + if issubclass(type(obj), OptionalModule): obj.confirm_available() - obj=obj._parent + obj = obj._parent if obj is None: return self - assert issubclass(type(obj),ExtensibleSerialInstrument) + assert issubclass(type(obj), ExtensibleSerialInstrument) if self.get_cmd is None: raise AttributeError("unreadable attribute") # Allow certain "magic" values to set the response string - for key, val in [('float',r"%f"), - ('int',r"%d"),]: + for key, val in [("float", r"%f"), ("int", r"%d")]: if self.response_string == key: self.response_string = val - if self.response_string in ['bool', 'raw', None]: + if self.response_string in ["bool", "raw", None]: value = obj.query(self.get_cmd) - if self.response_string == 'bool': + if self.response_string == "bool": value = bool(value) else: value = obj.parsed_query(self.get_cmd, self.response_string) return value def __set__(self, obj, value): - if issubclass(type(obj),OptionalModule): + if issubclass(type(obj), OptionalModule): obj.confirm_available() - obj=obj._parent - assert issubclass(type(obj),ExtensibleSerialInstrument) + obj = obj._parent + assert issubclass(type(obj), ExtensibleSerialInstrument) if self.set_cmd is None: raise AttributeError("can't set attribute") if self.validate is not None: if value not in self.validate: - raise ValueError('invalid value supplied - value must be one of {}'.format(self.validate)) + raise ValueError( + "invalid value supplied - value must be one of {}".format( + self.validate + ) + ) if self.valrange is not None: if value < min(self.valrange) or value > max(self.valrange): - raise ValueError('invalid value supplied - value must be in the range {}-{}'.format(*self.valrange)) + raise ValueError( + "invalid value supplied - value must be in the range {}-{}".format( + *self.valrange + ) + ) message = self.set_cmd - if '{0' in message: + if "{0" in message: message = message.format(value) - elif '%' in message: + elif "%" in message: message = message % value obj.write(message) if self.ack_writes == "readline": diff --git a/openflexure_microscope/stage/sangaboard/sangaboard.py b/openflexure_microscope/stage/sangaboard/sangaboard.py index b755d44b..31d6848a 100644 --- a/openflexure_microscope/stage/sangaboard/sangaboard.py +++ b/openflexure_microscope/stage/sangaboard/sangaboard.py @@ -9,12 +9,20 @@ It is (c) Richard Bowman & Julian Stirling 2019 and released under GNU GPL v3 """ from __future__ import print_function, division import time -from .extensible_serial_instrument import ExtensibleSerialInstrument, OptionalModule, QueriedProperty, EIGHTBITS, PARITY_NONE, STOPBITS_ONE +from .extensible_serial_instrument import ( + ExtensibleSerialInstrument, + OptionalModule, + QueriedProperty, + EIGHTBITS, + PARITY_NONE, + STOPBITS_ONE, +) import serial, serial.tools.list_ports import re import warnings import logging + class Sangaboard(ExtensibleSerialInstrument): """Class managing serial communications with a Sangaboard @@ -39,34 +47,45 @@ class Sangaboard(ExtensibleSerialInstrument): """ # List of valid and deprecated firmwares, for communication checks - valid_firmwares = [ - (0, 5), - (0, 4) - ] - deprecated_firmwares = [ - (0, 4) - ] + valid_firmwares = [(0, 5), (0, 4)] + deprecated_firmwares = [(0, 4)] # These are the settings for the sangaboards serial port, and can usually be left as default. - port_settings = {'baudrate':115200, 'bytesize':EIGHTBITS, 'parity':PARITY_NONE, 'stopbits':STOPBITS_ONE} - + port_settings = { + "baudrate": 115200, + "bytesize": EIGHTBITS, + "parity": PARITY_NONE, + "stopbits": STOPBITS_ONE, + } + # position, step time and ramp time are get/set using simple serial commands. - position = QueriedProperty(get_cmd="p?", response_string=r"%d %d %d", - doc="Get the position of the axes as a tuple of 3 integers.") - step_time = QueriedProperty(get_cmd="dt?", set_cmd="dt %d", response_string="minimum step delay %d", - doc="Get or set the minimum time between steps of the motors in microseconds.\n\n" - "The step time is ``1000000/max speed`` in steps/second. It is saved to EEPROM on " - "the sangaboard, so it will be persistent even if the motor controller is turned off.") - ramp_time = QueriedProperty(get_cmd="ramp_time?", set_cmd="ramp_time %d", response_string="ramp time %d", - doc="Get or set the acceleration time in microseconds.\n\n" - "The motors will accelerate/decelerate between stationary and maximum speed over `ramp_time` " - "microseconds. Zero means the motor runs at full speed initially, with no accleration " - "control. Small moves may last less than `2*ramp_time`, in which case the acceleration " - "will be the same, but the motor will never reach full speed. It is saved to EEPROM on " - "the sangaboard, so it will be persistent even if the motor controller is turned off.") + position = QueriedProperty( + get_cmd="p?", + response_string=r"%d %d %d", + doc="Get the position of the axes as a tuple of 3 integers.", + ) + step_time = QueriedProperty( + get_cmd="dt?", + set_cmd="dt %d", + response_string="minimum step delay %d", + doc="Get or set the minimum time between steps of the motors in microseconds.\n\n" + "The step time is ``1000000/max speed`` in steps/second. It is saved to EEPROM on " + "the sangaboard, so it will be persistent even if the motor controller is turned off.", + ) + ramp_time = QueriedProperty( + get_cmd="ramp_time?", + set_cmd="ramp_time %d", + response_string="ramp time %d", + doc="Get or set the acceleration time in microseconds.\n\n" + "The motors will accelerate/decelerate between stationary and maximum speed over `ramp_time` " + "microseconds. Zero means the motor runs at full speed initially, with no accleration " + "control. Small moves may last less than `2*ramp_time`, in which case the acceleration " + "will be the same, but the motor will never reach full speed. It is saved to EEPROM on " + "the sangaboard, so it will be persistent even if the motor controller is turned off.", + ) # The names of the sangaboard's axes. NB this also defines the number of axes - axis_names = ('x', 'y', 'z') + axis_names = ("x", "y", "z") # Once initialised, `firmware` is a string that identifies the firmware version firmware = None @@ -88,30 +107,38 @@ class Sangaboard(ExtensibleSerialInstrument): # Make absolutely sure that whatever port we're on is valid self.check_valid_firmware() - #Bit messy: Defining all valid modules as not available, then overwriting with available information if available. + # Bit messy: Defining all valid modules as not available, then overwriting with available information if available. self.light_sensor = LightSensor(False) for module in self.list_modules(): - module_type=module.split(':')[0].strip() - module_model=module.split(':')[1].strip() - if module_type.startswith('Light Sensor'): + module_type = module.split(":")[0].strip() + module_model = module.split(":")[1].strip() + if module_type.startswith("Light Sensor"): if module_model in self.supported_light_sensors: - self.light_sensor = LightSensor(True,parent=self,model=module_model) + self.light_sensor = LightSensor( + True, parent=self, model=module_model + ) else: - logging.warning("Light sensor model \"%s\" not recognised."%(module_model)) - elif module_type.startswith('Endstops'): + logging.warning( + 'Light sensor model "%s" not recognised.' % (module_model) + ) + elif module_type.startswith("Endstops"): self.endstops = Endstops(True, parent=self, model=module_model) else: - logging.warning("Module type \"{}\" not recognised.".format(module_type)) + logging.warning( + 'Module type "{}" not recognised.'.format(module_type) + ) - self.board = self.query("board",timeout=2).rstrip() + self.board = self.query("board", timeout=2).rstrip() except Exception as e: # If an error occurred while setting up (e.g. because the board isn't connected or something) # make sure we close the serial port cleanly (otherwise it hangs open). self.close() logging.error(e) - logging.error("You may need to update the firmware running on the Sangaboard.") + logging.error( + "You may need to update the firmware running on the Sangaboard." + ) raise e def test_communications(self): @@ -124,16 +151,22 @@ class Sangaboard(ExtensibleSerialInstrument): logging.debug("Running firmware checks") # Request firmware version from the board - self.firmware = self.query("version",timeout=2).rstrip() + self.firmware = self.query("version", timeout=2).rstrip() logging.info("Firmware response: {}".format(self.firmware)) # Check for valid firmware string if self.firmware: - match = re.match(r"Sangaboard Firmware v(([\d]+)(?:\.([\d]+))+)", self.firmware) + match = re.match( + r"Sangaboard Firmware v(([\d]+)(?:\.([\d]+))+)", self.firmware + ) if not match: # Try old firmware format - match = re.match(r"OpenFlexure Motor Board v(([\d]+)(?:\.([\d]+))+)", self.firmware) + match = re.match( + r"OpenFlexure Motor Board v(([\d]+)(?:\.([\d]+))+)", self.firmware + ) if not match: - logging.error("Version string \"{}\" not recognised.".format(self.firmware)) + logging.error( + 'Version string "{}" not recognised.'.format(self.firmware) + ) return False else: logging.error("No firmware version string was returned.") @@ -146,15 +179,17 @@ class Sangaboard(ExtensibleSerialInstrument): self.firmware_version = match.group(1) if version_tuple not in Sangaboard.valid_firmwares: - logging.error("This version of the Python module requires firmware v0.5 (with legacy support for v0.4)") + logging.error( + "This version of the Python module requires firmware v0.5 (with legacy support for v0.4)" + ) return False - + if version_tuple in Sangaboard.deprecated_firmwares: logging.warning( "Warning this Sangaboard is using v0.4 of the firmware, this will soon be depreciated! " "Please consider updating the Sangaboard firmware" ) - + return True def move_rel(self, displacement, axis=None): @@ -164,10 +199,12 @@ class Sangaboard(ExtensibleSerialInstrument): axis: None (for 3-axis moves) or one of 'x','y','z' """ if axis is not None: - assert axis in self.axis_names, "axis must be one of {}".format(self.axis_names) + assert axis in self.axis_names, "axis must be one of {}".format( + self.axis_names + ) self.query("mr{} {}".format(axis, int(displacement))) else: - #TODO: assert displacement is 3 integers + # TODO: assert displacement is 3 integers self.query("mr {} {} {}".format(*list(displacement))) def release_motors(self): @@ -181,12 +218,12 @@ class Sangaboard(ExtensibleSerialInstrument): queries the board for its position, then instructs it to make about relative move. """ - rel_mov = [f_pos-i_pos for f_pos,i_pos in zip(final,self.position)] + rel_mov = [f_pos - i_pos for f_pos, i_pos in zip(final, self.position)] return self.move_rel(rel_mov, **kwargs) def query(self, message, *args, **kwargs): """Send a message and read the response. See ExtensibleSerialInstrument.query()""" - time.sleep(0.001) # This is to protect the stage from us talking too fast! + time.sleep(0.001) # This is to protect the stage from us talking too fast! return ExtensibleSerialInstrument.query(self, message, *args, **kwargs) def list_modules(self): @@ -194,12 +231,15 @@ class Sangaboard(ExtensibleSerialInstrument): Each module will correspond to a string of the form ``Module Name: Model`` """ - modules = self.query("list_modules",multiline=True,termination_line="--END--\r\n").split('\r\n')[:-2] + modules = self.query( + "list_modules", multiline=True, termination_line="--END--\r\n" + ).split("\r\n")[:-2] return [str(module) for module in modules] def print_help(self): """Print the stage's built-in help message.""" - print(self.query("help",multiline=True,termination_line="--END--\r\n")) + print(self.query("help", multiline=True, termination_line="--END--\r\n")) + class LightSensor(OptionalModule): """An optional module giving access to the light sensor. @@ -209,19 +249,25 @@ class LightSensor(OptionalModule): module which is an instance of this class. It can be used to access the light sensor (usually via the I2C bus). """ + valid_gains = None _valid_gains_int = None integration_time = QueriedProperty( get_cmd="light_sensor_integration_time?", set_cmd="light_sensor_integration_time %d", response_string="light sensor integration time %d ms", - doc="Get or set the integration time of the light sensor in milliseconds." + doc="Get or set the integration time of the light sensor in milliseconds.", + ) + intensity = QueriedProperty( + get_cmd="light_sensor_intensity?", + response_string="%d", + doc="Read the current intensity measured by the light sensor (arbitrary units).", ) - intensity = QueriedProperty(get_cmd="light_sensor_intensity?", response_string="%d", - doc="Read the current intensity measured by the light sensor (arbitrary units).") - def __init__(self,available,parent=None,model="Generic"): - super(LightSensor, self).__init__(available,parent=parent,module_type="LightSensor",model=model) + def __init__(self, available, parent=None, model="Generic"): + super(LightSensor, self).__init__( + available, parent=parent, module_type="LightSensor", model=model + ) if available: self.valid_gains = self.__get_gain_values() self._valid_gains_int = [int(g) for g in self.valid_gains] @@ -232,21 +278,25 @@ class LightSensor(OptionalModule): Valid gain values are defined in the `valid_gains` property, and should be floating-point numbers.""" self.confirm_available() - gain = self._parent.query('light_sensor_gain?') - M = re.search('[0-9\.]+(?=x)',gain) - assert M is not None, "Cannot read gain string: \"{}\"".format(gain) - #gain is a float as non integer gains exist but are set with floor of value + gain = self._parent.query("light_sensor_gain?") + M = re.search("[0-9\.]+(?=x)", gain) + assert M is not None, 'Cannot read gain string: "{}"'.format(gain) + # gain is a float as non integer gains exist but are set with floor of value return float(M.group()) @gain.setter - def gain(self,val): + def gain(self, val): self.confirm_available() - assert int(val) in self._valid_gains_int, "Gain {} not valid must be one of: {}".format(val,self.valid_gains) - gain = self._parent.query('light_sensor_gain %d'%(int(val))) - M = re.search('[0-9\.]+(?=x)',gain) - assert M is not None, "Cannot read gain string: \"{}\"".format(gain) - #gain is a float as non integer gains exist but are set with floor of value - assert int(val) == int(float(M.group())), 'Gain of {} set, \"{}\" returned'.format(val,gain) + assert ( + int(val) in self._valid_gains_int + ), "Gain {} not valid must be one of: {}".format(val, self.valid_gains) + gain = self._parent.query("light_sensor_gain %d" % (int(val))) + M = re.search("[0-9\.]+(?=x)", gain) + assert M is not None, 'Cannot read gain string: "{}"'.format(gain) + # gain is a float as non integer gains exist but are set with floor of value + assert int(val) == int( + float(M.group()) + ), 'Gain of {} set, "{}" returned'.format(val, gain) def __get_gain_values(self): """Read the allowable values for the light sensor's gain. @@ -256,15 +306,16 @@ class LightSensor(OptionalModule): values, the list will be of strings. """ self.confirm_available() - gains = self._parent.query('light_sensor_gain_values?') + gains = self._parent.query("light_sensor_gain_values?") try: - M = re.findall('[0-9\.]+(?=x)',gains) + M = re.findall("[0-9\.]+(?=x)", gains) return [float(gain) for gain in M] except: # Fall back to strings if we don't get floats (unlikely) gain_strings = gains[20:].split(", ") return gain_strings + class Endstops(OptionalModule): """An optional module for use with endstops. @@ -273,35 +324,41 @@ class Endstops(OptionalModule): the type, state of the endstops, read and write maximum positions, and home. """ - installed=[] + installed = [] """List of installed endstop types (min, max, soft)""" - def __init__(self,available,parent=None,model="min"): - super(Endstops, self).__init__(available,parent=parent,model="Endstops") - self.installed=model.split(' ') + def __init__(self, available, parent=None, model="min"): + super(Endstops, self).__init__(available, parent=parent, model="Endstops") + self.installed = model.split(" ") - status = QueriedProperty(get_cmd="endstops?", response_string=r"%d %d %d", - doc="Get endstops status as {-1,0,1} for {min,no,max} endstop triggered for each axis") - maxima = QueriedProperty(get_cmd="max_p?", set_cmd="max %d %d %d", response_string="%d %d %d", - doc="Vector of maximum positions, homing to max endstops will measure this, "+ - "can be set to a known value for use with max only and min+soft endstops") + status = QueriedProperty( + get_cmd="endstops?", + response_string=r"%d %d %d", + doc="Get endstops status as {-1,0,1} for {min,no,max} endstop triggered for each axis", + ) + maxima = QueriedProperty( + get_cmd="max_p?", + set_cmd="max %d %d %d", + response_string="%d %d %d", + doc="Vector of maximum positions, homing to max endstops will measure this, " + + "can be set to a known value for use with max only and min+soft endstops", + ) - def home(self, direction="min", axes=['x','y','z']): + def home(self, direction="min", axes=["x", "y", "z"]): """ Home given/all axes in the given direction (min/max/both) :param direction: one of {min,max,both} :param axes: list of axes e.g. ['x','y'] """ - ax=0 - if 'x' in axes: - ax+=1 - if 'y' in axes: - ax+=2 - if 'z' in axes: - ax+=3 + ax = 0 + if "x" in axes: + ax += 1 + if "y" in axes: + ax += 2 + if "z" in axes: + ax += 3 if direction == "min" or direction == "both": - self._parent.query('home_min {}'.format(ax)) + self._parent.query("home_min {}".format(ax)) if direction == "max" or direction == "both": - self._parent.query('home_max {}'.format(ax)) - + self._parent.query("home_max {}".format(ax)) diff --git a/openflexure_microscope/task.py b/openflexure_microscope/task.py index e2a9a085..4e92169d 100644 --- a/openflexure_microscope/task.py +++ b/openflexure_microscope/task.py @@ -17,6 +17,7 @@ class TaskOrchestrator: tasks (list): List of `Task` objects """ + def __init__(self): self.tasks = [] @@ -106,6 +107,7 @@ class Task: `status` will be one of 'idle'', 'running', 'error', or 'success'. `return` eventually holds the return value of the task function. """ + def __init__(self, function, *args, **kwargs): # The task long-running method self.task = function @@ -117,39 +119,41 @@ class Task: self.id = uuid.uuid4().hex self.state = { - 'id': self.id, - 'status': 'idle', - 'return': None, - 'start_time': None, - 'end_time': None, + "id": self.id, + "status": "idle", + "return": None, + "start_time": None, + "end_time": None, } def process(self, f): """ Wraps the target function to handle recording `status` and `return` to `state`. """ + def wrapped(*args, **kwargs): - self.state['status'] = 'running' + self.state["status"] = "running" try: r = f(*args, **kwargs) - s = 'success' + s = "success" except Exception as e: logging.error(e) logging.error(traceback.format_exc()) r = str(e) - s = 'error' - self.state['return'] = r - self.state['status'] = s + s = "error" + self.state["return"] = r + self.state["status"] = s + return wrapped def run(self): """ Records the task start and end times to `state`, and runs the task wrapped in `process`. """ - self.state['start_time'] = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S") + self.state["start_time"] = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S") self.process(self.task)(*self.args, **self.kwargs) self._running = False - self.state['end_time'] = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S") + self.state["end_time"] = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S") def start(self): # Start and draw """ diff --git a/openflexure_microscope/utilities.py b/openflexure_microscope/utilities.py index 6352ed1d..69396fc5 100644 --- a/openflexure_microscope/utilities.py +++ b/openflexure_microscope/utilities.py @@ -17,7 +17,11 @@ def set_properties(obj, **kwargs): try: saved_properties[k] = getattr(obj, k) except AttributeError: - print("Warning: could not get {} on {}. This property will not be restored!".format(k, obj)) + print( + "Warning: could not get {} on {}. This property will not be restored!".format( + k, obj + ) + ) for k, v in kwargs.items(): setattr(obj, k, v) try: @@ -27,12 +31,14 @@ def set_properties(obj, **kwargs): setattr(obj, k, v) -def axes_to_array(coordinate_dictionary, axis_keys=('x', 'y', 'z'), base_array=None, asint=True): +def axes_to_array( + coordinate_dictionary, axis_keys=("x", "y", "z"), base_array=None, asint=True +): """Takes key-value pairs of a JSON value, and maps onto an array""" # If no base array is given if not base_array: # Create an array of zeros - base_array = [0]*len(axis_keys) + base_array = [0] * len(axis_keys) else: # Create a copy of the passed base_array base_array = copy.copy(base_array) @@ -40,7 +46,9 @@ def axes_to_array(coordinate_dictionary, axis_keys=('x', 'y', 'z'), base_array=N # Do the mapping for axis, key in enumerate(axis_keys): if key in coordinate_dictionary: - base_array[axis] = int(coordinate_dictionary[key]) if asint else coordinate_dictionary[key] + base_array[axis] = ( + int(coordinate_dictionary[key]) if asint else coordinate_dictionary[key] + ) return base_array @@ -84,4 +92,4 @@ def recursively_apply(data, func): return [recursively_apply(x, func) for x in data] # if the object is neither a map nor iterable else: - return func(data) \ No newline at end of file + return func(data)