Blackened everything

This commit is contained in:
Joel Collins 2019-09-15 14:17:52 +01:00
parent e213647217
commit 5966ce29be
57 changed files with 1938 additions and 1414 deletions

View file

@ -1,4 +1,4 @@
__all__ = ['Microscope', 'config', 'task', 'lock', 'utilities'] __all__ = ["Microscope", "config", "task", "lock", "utilities"]
__version__ = "0.1.0" __version__ = "0.1.0"
from .microscope import Microscope from .microscope import Microscope

View file

@ -1,3 +1,3 @@
__all__ = ['utilities'] __all__ = ["utilities"]
from . import utilities from . import utilities

View file

@ -6,8 +6,7 @@ import logging
import sys import sys
import os import os
from flask import ( from flask import Flask, jsonify, send_file
Flask, jsonify, send_file)
from serial import SerialException from serial import SerialException
from datetime import datetime from datetime import datetime
@ -39,7 +38,7 @@ from openflexure_microscope.stage.mock import MockStage
# Handle logging # Handle logging
is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "") 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 (__name__ == "__main__") or (not is_gunicorn):
# If imported, but not by gunicorn # If imported, but not by gunicorn
@ -48,18 +47,15 @@ if (__name__ == "__main__") or (not is_gunicorn):
else: else:
# Direct standard Python logging to file and console # Direct standard Python logging to file and console
root = logging.getLogger() root = logging.getLogger()
error_formatter = logging.Formatter("[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s") error_formatter = logging.Formatter(
"[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s"
rotating_logfile = logging.handlers.RotatingFileHandler(
DEFAULT_LOGFILE,
maxBytes=1000000,
backupCount=7
) )
error_handlers = [ rotating_logfile = logging.handlers.RotatingFileHandler(
rotating_logfile, DEFAULT_LOGFILE, maxBytes=1000000, backupCount=7
logging.StreamHandler() )
]
error_handlers = [rotating_logfile, logging.StreamHandler()]
for handler in error_handlers: for handler in error_handlers:
handler.setFormatter(error_formatter) handler.setFormatter(error_formatter)
@ -88,7 +84,7 @@ def uri(suffix, api_version, base=None):
app = Flask(__name__) app = Flask(__name__)
app.url_map.strict_slashes = False app.url_map.strict_slashes = False
CORS(app, resources=r'/api/*') CORS(app, resources=r"/api/*")
# Make errors more API friendly # Make errors more API friendly
handler = JSONExceptionHandler(app) handler = JSONExceptionHandler(app)
@ -121,10 +117,7 @@ def attach_microscope():
# Attach devices to microscope # Attach devices to microscope
logging.debug("Attaching devices to microscope...") logging.debug("Attaching devices to microscope...")
api_microscope.attach( api_microscope.attach(api_camera, api_stage)
api_camera,
api_stage
)
# Restore loaded capture array to camera object # Restore loaded capture array to camera object
logging.debug("Restoring captures...") logging.debug("Restoring captures...")
@ -140,26 +133,26 @@ def attach_microscope():
# Base routes # Base routes
base_blueprint = blueprints.base.construct_blueprint(api_microscope) 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 routes
stage_blueprint = blueprints.stage.construct_blueprint(api_microscope) 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 routes
camera_blueprint = blueprints.camera.construct_blueprint(api_microscope) 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 routes
plugin_blueprint = blueprints.plugins.construct_blueprint(api_microscope) 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 routes
task_blueprint = blueprints.task.construct_blueprint(api_microscope) 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(): def routes():
""" """
List of all connected API routes List of all connected API routes
@ -173,7 +166,7 @@ def routes():
return jsonify(list_routes(app)) return jsonify(list_routes(app))
@app.route('/log') @app.route("/log")
def err_log(): def err_log():
""" """
Most recent 1mb of log output Most recent 1mb of log output
@ -186,9 +179,9 @@ def err_log():
""" """
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
return send_file( return send_file(
DEFAULT_LOGFILE, DEFAULT_LOGFILE,
as_attachment=True, as_attachment=True,
attachment_filename='openflexure_microscope_{}.log'.format(timestamp) attachment_filename="openflexure_microscope_{}.log".format(timestamp),
) )
@ -219,4 +212,4 @@ def cleanup():
atexit.register(cleanup) atexit.register(cleanup)
if __name__ == "__main__": 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)

View file

@ -17,17 +17,14 @@ class JSONExceptionHandler(object):
def std_handler(self, error): def std_handler(self, error):
if isinstance(error, HTTPException): if isinstance(error, HTTPException):
message = error.description message = error.description
elif hasattr(error, 'message'): elif hasattr(error, "message"):
message = error.message message = error.message
else: else:
message = str(error) message = str(error)
status_code = error.code if isinstance(error, HTTPException) else 500 status_code = error.code if isinstance(error, HTTPException) else 500
response = { response = {"status_code": status_code, "message": escape(message)}
'status_code': status_code,
'message': escape(message)
}
return jsonify(response), status_code return jsonify(response), status_code
def init_app(self, app): def init_app(self, app):

View file

@ -10,7 +10,9 @@ class JsonResponse:
""" """
# Try to load as json # Try to load as json
try: 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 # If malformed JSON is passed, make an empty dictionary
except BadRequest as e: except BadRequest as e:
logging.error(e) logging.error(e)
@ -55,15 +57,12 @@ def gen(camera):
# the obtained frame is a jpeg # the obtained frame is a jpeg
frame = camera.get_frame() frame = camera.get_frame()
yield (b'--frame\r\n' yield (b"--frame\r\n" b"Content-Type: image/jpeg\r\n\r\n" + frame + b"\r\n")
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
def get_bool(get_arg): def get_bool(get_arg):
"""Convert GET request argument string to a Python bool""" """Convert GET request argument string to a Python bool"""
if (get_arg == 'true' or if get_arg == "true" or get_arg == "True" or get_arg == "1":
get_arg == 'True' or
get_arg == '1'):
return True return True
else: else:
return False return False
@ -80,10 +79,7 @@ def list_routes(app):
endpoint = rule.endpoint endpoint = rule.endpoint
methods = list(rule.methods) methods = list(rule.methods)
url = url_for(rule.endpoint, **options) url = url_for(rule.endpoint, **options)
line = { line = {"endpoint": endpoint, "methods": methods}
'endpoint': endpoint,
'methods': methods
}
output[url] = line output[url] = line
return output return output

View file

@ -6,7 +6,6 @@ import logging
class StreamAPI(MicroscopeView): class StreamAPI(MicroscopeView):
def get(self): def get(self):
""" """
Real-time MJPEG stream from the microscope camera Real-time MJPEG stream from the microscope camera
@ -22,11 +21,11 @@ class StreamAPI(MicroscopeView):
return Response( return Response(
gen(self.microscope.camera), gen(self.microscope.camera),
mimetype='multipart/x-mixed-replace; boundary=frame') mimetype="multipart/x-mixed-replace; boundary=frame",
)
class StateAPI(MicroscopeView): class StateAPI(MicroscopeView):
def get(self): def get(self):
""" """
JSON representation of the microscope object. JSON representation of the microscope object.
@ -77,7 +76,6 @@ class StateAPI(MicroscopeView):
class ConfigAPI(MicroscopeView): class ConfigAPI(MicroscopeView):
def get(self): def get(self):
""" """
JSON representation of the microscope config. JSON representation of the microscope config.
@ -216,21 +214,18 @@ class ConfigAPI(MicroscopeView):
def construct_blueprint(microscope_obj): def construct_blueprint(microscope_obj):
blueprint = Blueprint('base_blueprint', __name__) blueprint = Blueprint("base_blueprint", __name__)
blueprint.add_url_rule( blueprint.add_url_rule(
'/stream', "/stream", view_func=StreamAPI.as_view("stream", microscope=microscope_obj)
view_func=StreamAPI.as_view('stream', microscope=microscope_obj)
) )
blueprint.add_url_rule( blueprint.add_url_rule(
'/state', "/state", view_func=StateAPI.as_view("state", microscope=microscope_obj)
view_func=StateAPI.as_view('state', microscope=microscope_obj)
) )
blueprint.add_url_rule( blueprint.add_url_rule(
'/config', "/config", view_func=ConfigAPI.as_view("config", microscope=microscope_obj)
view_func=ConfigAPI.as_view('config', microscope=microscope_obj)
) )
return blueprint return blueprint

View file

@ -7,51 +7,70 @@ from . import capture, record, preview, function
def construct_blueprint(microscope_obj): def construct_blueprint(microscope_obj):
blueprint = Blueprint('camera_blueprint', __name__) blueprint = Blueprint("camera_blueprint", __name__)
# Metadata routes # Metadata routes
blueprint.add_url_rule( blueprint.add_url_rule(
'/capture/<capture_id>/metadata/<filename>', "/capture/<capture_id>/metadata/<filename>",
view_func=capture.MetadataAPI.as_view('metadata_download', microscope=microscope_obj)) view_func=capture.MetadataAPI.as_view(
"metadata_download", microscope=microscope_obj
),
)
blueprint.add_url_rule( blueprint.add_url_rule(
'/capture/<capture_id>/metadata', "/capture/<capture_id>/metadata",
view_func=capture.MetadataRedirectAPI.as_view('metadata_download_redirect', microscope=microscope_obj)) view_func=capture.MetadataRedirectAPI.as_view(
"metadata_download_redirect", microscope=microscope_obj
),
)
# Tag routes # Tag routes
blueprint.add_url_rule( blueprint.add_url_rule(
'/capture/<capture_id>/tags', "/capture/<capture_id>/tags",
view_func=capture.TagsAPI.as_view('capture_tags', microscope=microscope_obj)) view_func=capture.TagsAPI.as_view("capture_tags", microscope=microscope_obj),
)
# Capture routes # Capture routes
blueprint.add_url_rule( blueprint.add_url_rule(
'/capture/<capture_id>/download/<filename>', "/capture/<capture_id>/download/<filename>",
view_func=capture.DownloadAPI.as_view('capture_download', microscope=microscope_obj)) view_func=capture.DownloadAPI.as_view(
"capture_download", microscope=microscope_obj
),
)
blueprint.add_url_rule( blueprint.add_url_rule(
'/capture/<capture_id>/download', "/capture/<capture_id>/download",
view_func=capture.DownloadRedirectAPI.as_view('capture_download_redirect', microscope=microscope_obj)) view_func=capture.DownloadRedirectAPI.as_view(
"capture_download_redirect", microscope=microscope_obj
),
)
blueprint.add_url_rule( blueprint.add_url_rule(
'/capture/<capture_id>/', "/capture/<capture_id>/",
view_func=capture.CaptureAPI.as_view('capture', microscope=microscope_obj)) view_func=capture.CaptureAPI.as_view("capture", microscope=microscope_obj),
)
blueprint.add_url_rule( blueprint.add_url_rule(
'/capture/', "/capture/",
view_func=capture.ListAPI.as_view('capture_list', microscope=microscope_obj)) view_func=capture.ListAPI.as_view("capture_list", microscope=microscope_obj),
)
# Preview routes # Preview routes
blueprint.add_url_rule( blueprint.add_url_rule(
'/preview/<string:operation>', "/preview/<string:operation>",
view_func=preview.GPUPreviewAPI.as_view('gpu_preview', microscope=microscope_obj)) view_func=preview.GPUPreviewAPI.as_view(
"gpu_preview", microscope=microscope_obj
),
)
# Function routes # Function routes
blueprint.add_url_rule( blueprint.add_url_rule(
'/overlay', "/overlay",
view_func=function.OverlayAPI.as_view('overlay', microscope=microscope_obj)) view_func=function.OverlayAPI.as_view("overlay", microscope=microscope_obj),
)
blueprint.add_url_rule( blueprint.add_url_rule(
'/zoom', "/zoom", view_func=function.ZoomAPI.as_view("zoom", microscope=microscope_obj)
view_func=function.ZoomAPI.as_view('zoom', microscope=microscope_obj)) )
return(blueprint) return blueprint

View file

@ -6,7 +6,6 @@ from flask import Response, jsonify, request, abort, url_for, redirect, send_fil
class ListAPI(MicroscopeView): class ListAPI(MicroscopeView):
def get(self): def get(self):
""" """
Get list of image captures. Get list of image captures.
@ -30,12 +29,16 @@ class ListAPI(MicroscopeView):
:status 200: capture found :status 200: capture found
:status 404: no capture found with that id :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: if include_unavailable:
captures = [image.state for image in self.microscope.camera.images] captures = [image.state for image in self.microscope.camera.images]
else: 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) return jsonify(captures)
@ -100,38 +103,40 @@ class ListAPI(MicroscopeView):
""" """
payload = JsonResponse(request) payload = JsonResponse(request)
filename = payload.param('filename') filename = payload.param("filename")
temporary = payload.param('temporary', default=False, convert=bool) temporary = payload.param("temporary", default=False, convert=bool)
use_video_port = payload.param('use_video_port', default=False, convert=bool) use_video_port = payload.param("use_video_port", default=False, convert=bool)
bayer = payload.param('bayer', default=True, convert=bool) bayer = payload.param("bayer", default=True, convert=bool)
metadata = payload.param('metadata', default={}, convert=dict) metadata = payload.param("metadata", default={}, convert=dict)
tags = payload.param('tags', default=[], convert=list) tags = payload.param("tags", default=[], convert=list)
resize = payload.param('size', default=None) resize = payload.param("size", default=None)
if resize: if resize:
if ('width' in resize) and ('height' in resize): if ("width" in resize) and ("height" in resize):
resize = (int(resize['width']), int(resize['height'])) # Convert dict to tuple resize = (
int(resize["width"]),
int(resize["height"]),
) # Convert dict to tuple
else: else:
abort(404) abort(404)
# Explicitally acquire lock (prevents empty files being created if lock is unavailable) # Explicitally acquire lock (prevents empty files being created if lock is unavailable)
with self.microscope.camera.lock: with self.microscope.camera.lock:
output = self.microscope.camera.new_image( output = self.microscope.camera.new_image(
write_to_file=True, write_to_file=True, temporary=temporary, filename=filename
temporary=temporary, )
filename=filename)
self.microscope.camera.capture( self.microscope.camera.capture(
output, output, use_video_port=use_video_port, resize=resize, bayer=bayer
use_video_port=use_video_port, )
resize=resize,
bayer=bayer)
metadata.update({ metadata.update(
'position': self.microscope.state['stage']['position'], {
'microscope_id': self.microscope.id, "position": self.microscope.state["stage"]["position"],
'microscope_name': self.microscope.name "microscope_id": self.microscope.id,
}) "microscope_name": self.microscope.name,
}
)
output.put_metadata(metadata) output.put_metadata(metadata)
output.put_tags(tags) output.put_tags(tags)
@ -140,7 +145,6 @@ class ListAPI(MicroscopeView):
class CaptureAPI(MicroscopeView): class CaptureAPI(MicroscopeView):
def get(self, capture_id): def get(self, capture_id):
""" """
Get JSON representation of a capture Get JSON representation of a capture
@ -200,14 +204,15 @@ class CaptureAPI(MicroscopeView):
# Add API routes to returned state # Add API routes to returned state
uri_dict = { 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 available, also add download link
if capture_metadata['available']: if capture_metadata["available"]:
uri_dict['uri']['download'] = '{}download/{}'.format( uri_dict["uri"]["download"] = "{}download/{}".format(
url_for('.capture', capture_id=capture_obj.id), url_for(".capture", capture_id=capture_obj.id), capture_obj.filename
capture_obj.filename
) )
capture_metadata.update(uri_dict) capture_metadata.update(uri_dict)
@ -257,7 +262,7 @@ class CaptureAPI(MicroscopeView):
if not capture_obj: if not capture_obj:
return abort(404) # 404 Not Found return abort(404) # 404 Not Found
data_dict = JsonResponse(request).json data_dict = JsonResponse(request).json
capture_obj.put_metadata(data_dict) capture_obj.put_metadata(data_dict)
@ -299,17 +304,20 @@ class DownloadRedirectAPI(MicroscopeView):
""" """
capture_obj = self.microscope.camera.image_from_id(capture_id) 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 abort(404) # 404 Not Found
thumbnail = get_bool(request.args.get('thumbnail')) thumbnail = get_bool(request.args.get("thumbnail"))
return redirect(url_for( return redirect(
'.capture_download', url_for(
capture_id=capture_id, ".capture_download",
filename=capture_obj.filename, capture_id=capture_id,
thumbnail=thumbnail filename=capture_obj.filename,
), code=307) thumbnail=thumbnail,
),
code=307,
)
class DownloadAPI(MicroscopeView): class DownloadAPI(MicroscopeView):
@ -317,19 +325,22 @@ class DownloadAPI(MicroscopeView):
capture_obj = self.microscope.camera.image_from_id(capture_id) 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 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 no filename is specified, redirect to the capture's currently set filename
if not filename: if not filename:
return redirect(url_for( return redirect(
'capture_download', url_for(
capture_id=capture_id, "capture_download",
filename=capture_obj.filename, capture_id=capture_id,
thumbnail=thumbnail filename=capture_obj.filename,
), code=307) thumbnail=thumbnail,
),
code=307,
)
# Download the image data using the requested filename # Download the image data using the requested filename
if thumbnail: if thumbnail:
@ -337,9 +348,7 @@ class DownloadAPI(MicroscopeView):
else: else:
img = capture_obj.data img = capture_obj.data
return send_file( return send_file(img, mimetype="image/jpeg")
img,
mimetype='image/jpeg')
class MetadataRedirectAPI(MicroscopeView): class MetadataRedirectAPI(MicroscopeView):
@ -374,14 +383,17 @@ class MetadataRedirectAPI(MicroscopeView):
""" """
capture_obj = self.microscope.camera.image_from_id(capture_id) 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 abort(404) # 404 Not Found
return redirect(url_for( return redirect(
'.metadata_download', url_for(
capture_id=capture_id, ".metadata_download",
filename=capture_obj.metadataname capture_id=capture_id,
), code=307) filename=capture_obj.metadataname,
),
code=307,
)
class MetadataAPI(MicroscopeView): class MetadataAPI(MicroscopeView):
@ -389,23 +401,24 @@ class MetadataAPI(MicroscopeView):
capture_obj = self.microscope.camera.image_from_id(capture_id) 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 abort(404) # 404 Not Found
# If no filename is specified, redirect to the capture's currently set filename # If no filename is specified, redirect to the capture's currently set filename
if not filename: if not filename:
return redirect(url_for( return redirect(
'capture_download', url_for(
capture_id=capture_id, "capture_download",
filename=capture_obj.metadataname capture_id=capture_id,
), code=307) filename=capture_obj.metadataname,
),
code=307,
)
# Download the metadata using the requested filename # Download the metadata using the requested filename
data = capture_obj.yaml data = capture_obj.yaml
return Response( return Response(data, mimetype="text/yaml")
data,
mimetype="text/yaml")
class TagsAPI(MicroscopeView): class TagsAPI(MicroscopeView):
@ -430,13 +443,13 @@ class TagsAPI(MicroscopeView):
""" """
capture_obj = self.microscope.camera.image_from_id(capture_id) 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 abort(404) # 404 Not Found
metadata_tags = filter_dict(capture_obj.state, ['metadata', 'tags']) metadata_tags = filter_dict(capture_obj.state, ["metadata", "tags"])
return jsonify(metadata_tags) return jsonify(metadata_tags)
def put(self, capture_id): def put(self, capture_id):
""" """
Add tags to the capture Add tags to the capture
@ -460,9 +473,9 @@ class TagsAPI(MicroscopeView):
capture_obj = self.microscope.camera.image_from_id(capture_id) 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 abort(404) # 404 Not Found
data_dict = JsonResponse(request).json data_dict = JsonResponse(request).json
if type(data_dict) != list: if type(data_dict) != list:
@ -470,7 +483,7 @@ class TagsAPI(MicroscopeView):
capture_obj.put_tags(data_dict) 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) return jsonify(metadata_tags)
@ -507,6 +520,6 @@ class TagsAPI(MicroscopeView):
for tag in data_dict: for tag in data_dict:
capture_obj.delete_tag(str(tag)) capture_obj.delete_tag(str(tag))
metadata_tags = filter_dict(capture_obj.state, ['metadata', 'tags']) metadata_tags = filter_dict(capture_obj.state, ["metadata", "tags"])
return jsonify(metadata_tags) return jsonify(metadata_tags)

View file

@ -5,7 +5,6 @@ from flask import jsonify, request
class ZoomAPI(MicroscopeView): class ZoomAPI(MicroscopeView):
def get(self): def get(self):
""" """
Get the current zoom value Get the current zoom value
@ -17,9 +16,9 @@ class ZoomAPI(MicroscopeView):
:<header Content-Type: application/json :<header Content-Type: application/json
:status 200: preview started/stopped :status 200: preview started/stopped
""" """
zoom_value = self.microscope.camera.state['zoom_value'] zoom_value = self.microscope.camera.state["zoom_value"]
return jsonify({'zoom_value': zoom_value}) return jsonify({"zoom_value": zoom_value})
def post(self): def post(self):
""" """
@ -44,7 +43,7 @@ class ZoomAPI(MicroscopeView):
:status 200: preview started/stopped :status 200: preview started/stopped
""" """
payload = JsonResponse(request) payload = JsonResponse(request)
zoom_value = payload.param('zoom_value', default=1.0, convert=float) zoom_value = payload.param("zoom_value", default=1.0, convert=float)
self.microscope.camera.set_zoom(zoom_value) self.microscope.camera.set_zoom(zoom_value)
@ -52,7 +51,6 @@ class ZoomAPI(MicroscopeView):
class OverlayAPI(MicroscopeView): class OverlayAPI(MicroscopeView):
def get(self): def get(self):
""" """
Get overlay text Get overlay text
@ -67,7 +65,7 @@ class OverlayAPI(MicroscopeView):
text = self.microscope.camera.camera.annotate_text text = self.microscope.camera.camera.annotate_text
size = self.microscope.camera.camera.annotate_text_size size = self.microscope.camera.camera.annotate_text_size
return jsonify({'text': text, 'size': size}) return jsonify({"text": text, "size": size})
def post(self): def post(self):
""" """
@ -94,10 +92,10 @@ class OverlayAPI(MicroscopeView):
""" """
payload = JsonResponse(request) payload = JsonResponse(request)
text = payload.param('text', default="", convert=str) text = payload.param("text", default="", convert=str)
size = payload.param('size', default=50, convert=int) size = payload.param("size", default=50, convert=int)
self.microscope.camera.camera.annotate_text = text self.microscope.camera.camera.annotate_text = text
self.microscope.camera.camera.annotate_text_size = size self.microscope.camera.camera.annotate_text_size = size
return jsonify({'text': text, 'size': size}) return jsonify({"text": text, "size": size})

View file

@ -6,7 +6,6 @@ import logging
class GPUPreviewAPI(MicroscopeView): class GPUPreviewAPI(MicroscopeView):
def post(self, operation): def post(self, operation):
""" """
Start or stop the onboard GPU preview. Start or stop the onboard GPU preview.
@ -39,7 +38,7 @@ class GPUPreviewAPI(MicroscopeView):
if operation == "start": if operation == "start":
payload = JsonResponse(request) payload = JsonResponse(request)
window = payload.param('window', default=[]) window = payload.param("window", default=[])
logging.debug(window) logging.debug(window)
if len(window) != 4: if len(window) != 4:

View file

@ -6,6 +6,7 @@ from openflexure_microscope.api.v1.views import MicroscopeView
import logging import logging
import warnings import warnings
class PluginFormAPI(MicroscopeView): class PluginFormAPI(MicroscopeView):
def get(self): def get(self):
""" """
@ -26,12 +27,12 @@ class PluginFormAPI(MicroscopeView):
def construct_blueprint(microscope_obj): def construct_blueprint(microscope_obj):
blueprint = Blueprint('plugin_blueprint', __name__) blueprint = Blueprint("plugin_blueprint", __name__)
# Create a base route to return plugin API forms, if any exist # Create a base route to return plugin API forms, if any exist
blueprint.add_url_rule( blueprint.add_url_rule(
'/', "/",
view_func=PluginFormAPI.as_view('plugin_api_form', microscope=microscope_obj) view_func=PluginFormAPI.as_view("plugin_api_form", microscope=microscope_obj),
) )
all_routes = [] all_routes = []
@ -40,7 +41,7 @@ def construct_blueprint(microscope_obj):
for plugin_name, plugin_obj in microscope_obj.plugin.plugins: for plugin_name, plugin_obj in microscope_obj.plugin.plugins:
# If plugin contains valid endpoints # If plugin contains valid endpoints
if hasattr(plugin_obj, 'api_views') and isinstance(plugin_obj.api_views, dict): if hasattr(plugin_obj, "api_views") and isinstance(plugin_obj.api_views, dict):
# We'll keep a record of how each route was expanded # We'll keep a record of how each route was expanded
expanded_routes = {} expanded_routes = {}
@ -50,7 +51,7 @@ def construct_blueprint(microscope_obj):
# Remove all leading slashes from view route # Remove all leading slashes from view route
cleaned_route = view_route cleaned_route = view_route
while cleaned_route[0] == '/': while cleaned_route[0] == "/":
cleaned_route = cleaned_route[1:] cleaned_route = cleaned_route[1:]
# Construct a full view route from the plugin name # Construct a full view route from the plugin name
@ -61,12 +62,16 @@ def construct_blueprint(microscope_obj):
expanded_routes[view_route] = full_view_route expanded_routes[view_route] = full_view_route
# Check if endpoint name clashes # Check if endpoint name clashes
if full_view_route not in all_routes and issubclass(view_class, MicroscopeViewPlugin): if full_view_route not in all_routes and issubclass(
view_class, MicroscopeViewPlugin
):
# Add route to main route dictionary # Add route to main route dictionary
all_routes.append(full_view_route) all_routes.append(full_view_route)
# Create a Python-safe name for the route # Create a Python-safe name for the route
plugin_route_id = 'plugin{}'.format(full_view_route).replace('/', '_') plugin_route_id = "plugin{}".format(full_view_route).replace(
"/", "_"
)
# Add route to the plugins blueprint # Add route to the plugins blueprint
blueprint.add_url_rule( blueprint.add_url_rule(
@ -74,30 +79,37 @@ def construct_blueprint(microscope_obj):
view_func=view_class.as_view( view_func=view_class.as_view(
plugin_route_id, plugin_route_id,
microscope=microscope_obj, microscope=microscope_obj,
plugin=plugin_obj plugin=plugin_obj,
) ),
) )
else: else:
warnings.warn( warnings.warn(
"An endpoint /{} has already been loaded. Skipping {}.".format( "An endpoint /{} has already been loaded. Skipping {}.".format(
full_view_route, full_view_route, view_class
view_class
) )
) )
# If plugin includes an API form # If plugin includes an API form
if hasattr(plugin_obj, 'api_form') and isinstance(plugin_obj.api_form, dict): if hasattr(plugin_obj, "api_form") and isinstance(
plugin_obj.api_form, dict
):
api_form_info = plugin_obj.api_form api_form_info = plugin_obj.api_form
# TODO: Validate form? We need to make sure no single plugin can break all plugins. # TODO: Validate form? We need to make sure no single plugin can break all plugins.
api_form_info['id'] = plugin_name api_form_info["id"] = plugin_name
if 'forms' in api_form_info and isinstance(api_form_info['forms'], list): if "forms" in api_form_info and isinstance(
for form in api_form_info['forms']: api_form_info["forms"], list
if 'route' in form and form['route'] in expanded_routes.keys(): ):
form['route'] = expanded_routes[form['route']] for form in api_form_info["forms"]:
if "route" in form and form["route"] in expanded_routes.keys():
form["route"] = expanded_routes[form["route"]]
else: else:
logging.warn("No valid expandable route found for {}".format(form['route'])) logging.warn(
"No valid expandable route found for {}".format(
form["route"]
)
)
# Store the complete form in Microscope().plugin.form # Store the complete form in Microscope().plugin.form
microscope_obj.plugin.forms.append(api_form_info) microscope_obj.plugin.forms.append(api_form_info)
print(microscope_obj.plugin.forms) print(microscope_obj.plugin.forms)

View file

@ -8,7 +8,6 @@ import logging
class PositionAPI(MicroscopeView): class PositionAPI(MicroscopeView):
def get(self): def get(self):
""" """
Return current x, y and z positions of the stage. Return current x, y and z positions of the stage.
@ -42,7 +41,7 @@ class PositionAPI(MicroscopeView):
} }
""" """
out = filter_dict(self.microscope.state, ['stage', 'position']) out = filter_dict(self.microscope.state, ["stage", "position"])
return jsonify(out) return jsonify(out)
def post(self): def post(self):
@ -67,15 +66,19 @@ class PositionAPI(MicroscopeView):
position = [0, 0, 0] position = [0, 0, 0]
# Handle absolute positioning (calculate a relative move from current position and target) # Handle absolute positioning (calculate a relative move from current position and target)
if (payload.param('absolute') is True) and (self.microscope.stage): # Only if stage exists if (payload.param("absolute") is True) and (
target_position = axes_to_array(payload.json, ['x', 'y', 'z']) self.microscope.stage
): # Only if stage exists
target_position = axes_to_array(payload.json, ["x", "y", "z"])
logging.debug("TARGET: {}".format(target_position)) logging.debug("TARGET: {}".format(target_position))
position = [target_position[i] - self.microscope.stage.position[i] for i in range(3)] position = [
target_position[i] - self.microscope.stage.position[i] for i in range(3)
]
logging.debug("DELTA: {}".format(position)) logging.debug("DELTA: {}".format(position))
else: else:
# Get coordinates from payload # Get coordinates from payload
position = axes_to_array(payload.json, ['x', 'y', 'z'], [0, 0, 0]) position = axes_to_array(payload.json, ["x", "y", "z"], [0, 0, 0])
logging.debug(position) logging.debug(position)
@ -85,18 +88,18 @@ class PositionAPI(MicroscopeView):
with self.microscope.stage.lock: with self.microscope.stage.lock:
self.microscope.stage.move_rel(position) self.microscope.stage.move_rel(position)
out = filter_dict(self.microscope.state, ['stage', 'position']) out = filter_dict(self.microscope.state, ["stage", "position"])
return jsonify(out) return jsonify(out)
def construct_blueprint(microscope_obj): def construct_blueprint(microscope_obj):
blueprint = Blueprint('stage_blueprint', __name__) blueprint = Blueprint("stage_blueprint", __name__)
blueprint.add_url_rule( blueprint.add_url_rule(
'/position', "/position",
view_func=PositionAPI.as_view('position', microscope=microscope_obj) view_func=PositionAPI.as_view("position", microscope=microscope_obj),
) )
return blueprint return blueprint

View file

@ -3,7 +3,6 @@ from flask import jsonify, abort, Blueprint
class TaskListAPI(MicroscopeView): class TaskListAPI(MicroscopeView):
def get(self): def get(self):
""" """
Get list of long-running tasks. Get list of long-running tasks.
@ -66,7 +65,6 @@ class TaskListAPI(MicroscopeView):
class TaskAPI(MicroscopeView): class TaskAPI(MicroscopeView):
def get(self, task_id): def get(self, task_id):
""" """
Get JSON representation of a task Get JSON representation of a task
@ -123,14 +121,11 @@ class TaskAPI(MicroscopeView):
success = self.microscope.task.delete(task_id) success = self.microscope.task.delete(task_id)
if success: if success:
data = { data = {"status": "success", "message": "task successfully deleted"}
'status': 'success',
'message': 'task successfully deleted'
}
else: else:
data = { data = {
'status': 'fail', "status": "fail",
'message': 'task could not be deleted, as it is currently running or does not exist' "message": "task could not be deleted, as it is currently running or does not exist",
} }
return jsonify(data) return jsonify(data)
@ -138,16 +133,14 @@ class TaskAPI(MicroscopeView):
def construct_blueprint(microscope_obj): def construct_blueprint(microscope_obj):
blueprint = Blueprint('task_blueprint', __name__) blueprint = Blueprint("task_blueprint", __name__)
blueprint.add_url_rule( blueprint.add_url_rule(
'/', "/", view_func=TaskListAPI.as_view("task_list", microscope=microscope_obj)
view_func=TaskListAPI.as_view('task_list', microscope=microscope_obj)
) )
blueprint.add_url_rule( blueprint.add_url_rule(
'/<task_id>/', "/<task_id>/", view_func=TaskAPI.as_view("task", microscope=microscope_obj)
view_func=TaskAPI.as_view('task', microscope=microscope_obj)
) )
return blueprint return blueprint

View file

@ -6,6 +6,7 @@ class MicroscopeView(MethodView):
Create a generic MethodView with a globally available Create a generic MethodView with a globally available
microscope object passed as an argument. microscope object passed as an argument.
""" """
def __init__(self, microscope, **kwargs): def __init__(self, microscope, **kwargs):
self.microscope = microscope self.microscope = microscope
@ -19,6 +20,7 @@ class MicroscopeViewPlugin(MicroscopeView):
microscope object passed as an argument, and a plugin microscope object passed as an argument, and a plugin
reference stored in 'self'. Initially None. reference stored in 'self'. Initially None.
""" """
def __init__(self, microscope, plugin=None, **kwargs): def __init__(self, microscope, plugin=None, **kwargs):
self.plugin = plugin self.plugin = plugin

View file

@ -56,6 +56,7 @@ class CameraEvent(object):
An event-like class that signals all active clients when a new frame is available. An event-like class that signals all active clients when a new frame is available.
""" """
def __init__(self): def __init__(self):
self.events = {} self.events = {}
@ -112,9 +113,10 @@ class BaseCamera(metaclass=ABCMeta):
images (list): List of image capture objects images (list): List of image capture objects
videos (list): List of video capture objects videos (list): List of video capture objects
""" """
def __init__(self): def __init__(self):
self.thread = None self.thread = None
self.camera = None self.camera = None
self.lock = StrictLock(timeout=1) self.lock = StrictLock(timeout=1)
@ -128,11 +130,11 @@ class BaseCamera(metaclass=ABCMeta):
self.state = {} self.state = {}
self.paths = { self.paths = {
'image': BASE_CAPTURE_PATH, "image": BASE_CAPTURE_PATH,
'video': BASE_CAPTURE_PATH, "video": BASE_CAPTURE_PATH,
'image_tmp': TEMP_CAPTURE_PATH, "image_tmp": TEMP_CAPTURE_PATH,
'video_tpm': TEMP_CAPTURE_PATH "video_tpm": TEMP_CAPTURE_PATH,
} }
# Capture data # Capture data
self.images = [] self.images = []
@ -176,7 +178,7 @@ class BaseCamera(metaclass=ABCMeta):
self.last_access = time.time() self.last_access = time.time()
self.stop = False self.stop = False
if not self.state['stream_active']: if not self.state["stream_active"]:
# start background frame thread # start background frame thread
self.thread = threading.Thread(target=self._thread) self.thread = threading.Thread(target=self._thread)
self.thread.daemon = True self.thread.daemon = True
@ -196,12 +198,12 @@ class BaseCamera(metaclass=ABCMeta):
logging.debug("Stopping worker thread") logging.debug("Stopping worker thread")
timeout_time = time.time() + timeout timeout_time = time.time() + timeout
if self.state['stream_active']: if self.state["stream_active"]:
self.stop = True self.stop = True
self.thread.join() # Wait for stream thread to exit self.thread.join() # Wait for stream thread to exit
logging.debug("Waiting 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: if time.time() > timeout_time:
logging.debug("Timeout waiting for worker thread close.") logging.debug("Timeout waiting for worker thread close.")
raise TimeoutError("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 # CREATING NEW CAPTURES
def new_image( def new_image(
self, self,
write_to_file: bool = True, write_to_file: bool = True,
temporary: bool = True, temporary: bool = True,
filename: str = None, filename: str = None,
folder: str = "", folder: str = "",
fmt: str = 'jpeg'): fmt: str = "jpeg",
):
""" """
Create a new image capture object. Adds to the image list, and shunt all others. Create a new image capture object. Adds to the image list, and shunt all others.
@ -275,7 +278,7 @@ class BaseCamera(metaclass=ABCMeta):
filename = "{}.{}".format(filename, fmt) filename = "{}.{}".format(filename, fmt)
# Generate folder # 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) folder = os.path.join(base_folder, folder)
# Generate file path # Generate file path
@ -283,9 +286,8 @@ class BaseCamera(metaclass=ABCMeta):
# Create capture object # Create capture object
output = CaptureObject( output = CaptureObject(
write_to_file=write_to_file, write_to_file=write_to_file, temporary=temporary, filepath=filepath
temporary=temporary, )
filepath=filepath)
# Update capture list # Update capture list
shunt_captures(self.images) shunt_captures(self.images)
@ -294,12 +296,13 @@ class BaseCamera(metaclass=ABCMeta):
return output return output
def new_video( def new_video(
self, self,
write_to_file: bool = True, write_to_file: bool = True,
temporary: bool = False, temporary: bool = False,
filename: str = None, filename: str = None,
folder: str = "", folder: str = "",
fmt: str = 'h264'): fmt: str = "h264",
):
""" """
Create a new video capture object. Adds to the image list, and shunt all others. Create a new video capture object. Adds to the image list, and shunt all others.
@ -320,7 +323,7 @@ class BaseCamera(metaclass=ABCMeta):
filename = "{}.{}".format(filename, fmt) filename = "{}.{}".format(filename, fmt)
# Generate folder # 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) folder = os.path.join(base_folder, folder)
# Generate file path # Generate file path
@ -328,9 +331,8 @@ class BaseCamera(metaclass=ABCMeta):
# Create capture object # Create capture object
output = CaptureObject( output = CaptureObject(
write_to_file=write_to_file, write_to_file=write_to_file, temporary=temporary, filepath=filepath
temporary=temporary, )
filepath=filepath)
# Update capture list # Update capture list
shunt_captures(self.videos) shunt_captures(self.videos)
@ -345,7 +347,7 @@ class BaseCamera(metaclass=ABCMeta):
self.frames_iterator = self.frames() self.frames_iterator = self.frames()
logging.debug("Entering worker thread.") logging.debug("Entering worker thread.")
self.state['stream_active'] = True self.state["stream_active"] = True
for frame in self.frames_iterator: for frame in self.frames_iterator:
self.frame = frame self.frame = frame
@ -354,9 +356,13 @@ class BaseCamera(metaclass=ABCMeta):
# Handle timeout # Handle timeout
if ( if (
self.stream_timeout_enabled and # If using timeout self.stream_timeout_enabled
(time.time() - self.last_access > self.stream_timeout) and # And timeout time and ( # If using timeout
not self.state['preview_active'] # And GPU preview is not active 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() self.frames_iterator.close()
break break
@ -372,4 +378,4 @@ class BaseCamera(metaclass=ABCMeta):
logging.debug("BaseCamera worker thread exiting...") logging.debug("BaseCamera worker thread exiting...")
# Set stream_activate state # Set stream_activate state
self.state['stream_active'] = False self.state["stream_active"] = False

View file

@ -18,12 +18,12 @@ Attributes:
TEMP_CAPTURE_PATH (str): Base path to store all temporary captures (automatically emptied) TEMP_CAPTURE_PATH (str): Base path to store all temporary captures (automatically emptied)
""" """
PIL_FORMATS = ['JPG', 'JPEG', 'PNG', 'TIF', 'TIFF'] PIL_FORMATS = ["JPG", "JPEG", "PNG", "TIF", "TIFF"]
EXIF_FORMATS = ['JPG', 'JPEG', 'TIF', 'TIFF'] EXIF_FORMATS = ["JPG", "JPEG", "TIF", "TIFF"]
THUMBNAIL_SIZE = (200, 150) THUMBNAIL_SIZE = (200, 150)
BASE_CAPTURE_PATH = os.path.join(os.path.expanduser('~'), 'micrographs') BASE_CAPTURE_PATH = os.path.join(os.path.expanduser("~"), "micrographs")
TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, 'tmp') TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, "tmp")
# TODO: Move these methods to a camera utilities module? # TODO: Move these methods to a camera utilities module?
@ -50,8 +50,8 @@ def pull_usercomment_dict(filepath):
except InvalidImageDataError: except InvalidImageDataError:
logging.error("Invalid data at {}. Skipping.".format(filepath)) logging.error("Invalid data at {}. Skipping.".format(filepath))
return None return None
if 'Exif' in exif_dict and 37510 in exif_dict['Exif']: if "Exif" in exif_dict and 37510 in exif_dict["Exif"]:
return yaml.load(exif_dict['Exif'][37510].decode()) return yaml.load(exif_dict["Exif"][37510].decode())
else: else:
return None return None
@ -59,7 +59,9 @@ def pull_usercomment_dict(filepath):
def make_file_list(directory, formats): def make_file_list(directory, formats):
files = [] files = []
for fmt in formats: 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))) 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 # Create a placeholder capture
capture = CaptureObject( capture = CaptureObject(filepath=path)
filepath=path
)
# Build file path information # Build file path information
capture.split_file_path(capture.file) capture.split_file_path(capture.file)
# Populate capture parameters # Populate capture parameters
capture.id = exif_dict['id'] capture.id = exif_dict["id"]
capture.timestring = exif_dict['time']
capture.format = exif_dict['format']
capture._metadata = exif_dict['custom'] capture.timestring = exif_dict["time"]
capture.tags = exif_dict['tags'] capture.format = exif_dict["format"]
capture._metadata = exif_dict["custom"]
capture.tags = exif_dict["tags"]
return capture return capture
@ -138,10 +138,8 @@ class CaptureObject(object):
""" """
def __init__( def __init__(
self, self, write_to_file: bool = False, temporary: bool = False, filepath: str = ""
write_to_file: bool = False, ) -> None:
temporary: bool = False,
filepath: str = '') -> None:
"""Create a new StreamObject, to manage capture data.""" """Create a new StreamObject, to manage capture data."""
# Store a nice ID # Store a nice ID
@ -184,7 +182,9 @@ class CaptureObject(object):
"""Create StreamObject in context, to auto-clean disk data.""" """Create StreamObject in context, to auto-clean disk data."""
logging.debug( logging.debug(
"Entering context for {}. Stored files will be cleaned up automatically regardless of location.".format( "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.temporary = True # Flag file to be removed on close.
self.context_manager = True # Used in metadata 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. Construct a full file path, based on filename, folder, and file format.
Defaults to UUID. Defaults to UUID.
""" """
global TEMP_CAPTURE_PATH, BASE_CAPTURE_PATH global TEMP_CAPTURE_PATH, BASE_CAPTURE_PATH
if self.temporary: if self.temporary:
base_dir = TEMP_CAPTURE_PATH base_dir = TEMP_CAPTURE_PATH
else: else:
base_dir = BASE_CAPTURE_PATH 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): def split_file_path(self, filepath):
""" """
@ -221,7 +223,7 @@ class CaptureObject(object):
self.filefolder, self.filename = os.path.split(filepath) self.filefolder, self.filename = os.path.split(filepath)
# Split the filename out from it's file extension # Split the filename out from it's file extension
self.basename = os.path.splitext(self.filename)[0] self.basename = os.path.splitext(self.filename)[0]
self.format = self.filename.split('.')[-1] self.format = self.filename.split(".")[-1]
# Create folder and file # Create folder and file
if not os.path.exists(self.filefolder): if not os.path.exists(self.filefolder):
@ -298,7 +300,7 @@ class CaptureObject(object):
# Serialize metadata # Serialize metadata
metadata_string = yaml.safe_dump(self.metadata) metadata_string = yaml.safe_dump(self.metadata)
# Insert metadata into exif_dict # 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 # Convert new exif dict to exif bytes
exif_bytes = piexif.dump(exif_dict) exif_bytes = piexif.dump(exif_dict)
# Insert exif into file # Insert exif into file
@ -310,8 +312,14 @@ class CaptureObject(object):
Create basic metadata dictionary from basic capture data, Create basic metadata dictionary from basic capture data,
and any added custom metadata and tags. and any added custom metadata and tags.
""" """
d = {'id': self.id, 'filename': self.filename, 'time': self.timestring, d = {
'format': self.format, 'tags': self.tags, 'custom': self._metadata} "id": self.id,
"filename": self.filename,
"time": self.timestring,
"format": self.format,
"tags": self.tags,
"custom": self._metadata,
}
# Add custom metadata to dictionary # Add custom metadata to dictionary
return d return d
@ -339,19 +347,19 @@ class CaptureObject(object):
""" """
# Create basic state dictionary # 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 # Check bytestream
if self.stream_exists: if self.stream_exists:
d['bytestream'] = True d["bytestream"] = True
else: else:
d['bytestream'] = False d["bytestream"] = False
# Combined availability of data # Combined availability of data
if self.exists: if self.exists:
d['available'] = True d["available"] = True
else: else:
d['available'] = False d["available"] = False
return d return d
@ -373,7 +381,7 @@ class CaptureObject(object):
else: # If data bytestream is empty else: # If data bytestream is empty
if self.file_exists: # If data file exists if self.file_exists: # If data file exists
logging.info("Opening from file {}".format(self.file)) 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 = io.BytesIO(f.read()) # Load bytes from file
d.seek(0) # Rewind loaded bytestream d.seek(0) # Rewind loaded bytestream
# Create a copy of the bytestream bytes # Create a copy of the bytestream bytes
@ -413,7 +421,7 @@ class CaptureObject(object):
def load_file(self) -> bool: def load_file(self) -> bool:
"""Load data stored on disk to the in-memory bytestream.""" """Load data stored on disk to the in-memory bytestream."""
if self.file_exists: # If data file exists 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 = io.BytesIO(f.read()) # Load bytes from file
self.bytestream.seek(0) # Rewind data bytes again self.bytestream.seek(0) # Rewind data bytes again
return True return True
@ -423,7 +431,7 @@ class CaptureObject(object):
def save_file(self) -> bool: def save_file(self) -> bool:
"""Write the StreamObjects bytestream to a file.""" """Write the StreamObjects bytestream to a file."""
if self.stream_exists: # If there's a bytestream to save 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)) logging.debug("Writing bytestream to file {}".format(self.file))
f.seek(0, 0) # Seek to the start of the file f.seek(0, 0) # Seek to the start of the file
f.write(self.binary) # Write data bytes to file f.write(self.binary) # Write data bytes to file

View file

@ -36,6 +36,7 @@ import picamera.array
from typing import Tuple from typing import Tuple
from .base import BaseCamera, CaptureObject from .base import BaseCamera, CaptureObject
# Richard's fix gain # Richard's fix gain
from .set_picamera_gain import set_analog_gain, set_digital_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 # MAIN CLASS
class PiCameraStreamer(BaseCamera): class PiCameraStreamer(BaseCamera):
"""Raspberry Pi camera implementation of PiCameraStreamer.""" """Raspberry Pi camera implementation of PiCameraStreamer."""
picamera_settings_keys = [ picamera_settings_keys = [
'exposure_mode', "exposure_mode",
'analog_gain', "analog_gain",
'digital_gain', "digital_gain",
'shutter_speed', "shutter_speed",
'awb_gains', "awb_gains",
'awb_mode', "awb_mode",
'framerate', "framerate",
'saturation', "saturation",
'lens_shading_table' "lens_shading_table",
] ]
def __init__(self): def __init__(self):
# Run BaseCamera init # Run BaseCamera init
BaseCamera.__init__(self) BaseCamera.__init__(self)
# Attach to Pi camera # 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 # Store state of PiCameraStreamer
self.state.update({ self.state.update(
'stream_active': False, {"stream_active": False, "record_active": False, "preview_active": False}
'record_active': False, )
'preview_active': False,
})
# Reset variable states # Reset variable states
self.set_zoom(1.0) self.set_zoom(1.0)
@ -101,11 +103,11 @@ class PiCameraStreamer(BaseCamera):
""" """
conf_dict = { conf_dict = {
'stream_resolution': self.stream_resolution, "stream_resolution": self.stream_resolution,
'image_resolution': self.image_resolution, "image_resolution": self.image_resolution,
'numpy_resolution': self.numpy_resolution, "numpy_resolution": self.numpy_resolution,
'jpeg_quality': self.jpeg_quality, "jpeg_quality": self.jpeg_quality,
'picamera_settings': {}, "picamera_settings": {},
} }
# PiCamera parameters # PiCamera parameters
@ -113,7 +115,7 @@ class PiCameraStreamer(BaseCamera):
try: try:
value = getattr(self.camera, key) value = getattr(self.camera, key)
logging.debug("Reading PiCamera().{}: {}".format(key, value)) logging.debug("Reading PiCamera().{}: {}".format(key, value))
conf_dict['picamera_settings'][key] = value conf_dict["picamera_settings"][key] = value
except AttributeError: except AttributeError:
logging.debug("Unable to read PiCamera attribute {}".format(key)) logging.debug("Unable to read PiCamera attribute {}".format(key))
@ -138,21 +140,23 @@ class PiCameraStreamer(BaseCamera):
with self.lock: with self.lock:
# Apply valid config params to Picamera object # 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 # 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.") logging.info("Pausing stream to update config.")
self.stop_stream_recording() # Pause stream self.stop_stream_recording() # Pause stream
paused_stream = True # Remember to unpause stream when done paused_stream = True # Remember to unpause stream when done
# PiCamera parameters # PiCamera parameters
if 'picamera_settings' in config: # If new settings are given if "picamera_settings" in config: # If new settings are given
self.apply_picamera_settings(config['picamera_settings'], pause_for_effect=True) self.apply_picamera_settings(
config["picamera_settings"], pause_for_effect=True
)
# PiCameraStreamer parameters # PiCameraStreamer parameters
for key, value in config.items(): # For each provided setting 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) setattr(self, key, value)
# If stream was paused to update config, unpause # If stream was paused to update config, unpause
@ -162,67 +166,84 @@ class PiCameraStreamer(BaseCamera):
else: else:
raise Exception( 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 # Set exposure mode
if 'exposure_mode' in settings_dict: if "exposure_mode" in settings_dict:
logging.debug("Applying exposure_mode: {}".format(settings_dict['exposure_mode'])) logging.debug(
self.camera.exposure_mode = settings_dict['exposure_mode'] "Applying exposure_mode: {}".format(settings_dict["exposure_mode"])
)
self.camera.exposure_mode = settings_dict["exposure_mode"]
# Apply gains and let them settle # Apply gains and let them settle
if 'analog_gain' in settings_dict: if "analog_gain" in settings_dict:
logging.debug("Applying analog_gain: {}".format(settings_dict['analog_gain'])) logging.debug(
set_analog_gain(self.camera, float(settings_dict['analog_gain'])) "Applying analog_gain: {}".format(settings_dict["analog_gain"])
if 'digital_gain' in settings_dict: )
logging.debug("Applying digital_gain: {}".format(settings_dict['digital_gain'])) set_analog_gain(self.camera, float(settings_dict["analog_gain"]))
set_digital_gain(self.camera, float(settings_dict['digital_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 # Apply shutter speed
if 'shutter_speed' in settings_dict: if "shutter_speed" in settings_dict:
logging.debug("Applying shutter_speed: {}".format(settings_dict['shutter_speed'])) logging.debug(
self.camera.shutter_speed = int(settings_dict['shutter_speed']) "Applying shutter_speed: {}".format(settings_dict["shutter_speed"])
)
self.camera.shutter_speed = int(settings_dict["shutter_speed"])
time.sleep(0.2) # Let gains settle time.sleep(0.2) # Let gains settle
# Handle AWB in a half-smart way # 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") logging.debug("Applying awb_mode: off")
self.camera.awb_mode = 'off' self.camera.awb_mode = "off"
logging.debug("Applying awb_gains: {}".format(settings_dict['awb_gains'])) logging.debug("Applying awb_gains: {}".format(settings_dict["awb_gains"]))
self.camera.awb_gains = settings_dict['awb_gains'] self.camera.awb_gains = settings_dict["awb_gains"]
elif 'awb_mode' in settings_dict: elif "awb_mode" in settings_dict:
logging.debug("Applying awb_mode: {}".format(settings_dict['awb_mode'])) logging.debug("Applying awb_mode: {}".format(settings_dict["awb_mode"]))
self.camera.awb_mode = settings_dict['awb_mode'] self.camera.awb_mode = settings_dict["awb_mode"]
# Handle some properties that can be quickly applied # Handle some properties that can be quickly applied
batched_keys = ['framerate', 'saturation'] batched_keys = ["framerate", "saturation"]
for key in batched_keys: for key in batched_keys:
if (key in settings_dict) and hasattr(self.camera, key): if (key in settings_dict) and hasattr(self.camera, key):
logging.debug("Applying {}: {}".format(key, settings_dict[key])) logging.debug("Applying {}: {}".format(key, settings_dict[key]))
setattr(self.camera, key, settings_dict[key]) setattr(self.camera, key, settings_dict[key])
# Handle lens shading if camera supports it # Handle lens shading if camera supports it
if ('lens_shading_table' in settings_dict) and hasattr(self.camera, 'lens_shading_table'): if ("lens_shading_table" in settings_dict) and hasattr(
logging.debug("Applying lens_shading_table: {}".format(settings_dict['lens_shading_table'])) self.camera, "lens_shading_table"
self.camera.lens_shading_table = settings_dict['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 # Final optional pause to settle
if pause_for_effect: if pause_for_effect:
time.sleep(0.2) 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. Change the camera zoom, handling re-centering and scaling.
""" """
with self.lock: with self.lock:
self.state['zoom_value'] = float(zoom_value) self.state["zoom_value"] = float(zoom_value)
if self.state['zoom_value'] < 1: if self.state["zoom_value"] < 1:
self.state['zoom_value'] = 1 self.state["zoom_value"] = 1
# Richard's code for zooming ! # Richard's code for zooming !
fov = self.camera.zoom fov = self.camera.zoom
centre = np.array([fov[0] + fov[2] / 2.0, fov[1] + fov[3] / 2.0]) 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 # If the new zoom value would be invalid, move the centre to
# keep it within the camera's sensor (this is only relevant # 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) # 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 self.camera.preview.window = window
if fullscreen: if fullscreen:
self.camera.preview.fullscreen = fullscreen self.camera.preview.fullscreen = fullscreen
self.state['preview_active'] = True self.state["preview_active"] = True
except picamera.exc.PiCameraMMALError as e: 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: 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): def stop_preview(self):
"""Stop the on board GPU camera preview.""" """Stop the on board GPU camera preview."""
self.camera.stop_preview() self.camera.stop_preview()
self.state['preview_active'] = False self.state["preview_active"] = False
def start_recording( def start_recording(self, output, fmt: str = "h264", quality: int = 15):
self,
output,
fmt: str = 'h264',
quality: int = 15):
"""Start recording. """Start recording.
Start a new video recording, writing to a output object. Start a new video recording, writing to a output object.
@ -283,7 +306,7 @@ class PiCameraStreamer(BaseCamera):
""" """
with self.lock: with self.lock:
# Start recording method only if a current recording is not running # 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 output is a StreamObject
if isinstance(output, CaptureObject): if isinstance(output, CaptureObject):
@ -300,17 +323,19 @@ class PiCameraStreamer(BaseCamera):
format=fmt, format=fmt,
splitter_port=2, splitter_port=2,
resize=self.stream_resolution, resize=self.stream_resolution,
quality=quality) quality=quality,
)
# Update state dictionary # Update state dictionary
self.state['record_active'] = True self.state["record_active"] = True
return output return output
else: else:
logging.error( logging.error(
"Cannot start a new recording\ "Cannot start a new recording\
until the current recording has stopped.") until the current recording has stopped."
)
return None return None
def stop_recording(self): def stop_recording(self):
@ -322,12 +347,11 @@ class PiCameraStreamer(BaseCamera):
logging.info("Recording stopped") logging.info("Recording stopped")
# Update state dictionary # Update state dictionary
self.state['record_active'] = False self.state["record_active"] = False
def stop_stream_recording( def stop_stream_recording(
self, self, splitter_port: int = 1, resolution: Tuple[int, int] = None
splitter_port: int = 1, ) -> None:
resolution: Tuple[int, int] = None) -> None:
""" """
Sets the camera resolution to the still-image resolution, and stops recording if the stream is active. 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: except picamera.exc.PiCameraNotRecording:
logging.info("Not recording on splitter_port {}".format(splitter_port)) logging.info("Not recording on splitter_port {}".format(splitter_port))
else: 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 # 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 self.camera.resolution = resolution
def start_stream_recording( def start_stream_recording(
self, self, splitter_port: int = 1, resolution: Tuple[int, int] = None
splitter_port: int = 1, ) -> None:
resolution: Tuple[int, int] = None) -> None:
""" """
Sets the camera resolution to the video/stream resolution, and starts recording if the stream should be active. 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: with self.lock:
# If stream object was destroyed # If stream object was destroyed
if not hasattr(self, 'stream'): if not hasattr(self, "stream"):
self.stream = io.BytesIO() # Create a stream object self.stream = io.BytesIO() # Create a stream object
# If no explicit resolution is passed # If no explicit resolution is passed
if not resolution: 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 # Reduce the resolution for video streaming
try: try:
self.camera._check_recording_stopped() self.camera._check_recording_stopped()
except picamera.exc.PiCameraRuntimeError: except picamera.exc.PiCameraRuntimeError:
logging.info("Error while changing resolution: Recording already running.") logging.info(
"Error while changing resolution: Recording already running."
)
else: else:
self.camera.resolution = resolution self.camera.resolution = resolution
# If the stream should be active # If the stream should be active
if self.state['stream_active']: if self.state["stream_active"]:
try: try:
# Start recording on stream port # Start recording on stream port
self.camera.start_recording( self.camera.start_recording(
self.stream, self.stream,
format='mjpeg', format="mjpeg",
quality=self.jpeg_quality, 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 # (bitrate control makes JPEG size less good as a focus
# metric) # metric)
splitter_port=splitter_port) splitter_port=splitter_port,
)
except picamera.exc.PiCameraAlreadyRecording: except picamera.exc.PiCameraAlreadyRecording:
logging.info("Error while starting preview: Recording already running.") logging.info(
"Error while starting preview: Recording already running."
)
else: 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( def capture(
self, self,
output, output,
fmt: str = 'jpeg', fmt: str = "jpeg",
use_video_port: bool = False, use_video_port: bool = False,
resize: Tuple[int, int] = None, resize: Tuple[int, int] = None,
bayer: bool = True): bayer: bool = True,
):
""" """
Capture a still image to a StreamObject. Capture a still image to a StreamObject.
@ -439,7 +480,8 @@ class PiCameraStreamer(BaseCamera):
quality=100, quality=100,
resize=resize, resize=resize,
bayer=(not use_video_port) and bayer, 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 # Set resolution and start stream recording if necessary
if not use_video_port: if not use_video_port:
@ -448,9 +490,8 @@ class PiCameraStreamer(BaseCamera):
return output return output
def yuv( def yuv(
self, self, use_video_port: bool = True, resize: Tuple[int, int] = None
use_video_port: bool = True, ) -> np.ndarray:
resize: Tuple[int, int] = None) -> np.ndarray:
"""Capture an uncompressed still YUV image to a Numpy array. """Capture an uncompressed still YUV image to a Numpy array.
Args: Args:
@ -477,10 +518,8 @@ class PiCameraStreamer(BaseCamera):
logging.info("Capturing to {}".format(output)) logging.info("Capturing to {}".format(output))
self.camera.capture( self.camera.capture(
output, output, resize=size, format="yuv", use_video_port=use_video_port
resize=size, )
format='yuv',
use_video_port=use_video_port)
if not use_video_port: if not use_video_port:
self.start_stream_recording() self.start_stream_recording()
@ -488,9 +527,8 @@ class PiCameraStreamer(BaseCamera):
return output.array return output.array
def array( def array(
self, self, use_video_port: bool = True, resize: Tuple[int, int] = None
use_video_port: bool = True, ) -> np.ndarray:
resize: Tuple[int, int] = None) -> np.ndarray:
"""Capture an uncompressed still RGB image to a Numpy array. """Capture an uncompressed still RGB image to a Numpy array.
Args: Args:
@ -517,10 +555,8 @@ class PiCameraStreamer(BaseCamera):
logging.info("Capturing to {}".format(output)) logging.info("Capturing to {}".format(output))
self.camera.capture( self.camera.capture(
output, output, resize=size, format="rgb", use_video_port=use_video_port
resize=size, )
format='rgb',
use_video_port=use_video_port)
# Resume stream # Resume stream
self.start_stream_recording() self.start_stream_recording()

View file

@ -7,5 +7,4 @@ from ._exif import *
from ._exceptions import * from ._exceptions import *
VERSION = "1.1.2"
VERSION = '1.1.2'

View file

@ -12,20 +12,21 @@ def split_into_segments(data):
head = 2 head = 2
segments = [b"\xff\xd8"] segments = [b"\xff\xd8"]
while 1: while 1:
if data[head: head + 2] == b"\xff\xda": if data[head : head + 2] == b"\xff\xda":
segments.append(data[head:]) segments.append(data[head:])
break break
else: 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 endPoint = head + length + 2
seg = data[head: endPoint] seg = data[head:endPoint]
segments.append(seg) segments.append(seg)
head = endPoint head = endPoint
if (head >= len(data)): if head >= len(data):
raise InvalidImageDataError("Wrong JPEG data.") raise InvalidImageDataError("Wrong JPEG data.")
return segments return segments
def read_exif_from_file(filename): def read_exif_from_file(filename):
"""Slices JPEG meta data into a list from JPEG binary data. """Slices JPEG meta data into a list from JPEG binary data.
""" """
@ -39,11 +40,11 @@ def read_exif_from_file(filename):
HEAD_LENGTH = 4 HEAD_LENGTH = 4
exif = None exif = None
while 1: while 1:
length = struct.unpack(">H", head[2: 4])[0] length = struct.unpack(">H", head[2:4])[0]
if head[:2] == b"\xff\xe1": if head[:2] == b"\xff\xe1":
segment_data = f.read(length - 2) segment_data = f.read(length - 2)
if segment_data[:4] != b'Exif': if segment_data[:4] != b"Exif":
head = f.read(HEAD_LENGTH) head = f.read(HEAD_LENGTH)
continue continue
exif = head + segment_data exif = head + segment_data
@ -57,6 +58,7 @@ def read_exif_from_file(filename):
f.close() f.close()
return exif return exif
def get_exif_seg(segments): def get_exif_seg(segments):
"""Returns Exif from JPEG meta data list """Returns Exif from JPEG meta data list
""" """
@ -69,9 +71,11 @@ def get_exif_seg(segments):
def merge_segments(segments, exif=b""): def merge_segments(segments, exif=b""):
"""Merges Exif with APP0 and APP1 manipulations. """Merges Exif with APP0 and APP1 manipulations.
""" """
if segments[1][0:2] == b"\xff\xe0" and \ if (
segments[2][0:2] == b"\xff\xe1" and \ segments[1][0:2] == b"\xff\xe0"
segments[2][4:10] == b"Exif\x00\x00": and segments[2][0:2] == b"\xff\xe1"
and segments[2][4:10] == b"Exif\x00\x00"
):
if exif: if exif:
segments[2] = exif segments[2] = exif
segments.pop(1) segments.pop(1)
@ -82,8 +86,7 @@ def merge_segments(segments, exif=b""):
elif segments[1][0:2] == b"\xff\xe0": elif segments[1][0:2] == b"\xff\xe0":
if exif: if exif:
segments[1] = exif segments[1] = exif
elif segments[1][0:2] == b"\xff\xe1" and \ elif segments[1][0:2] == b"\xff\xe1" and segments[1][4:10] == b"Exif\x00\x00":
segments[1][4:10] == b"Exif\x00\x00":
if exif: if exif:
segments[1] = exif segments[1] = exif
elif exif is None: elif exif is None:

View file

@ -31,16 +31,20 @@ def dump(exif_dict_original):
else: else:
zeroth_ifd = {} zeroth_ifd = {}
if (("Exif" in exif_dict) and len(exif_dict["Exif"]) or if (
("Interop" in exif_dict) and len(exif_dict["Interop"]) ): ("Exif" in exif_dict)
and len(exif_dict["Exif"])
or ("Interop" in exif_dict)
and len(exif_dict["Interop"])
):
zeroth_ifd[ImageIFD.ExifTag] = 1 zeroth_ifd[ImageIFD.ExifTag] = 1
exif_is = True exif_is = True
exif_ifd = exif_dict["Exif"] exif_ifd = exif_dict["Exif"]
if ("Interop" in exif_dict) and len(exif_dict["Interop"]): if ("Interop" in exif_dict) and len(exif_dict["Interop"]):
exif_ifd[ExifIFD. InteroperabilityTag] = 1 exif_ifd[ExifIFD.InteroperabilityTag] = 1
interop_is = True interop_is = True
interop_ifd = exif_dict["Interop"] interop_ifd = exif_dict["Interop"]
elif ExifIFD. InteroperabilityTag in exif_ifd: elif ExifIFD.InteroperabilityTag in exif_ifd:
exif_ifd.pop(ExifIFD.InteroperabilityTag) exif_ifd.pop(ExifIFD.InteroperabilityTag)
elif ImageIFD.ExifTag in zeroth_ifd: elif ImageIFD.ExifTag in zeroth_ifd:
zeroth_ifd.pop(ImageIFD.ExifTag) zeroth_ifd.pop(ImageIFD.ExifTag)
@ -52,17 +56,20 @@ def dump(exif_dict_original):
elif ImageIFD.GPSTag in zeroth_ifd: elif ImageIFD.GPSTag in zeroth_ifd:
zeroth_ifd.pop(ImageIFD.GPSTag) zeroth_ifd.pop(ImageIFD.GPSTag)
if (("1st" in exif_dict) and if (
("thumbnail" in exif_dict) and ("1st" in exif_dict)
(exif_dict["thumbnail"] is not None)): and ("thumbnail" in exif_dict)
and (exif_dict["thumbnail"] is not None)
):
first_is = True first_is = True
exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] = 1 exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] = 1
exif_dict["1st"][ImageIFD.JPEGInterchangeFormatLength] = 1 exif_dict["1st"][ImageIFD.JPEGInterchangeFormatLength] = 1
first_ifd = exif_dict["1st"] first_ifd = exif_dict["1st"]
zeroth_set = _dict_to_bytes(zeroth_ifd, "0th", 0) zeroth_set = _dict_to_bytes(zeroth_ifd, "0th", 0)
zeroth_length = (len(zeroth_set[0]) + exif_is * 12 + gps_is * 12 + 4 + zeroth_length = (
len(zeroth_set[1])) len(zeroth_set[0]) + exif_is * 12 + gps_is * 12 + 4 + len(zeroth_set[1])
)
if exif_is: if exif_is:
exif_set = _dict_to_bytes(exif_ifd, "Exif", zeroth_length) exif_set = _dict_to_bytes(exif_ifd, "Exif", zeroth_length)
@ -115,8 +122,7 @@ def dump(exif_dict_original):
else: else:
gps_pointer = b"" gps_pointer = b""
if interop_is: if interop_is:
pointer_value = (TIFF_HEADER_LENGTH + pointer_value = TIFF_HEADER_LENGTH + zeroth_length + exif_length + gps_length
zeroth_length + exif_length + gps_length)
pointer_str = struct.pack(">I", pointer_value) pointer_str = struct.pack(">I", pointer_value)
key = ExifIFD.InteroperabilityTag key = ExifIFD.InteroperabilityTag
key_str = struct.pack(">H", key) key_str = struct.pack(">H", key)
@ -126,33 +132,46 @@ def dump(exif_dict_original):
else: else:
interop_pointer = b"" interop_pointer = b""
if first_is: if first_is:
pointer_value = (TIFF_HEADER_LENGTH + zeroth_length + pointer_value = (
exif_length + gps_length + interop_length) TIFF_HEADER_LENGTH
+ zeroth_length
+ exif_length
+ gps_length
+ interop_length
)
first_ifd_pointer = struct.pack(">L", pointer_value) first_ifd_pointer = struct.pack(">L", pointer_value)
thumbnail_pointer = (pointer_value + len(first_set[0]) + 24 + thumbnail_pointer = (
4 + len(first_set[1])) 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_p_bytes = b"\x02\x01\x00\x04\x00\x00\x00\x01" + struct.pack(
thumbnail_length_bytes = (b"\x02\x02\x00\x04\x00\x00\x00\x01" + ">L", thumbnail_pointer
struct.pack(">L", len(thumbnail))) )
first_bytes = (first_set[0] + thumbnail_p_bytes + thumbnail_length_bytes = b"\x02\x02\x00\x04\x00\x00\x00\x01" + struct.pack(
thumbnail_length_bytes + b"\x00\x00\x00\x00" + ">L", len(thumbnail)
first_set[1] + thumbnail) )
first_bytes = (
first_set[0]
+ thumbnail_p_bytes
+ thumbnail_length_bytes
+ b"\x00\x00\x00\x00"
+ first_set[1]
+ thumbnail
)
else: else:
first_ifd_pointer = b"\x00\x00\x00\x00" first_ifd_pointer = b"\x00\x00\x00\x00"
zeroth_bytes = (zeroth_set[0] + exif_pointer + gps_pointer + zeroth_bytes = (
first_ifd_pointer + zeroth_set[1]) zeroth_set[0] + exif_pointer + gps_pointer + first_ifd_pointer + zeroth_set[1]
)
if exif_is: if exif_is:
exif_bytes = exif_set[0] + interop_pointer + exif_set[1] exif_bytes = exif_set[0] + interop_pointer + exif_set[1]
return (header + zeroth_bytes + exif_bytes + gps_bytes + return header + zeroth_bytes + exif_bytes + gps_bytes + interop_bytes + first_bytes
interop_bytes + first_bytes)
def _get_thumbnail(jpeg): def _get_thumbnail(jpeg):
segments = split_into_segments(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) segments.pop(1)
thumbnail = b"".join(segments) thumbnail = b"".join(segments)
return thumbnail return thumbnail
@ -161,24 +180,31 @@ def _get_thumbnail(jpeg):
def _pack_byte(*args): def _pack_byte(*args):
return struct.pack("B" * len(args), *args) return struct.pack("B" * len(args), *args)
def _pack_signed_byte(*args): def _pack_signed_byte(*args):
return struct.pack("b" * len(args), *args) return struct.pack("b" * len(args), *args)
def _pack_short(*args): def _pack_short(*args):
return struct.pack(">" + "H" * len(args), *args) return struct.pack(">" + "H" * len(args), *args)
def _pack_signed_short(*args): def _pack_signed_short(*args):
return struct.pack(">" + "h" * len(args), *args) return struct.pack(">" + "h" * len(args), *args)
def _pack_long(*args): def _pack_long(*args):
return struct.pack(">" + "L" * len(args), *args) return struct.pack(">" + "L" * len(args), *args)
def _pack_slong(*args): def _pack_slong(*args):
return struct.pack(">" + "l" * len(args), *args) return struct.pack(">" + "l" * len(args), *args)
def _pack_float(*args): def _pack_float(*args):
return struct.pack(">" + "f" * len(args), *args) return struct.pack(">" + "f" * len(args), *args)
def _pack_double(*args): def _pack_double(*args):
return struct.pack(">" + "d" * len(args), *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: if value_type == TYPES.Byte:
length = len(raw_value) length = len(raw_value)
if length <= 4: if length <= 4:
value_str = (_pack_byte(*raw_value) + value_str = _pack_byte(*raw_value) + b"\x00" * (4 - length)
b"\x00" * (4 - length))
else: else:
value_str = struct.pack(">I", offset) value_str = struct.pack(">I", offset)
four_bytes_over = _pack_byte(*raw_value) four_bytes_over = _pack_byte(*raw_value)
elif value_type == TYPES.Short: elif value_type == TYPES.Short:
length = len(raw_value) length = len(raw_value)
if length <= 2: if length <= 2:
value_str = (_pack_short(*raw_value) + value_str = _pack_short(*raw_value) + b"\x00\x00" * (2 - length)
b"\x00\x00" * (2 - length))
else: else:
value_str = struct.pack(">I", offset) value_str = struct.pack(">I", offset)
four_bytes_over = _pack_short(*raw_value) four_bytes_over = _pack_short(*raw_value)
@ -241,8 +265,7 @@ def _value_to_bytes(raw_value, value_type, offset):
new_value = b"" new_value = b""
for n, val in enumerate(raw_value): for n, val in enumerate(raw_value):
num, den = val num, den = val
new_value += (struct.pack(">L", num) + new_value += struct.pack(">L", num) + struct.pack(">L", den)
struct.pack(">L", den))
value_str = struct.pack(">I", offset) value_str = struct.pack(">I", offset)
four_bytes_over = new_value four_bytes_over = new_value
elif value_type == TYPES.SRational: elif value_type == TYPES.SRational:
@ -255,8 +278,7 @@ def _value_to_bytes(raw_value, value_type, offset):
new_value = b"" new_value = b""
for n, val in enumerate(raw_value): for n, val in enumerate(raw_value):
num, den = val num, den = val
new_value += (struct.pack(">l", num) + new_value += struct.pack(">l", num) + struct.pack(">l", den)
struct.pack(">l", den))
value_str = struct.pack(">I", offset) value_str = struct.pack(">I", offset)
four_bytes_over = new_value four_bytes_over = new_value
elif value_type == TYPES.Undefined: 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) value_str = raw_value + b"\x00" * (4 - length)
except TypeError: except TypeError:
raise ValueError("Got invalid type to convert.") 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) length = len(raw_value)
if length <= 4: if length <= 4:
value_str = (_pack_signed_byte(*raw_value) + value_str = _pack_signed_byte(*raw_value) + b"\x00" * (4 - length)
b"\x00" * (4 - length))
else: else:
value_str = struct.pack(">I", offset) value_str = struct.pack(">I", offset)
four_bytes_over = _pack_signed_byte(*raw_value) 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) length = len(raw_value)
if length <= 2: if length <= 2:
value_str = (_pack_signed_short(*raw_value) + value_str = _pack_signed_short(*raw_value) + b"\x00\x00" * (2 - length)
b"\x00\x00" * (2 - length))
else: else:
value_str = struct.pack(">I", offset) value_str = struct.pack(">I", offset)
four_bytes_over = _pack_signed_short(*raw_value) four_bytes_over = _pack_signed_short(*raw_value)
@ -295,7 +315,7 @@ def _value_to_bytes(raw_value, value_type, offset):
else: else:
value_str = struct.pack(">I", offset) value_str = struct.pack(">I", offset)
four_bytes_over = _pack_float(*raw_value) four_bytes_over = _pack_float(*raw_value)
elif value_type == TYPES.DFloat: # Double elif value_type == TYPES.DFloat: # Double
length = len(raw_value) length = len(raw_value)
value_str = struct.pack(">I", offset) value_str = struct.pack(">I", offset)
four_bytes_over = _pack_double(*raw_value) 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) length_str = struct.pack(">I", length)
return length_str, value_str, four_bytes_over return length_str, value_str, four_bytes_over
def _dict_to_bytes(ifd_dict, ifd, ifd_offset): def _dict_to_bytes(ifd_dict, ifd, ifd_offset):
tag_count = len(ifd_dict) tag_count = len(ifd_dict)
entry_header = struct.pack(">H", tag_count) entry_header = struct.pack(">H", tag_count)
@ -318,7 +339,10 @@ def _dict_to_bytes(ifd_dict, ifd, ifd_offset):
continue continue
elif (ifd == "Exif") and (key == ExifIFD.InteroperabilityTag): elif (ifd == "Exif") and (key == ExifIFD.InteroperabilityTag):
continue continue
elif (ifd == "1st") and (key in (ImageIFD.JPEGInterchangeFormat, ImageIFD.JPEGInterchangeFormatLength)): elif (ifd == "1st") and (
key
in (ImageIFD.JPEGInterchangeFormat, ImageIFD.JPEGInterchangeFormatLength)
):
continue continue
raw_value = ifd_dict[key] 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) offset = TIFF_HEADER_LENGTH + entries_length + ifd_offset + len(values)
try: try:
length_str, value_str, four_bytes_over = _value_to_bytes(raw_value, length_str, value_str, four_bytes_over = _value_to_bytes(
value_type, raw_value, value_type, offset
offset) )
except ValueError: except ValueError:
raise ValueError( raise ValueError(
'"dump" got wrong type of exif value.\n' + '"dump" got wrong type of exif value.\n'
'{0} in {1} IFD. Got as {2}.'.format(key, ifd, type(ifd_dict[key])) + "{0} in {1} IFD. Got as {2}.".format(key, ifd, type(ifd_dict[key]))
) )
entries += key_str + type_str + length_str + value_str entries += key_str + type_str + length_str + value_str

View file

@ -14,314 +14,322 @@ class TYPES:
TAGS = { TAGS = {
'Image': {11: {'name': 'ProcessingSoftware', 'type': TYPES.Ascii}, "Image": {
254: {'name': 'NewSubfileType', 'type': TYPES.Long}, 11: {"name": "ProcessingSoftware", "type": TYPES.Ascii},
255: {'name': 'SubfileType', 'type': TYPES.Short}, 254: {"name": "NewSubfileType", "type": TYPES.Long},
256: {'name': 'ImageWidth', 'type': TYPES.Long}, 255: {"name": "SubfileType", "type": TYPES.Short},
257: {'name': 'ImageLength', 'type': TYPES.Long}, 256: {"name": "ImageWidth", "type": TYPES.Long},
258: {'name': 'BitsPerSample', 'type': TYPES.Short}, 257: {"name": "ImageLength", "type": TYPES.Long},
259: {'name': 'Compression', 'type': TYPES.Short}, 258: {"name": "BitsPerSample", "type": TYPES.Short},
262: {'name': 'PhotometricInterpretation', 'type': TYPES.Short}, 259: {"name": "Compression", "type": TYPES.Short},
263: {'name': 'Threshholding', 'type': TYPES.Short}, 262: {"name": "PhotometricInterpretation", "type": TYPES.Short},
264: {'name': 'CellWidth', 'type': TYPES.Short}, 263: {"name": "Threshholding", "type": TYPES.Short},
265: {'name': 'CellLength', 'type': TYPES.Short}, 264: {"name": "CellWidth", "type": TYPES.Short},
266: {'name': 'FillOrder', 'type': TYPES.Short}, 265: {"name": "CellLength", "type": TYPES.Short},
269: {'name': 'DocumentName', 'type': TYPES.Ascii}, 266: {"name": "FillOrder", "type": TYPES.Short},
270: {'name': 'ImageDescription', 'type': TYPES.Ascii}, 269: {"name": "DocumentName", "type": TYPES.Ascii},
271: {'name': 'Make', 'type': TYPES.Ascii}, 270: {"name": "ImageDescription", "type": TYPES.Ascii},
272: {'name': 'Model', 'type': TYPES.Ascii}, 271: {"name": "Make", "type": TYPES.Ascii},
273: {'name': 'StripOffsets', 'type': TYPES.Long}, 272: {"name": "Model", "type": TYPES.Ascii},
274: {'name': 'Orientation', 'type': TYPES.Short}, 273: {"name": "StripOffsets", "type": TYPES.Long},
277: {'name': 'SamplesPerPixel', 'type': TYPES.Short}, 274: {"name": "Orientation", "type": TYPES.Short},
278: {'name': 'RowsPerStrip', 'type': TYPES.Long}, 277: {"name": "SamplesPerPixel", "type": TYPES.Short},
279: {'name': 'StripByteCounts', 'type': TYPES.Long}, 278: {"name": "RowsPerStrip", "type": TYPES.Long},
282: {'name': 'XResolution', 'type': TYPES.Rational}, 279: {"name": "StripByteCounts", "type": TYPES.Long},
283: {'name': 'YResolution', 'type': TYPES.Rational}, 282: {"name": "XResolution", "type": TYPES.Rational},
284: {'name': 'PlanarConfiguration', 'type': TYPES.Short}, 283: {"name": "YResolution", "type": TYPES.Rational},
290: {'name': 'GrayResponseUnit', 'type': TYPES.Short}, 284: {"name": "PlanarConfiguration", "type": TYPES.Short},
291: {'name': 'GrayResponseCurve', 'type': TYPES.Short}, 290: {"name": "GrayResponseUnit", "type": TYPES.Short},
292: {'name': 'T4Options', 'type': TYPES.Long}, 291: {"name": "GrayResponseCurve", "type": TYPES.Short},
293: {'name': 'T6Options', 'type': TYPES.Long}, 292: {"name": "T4Options", "type": TYPES.Long},
296: {'name': 'ResolutionUnit', 'type': TYPES.Short}, 293: {"name": "T6Options", "type": TYPES.Long},
301: {'name': 'TransferFunction', 'type': TYPES.Short}, 296: {"name": "ResolutionUnit", "type": TYPES.Short},
305: {'name': 'Software', 'type': TYPES.Ascii}, 301: {"name": "TransferFunction", "type": TYPES.Short},
306: {'name': 'DateTime', 'type': TYPES.Ascii}, 305: {"name": "Software", "type": TYPES.Ascii},
315: {'name': 'Artist', 'type': TYPES.Ascii}, 306: {"name": "DateTime", "type": TYPES.Ascii},
316: {'name': 'HostComputer', 'type': TYPES.Ascii}, 315: {"name": "Artist", "type": TYPES.Ascii},
317: {'name': 'Predictor', 'type': TYPES.Short}, 316: {"name": "HostComputer", "type": TYPES.Ascii},
318: {'name': 'WhitePoint', 'type': TYPES.Rational}, 317: {"name": "Predictor", "type": TYPES.Short},
319: {'name': 'PrimaryChromaticities', 'type': TYPES.Rational}, 318: {"name": "WhitePoint", "type": TYPES.Rational},
320: {'name': 'ColorMap', 'type': TYPES.Short}, 319: {"name": "PrimaryChromaticities", "type": TYPES.Rational},
321: {'name': 'HalftoneHints', 'type': TYPES.Short}, 320: {"name": "ColorMap", "type": TYPES.Short},
322: {'name': 'TileWidth', 'type': TYPES.Short}, 321: {"name": "HalftoneHints", "type": TYPES.Short},
323: {'name': 'TileLength', 'type': TYPES.Short}, 322: {"name": "TileWidth", "type": TYPES.Short},
324: {'name': 'TileOffsets', 'type': TYPES.Short}, 323: {"name": "TileLength", "type": TYPES.Short},
325: {'name': 'TileByteCounts', 'type': TYPES.Short}, 324: {"name": "TileOffsets", "type": TYPES.Short},
330: {'name': 'SubIFDs', 'type': TYPES.Long}, 325: {"name": "TileByteCounts", "type": TYPES.Short},
332: {'name': 'InkSet', 'type': TYPES.Short}, 330: {"name": "SubIFDs", "type": TYPES.Long},
333: {'name': 'InkNames', 'type': TYPES.Ascii}, 332: {"name": "InkSet", "type": TYPES.Short},
334: {'name': 'NumberOfInks', 'type': TYPES.Short}, 333: {"name": "InkNames", "type": TYPES.Ascii},
336: {'name': 'DotRange', 'type': TYPES.Byte}, 334: {"name": "NumberOfInks", "type": TYPES.Short},
337: {'name': 'TargetPrinter', 'type': TYPES.Ascii}, 336: {"name": "DotRange", "type": TYPES.Byte},
338: {'name': 'ExtraSamples', 'type': TYPES.Short}, 337: {"name": "TargetPrinter", "type": TYPES.Ascii},
339: {'name': 'SampleFormat', 'type': TYPES.Short}, 338: {"name": "ExtraSamples", "type": TYPES.Short},
340: {'name': 'SMinSampleValue', 'type': TYPES.Short}, 339: {"name": "SampleFormat", "type": TYPES.Short},
341: {'name': 'SMaxSampleValue', 'type': TYPES.Short}, 340: {"name": "SMinSampleValue", "type": TYPES.Short},
342: {'name': 'TransferRange', 'type': TYPES.Short}, 341: {"name": "SMaxSampleValue", "type": TYPES.Short},
343: {'name': 'ClipPath', 'type': TYPES.Byte}, 342: {"name": "TransferRange", "type": TYPES.Short},
344: {'name': 'XClipPathUnits', 'type': TYPES.Long}, 343: {"name": "ClipPath", "type": TYPES.Byte},
345: {'name': 'YClipPathUnits', 'type': TYPES.Long}, 344: {"name": "XClipPathUnits", "type": TYPES.Long},
346: {'name': 'Indexed', 'type': TYPES.Short}, 345: {"name": "YClipPathUnits", "type": TYPES.Long},
347: {'name': 'JPEGTables', 'type': TYPES.Undefined}, 346: {"name": "Indexed", "type": TYPES.Short},
351: {'name': 'OPIProxy', 'type': TYPES.Short}, 347: {"name": "JPEGTables", "type": TYPES.Undefined},
512: {'name': 'JPEGProc', 'type': TYPES.Long}, 351: {"name": "OPIProxy", "type": TYPES.Short},
513: {'name': 'JPEGInterchangeFormat', 'type': TYPES.Long}, 512: {"name": "JPEGProc", "type": TYPES.Long},
514: {'name': 'JPEGInterchangeFormatLength', 'type': TYPES.Long}, 513: {"name": "JPEGInterchangeFormat", "type": TYPES.Long},
515: {'name': 'JPEGRestartInterval', 'type': TYPES.Short}, 514: {"name": "JPEGInterchangeFormatLength", "type": TYPES.Long},
517: {'name': 'JPEGLosslessPredictors', 'type': TYPES.Short}, 515: {"name": "JPEGRestartInterval", "type": TYPES.Short},
518: {'name': 'JPEGPointTransforms', 'type': TYPES.Short}, 517: {"name": "JPEGLosslessPredictors", "type": TYPES.Short},
519: {'name': 'JPEGQTables', 'type': TYPES.Long}, 518: {"name": "JPEGPointTransforms", "type": TYPES.Short},
520: {'name': 'JPEGDCTables', 'type': TYPES.Long}, 519: {"name": "JPEGQTables", "type": TYPES.Long},
521: {'name': 'JPEGACTables', 'type': TYPES.Long}, 520: {"name": "JPEGDCTables", "type": TYPES.Long},
529: {'name': 'YCbCrCoefficients', 'type': TYPES.Rational}, 521: {"name": "JPEGACTables", "type": TYPES.Long},
530: {'name': 'YCbCrSubSampling', 'type': TYPES.Short}, 529: {"name": "YCbCrCoefficients", "type": TYPES.Rational},
531: {'name': 'YCbCrPositioning', 'type': TYPES.Short}, 530: {"name": "YCbCrSubSampling", "type": TYPES.Short},
532: {'name': 'ReferenceBlackWhite', 'type': TYPES.Rational}, 531: {"name": "YCbCrPositioning", "type": TYPES.Short},
700: {'name': 'XMLPacket', 'type': TYPES.Byte}, 532: {"name": "ReferenceBlackWhite", "type": TYPES.Rational},
18246: {'name': 'Rating', 'type': TYPES.Short}, 700: {"name": "XMLPacket", "type": TYPES.Byte},
18249: {'name': 'RatingPercent', 'type': TYPES.Short}, 18246: {"name": "Rating", "type": TYPES.Short},
32781: {'name': 'ImageID', 'type': TYPES.Ascii}, 18249: {"name": "RatingPercent", "type": TYPES.Short},
33421: {'name': 'CFARepeatPatternDim', 'type': TYPES.Short}, 32781: {"name": "ImageID", "type": TYPES.Ascii},
33422: {'name': 'CFAPattern', 'type': TYPES.Byte}, 33421: {"name": "CFARepeatPatternDim", "type": TYPES.Short},
33423: {'name': 'BatteryLevel', 'type': TYPES.Rational}, 33422: {"name": "CFAPattern", "type": TYPES.Byte},
33432: {'name': 'Copyright', 'type': TYPES.Ascii}, 33423: {"name": "BatteryLevel", "type": TYPES.Rational},
33434: {'name': 'ExposureTime', 'type': TYPES.Rational}, 33432: {"name": "Copyright", "type": TYPES.Ascii},
34377: {'name': 'ImageResources', 'type': TYPES.Byte}, 33434: {"name": "ExposureTime", "type": TYPES.Rational},
34665: {'name': 'ExifTag', 'type': TYPES.Long}, 34377: {"name": "ImageResources", "type": TYPES.Byte},
34675: {'name': 'InterColorProfile', 'type': TYPES.Undefined}, 34665: {"name": "ExifTag", "type": TYPES.Long},
34853: {'name': 'GPSTag', 'type': TYPES.Long}, 34675: {"name": "InterColorProfile", "type": TYPES.Undefined},
34857: {'name': 'Interlace', 'type': TYPES.Short}, 34853: {"name": "GPSTag", "type": TYPES.Long},
34858: {'name': 'TimeZoneOffset', 'type': TYPES.Long}, 34857: {"name": "Interlace", "type": TYPES.Short},
34859: {'name': 'SelfTimerMode', 'type': TYPES.Short}, 34858: {"name": "TimeZoneOffset", "type": TYPES.Long},
37387: {'name': 'FlashEnergy', 'type': TYPES.Rational}, 34859: {"name": "SelfTimerMode", "type": TYPES.Short},
37388: {'name': 'SpatialFrequencyResponse', 'type': TYPES.Undefined}, 37387: {"name": "FlashEnergy", "type": TYPES.Rational},
37389: {'name': 'Noise', 'type': TYPES.Undefined}, 37388: {"name": "SpatialFrequencyResponse", "type": TYPES.Undefined},
37390: {'name': 'FocalPlaneXResolution', 'type': TYPES.Rational}, 37389: {"name": "Noise", "type": TYPES.Undefined},
37391: {'name': 'FocalPlaneYResolution', 'type': TYPES.Rational}, 37390: {"name": "FocalPlaneXResolution", "type": TYPES.Rational},
37392: {'name': 'FocalPlaneResolutionUnit', 'type': TYPES.Short}, 37391: {"name": "FocalPlaneYResolution", "type": TYPES.Rational},
37393: {'name': 'ImageNumber', 'type': TYPES.Long}, 37392: {"name": "FocalPlaneResolutionUnit", "type": TYPES.Short},
37394: {'name': 'SecurityClassification', 'type': TYPES.Ascii}, 37393: {"name": "ImageNumber", "type": TYPES.Long},
37395: {'name': 'ImageHistory', 'type': TYPES.Ascii}, 37394: {"name": "SecurityClassification", "type": TYPES.Ascii},
37397: {'name': 'ExposureIndex', 'type': TYPES.Rational}, 37395: {"name": "ImageHistory", "type": TYPES.Ascii},
37398: {'name': 'TIFFEPStandardID', 'type': TYPES.Byte}, 37397: {"name": "ExposureIndex", "type": TYPES.Rational},
37399: {'name': 'SensingMethod', 'type': TYPES.Short}, 37398: {"name": "TIFFEPStandardID", "type": TYPES.Byte},
40091: {'name': 'XPTitle', 'type': TYPES.Byte}, 37399: {"name": "SensingMethod", "type": TYPES.Short},
40092: {'name': 'XPComment', 'type': TYPES.Byte}, 40091: {"name": "XPTitle", "type": TYPES.Byte},
40093: {'name': 'XPAuthor', 'type': TYPES.Byte}, 40092: {"name": "XPComment", "type": TYPES.Byte},
40094: {'name': 'XPKeywords', 'type': TYPES.Byte}, 40093: {"name": "XPAuthor", "type": TYPES.Byte},
40095: {'name': 'XPSubject', 'type': TYPES.Byte}, 40094: {"name": "XPKeywords", "type": TYPES.Byte},
50341: {'name': 'PrintImageMatching', 'type': TYPES.Undefined}, 40095: {"name": "XPSubject", "type": TYPES.Byte},
50706: {'name': 'DNGVersion', 'type': TYPES.Byte}, 50341: {"name": "PrintImageMatching", "type": TYPES.Undefined},
50707: {'name': 'DNGBackwardVersion', 'type': TYPES.Byte}, 50706: {"name": "DNGVersion", "type": TYPES.Byte},
50708: {'name': 'UniqueCameraModel', 'type': TYPES.Ascii}, 50707: {"name": "DNGBackwardVersion", "type": TYPES.Byte},
50709: {'name': 'LocalizedCameraModel', 'type': TYPES.Byte}, 50708: {"name": "UniqueCameraModel", "type": TYPES.Ascii},
50710: {'name': 'CFAPlaneColor', 'type': TYPES.Byte}, 50709: {"name": "LocalizedCameraModel", "type": TYPES.Byte},
50711: {'name': 'CFALayout', 'type': TYPES.Short}, 50710: {"name": "CFAPlaneColor", "type": TYPES.Byte},
50712: {'name': 'LinearizationTable', 'type': TYPES.Short}, 50711: {"name": "CFALayout", "type": TYPES.Short},
50713: {'name': 'BlackLevelRepeatDim', 'type': TYPES.Short}, 50712: {"name": "LinearizationTable", "type": TYPES.Short},
50714: {'name': 'BlackLevel', 'type': TYPES.Rational}, 50713: {"name": "BlackLevelRepeatDim", "type": TYPES.Short},
50715: {'name': 'BlackLevelDeltaH', 'type': TYPES.SRational}, 50714: {"name": "BlackLevel", "type": TYPES.Rational},
50716: {'name': 'BlackLevelDeltaV', 'type': TYPES.SRational}, 50715: {"name": "BlackLevelDeltaH", "type": TYPES.SRational},
50717: {'name': 'WhiteLevel', 'type': TYPES.Short}, 50716: {"name": "BlackLevelDeltaV", "type": TYPES.SRational},
50718: {'name': 'DefaultScale', 'type': TYPES.Rational}, 50717: {"name": "WhiteLevel", "type": TYPES.Short},
50719: {'name': 'DefaultCropOrigin', 'type': TYPES.Short}, 50718: {"name": "DefaultScale", "type": TYPES.Rational},
50720: {'name': 'DefaultCropSize', 'type': TYPES.Short}, 50719: {"name": "DefaultCropOrigin", "type": TYPES.Short},
50721: {'name': 'ColorMatrix1', 'type': TYPES.SRational}, 50720: {"name": "DefaultCropSize", "type": TYPES.Short},
50722: {'name': 'ColorMatrix2', 'type': TYPES.SRational}, 50721: {"name": "ColorMatrix1", "type": TYPES.SRational},
50723: {'name': 'CameraCalibration1', 'type': TYPES.SRational}, 50722: {"name": "ColorMatrix2", "type": TYPES.SRational},
50724: {'name': 'CameraCalibration2', 'type': TYPES.SRational}, 50723: {"name": "CameraCalibration1", "type": TYPES.SRational},
50725: {'name': 'ReductionMatrix1', 'type': TYPES.SRational}, 50724: {"name": "CameraCalibration2", "type": TYPES.SRational},
50726: {'name': 'ReductionMatrix2', 'type': TYPES.SRational}, 50725: {"name": "ReductionMatrix1", "type": TYPES.SRational},
50727: {'name': 'AnalogBalance', 'type': TYPES.Rational}, 50726: {"name": "ReductionMatrix2", "type": TYPES.SRational},
50728: {'name': 'AsShotNeutral', 'type': TYPES.Short}, 50727: {"name": "AnalogBalance", "type": TYPES.Rational},
50729: {'name': 'AsShotWhiteXY', 'type': TYPES.Rational}, 50728: {"name": "AsShotNeutral", "type": TYPES.Short},
50730: {'name': 'BaselineExposure', 'type': TYPES.SRational}, 50729: {"name": "AsShotWhiteXY", "type": TYPES.Rational},
50731: {'name': 'BaselineNoise', 'type': TYPES.Rational}, 50730: {"name": "BaselineExposure", "type": TYPES.SRational},
50732: {'name': 'BaselineSharpness', 'type': TYPES.Rational}, 50731: {"name": "BaselineNoise", "type": TYPES.Rational},
50733: {'name': 'BayerGreenSplit', 'type': TYPES.Long}, 50732: {"name": "BaselineSharpness", "type": TYPES.Rational},
50734: {'name': 'LinearResponseLimit', 'type': TYPES.Rational}, 50733: {"name": "BayerGreenSplit", "type": TYPES.Long},
50735: {'name': 'CameraSerialNumber', 'type': TYPES.Ascii}, 50734: {"name": "LinearResponseLimit", "type": TYPES.Rational},
50736: {'name': 'LensInfo', 'type': TYPES.Rational}, 50735: {"name": "CameraSerialNumber", "type": TYPES.Ascii},
50737: {'name': 'ChromaBlurRadius', 'type': TYPES.Rational}, 50736: {"name": "LensInfo", "type": TYPES.Rational},
50738: {'name': 'AntiAliasStrength', 'type': TYPES.Rational}, 50737: {"name": "ChromaBlurRadius", "type": TYPES.Rational},
50739: {'name': 'ShadowScale', 'type': TYPES.SRational}, 50738: {"name": "AntiAliasStrength", "type": TYPES.Rational},
50740: {'name': 'DNGPrivateData', 'type': TYPES.Byte}, 50739: {"name": "ShadowScale", "type": TYPES.SRational},
50741: {'name': 'MakerNoteSafety', 'type': TYPES.Short}, 50740: {"name": "DNGPrivateData", "type": TYPES.Byte},
50778: {'name': 'CalibrationIlluminant1', 'type': TYPES.Short}, 50741: {"name": "MakerNoteSafety", "type": TYPES.Short},
50779: {'name': 'CalibrationIlluminant2', 'type': TYPES.Short}, 50778: {"name": "CalibrationIlluminant1", "type": TYPES.Short},
50780: {'name': 'BestQualityScale', 'type': TYPES.Rational}, 50779: {"name": "CalibrationIlluminant2", "type": TYPES.Short},
50781: {'name': 'RawDataUniqueID', 'type': TYPES.Byte}, 50780: {"name": "BestQualityScale", "type": TYPES.Rational},
50827: {'name': 'OriginalRawFileName', 'type': TYPES.Byte}, 50781: {"name": "RawDataUniqueID", "type": TYPES.Byte},
50828: {'name': 'OriginalRawFileData', 'type': TYPES.Undefined}, 50827: {"name": "OriginalRawFileName", "type": TYPES.Byte},
50829: {'name': 'ActiveArea', 'type': TYPES.Short}, 50828: {"name": "OriginalRawFileData", "type": TYPES.Undefined},
50830: {'name': 'MaskedAreas', 'type': TYPES.Short}, 50829: {"name": "ActiveArea", "type": TYPES.Short},
50831: {'name': 'AsShotICCProfile', 'type': TYPES.Undefined}, 50830: {"name": "MaskedAreas", "type": TYPES.Short},
50832: {'name': 'AsShotPreProfileMatrix', 'type': TYPES.SRational}, 50831: {"name": "AsShotICCProfile", "type": TYPES.Undefined},
50833: {'name': 'CurrentICCProfile', 'type': TYPES.Undefined}, 50832: {"name": "AsShotPreProfileMatrix", "type": TYPES.SRational},
50834: {'name': 'CurrentPreProfileMatrix', 'type': TYPES.SRational}, 50833: {"name": "CurrentICCProfile", "type": TYPES.Undefined},
50879: {'name': 'ColorimetricReference', 'type': TYPES.Short}, 50834: {"name": "CurrentPreProfileMatrix", "type": TYPES.SRational},
50931: {'name': 'CameraCalibrationSignature', 'type': TYPES.Byte}, 50879: {"name": "ColorimetricReference", "type": TYPES.Short},
50932: {'name': 'ProfileCalibrationSignature', 'type': TYPES.Byte}, 50931: {"name": "CameraCalibrationSignature", "type": TYPES.Byte},
50934: {'name': 'AsShotProfileName', 'type': TYPES.Byte}, 50932: {"name": "ProfileCalibrationSignature", "type": TYPES.Byte},
50935: {'name': 'NoiseReductionApplied', 'type': TYPES.Rational}, 50934: {"name": "AsShotProfileName", "type": TYPES.Byte},
50936: {'name': 'ProfileName', 'type': TYPES.Byte}, 50935: {"name": "NoiseReductionApplied", "type": TYPES.Rational},
50937: {'name': 'ProfileHueSatMapDims', 'type': TYPES.Long}, 50936: {"name": "ProfileName", "type": TYPES.Byte},
50938: {'name': 'ProfileHueSatMapData1', 'type': TYPES.Float}, 50937: {"name": "ProfileHueSatMapDims", "type": TYPES.Long},
50939: {'name': 'ProfileHueSatMapData2', 'type': TYPES.Float}, 50938: {"name": "ProfileHueSatMapData1", "type": TYPES.Float},
50940: {'name': 'ProfileToneCurve', 'type': TYPES.Float}, 50939: {"name": "ProfileHueSatMapData2", "type": TYPES.Float},
50941: {'name': 'ProfileEmbedPolicy', 'type': TYPES.Long}, 50940: {"name": "ProfileToneCurve", "type": TYPES.Float},
50942: {'name': 'ProfileCopyright', 'type': TYPES.Byte}, 50941: {"name": "ProfileEmbedPolicy", "type": TYPES.Long},
50964: {'name': 'ForwardMatrix1', 'type': TYPES.SRational}, 50942: {"name": "ProfileCopyright", "type": TYPES.Byte},
50965: {'name': 'ForwardMatrix2', 'type': TYPES.SRational}, 50964: {"name": "ForwardMatrix1", "type": TYPES.SRational},
50966: {'name': 'PreviewApplicationName', 'type': TYPES.Byte}, 50965: {"name": "ForwardMatrix2", "type": TYPES.SRational},
50967: {'name': 'PreviewApplicationVersion', 'type': TYPES.Byte}, 50966: {"name": "PreviewApplicationName", "type": TYPES.Byte},
50968: {'name': 'PreviewSettingsName', 'type': TYPES.Byte}, 50967: {"name": "PreviewApplicationVersion", "type": TYPES.Byte},
50969: {'name': 'PreviewSettingsDigest', 'type': TYPES.Byte}, 50968: {"name": "PreviewSettingsName", "type": TYPES.Byte},
50970: {'name': 'PreviewColorSpace', 'type': TYPES.Long}, 50969: {"name": "PreviewSettingsDigest", "type": TYPES.Byte},
50971: {'name': 'PreviewDateTime', 'type': TYPES.Ascii}, 50970: {"name": "PreviewColorSpace", "type": TYPES.Long},
50972: {'name': 'RawImageDigest', 'type': TYPES.Undefined}, 50971: {"name": "PreviewDateTime", "type": TYPES.Ascii},
50973: {'name': 'OriginalRawFileDigest', 'type': TYPES.Undefined}, 50972: {"name": "RawImageDigest", "type": TYPES.Undefined},
50974: {'name': 'SubTileBlockSize', 'type': TYPES.Long}, 50973: {"name": "OriginalRawFileDigest", "type": TYPES.Undefined},
50975: {'name': 'RowInterleaveFactor', 'type': TYPES.Long}, 50974: {"name": "SubTileBlockSize", "type": TYPES.Long},
50981: {'name': 'ProfileLookTableDims', 'type': TYPES.Long}, 50975: {"name": "RowInterleaveFactor", "type": TYPES.Long},
50982: {'name': 'ProfileLookTableData', 'type': TYPES.Float}, 50981: {"name": "ProfileLookTableDims", "type": TYPES.Long},
51008: {'name': 'OpcodeList1', 'type': TYPES.Undefined}, 50982: {"name": "ProfileLookTableData", "type": TYPES.Float},
51009: {'name': 'OpcodeList2', 'type': TYPES.Undefined}, 51008: {"name": "OpcodeList1", "type": TYPES.Undefined},
51022: {'name': 'OpcodeList3', 'type': TYPES.Undefined}, 51009: {"name": "OpcodeList2", "type": TYPES.Undefined},
60606: {'name': 'ZZZTestSlong1', 'type': TYPES.SLong}, 51022: {"name": "OpcodeList3", "type": TYPES.Undefined},
60607: {'name': 'ZZZTestSlong2', 'type': TYPES.SLong}, 60606: {"name": "ZZZTestSlong1", "type": TYPES.SLong},
60608: {'name': 'ZZZTestSByte', 'type': TYPES.SByte}, 60607: {"name": "ZZZTestSlong2", "type": TYPES.SLong},
60609: {'name': 'ZZZTestSShort', 'type': TYPES.SShort}, 60608: {"name": "ZZZTestSByte", "type": TYPES.SByte},
60610: {'name': 'ZZZTestDFloat', 'type': TYPES.DFloat},}, 60609: {"name": "ZZZTestSShort", "type": TYPES.SShort},
'Exif': {33434: {'name': 'ExposureTime', 'type': TYPES.Rational}, 60610: {"name": "ZZZTestDFloat", "type": TYPES.DFloat},
33437: {'name': 'FNumber', 'type': TYPES.Rational}, },
34850: {'name': 'ExposureProgram', 'type': TYPES.Short}, "Exif": {
34852: {'name': 'SpectralSensitivity', 'type': TYPES.Ascii}, 33434: {"name": "ExposureTime", "type": TYPES.Rational},
34855: {'name': 'ISOSpeedRatings', 'type': TYPES.Short}, 33437: {"name": "FNumber", "type": TYPES.Rational},
34856: {'name': 'OECF', 'type': TYPES.Undefined}, 34850: {"name": "ExposureProgram", "type": TYPES.Short},
34864: {'name': 'SensitivityType', 'type': TYPES.Short}, 34852: {"name": "SpectralSensitivity", "type": TYPES.Ascii},
34865: {'name': 'StandardOutputSensitivity', 'type': TYPES.Long}, 34855: {"name": "ISOSpeedRatings", "type": TYPES.Short},
34866: {'name': 'RecommendedExposureIndex', 'type': TYPES.Long}, 34856: {"name": "OECF", "type": TYPES.Undefined},
34867: {'name': 'ISOSpeed', 'type': TYPES.Long}, 34864: {"name": "SensitivityType", "type": TYPES.Short},
34868: {'name': 'ISOSpeedLatitudeyyy', 'type': TYPES.Long}, 34865: {"name": "StandardOutputSensitivity", "type": TYPES.Long},
34869: {'name': 'ISOSpeedLatitudezzz', 'type': TYPES.Long}, 34866: {"name": "RecommendedExposureIndex", "type": TYPES.Long},
36864: {'name': 'ExifVersion', 'type': TYPES.Undefined}, 34867: {"name": "ISOSpeed", "type": TYPES.Long},
36867: {'name': 'DateTimeOriginal', 'type': TYPES.Ascii}, 34868: {"name": "ISOSpeedLatitudeyyy", "type": TYPES.Long},
36868: {'name': 'DateTimeDigitized', 'type': TYPES.Ascii}, 34869: {"name": "ISOSpeedLatitudezzz", "type": TYPES.Long},
36880: {'name': 'OffsetTime', 'type': TYPES.Ascii}, 36864: {"name": "ExifVersion", "type": TYPES.Undefined},
36881: {'name': 'OffsetTimeOriginal', 'type': TYPES.Ascii}, 36867: {"name": "DateTimeOriginal", "type": TYPES.Ascii},
36882: {'name': 'OffsetTimeDigitized', 'type': TYPES.Ascii}, 36868: {"name": "DateTimeDigitized", "type": TYPES.Ascii},
37121: {'name': 'ComponentsConfiguration', 'type': TYPES.Undefined}, 36880: {"name": "OffsetTime", "type": TYPES.Ascii},
37122: {'name': 'CompressedBitsPerPixel', 'type': TYPES.Rational}, 36881: {"name": "OffsetTimeOriginal", "type": TYPES.Ascii},
37377: {'name': 'ShutterSpeedValue', 'type': TYPES.SRational}, 36882: {"name": "OffsetTimeDigitized", "type": TYPES.Ascii},
37378: {'name': 'ApertureValue', 'type': TYPES.Rational}, 37121: {"name": "ComponentsConfiguration", "type": TYPES.Undefined},
37379: {'name': 'BrightnessValue', 'type': TYPES.SRational}, 37122: {"name": "CompressedBitsPerPixel", "type": TYPES.Rational},
37380: {'name': 'ExposureBiasValue', 'type': TYPES.SRational}, 37377: {"name": "ShutterSpeedValue", "type": TYPES.SRational},
37381: {'name': 'MaxApertureValue', 'type': TYPES.Rational}, 37378: {"name": "ApertureValue", "type": TYPES.Rational},
37382: {'name': 'SubjectDistance', 'type': TYPES.Rational}, 37379: {"name": "BrightnessValue", "type": TYPES.SRational},
37383: {'name': 'MeteringMode', 'type': TYPES.Short}, 37380: {"name": "ExposureBiasValue", "type": TYPES.SRational},
37384: {'name': 'LightSource', 'type': TYPES.Short}, 37381: {"name": "MaxApertureValue", "type": TYPES.Rational},
37385: {'name': 'Flash', 'type': TYPES.Short}, 37382: {"name": "SubjectDistance", "type": TYPES.Rational},
37386: {'name': 'FocalLength', 'type': TYPES.Rational}, 37383: {"name": "MeteringMode", "type": TYPES.Short},
37396: {'name': 'SubjectArea', 'type': TYPES.Short}, 37384: {"name": "LightSource", "type": TYPES.Short},
37500: {'name': 'MakerNote', 'type': TYPES.Undefined}, 37385: {"name": "Flash", "type": TYPES.Short},
37510: {'name': 'UserComment', 'type': TYPES.Undefined}, 37386: {"name": "FocalLength", "type": TYPES.Rational},
37520: {'name': 'SubSecTime', 'type': TYPES.Ascii}, 37396: {"name": "SubjectArea", "type": TYPES.Short},
37521: {'name': 'SubSecTimeOriginal', 'type': TYPES.Ascii}, 37500: {"name": "MakerNote", "type": TYPES.Undefined},
37522: {'name': 'SubSecTimeDigitized', 'type': TYPES.Ascii}, 37510: {"name": "UserComment", "type": TYPES.Undefined},
37888: {'name': 'Temperature', 'type': TYPES.SRational}, 37520: {"name": "SubSecTime", "type": TYPES.Ascii},
37889: {'name': 'Humidity', 'type': TYPES.Rational}, 37521: {"name": "SubSecTimeOriginal", "type": TYPES.Ascii},
37890: {'name': 'Pressure', 'type': TYPES.Rational}, 37522: {"name": "SubSecTimeDigitized", "type": TYPES.Ascii},
37891: {'name': 'WaterDepth', 'type': TYPES.SRational}, 37888: {"name": "Temperature", "type": TYPES.SRational},
37892: {'name': 'Acceleration', 'type': TYPES.Rational}, 37889: {"name": "Humidity", "type": TYPES.Rational},
37893: {'name': 'CameraElevationAngle', 'type': TYPES.SRational}, 37890: {"name": "Pressure", "type": TYPES.Rational},
40960: {'name': 'FlashpixVersion', 'type': TYPES.Undefined}, 37891: {"name": "WaterDepth", "type": TYPES.SRational},
40961: {'name': 'ColorSpace', 'type': TYPES.Short}, 37892: {"name": "Acceleration", "type": TYPES.Rational},
40962: {'name': 'PixelXDimension', 'type': TYPES.Long}, 37893: {"name": "CameraElevationAngle", "type": TYPES.SRational},
40963: {'name': 'PixelYDimension', 'type': TYPES.Long}, 40960: {"name": "FlashpixVersion", "type": TYPES.Undefined},
40964: {'name': 'RelatedSoundFile', 'type': TYPES.Ascii}, 40961: {"name": "ColorSpace", "type": TYPES.Short},
40965: {'name': 'InteroperabilityTag', 'type': TYPES.Long}, 40962: {"name": "PixelXDimension", "type": TYPES.Long},
41483: {'name': 'FlashEnergy', 'type': TYPES.Rational}, 40963: {"name": "PixelYDimension", "type": TYPES.Long},
41484: {'name': 'SpatialFrequencyResponse', 'type': TYPES.Undefined}, 40964: {"name": "RelatedSoundFile", "type": TYPES.Ascii},
41486: {'name': 'FocalPlaneXResolution', 'type': TYPES.Rational}, 40965: {"name": "InteroperabilityTag", "type": TYPES.Long},
41487: {'name': 'FocalPlaneYResolution', 'type': TYPES.Rational}, 41483: {"name": "FlashEnergy", "type": TYPES.Rational},
41488: {'name': 'FocalPlaneResolutionUnit', 'type': TYPES.Short}, 41484: {"name": "SpatialFrequencyResponse", "type": TYPES.Undefined},
41492: {'name': 'SubjectLocation', 'type': TYPES.Short}, 41486: {"name": "FocalPlaneXResolution", "type": TYPES.Rational},
41493: {'name': 'ExposureIndex', 'type': TYPES.Rational}, 41487: {"name": "FocalPlaneYResolution", "type": TYPES.Rational},
41495: {'name': 'SensingMethod', 'type': TYPES.Short}, 41488: {"name": "FocalPlaneResolutionUnit", "type": TYPES.Short},
41728: {'name': 'FileSource', 'type': TYPES.Undefined}, 41492: {"name": "SubjectLocation", "type": TYPES.Short},
41729: {'name': 'SceneType', 'type': TYPES.Undefined}, 41493: {"name": "ExposureIndex", "type": TYPES.Rational},
41730: {'name': 'CFAPattern', 'type': TYPES.Undefined}, 41495: {"name": "SensingMethod", "type": TYPES.Short},
41985: {'name': 'CustomRendered', 'type': TYPES.Short}, 41728: {"name": "FileSource", "type": TYPES.Undefined},
41986: {'name': 'ExposureMode', 'type': TYPES.Short}, 41729: {"name": "SceneType", "type": TYPES.Undefined},
41987: {'name': 'WhiteBalance', 'type': TYPES.Short}, 41730: {"name": "CFAPattern", "type": TYPES.Undefined},
41988: {'name': 'DigitalZoomRatio', 'type': TYPES.Rational}, 41985: {"name": "CustomRendered", "type": TYPES.Short},
41989: {'name': 'FocalLengthIn35mmFilm', 'type': TYPES.Short}, 41986: {"name": "ExposureMode", "type": TYPES.Short},
41990: {'name': 'SceneCaptureType', 'type': TYPES.Short}, 41987: {"name": "WhiteBalance", "type": TYPES.Short},
41991: {'name': 'GainControl', 'type': TYPES.Short}, 41988: {"name": "DigitalZoomRatio", "type": TYPES.Rational},
41992: {'name': 'Contrast', 'type': TYPES.Short}, 41989: {"name": "FocalLengthIn35mmFilm", "type": TYPES.Short},
41993: {'name': 'Saturation', 'type': TYPES.Short}, 41990: {"name": "SceneCaptureType", "type": TYPES.Short},
41994: {'name': 'Sharpness', 'type': TYPES.Short}, 41991: {"name": "GainControl", "type": TYPES.Short},
41995: {'name': 'DeviceSettingDescription', 'type': TYPES.Undefined}, 41992: {"name": "Contrast", "type": TYPES.Short},
41996: {'name': 'SubjectDistanceRange', 'type': TYPES.Short}, 41993: {"name": "Saturation", "type": TYPES.Short},
42016: {'name': 'ImageUniqueID', 'type': TYPES.Ascii}, 41994: {"name": "Sharpness", "type": TYPES.Short},
42032: {'name': 'CameraOwnerName', 'type': TYPES.Ascii}, 41995: {"name": "DeviceSettingDescription", "type": TYPES.Undefined},
42033: {'name': 'BodySerialNumber', 'type': TYPES.Ascii}, 41996: {"name": "SubjectDistanceRange", "type": TYPES.Short},
42034: {'name': 'LensSpecification', 'type': TYPES.Rational}, 42016: {"name": "ImageUniqueID", "type": TYPES.Ascii},
42035: {'name': 'LensMake', 'type': TYPES.Ascii}, 42032: {"name": "CameraOwnerName", "type": TYPES.Ascii},
42036: {'name': 'LensModel', 'type': TYPES.Ascii}, 42033: {"name": "BodySerialNumber", "type": TYPES.Ascii},
42037: {'name': 'LensSerialNumber', 'type': TYPES.Ascii}, 42034: {"name": "LensSpecification", "type": TYPES.Rational},
42240: {'name': 'Gamma', 'type': TYPES.Rational}}, 42035: {"name": "LensMake", "type": TYPES.Ascii},
'GPS': {0: {'name': 'GPSVersionID', 'type': TYPES.Byte}, 42036: {"name": "LensModel", "type": TYPES.Ascii},
1: {'name': 'GPSLatitudeRef', 'type': TYPES.Ascii}, 42037: {"name": "LensSerialNumber", "type": TYPES.Ascii},
2: {'name': 'GPSLatitude', 'type': TYPES.Rational}, 42240: {"name": "Gamma", "type": TYPES.Rational},
3: {'name': 'GPSLongitudeRef', 'type': TYPES.Ascii}, },
4: {'name': 'GPSLongitude', 'type': TYPES.Rational}, "GPS": {
5: {'name': 'GPSAltitudeRef', 'type': TYPES.Byte}, 0: {"name": "GPSVersionID", "type": TYPES.Byte},
6: {'name': 'GPSAltitude', 'type': TYPES.Rational}, 1: {"name": "GPSLatitudeRef", "type": TYPES.Ascii},
7: {'name': 'GPSTimeStamp', 'type': TYPES.Rational}, 2: {"name": "GPSLatitude", "type": TYPES.Rational},
8: {'name': 'GPSSatellites', 'type': TYPES.Ascii}, 3: {"name": "GPSLongitudeRef", "type": TYPES.Ascii},
9: {'name': 'GPSStatus', 'type': TYPES.Ascii}, 4: {"name": "GPSLongitude", "type": TYPES.Rational},
10: {'name': 'GPSMeasureMode', 'type': TYPES.Ascii}, 5: {"name": "GPSAltitudeRef", "type": TYPES.Byte},
11: {'name': 'GPSDOP', 'type': TYPES.Rational}, 6: {"name": "GPSAltitude", "type": TYPES.Rational},
12: {'name': 'GPSSpeedRef', 'type': TYPES.Ascii}, 7: {"name": "GPSTimeStamp", "type": TYPES.Rational},
13: {'name': 'GPSSpeed', 'type': TYPES.Rational}, 8: {"name": "GPSSatellites", "type": TYPES.Ascii},
14: {'name': 'GPSTrackRef', 'type': TYPES.Ascii}, 9: {"name": "GPSStatus", "type": TYPES.Ascii},
15: {'name': 'GPSTrack', 'type': TYPES.Rational}, 10: {"name": "GPSMeasureMode", "type": TYPES.Ascii},
16: {'name': 'GPSImgDirectionRef', 'type': TYPES.Ascii}, 11: {"name": "GPSDOP", "type": TYPES.Rational},
17: {'name': 'GPSImgDirection', 'type': TYPES.Rational}, 12: {"name": "GPSSpeedRef", "type": TYPES.Ascii},
18: {'name': 'GPSMapDatum', 'type': TYPES.Ascii}, 13: {"name": "GPSSpeed", "type": TYPES.Rational},
19: {'name': 'GPSDestLatitudeRef', 'type': TYPES.Ascii}, 14: {"name": "GPSTrackRef", "type": TYPES.Ascii},
20: {'name': 'GPSDestLatitude', 'type': TYPES.Rational}, 15: {"name": "GPSTrack", "type": TYPES.Rational},
21: {'name': 'GPSDestLongitudeRef', 'type': TYPES.Ascii}, 16: {"name": "GPSImgDirectionRef", "type": TYPES.Ascii},
22: {'name': 'GPSDestLongitude', 'type': TYPES.Rational}, 17: {"name": "GPSImgDirection", "type": TYPES.Rational},
23: {'name': 'GPSDestBearingRef', 'type': TYPES.Ascii}, 18: {"name": "GPSMapDatum", "type": TYPES.Ascii},
24: {'name': 'GPSDestBearing', 'type': TYPES.Rational}, 19: {"name": "GPSDestLatitudeRef", "type": TYPES.Ascii},
25: {'name': 'GPSDestDistanceRef', 'type': TYPES.Ascii}, 20: {"name": "GPSDestLatitude", "type": TYPES.Rational},
26: {'name': 'GPSDestDistance', 'type': TYPES.Rational}, 21: {"name": "GPSDestLongitudeRef", "type": TYPES.Ascii},
27: {'name': 'GPSProcessingMethod', 'type': TYPES.Undefined}, 22: {"name": "GPSDestLongitude", "type": TYPES.Rational},
28: {'name': 'GPSAreaInformation', 'type': TYPES.Undefined}, 23: {"name": "GPSDestBearingRef", "type": TYPES.Ascii},
29: {'name': 'GPSDateStamp', 'type': TYPES.Ascii}, 24: {"name": "GPSDestBearing", "type": TYPES.Rational},
30: {'name': 'GPSDifferential', 'type': TYPES.Short}, 25: {"name": "GPSDestDistanceRef", "type": TYPES.Ascii},
31: {'name': 'GPSHPositioningError', 'type': TYPES.Rational}}, 26: {"name": "GPSDestDistance", "type": TYPES.Rational},
'Interop': {1: {'name': 'InteroperabilityIndex', 'type': TYPES.Ascii}}, 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["0th"] = TAGS["Image"]
TAGS["1st"] = TAGS["Image"] TAGS["1st"] = TAGS["Image"]
class ImageIFD: class ImageIFD:
"""Exif tag number reference - 0th IFD""" """Exif tag number reference - 0th IFD"""
ProcessingSoftware = 11 ProcessingSoftware = 11
NewSubfileType = 254 NewSubfileType = 254
SubfileType = 255 SubfileType = 255
@ -516,6 +524,7 @@ class ImageIFD:
class ExifIFD: class ExifIFD:
"""Exif tag number reference - Exif IFD""" """Exif tag number reference - Exif IFD"""
ExposureTime = 33434 ExposureTime = 33434
FNumber = 33437 FNumber = 33437
ExposureProgram = 34850 ExposureProgram = 34850
@ -599,6 +608,7 @@ class ExifIFD:
class GPSIFD: class GPSIFD:
"""Exif tag number reference - GPS IFD""" """Exif tag number reference - GPS IFD"""
GPSVersionID = 0 GPSVersionID = 0
GPSLatitudeRef = 1 GPSLatitudeRef = 1
GPSLatitude = 2 GPSLatitude = 2
@ -635,4 +645,5 @@ class GPSIFD:
class InteropIFD: class InteropIFD:
"""Exif tag number reference - Interoperability IFD""" """Exif tag number reference - Interoperability IFD"""
InteroperabilityIndex = 1 InteroperabilityIndex = 1

View file

@ -6,6 +6,7 @@ from ._common import *
from ._exceptions import InvalidImageDataError from ._exceptions import InvalidImageDataError
from . import _webp from . import _webp
def insert(exif, image, new_file=None): def insert(exif, image, new_file=None):
""" """
py:function:: piexif.insert(exif_bytes, filename) py:function:: piexif.insert(exif_bytes, filename)
@ -20,7 +21,7 @@ def insert(exif, image, new_file=None):
output_file = False output_file = False
# Prevents "UnicodeWarning: Unicode equal comparison failed" warnings on Python 2 # 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": if maybe_image and image[0:2] == b"\xff\xd8":
image_data = image image_data = image
@ -29,7 +30,7 @@ def insert(exif, image, new_file=None):
image_data = image image_data = image
file_type = "webp" file_type = "webp"
else: else:
with open(image, 'rb') as f: with open(image, "rb") as f:
image_data = f.read() image_data = f.read()
if image_data[0:2] == b"\xff\xd8": if image_data[0:2] == b"\xff\xd8":
file_type = "jpeg" file_type = "jpeg"
@ -57,4 +58,4 @@ def insert(exif, image, new_file=None):
with open(image, "wb+") as f: with open(image, "wb+") as f:
f.write(new_data) f.write(new_data)
else: else:
raise ValueError("Give a 3rd argument to 'insert' to output file") raise ValueError("Give a 3rd argument to 'insert' to output file")

View file

@ -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}) :return: Exif data({"0th":dict, "Exif":dict, "GPS":dict, "Interop":dict, "1st":dict, "thumbnail":bytes})
:rtype: dict :rtype: dict
""" """
exif_dict = {"0th":{}, exif_dict = {
"Exif":{}, "0th": {},
"GPS":{}, "Exif": {},
"Interop":{}, "GPS": {},
"1st":{}, "Interop": {},
"thumbnail":None} "1st": {},
"thumbnail": None,
}
exifReader = _ExifReader(input_data) exifReader = _ExifReader(input_data)
if exifReader.tiftag is None: if exifReader.tiftag is None:
return exif_dict return exif_dict
@ -34,8 +36,7 @@ def load(input_data, key_is_name=False):
else: else:
exifReader.endian_mark = ">" exifReader.endian_mark = ">"
pointer = struct.unpack(exifReader.endian_mark + "L", pointer = struct.unpack(exifReader.endian_mark + "L", exifReader.tiftag[4:8])[0]
exifReader.tiftag[4:8])[0]
exif_dict["0th"] = exifReader.get_ifd_dict(pointer, "0th") exif_dict["0th"] = exifReader.get_ifd_dict(pointer, "0th")
first_ifd_pointer = exif_dict["0th"].pop("first_ifd_pointer") first_ifd_pointer = exif_dict["0th"].pop("first_ifd_pointer")
if ImageIFD.ExifTag in exif_dict["0th"]: 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] pointer = exif_dict["Exif"][ExifIFD.InteroperabilityTag]
exif_dict["Interop"] = exifReader.get_ifd_dict(pointer, "Interop") exif_dict["Interop"] = exifReader.get_ifd_dict(pointer, "Interop")
if first_ifd_pointer != b"\x00\x00\x00\x00": if first_ifd_pointer != b"\x00\x00\x00\x00":
pointer = struct.unpack(exifReader.endian_mark + "L", pointer = struct.unpack(exifReader.endian_mark + "L", first_ifd_pointer)[0]
first_ifd_pointer)[0]
exif_dict["1st"] = exifReader.get_ifd_dict(pointer, "1st") exif_dict["1st"] = exifReader.get_ifd_dict(pointer, "1st")
if (ImageIFD.JPEGInterchangeFormat in exif_dict["1st"] and if (
ImageIFD.JPEGInterchangeFormatLength in exif_dict["1st"]): ImageIFD.JPEGInterchangeFormat in exif_dict["1st"]
end = (exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] + and ImageIFD.JPEGInterchangeFormatLength in exif_dict["1st"]
exif_dict["1st"][ImageIFD.JPEGInterchangeFormatLength]) ):
thumb = exifReader.tiftag[exif_dict["1st"][ImageIFD.JPEGInterchangeFormat]:end] end = (
exif_dict["1st"][ImageIFD.JPEGInterchangeFormat]
+ exif_dict["1st"][ImageIFD.JPEGInterchangeFormatLength]
)
thumb = exifReader.tiftag[
exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] : end
]
exif_dict["thumbnail"] = thumb exif_dict["thumbnail"] = thumb
if key_is_name: if key_is_name:
@ -66,7 +72,7 @@ def load(input_data, key_is_name=False):
class _ExifReader(object): class _ExifReader(object):
def __init__(self, data): def __init__(self, data):
# Prevents "UnicodeWarning: Unicode equal comparison failed" warnings on Python 2 # 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 if maybe_image and data[0:2] == b"\xff\xd8": # JPEG
segments = split_into_segments(data) segments = split_into_segments(data)
@ -82,7 +88,7 @@ class _ExifReader(object):
elif maybe_image and data[0:4] == b"Exif": # Exif elif maybe_image and data[0:4] == b"Exif": # Exif
self.tiftag = data[6:] self.tiftag = data[6:]
else: else:
with open(data, 'rb') as f: with open(data, "rb") as f:
magic_number = f.read(2) magic_number = f.read(2)
if magic_number == b"\xff\xd8": # JPEG if magic_number == b"\xff\xd8": # JPEG
app1 = read_exif_from_file(data) app1 = read_exif_from_file(data)
@ -91,13 +97,13 @@ class _ExifReader(object):
else: else:
self.tiftag = None self.tiftag = None
elif magic_number in (b"\x49\x49", b"\x4d\x4d"): # TIFF 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() self.tiftag = f.read()
else: else:
with open(data, 'rb') as f: with open(data, "rb") as f:
header = f.read(12) header = f.read(12)
if header[0:4] == b"RIFF"and header[8:12] == b"WEBP": if header[0:4] == b"RIFF" and header[8:12] == b"WEBP":
with open(data, 'rb') as f: with open(data, "rb") as f:
file_data = f.read() file_data = f.read()
self.tiftag = _webp.get_exif(file_data) self.tiftag = _webp.get_exif(file_data)
else: else:
@ -105,8 +111,9 @@ class _ExifReader(object):
def get_ifd_dict(self, pointer, ifd_name, read_unknown=False): def get_ifd_dict(self, pointer, ifd_name, read_unknown=False):
ifd_dict = {} ifd_dict = {}
tag_count = struct.unpack(self.endian_mark + "H", tag_count = struct.unpack(
self.tiftag[pointer: pointer+2])[0] self.endian_mark + "H", self.tiftag[pointer : pointer + 2]
)[0]
offset = pointer + 2 offset = pointer + 2
if ifd_name in ["0th", "1st"]: if ifd_name in ["0th", "1st"]:
t = "Image" t = "Image"
@ -115,26 +122,28 @@ class _ExifReader(object):
p_and_value = [] p_and_value = []
for x in range(tag_count): for x in range(tag_count):
pointer = offset + 12 * x pointer = offset + 12 * x
tag = struct.unpack(self.endian_mark + "H", tag = struct.unpack(
self.tiftag[pointer: pointer+2])[0] self.endian_mark + "H", self.tiftag[pointer : pointer + 2]
value_type = struct.unpack(self.endian_mark + "H", )[0]
self.tiftag[pointer + 2: pointer + 4])[0] value_type = struct.unpack(
value_num = struct.unpack(self.endian_mark + "L", self.endian_mark + "H", self.tiftag[pointer + 2 : pointer + 4]
self.tiftag[pointer + 4: pointer + 8] )[0]
)[0] value_num = struct.unpack(
value = self.tiftag[pointer+8: pointer+12] 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)) p_and_value.append((pointer, value_type, value_num, value))
v_set = (value_type, value_num, value, tag) v_set = (value_type, value_num, value, tag)
if tag in TAGS[t]: if tag in TAGS[t]:
ifd_dict[tag] = self.convert_value(v_set) ifd_dict[tag] = self.convert_value(v_set)
elif read_unknown: elif read_unknown:
ifd_dict[tag] = (v_set[0], v_set[1], v_set[2], self.tiftag) ifd_dict[tag] = (v_set[0], v_set[1], v_set[2], self.tiftag)
#else: # else:
# pass # pass
if ifd_name == "0th": if ifd_name == "0th":
pointer = offset + 12 * tag_count 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 return ifd_dict
def convert_value(self, val): def convert_value(self, val):
@ -143,114 +152,148 @@ class _ExifReader(object):
length = val[1] length = val[1]
value = val[2] value = val[2]
if t == TYPES.Byte: # BYTE if t == TYPES.Byte: # BYTE
if length > 4: if length > 4:
pointer = struct.unpack(self.endian_mark + "L", value)[0] pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = struct.unpack("B" * length, data = struct.unpack(
self.tiftag[pointer: pointer + length]) "B" * length, self.tiftag[pointer : pointer + length]
)
else: else:
data = struct.unpack("B" * length, value[0:length]) data = struct.unpack("B" * length, value[0:length])
elif t == TYPES.Ascii: # ASCII elif t == TYPES.Ascii: # ASCII
if length > 4: if length > 4:
pointer = struct.unpack(self.endian_mark + "L", value)[0] pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = self.tiftag[pointer: pointer+length - 1] data = self.tiftag[pointer : pointer + length - 1]
else: else:
data = value[0: length - 1] data = value[0 : length - 1]
elif t == TYPES.Short: # SHORT elif t == TYPES.Short: # SHORT
if length > 2: if length > 2:
pointer = struct.unpack(self.endian_mark + "L", value)[0] pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = struct.unpack(self.endian_mark + "H" * length, data = struct.unpack(
self.tiftag[pointer: pointer+length*2]) self.endian_mark + "H" * length,
self.tiftag[pointer : pointer + length * 2],
)
else: else:
data = struct.unpack(self.endian_mark + "H" * length, data = struct.unpack(
value[0:length * 2]) self.endian_mark + "H" * length, value[0 : length * 2]
elif t == TYPES.Long: # LONG )
elif t == TYPES.Long: # LONG
if length > 1: if length > 1:
pointer = struct.unpack(self.endian_mark + "L", value)[0] pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = struct.unpack(self.endian_mark + "L" * length, data = struct.unpack(
self.tiftag[pointer: pointer+length*4]) self.endian_mark + "L" * length,
self.tiftag[pointer : pointer + length * 4],
)
else: else:
data = struct.unpack(self.endian_mark + "L" * length, data = struct.unpack(self.endian_mark + "L" * length, value)
value) elif t == TYPES.Rational: # RATIONAL
elif t == TYPES.Rational: # RATIONAL
pointer = struct.unpack(self.endian_mark + "L", value)[0] pointer = struct.unpack(self.endian_mark + "L", value)[0]
if length > 1: if length > 1:
data = tuple( data = tuple(
(struct.unpack(self.endian_mark + "L", (
self.tiftag[pointer + x * 8: struct.unpack(
pointer + 4 + x * 8])[0], self.endian_mark + "L",
struct.unpack(self.endian_mark + "L", self.tiftag[pointer + x * 8 : pointer + 4 + x * 8],
self.tiftag[pointer + 4 + x * 8: )[0],
pointer + 8 + 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) for x in range(length)
) )
else: else:
data = (struct.unpack(self.endian_mark + "L", data = (
self.tiftag[pointer: pointer + 4])[0], struct.unpack(
struct.unpack(self.endian_mark + "L", self.endian_mark + "L", self.tiftag[pointer : pointer + 4]
self.tiftag[pointer + 4: pointer + 8] )[0],
)[0]) struct.unpack(
elif t == TYPES.SByte: # SIGNED BYTES self.endian_mark + "L", self.tiftag[pointer + 4 : pointer + 8]
)[0],
)
elif t == TYPES.SByte: # SIGNED BYTES
if length > 4: if length > 4:
pointer = struct.unpack(self.endian_mark + "L", value)[0] pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = struct.unpack("b" * length, data = struct.unpack(
self.tiftag[pointer: pointer + length]) "b" * length, self.tiftag[pointer : pointer + length]
)
else: else:
data = struct.unpack("b" * length, value[0:length]) data = struct.unpack("b" * length, value[0:length])
elif t == TYPES.Undefined: # UNDEFINED BYTES elif t == TYPES.Undefined: # UNDEFINED BYTES
if length > 4: if length > 4:
pointer = struct.unpack(self.endian_mark + "L", value)[0] pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = self.tiftag[pointer: pointer+length] data = self.tiftag[pointer : pointer + length]
else: else:
data = value[0:length] data = value[0:length]
elif t == TYPES.SShort: # SIGNED SHORT elif t == TYPES.SShort: # SIGNED SHORT
if length > 2: if length > 2:
pointer = struct.unpack(self.endian_mark + "L", value)[0] pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = struct.unpack(self.endian_mark + "h" * length, data = struct.unpack(
self.tiftag[pointer: pointer+length*2]) self.endian_mark + "h" * length,
self.tiftag[pointer : pointer + length * 2],
)
else: else:
data = struct.unpack(self.endian_mark + "h" * length, data = struct.unpack(
value[0:length * 2]) self.endian_mark + "h" * length, value[0 : length * 2]
elif t == TYPES.SLong: # SLONG )
elif t == TYPES.SLong: # SLONG
if length > 1: if length > 1:
pointer = struct.unpack(self.endian_mark + "L", value)[0] pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = struct.unpack(self.endian_mark + "l" * length, data = struct.unpack(
self.tiftag[pointer: pointer+length*4]) self.endian_mark + "l" * length,
self.tiftag[pointer : pointer + length * 4],
)
else: else:
data = struct.unpack(self.endian_mark + "l" * length, data = struct.unpack(self.endian_mark + "l" * length, value)
value) elif t == TYPES.SRational: # SRATIONAL
elif t == TYPES.SRational: # SRATIONAL
pointer = struct.unpack(self.endian_mark + "L", value)[0] pointer = struct.unpack(self.endian_mark + "L", value)[0]
if length > 1: if length > 1:
data = tuple( data = tuple(
(struct.unpack(self.endian_mark + "l", (
self.tiftag[pointer + x * 8: pointer + 4 + x * 8])[0], struct.unpack(
struct.unpack(self.endian_mark + "l", self.endian_mark + "l",
self.tiftag[pointer + 4 + x * 8: pointer + 8 + x * 8])[0]) self.tiftag[pointer + x * 8 : pointer + 4 + x * 8],
for x in range(length) )[0],
struct.unpack(
self.endian_mark + "l",
self.tiftag[pointer + 4 + x * 8 : pointer + 8 + x * 8],
)[0],
)
for x in range(length)
) )
else: else:
data = (struct.unpack(self.endian_mark + "l", data = (
self.tiftag[pointer: pointer + 4])[0], struct.unpack(
struct.unpack(self.endian_mark + "l", self.endian_mark + "l", self.tiftag[pointer : pointer + 4]
self.tiftag[pointer + 4: pointer + 8] )[0],
)[0]) struct.unpack(
elif t == TYPES.Float: # FLOAT self.endian_mark + "l", self.tiftag[pointer + 4 : pointer + 8]
)[0],
)
elif t == TYPES.Float: # FLOAT
if length > 1: if length > 1:
pointer = struct.unpack(self.endian_mark + "L", value)[0] pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = struct.unpack(self.endian_mark + "f" * length, data = struct.unpack(
self.tiftag[pointer: pointer+length*4]) self.endian_mark + "f" * length,
self.tiftag[pointer : pointer + length * 4],
)
else: else:
data = struct.unpack(self.endian_mark + "f" * length, data = struct.unpack(self.endian_mark + "f" * length, value)
value) elif t == TYPES.DFloat: # DOUBLE
elif t == TYPES.DFloat: # DOUBLE
pointer = struct.unpack(self.endian_mark + "L", value)[0] pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = struct.unpack(self.endian_mark + "d" * length, data = struct.unpack(
self.tiftag[pointer: pointer+length*8]) self.endian_mark + "d" * length,
self.tiftag[pointer : pointer + length * 8],
)
else: else:
raise ValueError("Exif might be wrong. Got incorrect value " + raise ValueError(
"type to decode.\n" + "Exif might be wrong. Got incorrect value "
"tag: " + str(val[3]) + "\ntype: " + str(t)) + "type to decode.\n"
+ "tag: "
+ str(val[3])
+ "\ntype: "
+ str(t)
)
if isinstance(data, tuple) and (len(data) == 1): if isinstance(data, tuple) and (len(data) == 1):
return data[0] return data[0]
@ -260,11 +303,20 @@ class _ExifReader(object):
def _get_key_name_dict(exif_dict): def _get_key_name_dict(exif_dict):
new_dict = { new_dict = {
"0th":{TAGS["Image"][n]["name"]:value for n, value in exif_dict["0th"].items()}, "0th": {
"Exif":{TAGS["Exif"][n]["name"]:value for n, value in exif_dict["Exif"].items()}, TAGS["Image"][n]["name"]: value for n, value in exif_dict["0th"].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()}, "Exif": {
"Interop":{TAGS["Interop"][n]["name"]:value for n, value in exif_dict["Interop"].items()}, TAGS["Exif"][n]["name"]: value for n, value in exif_dict["Exif"].items()
"thumbnail":exif_dict["thumbnail"], },
"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 return new_dict

View file

@ -3,6 +3,7 @@ import io
from ._common import * from ._common import *
from . import _webp from . import _webp
def remove(src, new_file=None): def remove(src, new_file=None):
""" """
py:function:: piexif.remove(filename) py:function:: piexif.remove(filename)
@ -19,7 +20,7 @@ def remove(src, new_file=None):
src_data = src src_data = src
file_type = "webp" file_type = "webp"
else: else:
with open(src, 'rb') as f: with open(src, "rb") as f:
src_data = f.read() src_data = f.read()
output_is_file = True output_is_file = True
if src_data[0:2] == b"\xff\xd8": if src_data[0:2] == b"\xff\xd8":
@ -53,4 +54,4 @@ def remove(src, new_file=None):
with open(src, "wb+") as f: with open(src, "wb+") as f:
f.write(new_data) f.write(new_data)
else: else:
raise ValueError("Give a second argument to 'remove' to output file") raise ValueError("Give a second argument to 'remove' to output file")

View file

@ -15,7 +15,7 @@ def transplant(exif_src, image, new_file=None):
if exif_src[0:2] == b"\xff\xd8": if exif_src[0:2] == b"\xff\xd8":
src_data = exif_src src_data = exif_src
else: else:
with open(exif_src, 'rb') as f: with open(exif_src, "rb") as f:
src_data = f.read() src_data = f.read()
segments = split_into_segments(src_data) segments = split_into_segments(src_data)
exif = get_exif_seg(segments) exif = get_exif_seg(segments)
@ -26,7 +26,7 @@ def transplant(exif_src, image, new_file=None):
if image[0:2] == b"\xff\xd8": if image[0:2] == b"\xff\xd8":
image_data = image image_data = image
else: else:
with open(image, 'rb') as f: with open(image, "rb") as f:
image_data = f.read() image_data = f.read()
output_file = True output_file = True
segments = split_into_segments(image_data) segments = split_into_segments(image_data)
@ -42,4 +42,4 @@ def transplant(exif_src, image, new_file=None):
with open(image, "wb+") as f: with open(image, "wb+") as f:
f.write(new_data) f.write(new_data)
else: else:
raise ValueError("Give a 3rd argument to 'transplant' to output file") raise ValueError("Give a 3rd argument to 'transplant' to output file")

View file

@ -1,4 +1,3 @@
import struct import struct
@ -18,26 +17,33 @@ def split(data):
chunks = [] chunks = []
while pointer + CHUNK_FOURCC_LENGTH + LENGTH_BYTES_LENGTH < file_size: 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 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("<L", chunk_length_bytes)[0] chunk_length = struct.unpack("<L", chunk_length_bytes)[0]
pointer += LENGTH_BYTES_LENGTH pointer += LENGTH_BYTES_LENGTH
chunk_data = data[pointer:pointer + chunk_length] chunk_data = data[pointer : pointer + chunk_length]
chunks.append({"fourcc":fourcc, "length_bytes":chunk_length_bytes, "data":chunk_data}) chunks.append(
{"fourcc": fourcc, "length_bytes": chunk_length_bytes, "data": chunk_data}
)
padding = 1 if chunk_length % 2 else 0 padding = 1 if chunk_length % 2 else 0
pointer += chunk_length + padding pointer += chunk_length + padding
return chunks return chunks
def merge_chunks(chunks): def merge_chunks(chunks):
merged = b"".join([chunk["fourcc"] merged = b"".join(
+ chunk["length_bytes"] [
+ chunk["data"] chunk["fourcc"]
+ (len(chunk["data"]) % 2) * b"\x00" + chunk["length_bytes"]
for chunk in chunks]) + chunk["data"]
+ (len(chunk["data"]) % 2) * b"\x00"
for chunk in chunks
]
)
return merged return merged
@ -50,6 +56,7 @@ def _get_size_from_vp8x(chunk):
height = height_minus_one + 1 height = height_minus_one + 1
return (width, height) return (width, height)
def _get_size_from_vp8(chunk): def _get_size_from_vp8(chunk):
BEGIN_CODE = b"\x9d\x01\x2a" BEGIN_CODE = b"\x9d\x01\x2a"
begin_index = chunk["data"].find(BEGIN_CODE) begin_index = chunk["data"].find(BEGIN_CODE)
@ -59,17 +66,21 @@ def _get_size_from_vp8(chunk):
BEGIN_CODE_LENGTH = len(BEGIN_CODE) BEGIN_CODE_LENGTH = len(BEGIN_CODE)
LENGTH_BYTES_LENGTH = 2 LENGTH_BYTES_LENGTH = 2
length_start = begin_index + BEGIN_CODE_LENGTH length_start = begin_index + BEGIN_CODE_LENGTH
width_bytes = chunk["data"][length_start:length_start + LENGTH_BYTES_LENGTH] width_bytes = chunk["data"][length_start : length_start + LENGTH_BYTES_LENGTH]
width = struct.unpack("<H", width_bytes)[0] width = struct.unpack("<H", width_bytes)[0]
height_bytes = chunk["data"][length_start + LENGTH_BYTES_LENGTH:length_start + 2 * LENGTH_BYTES_LENGTH] height_bytes = chunk["data"][
length_start + LENGTH_BYTES_LENGTH : length_start + 2 * LENGTH_BYTES_LENGTH
]
height = struct.unpack("<H", height_bytes)[0] height = struct.unpack("<H", height_bytes)[0]
return (width, height) return (width, height)
def _vp8L_contains_alpha(chunk_data): def _vp8L_contains_alpha(chunk_data):
flag = ord(chunk_data[4:5]) >> 5-1 & ord(b"\x01") flag = ord(chunk_data[4:5]) >> 5 - 1 & ord(b"\x01")
contains = 1 * flag contains = 1 * flag
return contains return contains
def _get_size_from_vp8L(chunk): def _get_size_from_vp8L(chunk):
b1 = chunk["data"][1:2] b1 = chunk["data"][1:2]
b2 = chunk["data"][2:3] 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_minus_one = (ord(b2) & ord(b"\x3F")) << 8 | ord(b1)
width = width_minus_one + 1 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 height = height_minus_one + 1
return (width, height) return (width, height)
def _get_size_from_anmf(chunk): def _get_size_from_anmf(chunk):
width_minus_one_bytes = chunk["data"][6:9] + b"\x00" width_minus_one_bytes = chunk["data"][6:9] + b"\x00"
width_minus_one = struct.unpack("<L", width_minus_one_bytes)[0] width_minus_one = struct.unpack("<L", width_minus_one_bytes)[0]
@ -92,12 +106,22 @@ def _get_size_from_anmf(chunk):
height_minus_one = struct.unpack("<L", height_minus_one_bytes)[0] height_minus_one = struct.unpack("<L", height_minus_one_bytes)[0]
height = height_minus_one + 1 height = height_minus_one + 1
return (width, height) return (width, height)
def set_vp8x(chunks): def set_vp8x(chunks):
width = None width = None
height = None height = None
flags = [b"0", b"0", b"0", b"0", b"0", b"0", b"0", b"0"] # [0, 0, ICC, Alpha, EXIF, XMP, Anim, 0] flags = [
b"0",
b"0",
b"0",
b"0",
b"0",
b"0",
b"0",
b"0",
] # [0, 0, ICC, Alpha, EXIF, XMP, Anim, 0]
for chunk in chunks: for chunk in chunks:
if chunk["fourcc"] == b"VP8X": if chunk["fourcc"] == b"VP8X":
@ -136,11 +160,16 @@ def set_vp8x(chunks):
data_bytes = flags_bytes + padding_bytes + width_bytes + height_bytes data_bytes = flags_bytes + padding_bytes + width_bytes + height_bytes
vp8x_chunk = {"fourcc":header_bytes, "length_bytes":length_bytes, "data":data_bytes} vp8x_chunk = {
"fourcc": header_bytes,
"length_bytes": length_bytes,
"data": data_bytes,
}
chunks.insert(0, vp8x_chunk) chunks.insert(0, vp8x_chunk)
return chunks return chunks
def get_file_header(chunks): def get_file_header(chunks):
WEBP_HEADER_LENGTH = 4 WEBP_HEADER_LENGTH = 4
FOURCC_LENGTH = 4 FOURCC_LENGTH = 4
@ -158,7 +187,6 @@ def get_file_header(chunks):
return file_header return file_header
def get_exif(data): def get_exif(data):
if data[0:4] != b"RIFF" or data[8:12] != b"WEBP": if data[0:4] != b"RIFF" or data[8:12] != b"WEBP":
raise ValueError("Not WebP") raise ValueError("Not WebP")
@ -179,15 +207,15 @@ def get_exif(data):
chunks = [] chunks = []
exif = b"" exif = b""
while pointer < file_size: while pointer < file_size:
fourcc = data[pointer:pointer + CHUNK_FOURCC_LENGTH] fourcc = data[pointer : pointer + CHUNK_FOURCC_LENGTH]
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("<L", chunk_length_bytes)[0] chunk_length = struct.unpack("<L", chunk_length_bytes)[0]
if chunk_length % 2: if chunk_length % 2:
chunk_length += 1 chunk_length += 1
pointer += LENGTH_BYTES_LENGTH pointer += LENGTH_BYTES_LENGTH
if fourcc == b"EXIF": if fourcc == b"EXIF":
return data[pointer:pointer + chunk_length] return data[pointer : pointer + chunk_length]
pointer += chunk_length pointer += chunk_length
return None # if there isn't exif, return None. return None # if there isn't exif, return None.
@ -195,7 +223,11 @@ def get_exif(data):
def insert_exif_into_chunks(chunks, exif_bytes): def insert_exif_into_chunks(chunks, exif_bytes):
EXIF_HEADER = b"EXIF" EXIF_HEADER = b"EXIF"
exif_length_bytes = struct.pack("<L", len(exif_bytes)) exif_length_bytes = struct.pack("<L", len(exif_bytes))
exif_chunk = {"fourcc":EXIF_HEADER, "length_bytes":exif_length_bytes, "data":exif_bytes} exif_chunk = {
"fourcc": EXIF_HEADER,
"length_bytes": exif_length_bytes,
"data": exif_bytes,
}
xmp_index = None xmp_index = None
animation_index = None animation_index = None

View file

@ -2,26 +2,26 @@ class UserComment:
# #
# Names of encodings that we publicly support. # Names of encodings that we publicly support.
# #
ASCII = 'ascii' ASCII = "ascii"
JIS = 'jis' JIS = "jis"
UNICODE = 'unicode' UNICODE = "unicode"
ENCODINGS = (ASCII, JIS, UNICODE) ENCODINGS = (ASCII, JIS, UNICODE)
# #
# The actual encodings accepted by the standard library differ slightly from # The actual encodings accepted by the standard library differ slightly from
# the above. # the above.
# #
_JIS = 'shift_jis' _JIS = "shift_jis"
_UNICODE = 'utf_16_be' _UNICODE = "utf_16_be"
_PREFIX_SIZE = 8 _PREFIX_SIZE = 8
# #
# From Table 9: Character Codes and their Designation # From Table 9: Character Codes and their Designation
# #
_ASCII_PREFIX = b'\x41\x53\x43\x49\x49\x00\x00\x00' _ASCII_PREFIX = b"\x41\x53\x43\x49\x49\x00\x00\x00"
_JIS_PREFIX = b'\x4a\x49\x53\x00\x00\x00\x00\x00' _JIS_PREFIX = b"\x4a\x49\x53\x00\x00\x00\x00\x00"
_UNICODE_PREFIX = b'\x55\x4e\x49\x43\x4f\x44\x45\x00' _UNICODE_PREFIX = b"\x55\x4e\x49\x43\x4f\x44\x45\x00"
_UNDEFINED_PREFIX = b'\x00\x00\x00\x00\x00\x00\x00\x00' _UNDEFINED_PREFIX = b"\x00\x00\x00\x00\x00\x00\x00\x00"
@classmethod @classmethod
def load(cls, data): def load(cls, data):
@ -35,18 +35,20 @@ class UserComment:
or the encoding is unsupported. or the encoding is unsupported.
""" """
if len(data) < cls._PREFIX_SIZE: if len(data) < cls._PREFIX_SIZE:
raise ValueError('not enough data to decode UserComment') raise ValueError("not enough data to decode UserComment")
prefix = data[:cls._PREFIX_SIZE] prefix = data[: cls._PREFIX_SIZE]
body = data[cls._PREFIX_SIZE:] body = data[cls._PREFIX_SIZE :]
if prefix == cls._UNDEFINED_PREFIX: if prefix == cls._UNDEFINED_PREFIX:
raise ValueError('prefix is UNDEFINED, unable to decode UserComment') raise ValueError("prefix is UNDEFINED, unable to decode UserComment")
try: try:
encoding = { encoding = {
cls._ASCII_PREFIX: cls.ASCII, cls._JIS_PREFIX: cls._JIS, cls._UNICODE_PREFIX: cls._UNICODE, cls._ASCII_PREFIX: cls.ASCII,
cls._JIS_PREFIX: cls._JIS,
cls._UNICODE_PREFIX: cls._UNICODE,
}[prefix] }[prefix]
except KeyError: except KeyError:
raise ValueError('unable to determine appropriate encoding') raise ValueError("unable to determine appropriate encoding")
return body.decode(encoding, errors='replace') return body.decode(encoding, errors="replace")
@classmethod @classmethod
def dump(cls, data, encoding="ascii"): def dump(cls, data, encoding="ascii"):
@ -60,7 +62,15 @@ class UserComment:
:raises: ValueError if the encoding is unsupported. :raises: ValueError if the encoding is unsupported.
""" """
if encoding not in cls.ENCODINGS: if encoding not in cls.ENCODINGS:
raise ValueError('encoding %r must be one of %r' % (encoding, cls.ENCODINGS)) raise ValueError(
prefix = {cls.ASCII: cls._ASCII_PREFIX, cls.JIS: cls._JIS_PREFIX, cls.UNICODE: cls._UNICODE_PREFIX}[encoding] "encoding %r must be one of %r" % (encoding, cls.ENCODINGS)
internal_encoding = {cls.UNICODE: cls._UNICODE, cls.JIS: cls._JIS}.get(encoding, encoding) )
return prefix + data.encode(internal_encoding, errors='replace') prefix = {
cls.ASCII: cls._ASCII_PREFIX,
cls.JIS: cls._JIS_PREFIX,
cls.UNICODE: cls._UNICODE_PREFIX,
}[encoding]
internal_encoding = {cls.UNICODE: cls._UNICODE, cls.JIS: cls._JIS}.get(
encoding, encoding
)
return prefix + data.encode(internal_encoding, errors="replace")

View file

@ -20,14 +20,12 @@ def set_gain(camera, gain, value):
if gain not in [MMAL_PARAMETER_ANALOG_GAIN, MMAL_PARAMETER_DIGITAL_GAIN]: if gain not in [MMAL_PARAMETER_ANALOG_GAIN, MMAL_PARAMETER_DIGITAL_GAIN]:
raise ValueError("The gain parameter was not valid") raise ValueError("The gain parameter was not valid")
ret = mmal.mmal_port_parameter_set_rational( ret = mmal.mmal_port_parameter_set_rational(
camera._camera.control._port, camera._camera.control._port, gain, to_rational(value)
gain,
to_rational(value)
) )
if ret == 4: if ret == 4:
raise exc.PiCameraMMALError( raise exc.PiCameraMMALError(
ret, ret,
"Are you running the latest version of the userland libraries? Gain setting was introduced in late 2017." "Are you running the latest version of the userland libraries? Gain setting was introduced in late 2017.",
) )
elif ret != 0: elif ret != 0:
raise exc.PiCameraMMALError(ret) raise exc.PiCameraMMALError(ret)
@ -56,21 +54,27 @@ if __name__ == "__main__":
# fix the shutter speed # fix the shutter speed
cam.shutter_speed = cam.exposure_speed cam.shutter_speed = cam.exposure_speed
logging.info("Current a/d gains: {}, {}".format(cam.analog_gain, cam.digital_gain)) logging.info(
"Current a/d gains: {}, {}".format(cam.analog_gain, cam.digital_gain)
)
logging.info("Attempting to set analogue gain to 1") logging.info("Attempting to set analogue gain to 1")
set_analog_gain(cam, 1) set_analog_gain(cam, 1)
logging.info("Attempting to set digital gain to 1") logging.info("Attempting to set digital gain to 1")
set_digital_gain(cam, 1) set_digital_gain(cam, 1)
# The old code is left in here in case it is a useful example... # The old code is left in here in case it is a useful example...
# ret = mmal.mmal_port_parameter_set_rational(cam._camera.control._port, # ret = mmal.mmal_port_parameter_set_rational(cam._camera.control._port,
# MMAL_PARAMETER_DIGITAL_GAIN, # MMAL_PARAMETER_DIGITAL_GAIN,
# to_rational(1)) # to_rational(1))
# print("Return code: {}".format(ret)) # print("Return code: {}".format(ret))
try: try:
while True: while True:
logging.info("Current a/d gains: {}, {}".format(cam.analog_gain, cam.digital_gain)) logging.info(
"Current a/d gains: {}, {}".format(
cam.analog_gain, cam.digital_gain
)
)
time.sleep(1) time.sleep(1)
except KeyboardInterrupt: except KeyboardInterrupt:
logging.info("Stopping...") logging.info("Stopping...")

View file

@ -65,7 +65,6 @@ def convert_type_to_json_safe(v):
return v return v
def settings_to_json(data: dict): def settings_to_json(data: dict):
""" """
Make a copy of an input dictionary that's safe for JSON return Make a copy of an input dictionary that's safe for JSON return

View file

@ -7,8 +7,8 @@ class TaskDeniedException(Exception):
class LockError(ThreadError): class LockError(ThreadError):
ERROR_CODES = { ERROR_CODES = {
'ACQUIRE_ERROR': "Unable to acquire. Lock in use by another thread.", "ACQUIRE_ERROR": "Unable to acquire. Lock in use by another thread.",
'IN_USE_ERROR': "Lock in use by another thread." "IN_USE_ERROR": "Lock in use by another thread.",
} }
def __init__(self, code, lock): def __init__(self, code, lock):

View file

@ -14,6 +14,7 @@ class StrictLock(object):
_lock (:py:class:`threading.RLock`): Parent RLock object _lock (:py:class:`threading.RLock`): Parent RLock object
timeout (int): Time, in seconds, lock acquisition will wait before raising an exception timeout (int): Time, in seconds, lock acquisition will wait before raising an exception
""" """
def __init__(self, timeout=1): def __init__(self, timeout=1):
self._lock = RLock() self._lock = RLock()
self.timeout = timeout self.timeout = timeout
@ -29,7 +30,7 @@ class StrictLock(object):
if result: if result:
return result return result
else: else:
raise LockError('ACQUIRE_ERROR', self) raise LockError("ACQUIRE_ERROR", self)
def __exit__(self, *args): def __exit__(self, *args):
self._lock.release() self._lock.release()
@ -51,6 +52,7 @@ class CompositeLock(object):
locks (list): List of parent RLock objects locks (list): List of parent RLock objects
timeout (int): Time, in seconds, lock acquisition will wait before raising an exception timeout (int): Time, in seconds, lock acquisition will wait before raising an exception
""" """
def __init__(self, locks, timeout=1): def __init__(self, locks, timeout=1):
self.locks = locks self.locks = locks
self.timeout = timeout self.timeout = timeout
@ -63,7 +65,7 @@ class CompositeLock(object):
if all(result): if all(result):
return result return result
else: else:
raise LockError('ACQUIRE_ERROR', self) raise LockError("ACQUIRE_ERROR", self)
def __exit__(self, *args): def __exit__(self, *args):
for lock in self.locks: for lock in self.locks:

View file

@ -1,3 +1,17 @@
__all__ = ['module_from_file', 'load_plugin_class', 'load_plugin_module', 'class_from_map', 'PluginMount', 'MicroscopePlugin'] __all__ = [
"module_from_file",
"load_plugin_class",
"load_plugin_module",
"class_from_map",
"PluginMount",
"MicroscopePlugin",
]
from .loader import module_from_file, load_plugin_class, load_plugin_module, class_from_map, PluginMount, MicroscopePlugin from .loader import (
module_from_file,
load_plugin_class,
load_plugin_module,
class_from_map,
PluginMount,
MicroscopePlugin,
)

View file

@ -1,2 +1,2 @@
__all__ = ['AutofocusPlugin'] __all__ = ["AutofocusPlugin"]
from .plugin import AutofocusPlugin from .plugin import AutofocusPlugin

View file

@ -1,18 +1,24 @@
import numpy as np import numpy as np
import logging import logging
from openflexure_microscope.devel import MicroscopeViewPlugin, JsonResponse, request, jsonify from openflexure_microscope.devel import (
MicroscopeViewPlugin,
JsonResponse,
request,
jsonify,
)
class MeasureSharpnessAPI(MicroscopeViewPlugin): class MeasureSharpnessAPI(MicroscopeViewPlugin):
def post(self): def post(self):
payload = JsonResponse(request) payload = JsonResponse(request)
return jsonify({'sharpness': self.plugin.measure_sharpness()}) return jsonify({"sharpness": self.plugin.measure_sharpness()})
class AutofocusAPI(MicroscopeViewPlugin): class AutofocusAPI(MicroscopeViewPlugin):
def post(self): def post(self):
payload = JsonResponse(request) payload = JsonResponse(request)
# Figure out the range of z values to use # Figure out the range of z values to use
dz = payload.param("dz", default=np.linspace(-300, 300, 7), convert=np.array) dz = payload.param("dz", default=np.linspace(-300, 300, 7), convert=np.array)
@ -22,10 +28,11 @@ class AutofocusAPI(MicroscopeViewPlugin):
# return a handle on the autofocus task # return a handle on the autofocus task
return jsonify(task.state), 202 return jsonify(task.state), 202
class FastAutofocusAPI(MicroscopeViewPlugin): class FastAutofocusAPI(MicroscopeViewPlugin):
def post(self): def post(self):
payload = JsonResponse(request) payload = JsonResponse(request)
# Figure out the parameters to use # Figure out the parameters to use
dz = payload.param("dz", default=2000, convert=int) dz = payload.param("dz", default=2000, convert=int)
backlash = payload.param("backlash", default=0, convert=int) backlash = payload.param("backlash", default=0, convert=int)
@ -33,7 +40,9 @@ class FastAutofocusAPI(MicroscopeViewPlugin):
backlash = 0 backlash = 0
logging.info("Running autofocus...") logging.info("Running autofocus...")
task = self.microscope.task.start(self.plugin.fast_autofocus, dz, backlash=backlash) task = self.microscope.task.start(
self.plugin.fast_autofocus, dz, backlash=backlash
)
# return a handle on the autofocus task # return a handle on the autofocus task
return jsonify(task.state), 202 return jsonify(task.state), 202

View file

@ -4,7 +4,8 @@ import threading
import logging import logging
from scipy import ndimage from scipy import ndimage
class JPEGSharpnessMonitor():
class JPEGSharpnessMonitor:
def __init__(self, microscope, timeout=60): def __init__(self, microscope, timeout=60):
self.microscope = microscope self.microscope = microscope
self.camera = microscope.camera self.camera = microscope.camera
@ -17,32 +18,34 @@ class JPEGSharpnessMonitor():
self.timeout = timeout self.timeout = timeout
self.keep_alive() self.keep_alive()
self.background_thread = None self.background_thread = None
def is_alive(self): def is_alive(self):
if self.background_thread is None: if self.background_thread is None:
return False return False
else: else:
return self.background_thread.is_alive() return self.background_thread.is_alive()
def should_stop(self): def should_stop(self):
import time import time
return time.time() - self.kept_alive > self.timeout return time.time() - self.kept_alive > self.timeout
def keep_alive(self): def keep_alive(self):
import time import time
self.kept_alive = time.time() self.kept_alive = time.time()
def start(self): def start(self):
"Start monitoring sharpness by looking at JPEG size" "Start monitoring sharpness by looking at JPEG size"
self.background_thread = threading.Thread(target=self._measure_jpegs) self.background_thread = threading.Thread(target=self._measure_jpegs)
self.background_thread.start() self.background_thread.start()
return self return self
def stop(self): def stop(self):
"Stop the background thread" "Stop the background thread"
self.stop_event.set() self.stop_event.set()
self.background_thread.join() self.background_thread.join()
def _measure_jpegs(self): def _measure_jpegs(self):
"Function that runs in a background thread to record sharpness" "Function that runs in a background thread to record sharpness"
logging.info("Starting sharpness measurement in background thread") 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 the size of a frame from the MJPEG stream"""
return len(self.camera.get_frame()) return len(self.camera.get_frame())
def focus_rel(self, dz, backlash=False, **kwargs): def focus_rel(self, dz, backlash=False, **kwargs):
self.keep_alive() self.keep_alive()
self.stage_times.append(time.time()) self.stage_times.append(time.time())
@ -69,7 +71,7 @@ class JPEGSharpnessMonitor():
self.stage_positions.append(self.stage.position) self.stage_positions.append(self.stage.position)
i = len(self.stage_positions) - 2 i = len(self.stage_positions) - 2
return i, self.stage_positions[-1][2] return i, self.stage_positions[-1][2]
def move_data(self, istart, istop=None): def move_data(self, istart, istop=None):
"Extract sharpness as a function of (interpolated) z" "Extract sharpness as a function of (interpolated) z"
global np, logging global np, logging
@ -92,18 +94,21 @@ class JPEGSharpnessMonitor():
"""Return the z position of the sharpest image on a given move""" """Return the z position of the sharpest image on a given move"""
jt, jz, js = self.move_data(index) jt, jz, js = self.move_data(index)
return jz[np.argmax(js)] return jz[np.argmax(js)]
def data_dict(self): def data_dict(self):
"""Return the gathered data as a single convenient dictionary""" """Return the gathered data as a single convenient dictionary"""
data = {} 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) data[k] = getattr(self, k)
return data return data
def decimate_to(shape, image): def decimate_to(shape, image):
"""Decimate an image to reduce its size if it's too big.""" """Decimate an image to reduce its size if it's too big."""
decimation = np.max(np.ceil(np.array(image.shape, dtype=np.float)[:len(shape)]/np.array(shape))) decimation = np.max(
return image[::int(decimation), ::int(decimation), ...] 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): 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(decimate_to((1000,1000), rgb_image),2)
image_bw = np.mean(rgb_image, 2) image_bw = np.mean(rgb_image, 2)
image_lap = ndimage.filters.laplace(image_bw) image_lap = ndimage.filters.laplace(image_bw)
return np.mean(image_lap.astype(np.float)**4) return np.mean(image_lap.astype(np.float) ** 4)
def sharpness_edge(image): def sharpness_edge(image):
"""Return a sharpness metric optimised for vertical lines""" """Return a sharpness metric optimised for vertical lines"""
gray = np.mean(image.astype(float), 2) gray = np.mean(image.astype(float), 2)
n = 20 n = 20
edge = np.array([[-1]*n + [1]*n]) edge = np.array([[-1] * n + [1] * n])
return np.sum([np.sum(ndimage.filters.convolve(gray, W)**2) for W in [edge, edge.T]]) return np.sum(
[np.sum(ndimage.filters.convolve(gray, W) ** 2) for W in [edge, edge.T]]
)

View file

@ -10,15 +10,16 @@ from .api import MeasureSharpnessAPI, AutofocusAPI, FastAutofocusAPI
from openflexure_microscope.devel import MicroscopePlugin from openflexure_microscope.devel import MicroscopePlugin
class AutofocusPlugin(MicroscopePlugin): class AutofocusPlugin(MicroscopePlugin):
""" """
Basic autofocus plugin Basic autofocus plugin
""" """
api_views = { api_views = {
'/measure_sharpness': MeasureSharpnessAPI, "/measure_sharpness": MeasureSharpnessAPI,
'/autofocus': AutofocusAPI, "/autofocus": AutofocusAPI,
'/fast_autofocus': FastAutofocusAPI, "/fast_autofocus": FastAutofocusAPI,
} }
### SLOW AUTOFOCUS ### SLOW AUTOFOCUS
@ -70,21 +71,24 @@ class AutofocusPlugin(MicroscopePlugin):
m.focus_rel(dz) m.focus_rel(dz)
return m.sharpest_z_on_move(0) return m.sharpest_z_on_move(0)
def fast_autofocus(self, dz=2000, backlash=None): def fast_autofocus(self, dz=2000, backlash=None):
"""Perform a down-up-down-up autofocus""" """Perform a down-up-down-up autofocus"""
with self.monitor_sharpness() as m: 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) i, z = m.focus_rel(dz)
fz = m.sharpest_z_on_move(i) fz = m.sharpest_z_on_move(i)
if backlash is None: 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: else:
i, z = m.focus_rel(fz - z - backlash) i, z = m.focus_rel(fz - z - backlash)
m.focus_rel(fz - z) m.focus_rel(fz - z)
return m.data_dict() 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. """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. 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 # Ensure the MJPEG stream has started
self.microscope.camera.start_stream_recording() 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: if initial_move_up:
m.focus_rel(df/2) m.focus_rel(df / 2)
# move down # move down
i, z = m.focus_rel(-df) i, z = m.focus_rel(-df)
# now inspect where the sharpest point is, and estimate the sharpness # now inspect where the sharpest point is, and estimate the sharpness
# (JPEG size) that we should find at the start of the Z stack # (JPEG size) that we should find at the start of the Z stack
jt, jz, js = m.move_data(i) jt, jz, js = m.move_data(i)
best_z = jz[np.argmax(js)] 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 # 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 # We've deliberately undershot - figure out how much further we should move based on the curve
current_js = m.jpeg_size() current_js = m.jpeg_size()
imax = np.argmax(js) # we want to crop out just the bit below the peak imax = np.argmax(js) # we want to crop out just the bit below the peak
js = js[imax:] # NB z is in DECREASING order js = js[imax:] # NB z is in DECREASING order
jz = jz[imax:] 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 # TODO: fancy interpolation stuff
# So, the Z position corresponding to our current sharpness value is zs[inow] # So, the Z position corresponding to our current sharpness value is zs[inow]
# That means we should move forwards, by best_z - zs[inow] # That means we should move forwards, by best_z - zs[inow]
correction_move = best_z + target_z - jz[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) m.focus_rel(correction_move)
return m.data_dict() return m.data_dict()

View file

@ -1,2 +1,2 @@
__all__ = ['Plugin'] __all__ = ["Plugin"]
from .plugin import Plugin from .plugin import Plugin

View file

@ -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 import logging
@ -19,9 +25,7 @@ class Plugin(MicroscopePlugin):
A set of default plugins A set of default plugins
""" """
api_views = { api_views = {"/recalibrate": RecalibrateAPIView}
'/recalibrate': RecalibrateAPIView,
}
def recalibrate(self): def recalibrate(self):
"""Reset the camera's settings. """Reset the camera's settings.
@ -32,8 +36,10 @@ class Plugin(MicroscopePlugin):
""" """
scamera = self.microscope.camera scamera = self.microscope.camera
with scamera.lock: with scamera.lock:
assert not scamera.state['record_active'], "Can't recalibrate while recording!" assert not scamera.state[
streaming = scamera.state['stream_active'] "record_active"
], "Can't recalibrate while recording!"
streaming = scamera.state["stream_active"]
if streaming: if streaming:
logging.info("Stopping stream before recalibration") logging.info("Stopping stream before recalibration")
scamera.stop_stream_recording(resolution=(640, 480)) scamera.stop_stream_recording(resolution=(640, 480))

View file

@ -4,10 +4,11 @@ import time
from picamera import PiCamera from picamera import PiCamera
from picamera.array import PiRGBArray, PiBayerArray from picamera.array import PiRGBArray, PiBayerArray
def rgb_image(camera, resize=None, **kwargs): def rgb_image(camera, resize=None, **kwargs):
"""Capture an image and return an RGB numpy array""" """Capture an image and return an RGB numpy array"""
with PiRGBArray(camera, size=resize) as output: with PiRGBArray(camera, size=resize) as output:
camera.capture(output, format='rgb', resize=resize, **kwargs) camera.capture(output, format="rgb", resize=resize, **kwargs)
return output.array return output.array
@ -19,7 +20,9 @@ def flat_lens_shading_table(camera):
library (with lens shading table support) it will raise an error. library (with lens shading table support) it will raise an error.
""" """
if not hasattr(PiCamera, "lens_shading_table"): 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 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="") print("Adjusting shutter speed to hit setpoint {}".format(setpoint), end="")
for i in range(3): for i in range(3):
print(".", end="") 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) time.sleep(1)
print("done") print("done")
@ -38,7 +43,9 @@ def auto_expose_and_freeze_settings(camera):
print("Allowing the camera to auto-expose") print("Allowing the camera to auto-expose")
camera.awb_mode = "auto" camera.awb_mode = "auto"
camera.exposure_mode = "auto" camera.exposure_mode = "auto"
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): for i in range(6):
print(".", end="") print(".", end="")
time.sleep(0.5) time.sleep(0.5)
@ -53,17 +60,26 @@ def auto_expose_and_freeze_settings(camera):
camera.awb_mode = "off" camera.awb_mode = "off"
camera.awb_gains = g camera.awb_gains = g
print("Auto white balance disabled, gains are {}".format(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) adjust_exposure_to_setpoint(camera, 215)
def channels_from_bayer_array(bayer_array): def channels_from_bayer_array(bayer_array):
"""Given the 'array' from a PiBayerArray, return the 4 channels.""" """Given the 'array' from a PiBayerArray, return the 4 channels."""
bayer_pattern = [(i//2, i % 2) for i in range(4)] bayer_pattern = [(i // 2, i % 2) for i in range(4)]
channels = np.zeros((4, bayer_array.shape[0]//2, bayer_array.shape[1]//2), dtype=bayer_array.dtype) channels = np.zeros(
(4, bayer_array.shape[0] // 2, bayer_array.shape[1] // 2),
dtype=bayer_array.dtype,
)
for i, offset in enumerate(bayer_pattern): for i, offset in enumerate(bayer_pattern):
# We simplify life by dealing with only one channel at a time. # We simplify life by dealing with only one channel at a time.
channels[i, :, :] = np.sum(bayer_array[offset[0]::2, offset[1]::2, :], axis=2) channels[i, :, :] = np.sum(
bayer_array[offset[0] :: 2, offset[1] :: 2, :], axis=2
)
return channels return channels
@ -86,25 +102,27 @@ def lst_from_channels(channels):
# pad the image by copying edge pixels, so that it is exactly 32 times the # 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 # 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 # 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! # less computationally efficient!
padded_image_channel = np.pad(image_channel, padded_image_channel = np.pad(
[(0, lw*32 - iw), (0, lh*32 - ih)], image_channel, [(0, lw * 32 - iw), (0, lh * 32 - ih)], mode="edge"
mode="edge") # Pad image to the right and bottom ) # Pad image to the right and bottom
print("Channel shape: {}x{}, shading table shape: {}x{}, after padding {}".format(iw, print(
ih, "Channel shape: {}x{}, shading table shape: {}x{}, after padding {}".format(
lw*32, iw, ih, lw * 32, lh * 32, padded_image_channel.shape
lh*32, )
padded_image_channel.shape)) )
# Next, fill the shading table (except edge pixels). Please excuse the # Next, fill the shading table (except edge pixels). Please excuse the
# for loop - I know it's not fast but this code needn't be! # for loop - I know it's not fast but this code needn't be!
box = 3 # We average together a square of this side length for each pixel. box = 3 # We average together a square of this side length for each pixel.
# NB this isn't quite what 6by9's program does - it averages 3 pixels # NB this isn't quite what 6by9's program does - it averages 3 pixels
# horizontally, but not vertically. # horizontally, but not vertically.
for dx in np.arange(box) - box//2: for dx in np.arange(box) - box // 2:
for dy in np.arange(box) - box//2: for dy in np.arange(box) - box // 2:
ls_channel[:, :] += padded_image_channel[16+dx::32, 16+dy::32] - 64 ls_channel[:, :] += (
ls_channel /= box**2 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. # The original C code written by 6by9 normalises to the central 64 pixels in each channel.
# ls_channel /= np.mean(image_channel[iw//2-4:iw//2+4, ih//2-4:ih//2+4]) # ls_channel /= np.mean(image_channel[iw//2-4:iw//2+4, ih//2-4:ih//2+4])
# I have had better results just normalising to the maximum: # I have had better results just normalising to the maximum:
@ -114,10 +132,10 @@ def lst_from_channels(channels):
# For most sensible lenses I'd expect that 1.0 is the maximum value. # 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 # NB ls_channel should be a "view" of the whole lens shading array, so we don't
# need to update the big array here. # 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. # lens shading - that's 1/lens_shading_table_float as we currently have it.
gains = 32.0/lens_shading # 32 is unity gain gains = 32.0 / lens_shading # 32 is unity gain
gains[gains > 255] = 255 # clip at 255, maximum gain is 255/32 gains[gains > 255] = 255 # clip at 255, maximum gain is 255/32
gains[gains < 32] = 32 # clip at 32, minimum gain is 1 (is this necessary?) gains[gains < 32] = 32 # clip at 32, minimum gain is 1 (is this necessary?)
lens_shading_table = gains.astype(np.uint8) lens_shading_table = gains.astype(np.uint8)
@ -145,7 +163,7 @@ def recalibrate_camera(camera):
# raw_image is a 3D array, with full resolution and 3 colour channels. No # raw_image is a 3D array, with full resolution and 3 colour channels. No
# de-mosaicing has been done, so 2/3 of the values are zero (3/4 for R and B # de-mosaicing has been done, so 2/3 of the values are zero (3/4 for R and B
# channels, 1/2 for green because there's twice as many green pixels). # channels, 1/2 for green because there's twice as many green pixels).
channels = channels_from_bayer_array(raw_image) channels = channels_from_bayer_array(raw_image)
lens_shading_table = lst_from_channels(channels) lens_shading_table = lst_from_channels(channels)
camera.lens_shading_table = lens_shading_table camera.lens_shading_table = lens_shading_table
@ -154,8 +172,10 @@ def recalibrate_camera(camera):
# Fix the AWB gains so the image is neutral # Fix the AWB gains so the image is neutral
channel_means = np.mean(np.mean(rgb_image(camera), axis=0, dtype=np.float), axis=0) channel_means = np.mean(np.mean(rgb_image(camera), axis=0, dtype=np.float), axis=0)
old_gains = camera.awb_gains old_gains = camera.awb_gains
camera.awb_gains = (channel_means[1]/channel_means[0] * old_gains[0], camera.awb_gains = (
channel_means[1]/channel_means[2]*old_gains[1]) channel_means[1] / channel_means[0] * old_gains[0],
channel_means[1] / channel_means[2] * old_gains[1],
)
time.sleep(1) time.sleep(1)
# Ensure the background is bright but not saturated # Ensure the background is bright but not saturated
adjust_exposure_to_setpoint(camera, 230) adjust_exposure_to_setpoint(camera, 230)

View file

@ -1,2 +1,2 @@
__all__ = ['ScanPlugin'] __all__ = ["ScanPlugin"]
from .plugin import ScanPlugin from .plugin import ScanPlugin

View file

@ -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 import logging
class TileScanAPI(MicroscopeViewPlugin): class TileScanAPI(MicroscopeViewPlugin):
def post(self): def post(self):
payload = JsonResponse(request) payload = JsonResponse(request)
# Get params # Get params
filename = payload.param('filename') filename = payload.param("filename")
temporary = payload.param('temporary', default=False, convert=bool) 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] 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] grid = [int(i) for i in grid]
style = payload.param('style', default='raster', convert=str) style = payload.param("style", default="raster", convert=str)
autofocus_dz = payload.param('autofocus_dz', default=50, convert=int) autofocus_dz = payload.param("autofocus_dz", default=50, convert=int)
fast_autofocus = payload.param('fast_autofocus', default=False, convert=bool) fast_autofocus = payload.param("fast_autofocus", default=False, convert=bool)
use_video_port = payload.param('use_video_port', default=True, convert=bool) use_video_port = payload.param("use_video_port", default=True, convert=bool)
resize = payload.param('size', default=None) resize = payload.param("size", default=None)
if resize: if resize:
if ('width' in resize) and ('height' in resize): if ("width" in resize) and ("height" in resize):
resize = (int(resize['width']), int(resize['height'])) # Convert dict to tuple resize = (
int(resize["width"]),
int(resize["height"]),
) # Convert dict to tuple
else: else:
abort(404) abort(404)
bayer = payload.param('bayer', default=False, convert=bool) bayer = payload.param("bayer", default=False, convert=bool)
metadata = payload.param('metadata', default={}, convert=dict) metadata = payload.param("metadata", default={}, convert=dict)
tags = payload.param('tags', default=[], convert=list) tags = payload.param("tags", default=[], convert=list)
logging.info("Running tile scan...") logging.info("Running tile scan...")
task = self.microscope.task.start( task = self.microscope.task.start(
@ -46,7 +56,7 @@ class TileScanAPI(MicroscopeViewPlugin):
bayer=bayer, bayer=bayer,
fast_autofocus=fast_autofocus, fast_autofocus=fast_autofocus,
metadata=metadata, metadata=metadata,
tags=tags tags=tags,
) )
# return a handle on the autofocus task # return a handle on the autofocus task

View file

@ -11,29 +11,31 @@ from openflexure_microscope.devel import MicroscopePlugin
from .api import TileScanAPI 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, Given an initial position, step sizes, and number of steps,
construct a 2-dimensional list of scan x-y positions. construct a 2-dimensional list of scan x-y positions.
""" """
arr = [] arr = []
for i in range(n_steps[0]): # x axis for i in range(n_steps[0]): # x axis
arr.append([]) arr.append([])
for j in range(n_steps[1]): # y axis for j in range(n_steps[1]): # y axis
# Create a coordinate array # 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 # Append coordinate array to position grid
arr[i].append(tuple(coord)) arr[i].append(tuple(coord))
# Style modifiers # Style modifiers
if style == 'snake': if style == "snake":
for i, line in enumerate(arr): for i, line in enumerate(arr):
if i % 2 != 0: if i % 2 != 0:
line.reverse() line.reverse()
return arr return arr
def flatten_grid(grid): def flatten_grid(grid):
""" """
Convert a 3D list of scan positions into a flat list Convert a 3D list of scan positions into a flat list
@ -43,24 +45,25 @@ def flatten_grid(grid):
grid = list(itertools.chain(*grid)) grid = list(itertools.chain(*grid))
return grid return grid
class ScanPlugin(MicroscopePlugin): class ScanPlugin(MicroscopePlugin):
""" """
Stack and tile plugin Stack and tile plugin
""" """
api_views = { api_views = {"/tile": TileScanAPI}
'/tile': TileScanAPI,
}
def capture(self, def capture(
basename, self,
scan_id, basename,
temporary: bool = False, scan_id,
use_video_port: bool = False, temporary: bool = False,
resize: Tuple[int, int] = None, use_video_port: bool = False,
bayer: bool = False, resize: Tuple[int, int] = None,
metadata: dict = {}, bayer: bool = False,
tags: list = []): metadata: dict = {},
tags: list = [],
):
# Construct a tile filename # Construct a tile filename
filename = "{}_{}_{}_{}".format(basename, *self.microscope.stage.position) filename = "{}_{}_{}_{}".format(basename, *self.microscope.stage.position)
@ -68,47 +71,46 @@ class ScanPlugin(MicroscopePlugin):
# Create output object # Create output object
output = self.microscope.camera.new_image( output = self.microscope.camera.new_image(
write_to_file=True, write_to_file=True, temporary=temporary, filename=filename, folder=folder
temporary=temporary, )
filename=filename,
folder=folder)
# Capture # Capture
self.microscope.camera.capture( self.microscope.camera.capture(
output, output, use_video_port=use_video_port, resize=resize, bayer=bayer
use_video_port=use_video_port, )
resize=resize,
bayer=bayer)
# Affix metadata # Affix metadata
if 'scan' not in tags: if "scan" not in tags:
tags.append('scan') tags.append("scan")
metadata.update({ metadata.update(
'position': self.microscope.state['stage']['position'], {
'scan_id': scan_id, "position": self.microscope.state["stage"]["position"],
'basename': basename, "scan_id": scan_id,
'microscope_id': self.microscope.id, "basename": basename,
'microscope_name': self.microscope.name "microscope_id": self.microscope.id,
}) "microscope_name": self.microscope.name,
}
)
output.put_metadata(metadata) output.put_metadata(metadata)
output.put_tags(tags) output.put_tags(tags)
def tile( def tile(
self, self,
basename: str = None, basename: str = None,
temporary: bool = False, temporary: bool = False,
step_size: int = [2000, 1500, 100], step_size: int = [2000, 1500, 100],
grid: list = [3, 3, 5], grid: list = [3, 3, 5],
style='raster', style="raster",
autofocus_dz: int = 50, autofocus_dz: int = 50,
use_video_port: bool = False, use_video_port: bool = False,
resize: Tuple[int, int] = None, resize: Tuple[int, int] = None,
bayer: bool = False, bayer: bool = False,
fast_autofocus = False, fast_autofocus=False,
metadata: dict = {}, metadata: dict = {},
tags: list = []): tags: list = [],
):
# Generate a basename if none given # Generate a basename if none given
if not basename: if not basename:
@ -121,42 +123,47 @@ class ScanPlugin(MicroscopePlugin):
initial_position = self.microscope.stage.position initial_position = self.microscope.stage.position
# Add scan metadata # Add scan metadata
if not 'time' in metadata: if not "time" in metadata:
metadata['time'] = generate_basename() metadata["time"] = generate_basename()
# Check if autofocus is enabled # 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 autofocus_enabled = True
else: else:
autofocus_enabled = False autofocus_enabled = False
if fast_autofocus and not hasattr(self.microscope.plugin.default_autofocus, 'monitor_sharpness'): if fast_autofocus and not hasattr(
logging.error("Can't use fast autofocus in the scan - the default plugin doesn't support monitor_sharpness; maybe it is too old?") 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 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) # Construct an x-y grid (worry about z later)
x_y_grid = construct_grid( x_y_grid = construct_grid(
initial_position, initial_position, step_size[:2], grid[:2], style=style
step_size[:2],
grid[:2],
style=style
) )
# Keep the initial Z position the same as our current position # Keep the initial Z position the same as our current position
next_z = initial_position[2] next_z = initial_position[2]
if fast_autofocus: # If fast autofocus is enabled, make if fast_autofocus: # If fast autofocus is enabled, make
next_z += autofocus_dz/2 # sure we start from the top of the range 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 initial_z = next_z # Save this value for use in raster scans
# Now step through each point in the x-y coordinate array # Now step through each point in the x-y coordinate array
for line in x_y_grid: for line in x_y_grid:
# If rastering, rather than snake (or eventually spiral) # If rastering, rather than snake (or eventually spiral)
# Return focus to initial position # Return focus to initial position
if style == 'raster': if style == "raster":
next_z = initial_z next_z = initial_z
logging.debug("Returning to initial z position") 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: for x_y in line:
# Move to new grid position without changing z # Move to new grid position without changing z
@ -166,20 +173,21 @@ class ScanPlugin(MicroscopePlugin):
if autofocus_enabled: if autofocus_enabled:
if fast_autofocus: if fast_autofocus:
self.microscope.plugin.default_autofocus.fast_up_down_up_autofocus( self.microscope.plugin.default_autofocus.fast_up_down_up_autofocus(
dz=autofocus_dz, dz=autofocus_dz,
target_z=-z_stack_dz/2.0, # Finish below the focus target_z=-z_stack_dz / 2.0, # Finish below the focus
initial_move_up=False, # We're already at the top of the scan 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? # TODO: save the focus data for future reference? Use it for diagnostics?
else: else:
logging.debug("Running autofocus") logging.debug("Running autofocus")
self.microscope.plugin.default_autofocus.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") logging.debug("Finished autofocus")
time.sleep(1) # TODO: Remove time.sleep(1) # TODO: Remove
# If we're not doing a z-stack, just capture # If we're not doing a z-stack, just capture
if (grid[2] <= 1): if grid[2] <= 1:
self.capture( self.capture(
basename, basename,
scan_id, scan_id,
@ -188,7 +196,7 @@ class ScanPlugin(MicroscopePlugin):
resize=resize, resize=resize,
bayer=bayer, bayer=bayer,
metadata=metadata, metadata=metadata,
tags=tags tags=tags,
) )
else: else:
logging.debug("Entering z-stack") logging.debug("Entering z-stack")
@ -198,38 +206,43 @@ class ScanPlugin(MicroscopePlugin):
scan_id=scan_id, scan_id=scan_id,
step_size=step_size[2], step_size=step_size[2],
steps=grid[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, return_to_start=not fast_autofocus,
use_video_port=use_video_port, use_video_port=use_video_port,
resize=resize, resize=resize,
bayer=bayer, bayer=bayer,
metadata=metadata, metadata=metadata,
tags=tags tags=tags,
) )
# Make sure we use our current best estimate of focus (i.e. the current position) next point # Make sure we use our current best estimate of focus (i.e. the current position) next point
next_z = self.microscope.stage.position[2] next_z = self.microscope.stage.position[2]
if fast_autofocus: 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: 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)) logging.debug("Returning to {}".format(initial_position))
self.microscope.stage.move_abs(initial_position) self.microscope.stage.move_abs(initial_position)
def stack( def stack(
self, self,
basename: str = None, basename: str = None,
temporary: bool = False, temporary: bool = False,
scan_id: str = None, scan_id: str = None,
step_size: int = 100, step_size: int = 100,
steps: int = 5, steps: int = 5,
center: bool = True, center: bool = True,
return_to_start: bool = True, return_to_start: bool = True,
use_video_port: bool = False, use_video_port: bool = False,
resize: Tuple[int, int] = None, resize: Tuple[int, int] = None,
bayer: bool = False, bayer: bool = False,
metadata: dict = {}, metadata: dict = {},
tags: list = []): tags: list = [],
):
# Generate a basename if none given # Generate a basename if none given
if not basename: if not basename:
@ -240,13 +253,12 @@ class ScanPlugin(MicroscopePlugin):
scan_id = uuid.uuid4().hex scan_id = uuid.uuid4().hex
# Add scan metadata # Add scan metadata
if not 'time' in metadata: if not "time" in metadata:
metadata['time'] = generate_basename() metadata["time"] = generate_basename()
# Store initial position # Store initial position
initial_position = self.microscope.stage.position initial_position = self.microscope.stage.position
with self.microscope.lock: with self.microscope.lock:
# Move to center scan # Move to center scan
if center: if center:
@ -264,7 +276,7 @@ class ScanPlugin(MicroscopePlugin):
resize=resize, resize=resize,
bayer=bayer, bayer=bayer,
metadata=metadata, metadata=metadata,
tags=tags tags=tags,
) )
if i != steps - 1: if i != steps - 1:

View file

@ -1,4 +1,4 @@
__all__ = ['Plugin', 'HelloWorldAPI', 'IdentifyAPI'] __all__ = ["Plugin", "HelloWorldAPI", "IdentifyAPI"]
from .plugin import Plugin from .plugin import Plugin
from .api import HelloWorldAPI, IdentifyAPI from .api import HelloWorldAPI, IdentifyAPI

View file

@ -9,11 +9,14 @@ class IdentifyAPI(MicroscopeViewPlugin):
""" """
A simple example API plugin, attached through the main microscope plugin. A simple example API plugin, attached through the main microscope plugin.
""" """
def get(self): def get(self):
""" """
Method to call when an HTTP GET request is made. 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)) return Response(escape(data))
@ -28,7 +31,7 @@ class HelloWorldAPI(MicroscopeViewPlugin):
""" """
# If the microscope does not already contain our plugin_string attribute # 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 # Make a string, using the MicroscopeViewPlugin.plugin shortcut
self.microscope.plugin_string = self.plugin.hello_world() self.microscope.plugin_string = self.plugin.hello_world()
@ -43,7 +46,7 @@ class HelloWorldAPI(MicroscopeViewPlugin):
payload = JsonResponse(request) 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. # 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 if new_plugin_string: # If not None or empty
# Set microscope attribute to the specified string # 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. An example API plugin that uses a long-running plugin method.
""" """
def post(self): def post(self):
""" """
Method to call when an HTTP POST request is made. Method to call when an HTTP POST request is made.
@ -64,7 +68,7 @@ class LongRunningAPI(MicroscopeViewPlugin):
payload = JsonResponse(request) payload = JsonResponse(request)
# Extract a values from the JSON payload. # 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 # Attach the long-running method as a microscope task
try: try:
@ -79,6 +83,7 @@ class SomeExceptionAPI(MicroscopeViewPlugin):
""" """
An example API plugin that uses a long-running but broken plugin method. An example API plugin that uses a long-running but broken plugin method.
""" """
def post(self): def post(self):
""" """
Method to call when an HTTP POST request is made. Method to call when an HTTP POST request is made.

View file

@ -11,10 +11,10 @@ class Plugin(MicroscopePlugin):
""" """
api_views = { api_views = {
'/identify': IdentifyAPI, "/identify": IdentifyAPI,
'/hello': HelloWorldAPI, "/hello": HelloWorldAPI,
'/long_running': LongRunningAPI, "/long_running": LongRunningAPI,
'/some_exception': SomeExceptionAPI, "/some_exception": SomeExceptionAPI,
} }
def identify(self): def identify(self):
@ -22,7 +22,9 @@ class Plugin(MicroscopePlugin):
Demonstrate access to Microscope.camera, and Microscope.stage 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) print(response)
return response return response
@ -39,7 +41,7 @@ class Plugin(MicroscopePlugin):
""" """
with self.microscope.lock: with self.microscope.lock:
time.sleep(3) time.sleep(3)
result = 15./0 result = 15.0 / 0
return result return result

View file

@ -5,14 +5,14 @@ import logging
class ConColors: class ConColors:
HEADER = '\033[95m' HEADER = "\033[95m"
OKBLUE = '\033[94m' OKBLUE = "\033[94m"
OKGREEN = '\033[92m' OKGREEN = "\033[92m"
WARNING = '\033[93m' WARNING = "\033[93m"
FAIL = '\033[91m' FAIL = "\033[91m"
ENDC = '\033[0m' ENDC = "\033[0m"
BOLD = '\033[1m' BOLD = "\033[1m"
UNDERLINE = '\033[4m' UNDERLINE = "\033[4m"
def module_from_file(plugin_path): def module_from_file(plugin_path):
@ -23,7 +23,11 @@ def module_from_file(plugin_path):
# Check if the path is to a file # Check if the path is to a file
if not os.path.isfile(plugin_path): if not os.path.isfile(plugin_path):
logging.warning(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 return None, None, None
else: else:
@ -35,6 +39,7 @@ def module_from_file(plugin_path):
return plugin_spec, plugin_module, plugin_name return plugin_spec, plugin_module, plugin_name
def check_module(module_path): def check_module(module_path):
""" """
Used to check if a module exists, without ever importing it. 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): def name_from_module(plugin_path):
path_array = plugin_path.split('.') path_array = plugin_path.split(".")
# Truncate namespace of default plugins # 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:] path_array = path_array[2:]
return '/'.join(path_array) return "/".join(path_array)
def load_plugin_module(plugin_path): def load_plugin_module(plugin_path):
@ -104,7 +109,13 @@ def load_plugin_class(plugin_path, plugin_class_name):
try: try:
plugin_class = getattr(plugin_module, plugin_class_name) plugin_class = getattr(plugin_module, plugin_class_name)
except AttributeError: except AttributeError:
logging.warning(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 return None, None
else: else:
return plugin_class, plugin_name return plugin_class, plugin_name
@ -113,10 +124,14 @@ def load_plugin_class(plugin_path, plugin_class_name):
def class_from_map(plugin_map): def class_from_map(plugin_map):
plugin_arr = plugin_map.split(':') plugin_arr = plugin_map.split(":")
if not len(plugin_arr) == 2: if not len(plugin_arr) == 2:
logging.warning(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 return None, None
else: else:
return load_plugin_class(*plugin_arr) return load_plugin_class(*plugin_arr)
@ -129,6 +144,7 @@ class PluginMount(object):
Args: Args:
parent (:py:class:`openflexure_microscope.microscope.Microscope`): The parent Microscope object to attach to. parent (:py:class:`openflexure_microscope.microscope.Microscope`): The parent Microscope object to attach to.
""" """
def __init__(self, parent): def __init__(self, parent):
self.parent = parent self.parent = parent
self.plugins = [] # List of plugin objects self.plugins = [] # List of plugin objects
@ -141,13 +157,17 @@ class PluginMount(object):
@property @property
def members(self): def members(self):
ignores = ['state', 'members', 'attach'] ignores = ["state", "members", "attach"]
plugin_array = [] plugin_array = []
for obj_name in dir(self): 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) obj = getattr(self, obj_name)
if isinstance(obj, MicroscopePlugin): 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_info = (obj_name, plugin_members)
plugin_array.append(plugin_info) plugin_array.append(plugin_info)
return plugin_array return plugin_array
@ -168,10 +188,20 @@ class PluginMount(object):
if plugin_class and plugin_name: if plugin_class and plugin_name:
plugin_object = plugin_class() plugin_object = plugin_class()
if hasattr(self, plugin_name): # If a plugin with the same name is already attached. if hasattr(
logging.warning(ConColors.WARNING + "A plugin named {} has already been loaded. Skipping {}.".format(plugin_name, plugin_map) + ConColors.ENDC) 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 # Attach plugin_object to the plugin mount
setattr(self, pythonsafe_plugin_name, plugin_object) setattr(self, pythonsafe_plugin_name, plugin_object)
self.plugins.append((plugin_name, plugin_object)) self.plugins.append((plugin_name, plugin_object))
@ -179,11 +209,16 @@ class PluginMount(object):
# Grant plugin access to the hardware # Grant plugin access to the hardware
plugin_object.microscope = self.parent plugin_object.microscope = self.parent
logging.info(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: else:
logging.error(f"Error loading plugin {plugin_map}. Moving on.") logging.error(f"Error loading plugin {plugin_map}. Moving on.")
class MicroscopePlugin: class MicroscopePlugin:
""" """
Parent class for all microscope plugins. Parent class for all microscope plugins.
@ -195,4 +230,6 @@ class MicroscopePlugin:
api_views = {} # Initially empty dictionary of API views associated with the plugin api_views = {} # Initially empty dictionary of API views associated with the plugin
def __init__(self): 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

View file

@ -1,2 +1,2 @@
from .plugin import ExamplePlugin from .plugin import ExamplePlugin
from . import api from . import api

View file

@ -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 import logging
class DoAPI(MicroscopeViewPlugin): class DoAPI(MicroscopeViewPlugin):
""" """
A simple example API plugin A simple example API plugin
@ -15,27 +24,21 @@ class DoAPI(MicroscopeViewPlugin):
payload = JsonResponse(request) payload = JsonResponse(request)
# Extract a values from the JSON payload. # Extract a values from the JSON payload.
val_int = payload.param('val_int', default=None, convert=int) val_int = payload.param("val_int", default=None, convert=int)
val_str = payload.param('val_str', default=None, convert=str) val_str = payload.param("val_str", default=None, convert=str)
val_radio = payload.param('val_radio', default=None) val_radio = payload.param("val_radio", default=None)
val_check = payload.param('val_check', default=None) val_check = payload.param("val_check", default=None)
val_select = payload.param('val_select', default=None) val_select = payload.param("val_select", default=None)
val_disposable = payload.param('val_disposable', default=None) val_disposable = payload.param("val_disposable", default=None)
self.plugin.set_values( self.plugin.set_values(
val_int, val_int, val_str, val_radio, val_check, val_select, val_disposable
val_str,
val_radio,
val_check,
val_select,
val_disposable
) )
print(self.plugin.get_values_dict()) print(self.plugin.get_values_dict())
return jsonify({ return jsonify({"response": "completed"})
'response': 'completed'
})
class TaskAPI(MicroscopeViewPlugin): class TaskAPI(MicroscopeViewPlugin):
""" """
@ -43,19 +46,19 @@ class TaskAPI(MicroscopeViewPlugin):
""" """
def get(self): def get(self):
return jsonify({ return jsonify({"run_time": self.plugin.run_time})
'run_time': self.plugin.run_time
})
def post(self): def post(self):
# Get payload JSON # Get payload JSON
payload = JsonResponse(request) payload = JsonResponse(request)
# Extract a values from the JSON payload. # 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...") 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 a handle on the autofocus task
return jsonify(task.state), 202 return jsonify(task.state), 202

View file

@ -9,19 +9,18 @@ from .api import DoAPI, TaskAPI
HERE = os.path.dirname(os.path.realpath(__file__)) HERE = os.path.dirname(os.path.realpath(__file__))
FORM_PATH = os.path.join(HERE, "forms.json") FORM_PATH = os.path.join(HERE, "forms.json")
class ExamplePlugin(MicroscopePlugin): class ExamplePlugin(MicroscopePlugin):
""" """
An example plugin using a comprehensive form An example plugin using a comprehensive form
""" """
global FORM_PATH global FORM_PATH
with open(FORM_PATH, 'r') as sc: with open(FORM_PATH, "r") as sc:
api_form = json.load(sc) api_form = json.load(sc)
api_views = { api_views = {"/do": DoAPI, "/task": TaskAPI}
'/do': DoAPI,
'/task': TaskAPI
}
def __init__(self): def __init__(self):
self.val_int = 10 self.val_int = 10
@ -33,7 +32,9 @@ class ExamplePlugin(MicroscopePlugin):
self.run_time = 5 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 Demonstrate a plugin with form
""" """
@ -54,12 +55,12 @@ class ExamplePlugin(MicroscopePlugin):
def get_values_dict(self): def get_values_dict(self):
return { return {
'val_int': self.val_int, "val_int": self.val_int,
'val_str': self.val_str, "val_str": self.val_str,
'val_radio': self.val_radio, "val_radio": self.val_radio,
'val_check': self.val_check, "val_check": self.val_check,
'val_select': self.val_select, "val_select": self.val_select,
'val_unused': self.val_unused "val_unused": self.val_unused,
} }
def generate_random_numbers_for_a_while(self, run_time: int): def generate_random_numbers_for_a_while(self, run_time: int):
@ -68,5 +69,5 @@ class ExamplePlugin(MicroscopePlugin):
for _ in range(run_time): for _ in range(run_time):
vals.append(random.random()) vals.append(random.random())
time.sleep(1) time.sleep(1)
return vals return vals

View file

@ -1,3 +1,3 @@
__all__ = ['base', 'mock', 'sanga'] __all__ = ["base", "mock", "sanga"]
from . import base, mock, sanga from . import base, mock, sanga

View file

@ -8,6 +8,7 @@ class BaseStage(metaclass=ABCMeta):
lock (:py:class:`openflexure_microscope.lock.StrictLock`): Strict lock controlling thread lock (:py:class:`openflexure_microscope.lock.StrictLock`): Strict lock controlling thread
access to camera hardware access to camera hardware
""" """
def __init__(self): def __init__(self):
self.lock = StrictLock(timeout=5) self.lock = StrictLock(timeout=5)

View file

@ -4,6 +4,7 @@ from openflexure_microscope.utilities import axes_to_array
from collections.abc import Iterable from collections.abc import Iterable
import numpy as np import numpy as np
class MockStage(BaseStage): class MockStage(BaseStage):
def __init__(self, port=None, **kwargs): def __init__(self, port=None, **kwargs):
BaseStage.__init__(self) BaseStage.__init__(self)
@ -15,13 +16,13 @@ class MockStage(BaseStage):
def state(self): def state(self):
"""The general state dictionary of the board.""" """The general state dictionary of the board."""
state = { state = {
'position': { "position": {
'x': self.position[0], "x": self.position[0],
'y': self.position[1], "y": self.position[1],
'z': self.position[2], "z": self.position[2],
}, },
'board': None, "board": None,
'version': '0' "version": "0",
} }
return state return state
@ -29,21 +30,15 @@ class MockStage(BaseStage):
"""Update settings from a config dictionary""" """Update settings from a config dictionary"""
# Set backlash. Expects a dictionary with axis labels # Set backlash. Expects a dictionary with axis labels
if 'backlash' in config: if "backlash" in config:
# Construct backlash array # 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 self.backlash = backlash
def read_config(self) -> dict: def read_config(self) -> dict:
"""Return the current settings as a dictionary""" """Return the current settings as a dictionary"""
blsh = self.backlash.tolist() blsh = self.backlash.tolist()
config = { config = {"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]}}
'backlash': {
'x': blsh[0],
'y': blsh[1],
'z': blsh[2],
}
}
return config return config
@property @property
@ -69,7 +64,7 @@ class MockStage(BaseStage):
assert len(blsh) == self.n_axes assert len(blsh) == self.n_axes
self._backlash = np.array(blsh) self._backlash = np.array(blsh)
else: 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): def move_rel(self, displacement, axis=None, backlash=True):
pass pass

View file

@ -18,26 +18,29 @@ class SangaStage(BaseStage):
board (:py:class:`openflexure_microscope.stage.sangaboard.Sangaboard`): Parent Sangaboard object. board (:py:class:`openflexure_microscope.stage.sangaboard.Sangaboard`): Parent Sangaboard object.
_backlash (list): 3-element (element-per-axis) list of backlash compensation in steps. _backlash (list): 3-element (element-per-axis) list of backlash compensation in steps.
""" """
def __init__(self, port=None, **kwargs): def __init__(self, port=None, **kwargs):
"""Class managing serial communications with the motors for an Openflexure stage""" """Class managing serial communications with the motors for an Openflexure stage"""
BaseStage.__init__(self) BaseStage.__init__(self)
self.board = Sangaboard(port, **kwargs) self.board = Sangaboard(port, **kwargs)
self._backlash = None # Initialise backlash storage, used by property setter/getter self._backlash = (
self.axis_names = ['x', 'y', 'z'] # Assume all sangaboards are 3 axis None
) # Initialise backlash storage, used by property setter/getter
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
@property @property
def state(self): def state(self):
"""The general state dictionary of the board.""" """The general state dictionary of the board."""
state = { state = {
'position': { "position": {
'x': self.position[0], "x": self.position[0],
'y': self.position[1], "y": self.position[1],
'z': self.position[2], "z": self.position[2],
}, },
'board': self.board.board, "board": self.board.board,
'firmware': self.board.firmware "firmware": self.board.firmware,
} }
return state return state
@ -80,27 +83,21 @@ class SangaStage(BaseStage):
assert len(blsh) == self.n_axes assert len(blsh) == self.n_axes
self._backlash = np.array(blsh) self._backlash = np.array(blsh)
else: 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): def apply_config(self, config: dict):
"""Update settings from a config dictionary""" """Update settings from a config dictionary"""
# Set backlash. Expects a dictionary with axis labels # Set backlash. Expects a dictionary with axis labels
if 'backlash' in config: if "backlash" in config:
# Construct backlash array # 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 self.backlash = backlash
def read_config(self) -> dict: def read_config(self) -> dict:
"""Return the current settings as a dictionary""" """Return the current settings as a dictionary"""
blsh = self.backlash.tolist() blsh = self.backlash.tolist()
config = { config = {"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]}}
'backlash': {
'x': blsh[0],
'y': blsh[1],
'z': blsh[2],
}
}
return config return config
@ -117,7 +114,9 @@ class SangaStage(BaseStage):
if axis is not None: if axis is not None:
# backlash correction is easier if we're always in 3D # backlash correction is easier if we're always in 3D
# so this code just converts single-axis moves into all-axis moves. # 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.zeros(self.n_axes, dtype=np.int)
move[np.argmax(np.array(self.axis_names) == axis)] = int(displacement) move[np.argmax(np.array(self.axis_names) == axis)] = int(displacement)
displacement = move displacement = move
@ -134,7 +133,7 @@ class SangaStage(BaseStage):
initial_move -= np.where( initial_move -= np.where(
self.backlash * displacement < 0, self.backlash * displacement < 0,
self.backlash, 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) self.board.move_rel(initial_move)
if np.any(displacement - initial_move != 0): if np.any(displacement - initial_move != 0):
@ -160,7 +159,9 @@ class SangaStage(BaseStage):
""" """
starting_position = self.position starting_position = self.position
rel_positions = np.array(rel_positions) 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: try:
self.move_rel(rel_positions[0], backlash=backlash) self.move_rel(rel_positions[0], backlash=backlash)
yield 0 yield 0
@ -186,7 +187,7 @@ class SangaStage(BaseStage):
def close(self): def close(self):
"""Cleanly close communication with the stage""" """Cleanly close communication with the stage"""
if hasattr(self, 'board'): if hasattr(self, "board"):
self.board.close() self.board.close()
# Methods specific to Sangaboard # Methods specific to Sangaboard
@ -205,12 +206,16 @@ class SangaStage(BaseStage):
need to worry about that here. need to worry about that here.
""" """
if type is not None: if type is not None:
print("An exception occurred inside a with block, resetting position \ print(
to its value at the start of the with block") "An exception occurred inside a with block, resetting position \
to its value at the start of the with block"
)
try: try:
time.sleep(0.5) time.sleep(0.5)
self.move_abs(self._position_on_enter) self.move_abs(self._position_on_enter)
except Exception as e: 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...") print("Move completed, raising exception...")
raise value # Propagate the exception raise value # Propagate the exception

View file

@ -29,6 +29,7 @@ import time
import logging import logging
import warnings import warnings
class ExtensibleSerialInstrument(object): class ExtensibleSerialInstrument(object):
""" """
An instrument that communicates by sending strings back and forth over serial 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 blocked for a long time. The lock is reentrant so there's no issue with
acquiring it twice. 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 ignore_echo = False
port_settings = {} port_settings = {}
@ -61,7 +67,7 @@ class ExtensibleSerialInstrument(object):
logging.info("Updating ESI port settings") logging.info("Updating ESI port settings")
self.port_settings.update(kwargs) self.port_settings.update(kwargs)
logging.info("Opening ESI connection to port {}".format(port)) 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)) logging.info("Opened ESI connection to port {}".format(port))
def open(self, port=None, quiet=True): 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. then we don't warn when ports are opened multiple times.
""" """
with self.communications_lock: with self.communications_lock:
if hasattr(self,'_ser') and self._ser.isOpen(): if hasattr(self, "_ser") and self._ser.isOpen():
if not quiet: logging.warning("Attempted to open an already-open port!") if not quiet:
logging.warning("Attempted to open an already-open port!")
return return
if port is None: if port is None:
port=self.find_port() 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?" assert (
self._ser = serial.Serial(port,**self.port_settings) port is not None
#the block above wraps the serial IO layer with a text IO layer ), "We don't have a serial port to open, meaning you didn't specify a valid port. Are you sure the instrument is connected?"
#this allows us to read/write in neat lines. NB the buffer size must self._ser = serial.Serial(port, **self.port_settings)
#be set to 1 byte for maximum responsiveness. # the block above wraps the serial IO layer with a text IO layer
assert self.test_communications(), "The instrument doesn't seem to be responding. Did you specify the right port?" # 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): def close(self):
"""Cleanly close the device. Includes proper logging statements.""" """Cleanly close the device. Includes proper logging statements."""
@ -95,11 +106,12 @@ class ExtensibleSerialInstrument(object):
def __del__(self): def __del__(self):
"""Emergency close the device. Try to avoid having to use this.""" """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: with self.communications_lock:
print( print(
"Closing an open serial communication has been triggered by garbage collection!\n"\ "Closing an open serial communication has been triggered by garbage collection!\n"
"Please close this device more sensibly in future.") "Please close this device more sensibly in future."
)
try: try:
self._ser.close() self._ser.close()
except Exception as e: except Exception as e:
@ -111,30 +123,39 @@ class ExtensibleSerialInstrument(object):
def __exit__(self, type, value, traceback): def __exit__(self, type, value, traceback):
"""Cleanly close down the instrument at end of a with block.""" """Cleanly close down the instrument at end of a with block."""
self.close() self.close()
def write(self,query_string): def write(self, query_string):
"""Write a string to the serial port""" """Write a string to the serial port"""
with self.communications_lock: 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?" assert (
#TODO: Check if this code is needed and if not kill it self._ser.isOpen()
# try: ), "Attempted to write to the serial port before it was opened. Perhaps you need to call the 'open' method first?"
# if self._ser.outWaiting()>0: self._ser.flushOutput() #ensure there's nothing waiting # TODO: Check if this code is needed and if not kill it
# except AttributeError: # try:
# if self._ser.out_waiting>0: self._ser.flushOutput() #ensure there's nothing waiting # if self._ser.outWaiting()>0: self._ser.flushOutput() #ensure there's nothing waiting
data=query_string+self.termination_character # except AttributeError:
data=data.encode() # 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) self._ser.write(data)
def flush_input_buffer(self): def flush_input_buffer(self):
"""Make sure there's nothing waiting to be read, and clear the buffer if there is.""" """Make sure there's nothing waiting to be read, and clear the buffer if there is."""
with self.communications_lock: 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): def readline(self, timeout=None):
"""Read one line from the serial port.""" """Read one line from the serial port."""
with self.communications_lock: 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 _communications_lock = None
@property @property
def communications_lock(self): def communications_lock(self):
"""A lock object used to protect access to the communications bus""" """A lock object used to protect access to the communications bus"""
@ -143,7 +164,7 @@ class ExtensibleSerialInstrument(object):
if self._communications_lock is None: if self._communications_lock is None:
self._communications_lock = threading.RLock() self._communications_lock = threading.RLock()
return self._communications_lock return self._communications_lock
def read_multiline(self, termination_line=None, timeout=None): def read_multiline(self, termination_line=None, timeout=None):
"""Read one line from the underlying bus. Must be overriden. """Read one line from the underlying bus. Must be overriden.
@ -152,15 +173,19 @@ class ExtensibleSerialInstrument(object):
with self.communications_lock: with self.communications_lock:
if termination_line is None: if termination_line is None:
termination_line = self.termination_line 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 = "" response = ""
last_line = "dummy" 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) last_line = self.readline(timeout)
response += last_line response += last_line
return response 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. Write a string to the stage controller and return its response.
@ -170,22 +195,31 @@ class ExtensibleSerialInstrument(object):
with self.communications_lock: with self.communications_lock:
self.flush_input_buffer() self.flush_input_buffer()
self.write(queryString) 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() first_line = self.readline(timeout).strip()
if first_line == queryString: if first_line == queryString:
return self.readline(timeout).strip() return self.readline(timeout).strip()
else: else:
logging.info('This command did not echo!!!') logging.info("This command did not echo!!!")
return first_line return first_line
if termination_line is not None: if termination_line is not None:
multiline = True multiline = True
if multiline: if multiline:
return self.read_multiline(termination_line) return self.read_multiline(termination_line)
else: else:
return self.readline(timeout).strip() #question: should we strip the final newline? return self.readline(
timeout
def parsed_query(self, query_string, response_string=r"%d", re_flags=0, parse_function=None, **kwargs): ).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. Perform a query, returning a parsed form of the response.
@ -204,46 +238,81 @@ class ExtensibleSerialInstrument(object):
""" """
response_regex = response_string response_regex = response_string
noop = lambda x: x #placeholder null parse function noop = lambda x: x # placeholder null parse function
placeholders = [ #tuples of (regex matching placeholder, regex to replace it with, parse function) placeholders = [ # tuples of (regex matching placeholder, regex to replace it with, parse function)
(r"%c", r".", noop), (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"%d", r"[-+]?\\d+", int),
(r"%[eEfg]", r"[-+]?(?:\\d+(?:\.\\d*)?|\.\\d+)(?:[eE][-+]?\\d+)?", float), (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"%s", r"\\s+", noop),
(r"%u", r"\\d+", int), (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 = [] matched_placeholders = []
for placeholder, regex, parse_fun in placeholders: for placeholder, regex, parse_fun in placeholders:
response_regex = re.sub(placeholder, '('+regex+')', response_regex) #substitute regex for placeholder response_regex = re.sub(
matched_placeholders.extend([(parse_fun, m.start()) for m in re.finditer(placeholder, response_string)]) #save the positions of the placeholders 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: 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 parse_function = [
if not hasattr(parse_function,'__iter__'): f for f, s in sorted(matched_placeholders, key=lambda m: m[1])
parse_function = [parse_function] #make sure it's a list. ] # order parse functions by their occurrence in the original string
if not hasattr(parse_function, "__iter__"):
reply = self.query(query_string, **kwargs) #do the query parse_function = [parse_function] # make sure it's a list.
#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 reply = self.query(query_string, **kwargs) # do the query
waited=False # if match this could be because another response entered the buffer between write and read. Sleep for short while then
res=re.search(response_regex, reply, flags=re_flags) # 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: while res is None:
if not waited: if not waited:
time.sleep(.1) time.sleep(0.1)
waited=True waited = True
original_reply = reply original_reply = reply
if self._ser.inWaiting(): if self._ser.inWaiting():
reply = self.readline().strip() 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: 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: 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: 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: if len(parsed_result) == 1:
return parsed_result[0] return parsed_result[0]
else: else:
@ -251,10 +320,15 @@ class ExtensibleSerialInstrument(object):
except ValueError: except ValueError:
logging.info("Matched Groups: {}".format(res.groups())) logging.info("Matched Groups: {}".format(res.groups()))
logging.info("Parsing Functions {}:".format(parse_function)) 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): def int_query(self, query_string, **kwargs):
"""Perform a query and return the result(s) as integer(s) (see parsedQuery)""" """Perform a query and return the result(s) as integer(s) (see parsedQuery)"""
return self.parsed_query(query_string, "%d", **kwargs) return self.parsed_query(query_string, "%d", **kwargs)
def float_query(self, query_string, **kwargs): def float_query(self, query_string, **kwargs):
"""Perform a query and return the result(s) as float(s) (see parsedQuery)""" """Perform a query and return the result(s) as float(s) (see parsedQuery)"""
return self.parsed_query(query_string, "%f", **kwargs) return self.parsed_query(query_string, "%f", **kwargs)
@ -273,7 +347,13 @@ class ExtensibleSerialInstrument(object):
if our instrument is there.""" if our instrument is there."""
with self.communications_lock: with self.communications_lock:
success = False 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: try:
logging.info("Trying port {}".format(port_name)) logging.info("Trying port {}".format(port_name))
self.open(port_name) self.open(port_name)
@ -285,14 +365,15 @@ class ExtensibleSerialInstrument(object):
try: try:
self.close() self.close()
except: 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: if success:
break #again, make sure this happens *after* closing the port break # again, make sure this happens *after* closing the port
if success: if success:
return port_name return port_name
else: else:
return None return None
class OptionalModule(object): class OptionalModule(object):
"""This allows a `ExtensibleSerialInstrument` to have optional features. """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 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. the serial instrument, and can be added or removed at run-time.
""" """
def __init__(self,available,parent=None,module_type="Undefined",model="Generic"): def __init__(
assert type(available) is bool, 'Option module availablity should be a boolean not a {}'.format(type(available)) self, available, parent=None, module_type="Undefined", model="Generic"
self._available=available ):
self._parent=parent assert (
assert type(module_type) is str, 'Option module type should be a string not a {}'.format(type(module_typ)) type(available) is bool
self.module_type=module_type ), "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: if available:
assert type(model) is str, 'Option module type should be a string not a {}'.format(type(model)) assert (
self.model=model type(model) is str
), "Option module type should be a string not a {}".format(type(model))
self.model = model
else: else:
self.model=None self.model = None
@property @property
def available(self): def available(self):
return self._available return self._available
def confirm_available(self): def confirm_available(self):
"""Check if module is available, no return, will raise exception if not available!""" """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): def describe(self):
"""Consistently spaced desciption for listing modules""" """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): class QueriedProperty(object):
"""A Property interface that reads and writes from the instrument on the bus. """A Property interface that reads and writes from the instrument on the bus.
@ -359,8 +450,18 @@ class QueriedProperty(object):
:ack_writes: :ack_writes:
set to "readline" to discard a line of input after writing. 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.response_string = response_string
self.get_cmd = get_cmd self.get_cmd = get_cmd
self.set_cmd = set_cmd self.set_cmd = set_cmd
@ -369,48 +470,54 @@ class QueriedProperty(object):
self.fdel = fdel self.fdel = fdel
self.ack_writes = ack_writes self.ack_writes = ack_writes
self.__doc__ = doc self.__doc__ = doc
# TODO: standardise the return (single value only vs parsed result), consider bool # TODO: standardise the return (single value only vs parsed result), consider bool
def __get__(self, obj, objtype=None): def __get__(self, obj, objtype=None):
if issubclass(type(obj),OptionalModule): if issubclass(type(obj), OptionalModule):
obj.confirm_available() obj.confirm_available()
obj=obj._parent obj = obj._parent
if obj is None: if obj is None:
return self return self
assert issubclass(type(obj),ExtensibleSerialInstrument) assert issubclass(type(obj), ExtensibleSerialInstrument)
if self.get_cmd is None: if self.get_cmd is None:
raise AttributeError("unreadable attribute") raise AttributeError("unreadable attribute")
# Allow certain "magic" values to set the response string # Allow certain "magic" values to set the response string
for key, val in [('float',r"%f"), for key, val in [("float", r"%f"), ("int", r"%d")]:
('int',r"%d"),]:
if self.response_string == key: if self.response_string == key:
self.response_string = val 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) value = obj.query(self.get_cmd)
if self.response_string == 'bool': if self.response_string == "bool":
value = bool(value) value = bool(value)
else: else:
value = obj.parsed_query(self.get_cmd, self.response_string) value = obj.parsed_query(self.get_cmd, self.response_string)
return value return value
def __set__(self, obj, value): def __set__(self, obj, value):
if issubclass(type(obj),OptionalModule): if issubclass(type(obj), OptionalModule):
obj.confirm_available() obj.confirm_available()
obj=obj._parent obj = obj._parent
assert issubclass(type(obj),ExtensibleSerialInstrument) assert issubclass(type(obj), ExtensibleSerialInstrument)
if self.set_cmd is None: if self.set_cmd is None:
raise AttributeError("can't set attribute") raise AttributeError("can't set attribute")
if self.validate is not None: if self.validate is not None:
if value not in self.validate: 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 self.valrange is not None:
if value < min(self.valrange) or value > max(self.valrange): 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 message = self.set_cmd
if '{0' in message: if "{0" in message:
message = message.format(value) message = message.format(value)
elif '%' in message: elif "%" in message:
message = message % value message = message % value
obj.write(message) obj.write(message)
if self.ack_writes == "readline": if self.ack_writes == "readline":

View file

@ -9,12 +9,20 @@ It is (c) Richard Bowman & Julian Stirling 2019 and released under GNU GPL v3
""" """
from __future__ import print_function, division from __future__ import print_function, division
import time 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 serial, serial.tools.list_ports
import re import re
import warnings import warnings
import logging import logging
class Sangaboard(ExtensibleSerialInstrument): class Sangaboard(ExtensibleSerialInstrument):
"""Class managing serial communications with a Sangaboard """Class managing serial communications with a Sangaboard
@ -39,34 +47,45 @@ class Sangaboard(ExtensibleSerialInstrument):
""" """
# List of valid and deprecated firmwares, for communication checks # List of valid and deprecated firmwares, for communication checks
valid_firmwares = [ valid_firmwares = [(0, 5), (0, 4)]
(0, 5), deprecated_firmwares = [(0, 4)]
(0, 4)
]
deprecated_firmwares = [
(0, 4)
]
# These are the settings for the sangaboards serial port, and can usually be left as default. # 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, step time and ramp time are get/set using simple serial commands.
position = QueriedProperty(get_cmd="p?", response_string=r"%d %d %d", position = QueriedProperty(
doc="Get the position of the axes as a tuple of 3 integers.") get_cmd="p?",
step_time = QueriedProperty(get_cmd="dt?", set_cmd="dt %d", response_string="minimum step delay %d", response_string=r"%d %d %d",
doc="Get or set the minimum time between steps of the motors in microseconds.\n\n" doc="Get the position of the axes as a tuple of 3 integers.",
"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.") step_time = QueriedProperty(
ramp_time = QueriedProperty(get_cmd="ramp_time?", set_cmd="ramp_time %d", response_string="ramp time %d", get_cmd="dt?",
doc="Get or set the acceleration time in microseconds.\n\n" set_cmd="dt %d",
"The motors will accelerate/decelerate between stationary and maximum speed over `ramp_time` " response_string="minimum step delay %d",
"microseconds. Zero means the motor runs at full speed initially, with no accleration " doc="Get or set the minimum time between steps of the motors in microseconds.\n\n"
"control. Small moves may last less than `2*ramp_time`, in which case the acceleration " "The step time is ``1000000/max speed`` in steps/second. It is saved to EEPROM on "
"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 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 # 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 # Once initialised, `firmware` is a string that identifies the firmware version
firmware = None firmware = None
@ -88,30 +107,38 @@ class Sangaboard(ExtensibleSerialInstrument):
# Make absolutely sure that whatever port we're on is valid # Make absolutely sure that whatever port we're on is valid
self.check_valid_firmware() 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) self.light_sensor = LightSensor(False)
for module in self.list_modules(): for module in self.list_modules():
module_type=module.split(':')[0].strip() module_type = module.split(":")[0].strip()
module_model=module.split(':')[1].strip() module_model = module.split(":")[1].strip()
if module_type.startswith('Light Sensor'): if module_type.startswith("Light Sensor"):
if module_model in self.supported_light_sensors: 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: else:
logging.warning("Light sensor model \"%s\" not recognised."%(module_model)) logging.warning(
elif module_type.startswith('Endstops'): 'Light sensor model "%s" not recognised.' % (module_model)
)
elif module_type.startswith("Endstops"):
self.endstops = Endstops(True, parent=self, model=module_model) self.endstops = Endstops(True, parent=self, model=module_model)
else: 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: except Exception as e:
# If an error occurred while setting up (e.g. because the board isn't connected or something) # 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). # make sure we close the serial port cleanly (otherwise it hangs open).
self.close() self.close()
logging.error(e) 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 raise e
def test_communications(self): def test_communications(self):
@ -124,16 +151,22 @@ class Sangaboard(ExtensibleSerialInstrument):
logging.debug("Running firmware checks") logging.debug("Running firmware checks")
# Request firmware version from the board # 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)) logging.info("Firmware response: {}".format(self.firmware))
# Check for valid firmware string # Check for valid firmware string
if self.firmware: 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: if not match:
# Try old firmware format # 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: if not match:
logging.error("Version string \"{}\" not recognised.".format(self.firmware)) logging.error(
'Version string "{}" not recognised.'.format(self.firmware)
)
return False return False
else: else:
logging.error("No firmware version string was returned.") logging.error("No firmware version string was returned.")
@ -146,15 +179,17 @@ class Sangaboard(ExtensibleSerialInstrument):
self.firmware_version = match.group(1) self.firmware_version = match.group(1)
if version_tuple not in Sangaboard.valid_firmwares: 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 return False
if version_tuple in Sangaboard.deprecated_firmwares: if version_tuple in Sangaboard.deprecated_firmwares:
logging.warning( logging.warning(
"Warning this Sangaboard is using v0.4 of the firmware, this will soon be depreciated! " "Warning this Sangaboard is using v0.4 of the firmware, this will soon be depreciated! "
"Please consider updating the Sangaboard firmware" "Please consider updating the Sangaboard firmware"
) )
return True return True
def move_rel(self, displacement, axis=None): 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' axis: None (for 3-axis moves) or one of 'x','y','z'
""" """
if axis is not None: 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))) self.query("mr{} {}".format(axis, int(displacement)))
else: else:
#TODO: assert displacement is 3 integers # TODO: assert displacement is 3 integers
self.query("mr {} {} {}".format(*list(displacement))) self.query("mr {} {} {}".format(*list(displacement)))
def release_motors(self): def release_motors(self):
@ -181,12 +218,12 @@ class Sangaboard(ExtensibleSerialInstrument):
queries the board for its position, then instructs it to make about queries the board for its position, then instructs it to make about
relative move. 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) return self.move_rel(rel_mov, **kwargs)
def query(self, message, *args, **kwargs): def query(self, message, *args, **kwargs):
"""Send a message and read the response. See ExtensibleSerialInstrument.query()""" """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) return ExtensibleSerialInstrument.query(self, message, *args, **kwargs)
def list_modules(self): def list_modules(self):
@ -194,12 +231,15 @@ class Sangaboard(ExtensibleSerialInstrument):
Each module will correspond to a string of the form ``Module Name: Model`` 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] return [str(module) for module in modules]
def print_help(self): def print_help(self):
"""Print the stage's built-in help message.""" """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): class LightSensor(OptionalModule):
"""An optional module giving access to the light sensor. """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 module which is an instance of this class. It can be used to access
the light sensor (usually via the I2C bus). the light sensor (usually via the I2C bus).
""" """
valid_gains = None valid_gains = None
_valid_gains_int = None _valid_gains_int = None
integration_time = QueriedProperty( integration_time = QueriedProperty(
get_cmd="light_sensor_integration_time?", get_cmd="light_sensor_integration_time?",
set_cmd="light_sensor_integration_time %d", set_cmd="light_sensor_integration_time %d",
response_string="light sensor integration time %d ms", 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"): def __init__(self, available, parent=None, model="Generic"):
super(LightSensor, self).__init__(available,parent=parent,module_type="LightSensor",model=model) super(LightSensor, self).__init__(
available, parent=parent, module_type="LightSensor", model=model
)
if available: if available:
self.valid_gains = self.__get_gain_values() self.valid_gains = self.__get_gain_values()
self._valid_gains_int = [int(g) for g in self.valid_gains] 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.""" Valid gain values are defined in the `valid_gains` property, and should be floating-point numbers."""
self.confirm_available() self.confirm_available()
gain = self._parent.query('light_sensor_gain?') gain = self._parent.query("light_sensor_gain?")
M = re.search('[0-9\.]+(?=x)',gain) M = re.search("[0-9\.]+(?=x)", gain)
assert M is not None, "Cannot read gain string: \"{}\"".format(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 is a float as non integer gains exist but are set with floor of value
return float(M.group()) return float(M.group())
@gain.setter @gain.setter
def gain(self,val): def gain(self, val):
self.confirm_available() self.confirm_available()
assert int(val) in self._valid_gains_int, "Gain {} not valid must be one of: {}".format(val,self.valid_gains) assert (
gain = self._parent.query('light_sensor_gain %d'%(int(val))) int(val) in self._valid_gains_int
M = re.search('[0-9\.]+(?=x)',gain) ), "Gain {} not valid must be one of: {}".format(val, self.valid_gains)
assert M is not None, "Cannot read gain string: \"{}\"".format(gain) gain = self._parent.query("light_sensor_gain %d" % (int(val)))
#gain is a float as non integer gains exist but are set with floor of value M = re.search("[0-9\.]+(?=x)", gain)
assert int(val) == int(float(M.group())), 'Gain of {} set, \"{}\" returned'.format(val,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): def __get_gain_values(self):
"""Read the allowable values for the light sensor's gain. """Read the allowable values for the light sensor's gain.
@ -256,15 +306,16 @@ class LightSensor(OptionalModule):
values, the list will be of strings. values, the list will be of strings.
""" """
self.confirm_available() self.confirm_available()
gains = self._parent.query('light_sensor_gain_values?') gains = self._parent.query("light_sensor_gain_values?")
try: try:
M = re.findall('[0-9\.]+(?=x)',gains) M = re.findall("[0-9\.]+(?=x)", gains)
return [float(gain) for gain in M] return [float(gain) for gain in M]
except: except:
# Fall back to strings if we don't get floats (unlikely) # Fall back to strings if we don't get floats (unlikely)
gain_strings = gains[20:].split(", ") gain_strings = gains[20:].split(", ")
return gain_strings return gain_strings
class Endstops(OptionalModule): class Endstops(OptionalModule):
"""An optional module for use with endstops. """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. the type, state of the endstops, read and write maximum positions, and home.
""" """
installed=[] installed = []
"""List of installed endstop types (min, max, soft)""" """List of installed endstop types (min, max, soft)"""
def __init__(self,available,parent=None,model="min"): def __init__(self, available, parent=None, model="min"):
super(Endstops, self).__init__(available,parent=parent,model="Endstops") super(Endstops, self).__init__(available, parent=parent, model="Endstops")
self.installed=model.split(' ') self.installed = model.split(" ")
status = QueriedProperty(get_cmd="endstops?", response_string=r"%d %d %d", status = QueriedProperty(
doc="Get endstops status as {-1,0,1} for {min,no,max} endstop triggered for each axis") get_cmd="endstops?",
maxima = QueriedProperty(get_cmd="max_p?", set_cmd="max %d %d %d", response_string="%d %d %d", response_string=r"%d %d %d",
doc="Vector of maximum positions, homing to max endstops will measure this, "+ doc="Get endstops status as {-1,0,1} for {min,no,max} endstop triggered for each axis",
"can be set to a known value for use with max only and min+soft endstops") )
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) """ Home given/all axes in the given direction (min/max/both)
:param direction: one of {min,max,both} :param direction: one of {min,max,both}
:param axes: list of axes e.g. ['x','y'] :param axes: list of axes e.g. ['x','y']
""" """
ax=0 ax = 0
if 'x' in axes: if "x" in axes:
ax+=1 ax += 1
if 'y' in axes: if "y" in axes:
ax+=2 ax += 2
if 'z' in axes: if "z" in axes:
ax+=3 ax += 3
if direction == "min" or direction == "both": 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": if direction == "max" or direction == "both":
self._parent.query('home_max {}'.format(ax)) self._parent.query("home_max {}".format(ax))

View file

@ -17,6 +17,7 @@ class TaskOrchestrator:
tasks (list): List of `Task` objects tasks (list): List of `Task` objects
""" """
def __init__(self): def __init__(self):
self.tasks = [] self.tasks = []
@ -106,6 +107,7 @@ class Task:
`status` will be one of 'idle'', 'running', 'error', or 'success'. `status` will be one of 'idle'', 'running', 'error', or 'success'.
`return` eventually holds the return value of the task function. `return` eventually holds the return value of the task function.
""" """
def __init__(self, function, *args, **kwargs): def __init__(self, function, *args, **kwargs):
# The task long-running method # The task long-running method
self.task = function self.task = function
@ -117,39 +119,41 @@ class Task:
self.id = uuid.uuid4().hex self.id = uuid.uuid4().hex
self.state = { self.state = {
'id': self.id, "id": self.id,
'status': 'idle', "status": "idle",
'return': None, "return": None,
'start_time': None, "start_time": None,
'end_time': None, "end_time": None,
} }
def process(self, f): def process(self, f):
""" """
Wraps the target function to handle recording `status` and `return` to `state`. Wraps the target function to handle recording `status` and `return` to `state`.
""" """
def wrapped(*args, **kwargs): def wrapped(*args, **kwargs):
self.state['status'] = 'running' self.state["status"] = "running"
try: try:
r = f(*args, **kwargs) r = f(*args, **kwargs)
s = 'success' s = "success"
except Exception as e: except Exception as e:
logging.error(e) logging.error(e)
logging.error(traceback.format_exc()) logging.error(traceback.format_exc())
r = str(e) r = str(e)
s = 'error' s = "error"
self.state['return'] = r self.state["return"] = r
self.state['status'] = s self.state["status"] = s
return wrapped return wrapped
def run(self): def run(self):
""" """
Records the task start and end times to `state`, and runs the task wrapped in `process`. 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.process(self.task)(*self.args, **self.kwargs)
self._running = False 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 def start(self): # Start and draw
""" """

View file

@ -17,7 +17,11 @@ def set_properties(obj, **kwargs):
try: try:
saved_properties[k] = getattr(obj, k) saved_properties[k] = getattr(obj, k)
except AttributeError: 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(): for k, v in kwargs.items():
setattr(obj, k, v) setattr(obj, k, v)
try: try:
@ -27,12 +31,14 @@ def set_properties(obj, **kwargs):
setattr(obj, k, v) 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""" """Takes key-value pairs of a JSON value, and maps onto an array"""
# If no base array is given # If no base array is given
if not base_array: if not base_array:
# Create an array of zeros # Create an array of zeros
base_array = [0]*len(axis_keys) base_array = [0] * len(axis_keys)
else: else:
# Create a copy of the passed base_array # Create a copy of the passed base_array
base_array = copy.copy(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 # Do the mapping
for axis, key in enumerate(axis_keys): for axis, key in enumerate(axis_keys):
if key in coordinate_dictionary: 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 return base_array
@ -84,4 +92,4 @@ def recursively_apply(data, func):
return [recursively_apply(x, func) for x in data] return [recursively_apply(x, func) for x in data]
# if the object is neither a map nor iterable # if the object is neither a map nor iterable
else: else:
return func(data) return func(data)