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"
from .microscope import Microscope

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,6 @@ from flask import jsonify, request
class ZoomAPI(MicroscopeView):
def get(self):
"""
Get the current zoom value
@ -17,9 +16,9 @@ class ZoomAPI(MicroscopeView):
:<header Content-Type: application/json
: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):
"""
@ -44,7 +43,7 @@ class ZoomAPI(MicroscopeView):
:status 200: preview started/stopped
"""
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)
@ -52,7 +51,6 @@ class ZoomAPI(MicroscopeView):
class OverlayAPI(MicroscopeView):
def get(self):
"""
Get overlay text
@ -67,7 +65,7 @@ class OverlayAPI(MicroscopeView):
text = self.microscope.camera.camera.annotate_text
size = self.microscope.camera.camera.annotate_text_size
return jsonify({'text': text, 'size': size})
return jsonify({"text": text, "size": size})
def post(self):
"""
@ -94,10 +92,10 @@ class OverlayAPI(MicroscopeView):
"""
payload = JsonResponse(request)
text = payload.param('text', default="", convert=str)
size = payload.param('size', default=50, convert=int)
text = payload.param("text", default="", convert=str)
size = payload.param("size", default=50, convert=int)
self.microscope.camera.camera.annotate_text = text
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):
def post(self, operation):
"""
Start or stop the onboard GPU preview.
@ -39,7 +38,7 @@ class GPUPreviewAPI(MicroscopeView):
if operation == "start":
payload = JsonResponse(request)
window = payload.param('window', default=[])
window = payload.param("window", default=[])
logging.debug(window)
if len(window) != 4:

View file

@ -6,6 +6,7 @@ from openflexure_microscope.api.v1.views import MicroscopeView
import logging
import warnings
class PluginFormAPI(MicroscopeView):
def get(self):
"""
@ -26,12 +27,12 @@ class PluginFormAPI(MicroscopeView):
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
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 = []
@ -40,7 +41,7 @@ def construct_blueprint(microscope_obj):
for plugin_name, plugin_obj in microscope_obj.plugin.plugins:
# 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
expanded_routes = {}
@ -50,7 +51,7 @@ def construct_blueprint(microscope_obj):
# Remove all leading slashes from view route
cleaned_route = view_route
while cleaned_route[0] == '/':
while cleaned_route[0] == "/":
cleaned_route = cleaned_route[1:]
# 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
# 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
all_routes.append(full_view_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
blueprint.add_url_rule(
@ -74,30 +79,37 @@ def construct_blueprint(microscope_obj):
view_func=view_class.as_view(
plugin_route_id,
microscope=microscope_obj,
plugin=plugin_obj
)
plugin=plugin_obj,
),
)
else:
warnings.warn(
"An endpoint /{} has already been loaded. Skipping {}.".format(
full_view_route,
view_class
full_view_route, view_class
)
)
# 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
# TODO: Validate form? We need to make sure no single plugin can break all plugins.
api_form_info['id'] = plugin_name
if 'forms' in api_form_info and isinstance(api_form_info['forms'], list):
for form in api_form_info['forms']:
if 'route' in form and form['route'] in expanded_routes.keys():
form['route'] = expanded_routes[form['route']]
api_form_info["id"] = plugin_name
if "forms" in api_form_info and isinstance(
api_form_info["forms"], list
):
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:
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
microscope_obj.plugin.forms.append(api_form_info)
print(microscope_obj.plugin.forms)

View file

@ -8,7 +8,6 @@ import logging
class PositionAPI(MicroscopeView):
def get(self):
"""
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)
def post(self):
@ -67,15 +66,19 @@ class PositionAPI(MicroscopeView):
position = [0, 0, 0]
# 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
target_position = axes_to_array(payload.json, ['x', 'y', 'z'])
if (payload.param("absolute") is True) and (
self.microscope.stage
): # Only if stage exists
target_position = axes_to_array(payload.json, ["x", "y", "z"])
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))
else:
# 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)
@ -85,18 +88,18 @@ class PositionAPI(MicroscopeView):
with self.microscope.stage.lock:
self.microscope.stage.move_rel(position)
out = filter_dict(self.microscope.state, ['stage', 'position'])
out = filter_dict(self.microscope.state, ["stage", "position"])
return jsonify(out)
def construct_blueprint(microscope_obj):
blueprint = Blueprint('stage_blueprint', __name__)
blueprint = Blueprint("stage_blueprint", __name__)
blueprint.add_url_rule(
'/position',
view_func=PositionAPI.as_view('position', microscope=microscope_obj)
"/position",
view_func=PositionAPI.as_view("position", microscope=microscope_obj),
)
return blueprint

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,6 +3,7 @@ import io
from ._common import *
from . import _webp
def remove(src, new_file=None):
"""
py:function:: piexif.remove(filename)
@ -19,7 +20,7 @@ def remove(src, new_file=None):
src_data = src
file_type = "webp"
else:
with open(src, 'rb') as f:
with open(src, "rb") as f:
src_data = f.read()
output_is_file = True
if src_data[0:2] == b"\xff\xd8":
@ -53,4 +54,4 @@ def remove(src, new_file=None):
with open(src, "wb+") as f:
f.write(new_data)
else:
raise ValueError("Give a second argument to 'remove' to output file")
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":
src_data = exif_src
else:
with open(exif_src, 'rb') as f:
with open(exif_src, "rb") as f:
src_data = f.read()
segments = split_into_segments(src_data)
exif = get_exif_seg(segments)
@ -26,7 +26,7 @@ def transplant(exif_src, image, new_file=None):
if image[0:2] == b"\xff\xd8":
image_data = image
else:
with open(image, 'rb') as f:
with open(image, "rb") as f:
image_data = f.read()
output_file = True
segments = split_into_segments(image_data)
@ -42,4 +42,4 @@ def transplant(exif_src, image, new_file=None):
with open(image, "wb+") as f:
f.write(new_data)
else:
raise ValueError("Give a 3rd argument to 'transplant' to output file")
raise ValueError("Give a 3rd argument to 'transplant' to output file")

View file

@ -1,4 +1,3 @@
import struct
@ -18,26 +17,33 @@ def split(data):
chunks = []
while pointer + CHUNK_FOURCC_LENGTH + LENGTH_BYTES_LENGTH < file_size:
fourcc = data[pointer:pointer + CHUNK_FOURCC_LENGTH]
fourcc = data[pointer : pointer + CHUNK_FOURCC_LENGTH]
pointer += CHUNK_FOURCC_LENGTH
chunk_length_bytes = data[pointer:pointer + LENGTH_BYTES_LENGTH]
chunk_length_bytes = data[pointer : pointer + LENGTH_BYTES_LENGTH]
chunk_length = struct.unpack("<L", chunk_length_bytes)[0]
pointer += LENGTH_BYTES_LENGTH
chunk_data = data[pointer:pointer + chunk_length]
chunks.append({"fourcc":fourcc, "length_bytes":chunk_length_bytes, "data":chunk_data})
chunk_data = data[pointer : pointer + chunk_length]
chunks.append(
{"fourcc": fourcc, "length_bytes": chunk_length_bytes, "data": chunk_data}
)
padding = 1 if chunk_length % 2 else 0
pointer += chunk_length + padding
return chunks
def merge_chunks(chunks):
merged = b"".join([chunk["fourcc"]
+ chunk["length_bytes"]
+ chunk["data"]
+ (len(chunk["data"]) % 2) * b"\x00"
for chunk in chunks])
merged = b"".join(
[
chunk["fourcc"]
+ chunk["length_bytes"]
+ chunk["data"]
+ (len(chunk["data"]) % 2) * b"\x00"
for chunk in chunks
]
)
return merged
@ -50,6 +56,7 @@ def _get_size_from_vp8x(chunk):
height = height_minus_one + 1
return (width, height)
def _get_size_from_vp8(chunk):
BEGIN_CODE = b"\x9d\x01\x2a"
begin_index = chunk["data"].find(BEGIN_CODE)
@ -59,17 +66,21 @@ def _get_size_from_vp8(chunk):
BEGIN_CODE_LENGTH = len(BEGIN_CODE)
LENGTH_BYTES_LENGTH = 2
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]
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]
return (width, height)
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
return contains
def _get_size_from_vp8L(chunk):
b1 = chunk["data"][1:2]
b2 = chunk["data"][2:3]
@ -79,11 +90,14 @@ def _get_size_from_vp8L(chunk):
width_minus_one = (ord(b2) & ord(b"\x3F")) << 8 | ord(b1)
width = width_minus_one + 1
height_minus_one = (ord(b4) & ord(b"\x0F")) << 10 | ord(b3) << 2 | (ord(b2) & ord(b"\xC0")) >> 6
height_minus_one = (
(ord(b4) & ord(b"\x0F")) << 10 | ord(b3) << 2 | (ord(b2) & ord(b"\xC0")) >> 6
)
height = height_minus_one + 1
return (width, height)
def _get_size_from_anmf(chunk):
width_minus_one_bytes = chunk["data"][6:9] + b"\x00"
width_minus_one = struct.unpack("<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 = height_minus_one + 1
return (width, height)
def set_vp8x(chunks):
width = 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:
if chunk["fourcc"] == b"VP8X":
@ -136,11 +160,16 @@ def set_vp8x(chunks):
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)
return chunks
def get_file_header(chunks):
WEBP_HEADER_LENGTH = 4
FOURCC_LENGTH = 4
@ -158,7 +187,6 @@ def get_file_header(chunks):
return file_header
def get_exif(data):
if data[0:4] != b"RIFF" or data[8:12] != b"WEBP":
raise ValueError("Not WebP")
@ -179,15 +207,15 @@ def get_exif(data):
chunks = []
exif = b""
while pointer < file_size:
fourcc = data[pointer:pointer + CHUNK_FOURCC_LENGTH]
fourcc = data[pointer : pointer + CHUNK_FOURCC_LENGTH]
pointer += CHUNK_FOURCC_LENGTH
chunk_length_bytes = data[pointer:pointer + LENGTH_BYTES_LENGTH]
chunk_length_bytes = data[pointer : pointer + LENGTH_BYTES_LENGTH]
chunk_length = struct.unpack("<L", chunk_length_bytes)[0]
if chunk_length % 2:
chunk_length += 1
pointer += LENGTH_BYTES_LENGTH
if fourcc == b"EXIF":
return data[pointer:pointer + chunk_length]
return data[pointer : pointer + chunk_length]
pointer += chunk_length
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):
EXIF_HEADER = b"EXIF"
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
animation_index = None

View file

@ -2,26 +2,26 @@ class UserComment:
#
# Names of encodings that we publicly support.
#
ASCII = 'ascii'
JIS = 'jis'
UNICODE = 'unicode'
ASCII = "ascii"
JIS = "jis"
UNICODE = "unicode"
ENCODINGS = (ASCII, JIS, UNICODE)
#
# The actual encodings accepted by the standard library differ slightly from
# the above.
#
_JIS = 'shift_jis'
_UNICODE = 'utf_16_be'
_JIS = "shift_jis"
_UNICODE = "utf_16_be"
_PREFIX_SIZE = 8
#
# From Table 9: Character Codes and their Designation
#
_ASCII_PREFIX = b'\x41\x53\x43\x49\x49\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'
_UNDEFINED_PREFIX = b'\x00\x00\x00\x00\x00\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"
_UNICODE_PREFIX = b"\x55\x4e\x49\x43\x4f\x44\x45\x00"
_UNDEFINED_PREFIX = b"\x00\x00\x00\x00\x00\x00\x00\x00"
@classmethod
def load(cls, data):
@ -35,18 +35,20 @@ class UserComment:
or the encoding is unsupported.
"""
if len(data) < cls._PREFIX_SIZE:
raise ValueError('not enough data to decode UserComment')
prefix = data[:cls._PREFIX_SIZE]
body = data[cls._PREFIX_SIZE:]
raise ValueError("not enough data to decode UserComment")
prefix = data[: cls._PREFIX_SIZE]
body = data[cls._PREFIX_SIZE :]
if prefix == cls._UNDEFINED_PREFIX:
raise ValueError('prefix is UNDEFINED, unable to decode UserComment')
raise ValueError("prefix is UNDEFINED, unable to decode UserComment")
try:
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]
except KeyError:
raise ValueError('unable to determine appropriate encoding')
return body.decode(encoding, errors='replace')
raise ValueError("unable to determine appropriate encoding")
return body.decode(encoding, errors="replace")
@classmethod
def dump(cls, data, encoding="ascii"):
@ -60,7 +62,15 @@ class UserComment:
:raises: ValueError if the encoding is unsupported.
"""
if encoding not in cls.ENCODINGS:
raise ValueError('encoding %r must be one of %r' % (encoding, cls.ENCODINGS))
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')
raise ValueError(
"encoding %r must be one of %r" % (encoding, cls.ENCODINGS)
)
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]:
raise ValueError("The gain parameter was not valid")
ret = mmal.mmal_port_parameter_set_rational(
camera._camera.control._port,
gain,
to_rational(value)
camera._camera.control._port, gain, to_rational(value)
)
if ret == 4:
raise exc.PiCameraMMALError(
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:
raise exc.PiCameraMMALError(ret)
@ -56,21 +54,27 @@ if __name__ == "__main__":
# fix the shutter speed
cam.shutter_speed = cam.exposure_speed
logging.info("Current a/d gains: {}, {}".format(cam.analog_gain, cam.digital_gain))
logging.info(
"Current a/d gains: {}, {}".format(cam.analog_gain, cam.digital_gain)
)
logging.info("Attempting to set analogue gain to 1")
set_analog_gain(cam, 1)
logging.info("Attempting to set digital gain to 1")
set_digital_gain(cam, 1)
# The old code is left in here in case it is a useful example...
# ret = mmal.mmal_port_parameter_set_rational(cam._camera.control._port,
# ret = mmal.mmal_port_parameter_set_rational(cam._camera.control._port,
# MMAL_PARAMETER_DIGITAL_GAIN,
# to_rational(1))
# print("Return code: {}".format(ret))
try:
while True:
logging.info("Current a/d gains: {}, {}".format(cam.analog_gain, cam.digital_gain))
logging.info(
"Current a/d gains: {}, {}".format(
cam.analog_gain, cam.digital_gain
)
)
time.sleep(1)
except KeyboardInterrupt:
logging.info("Stopping...")

View file

@ -65,7 +65,6 @@ def convert_type_to_json_safe(v):
return v
def settings_to_json(data: dict):
"""
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):
ERROR_CODES = {
'ACQUIRE_ERROR': "Unable to acquire. Lock in use by another thread.",
'IN_USE_ERROR': "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.",
}
def __init__(self, code, lock):

View file

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

View file

@ -1,18 +1,24 @@
import numpy as np
import logging
from openflexure_microscope.devel import MicroscopeViewPlugin, JsonResponse, request, jsonify
from openflexure_microscope.devel import (
MicroscopeViewPlugin,
JsonResponse,
request,
jsonify,
)
class MeasureSharpnessAPI(MicroscopeViewPlugin):
def post(self):
payload = JsonResponse(request)
return jsonify({'sharpness': self.plugin.measure_sharpness()})
return jsonify({"sharpness": self.plugin.measure_sharpness()})
class AutofocusAPI(MicroscopeViewPlugin):
def post(self):
payload = JsonResponse(request)
# Figure out the range of z values to use
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 jsonify(task.state), 202
class FastAutofocusAPI(MicroscopeViewPlugin):
def post(self):
payload = JsonResponse(request)
# Figure out the parameters to use
dz = payload.param("dz", default=2000, convert=int)
backlash = payload.param("backlash", default=0, convert=int)
@ -33,7 +40,9 @@ class FastAutofocusAPI(MicroscopeViewPlugin):
backlash = 0
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 jsonify(task.state), 202
return jsonify(task.state), 202

View file

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

View file

@ -10,15 +10,16 @@ from .api import MeasureSharpnessAPI, AutofocusAPI, FastAutofocusAPI
from openflexure_microscope.devel import MicroscopePlugin
class AutofocusPlugin(MicroscopePlugin):
"""
Basic autofocus plugin
"""
api_views = {
'/measure_sharpness': MeasureSharpnessAPI,
'/autofocus': AutofocusAPI,
'/fast_autofocus': FastAutofocusAPI,
"/measure_sharpness": MeasureSharpnessAPI,
"/autofocus": AutofocusAPI,
"/fast_autofocus": FastAutofocusAPI,
}
### SLOW AUTOFOCUS
@ -70,21 +71,24 @@ class AutofocusPlugin(MicroscopePlugin):
m.focus_rel(dz)
return m.sharpest_z_on_move(0)
def fast_autofocus(self, dz=2000, backlash=None):
"""Perform a down-up-down-up autofocus"""
with self.monitor_sharpness() as m:
i, z = m.focus_rel(-dz/2)
i, z = m.focus_rel(-dz / 2)
i, z = m.focus_rel(dz)
fz = m.sharpest_z_on_move(i)
if backlash is None:
i, z = m.focus_rel(-dz) # move all the way to the start so it's consistent
i, z = m.focus_rel(
-dz
) # move all the way to the start so it's consistent
else:
i, z = m.focus_rel(fz - z - backlash)
m.focus_rel(fz - z)
return m.data_dict()
def fast_up_down_up_autofocus(self, dz=2000, target_z=0, initial_move_up=True, mini_backlash=150):
def fast_up_down_up_autofocus(
self, dz=2000, target_z=0, initial_move_up=True, mini_backlash=150
):
"""Autofocus by measuring on the way down, and moving back up with feedback.
This autofocus method is very efficient, as it only passes the peak once.
@ -124,32 +128,41 @@ class AutofocusPlugin(MicroscopePlugin):
# Ensure the MJPEG stream has started
self.microscope.camera.start_stream_recording()
df = dz #TODO: refactor so I actually use dz in the code below!
df = dz # TODO: refactor so I actually use dz in the code below!
if initial_move_up:
m.focus_rel(df/2)
m.focus_rel(df / 2)
# move down
i, z = m.focus_rel(-df)
# now inspect where the sharpest point is, and estimate the sharpness
# (JPEG size) that we should find at the start of the Z stack
jt, jz, js = m.move_data(i)
best_z = jz[np.argmax(js)]
target_s = np.interp([best_z+target_z], jz[::-1], js[::-1]) #NB jz is decreasing
target_s = np.interp(
[best_z + target_z], jz[::-1], js[::-1]
) # NB jz is decreasing
# now move to the start of the z stack
i, z = m.focus_rel(best_z + target_z - z + mini_backlash) # takes us to the start of the stack
i, z = m.focus_rel(
best_z + target_z - z + mini_backlash
) # takes us to the start of the stack
# We've deliberately undershot - figure out how much further we should move based on the curve
current_js = m.jpeg_size()
imax = np.argmax(js) # we want to crop out just the bit below the peak
js = js[imax:] # NB z is in DECREASING order
imax = np.argmax(js) # we want to crop out just the bit below the peak
js = js[imax:] # NB z is in DECREASING order
jz = jz[imax:]
inow = np.argmax(js < current_js) # use the curve we recorded to estimate our position
inow = np.argmax(
js < current_js
) # use the curve we recorded to estimate our position
# TODO: fancy interpolation stuff
# So, the Z position corresponding to our current sharpness value is zs[inow]
# That means we should move forwards, by best_z - zs[inow]
correction_move = best_z + target_z - jz[inow]
logging.debug("Fast autofocus scan: correcting backlash by moving {} steps".format(correction_move))
logging.debug(
"Fast autofocus scan: correcting backlash by moving {} steps".format(
correction_move
)
)
m.focus_rel(correction_move)
return m.data_dict()

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
__all__ = ['Plugin', 'HelloWorldAPI', 'IdentifyAPI']
__all__ = ["Plugin", "HelloWorldAPI", "IdentifyAPI"]
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.
"""
def get(self):
"""
Method to call when an HTTP GET request is made.
"""
data = self.plugin.identify() # Call a method from our plugin, using the MicroscopeViewPlugin.plugin shortcut
data = (
self.plugin.identify()
) # Call a method from our plugin, using the MicroscopeViewPlugin.plugin shortcut
return Response(escape(data))
@ -28,7 +31,7 @@ class HelloWorldAPI(MicroscopeViewPlugin):
"""
# If the microscope does not already contain our plugin_string attribute
if not hasattr(self.microscope, 'plugin_string'):
if not hasattr(self.microscope, "plugin_string"):
# Make a string, using the MicroscopeViewPlugin.plugin shortcut
self.microscope.plugin_string = self.plugin.hello_world()
@ -43,7 +46,7 @@ class HelloWorldAPI(MicroscopeViewPlugin):
payload = JsonResponse(request)
# Extract a value from the JSON key 'plugin_string', and convert to a string. If no value is given, default to empty.
new_plugin_string = payload.param('plugin_string', default='', convert=str)
new_plugin_string = payload.param("plugin_string", default="", convert=str)
if new_plugin_string: # If not None or empty
# Set microscope attribute to the specified string
@ -56,6 +59,7 @@ class LongRunningAPI(MicroscopeViewPlugin):
"""
An example API plugin that uses a long-running plugin method.
"""
def post(self):
"""
Method to call when an HTTP POST request is made.
@ -64,7 +68,7 @@ class LongRunningAPI(MicroscopeViewPlugin):
payload = JsonResponse(request)
# Extract a values from the JSON payload.
time_to_run = payload.param('time', default=10, convert=int)
time_to_run = payload.param("time", default=10, convert=int)
# Attach the long-running method as a microscope task
try:
@ -79,6 +83,7 @@ class SomeExceptionAPI(MicroscopeViewPlugin):
"""
An example API plugin that uses a long-running but broken plugin method.
"""
def post(self):
"""
Method to call when an HTTP POST request is made.

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
__all__ = ['base', 'mock', 'sanga']
__all__ = ["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
access to camera hardware
"""
def __init__(self):
self.lock = StrictLock(timeout=5)

View file

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

View file

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

View file

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

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
import time
from .extensible_serial_instrument import ExtensibleSerialInstrument, OptionalModule, QueriedProperty, EIGHTBITS, PARITY_NONE, STOPBITS_ONE
from .extensible_serial_instrument import (
ExtensibleSerialInstrument,
OptionalModule,
QueriedProperty,
EIGHTBITS,
PARITY_NONE,
STOPBITS_ONE,
)
import serial, serial.tools.list_ports
import re
import warnings
import logging
class Sangaboard(ExtensibleSerialInstrument):
"""Class managing serial communications with a Sangaboard
@ -39,34 +47,45 @@ class Sangaboard(ExtensibleSerialInstrument):
"""
# List of valid and deprecated firmwares, for communication checks
valid_firmwares = [
(0, 5),
(0, 4)
]
deprecated_firmwares = [
(0, 4)
]
valid_firmwares = [(0, 5), (0, 4)]
deprecated_firmwares = [(0, 4)]
# These are the settings for the sangaboards serial port, and can usually be left as default.
port_settings = {'baudrate':115200, 'bytesize':EIGHTBITS, 'parity':PARITY_NONE, 'stopbits':STOPBITS_ONE}
port_settings = {
"baudrate": 115200,
"bytesize": EIGHTBITS,
"parity": PARITY_NONE,
"stopbits": STOPBITS_ONE,
}
# position, step time and ramp time are get/set using simple serial commands.
position = QueriedProperty(get_cmd="p?", response_string=r"%d %d %d",
doc="Get the position of the axes as a tuple of 3 integers.")
step_time = QueriedProperty(get_cmd="dt?", set_cmd="dt %d", response_string="minimum step delay %d",
doc="Get or set the minimum time between steps of the motors in microseconds.\n\n"
"The step time is ``1000000/max speed`` in steps/second. It is saved to EEPROM on "
"the sangaboard, so it will be persistent even if the motor controller is turned off.")
ramp_time = QueriedProperty(get_cmd="ramp_time?", set_cmd="ramp_time %d", response_string="ramp time %d",
doc="Get or set the acceleration time in microseconds.\n\n"
"The motors will accelerate/decelerate between stationary and maximum speed over `ramp_time` "
"microseconds. Zero means the motor runs at full speed initially, with no accleration "
"control. Small moves may last less than `2*ramp_time`, in which case the acceleration "
"will be the same, but the motor will never reach full speed. It is saved to EEPROM on "
"the sangaboard, so it will be persistent even if the motor controller is turned off.")
position = QueriedProperty(
get_cmd="p?",
response_string=r"%d %d %d",
doc="Get the position of the axes as a tuple of 3 integers.",
)
step_time = QueriedProperty(
get_cmd="dt?",
set_cmd="dt %d",
response_string="minimum step delay %d",
doc="Get or set the minimum time between steps of the motors in microseconds.\n\n"
"The step time is ``1000000/max speed`` in steps/second. It is saved to EEPROM on "
"the sangaboard, so it will be persistent even if the motor controller is turned off.",
)
ramp_time = QueriedProperty(
get_cmd="ramp_time?",
set_cmd="ramp_time %d",
response_string="ramp time %d",
doc="Get or set the acceleration time in microseconds.\n\n"
"The motors will accelerate/decelerate between stationary and maximum speed over `ramp_time` "
"microseconds. Zero means the motor runs at full speed initially, with no accleration "
"control. Small moves may last less than `2*ramp_time`, in which case the acceleration "
"will be the same, but the motor will never reach full speed. It is saved to EEPROM on "
"the sangaboard, so it will be persistent even if the motor controller is turned off.",
)
# The names of the sangaboard's axes. NB this also defines the number of axes
axis_names = ('x', 'y', 'z')
axis_names = ("x", "y", "z")
# Once initialised, `firmware` is a string that identifies the firmware version
firmware = None
@ -88,30 +107,38 @@ class Sangaboard(ExtensibleSerialInstrument):
# Make absolutely sure that whatever port we're on is valid
self.check_valid_firmware()
#Bit messy: Defining all valid modules as not available, then overwriting with available information if available.
# Bit messy: Defining all valid modules as not available, then overwriting with available information if available.
self.light_sensor = LightSensor(False)
for module in self.list_modules():
module_type=module.split(':')[0].strip()
module_model=module.split(':')[1].strip()
if module_type.startswith('Light Sensor'):
module_type = module.split(":")[0].strip()
module_model = module.split(":")[1].strip()
if module_type.startswith("Light Sensor"):
if module_model in self.supported_light_sensors:
self.light_sensor = LightSensor(True,parent=self,model=module_model)
self.light_sensor = LightSensor(
True, parent=self, model=module_model
)
else:
logging.warning("Light sensor model \"%s\" not recognised."%(module_model))
elif module_type.startswith('Endstops'):
logging.warning(
'Light sensor model "%s" not recognised.' % (module_model)
)
elif module_type.startswith("Endstops"):
self.endstops = Endstops(True, parent=self, model=module_model)
else:
logging.warning("Module type \"{}\" not recognised.".format(module_type))
logging.warning(
'Module type "{}" not recognised.'.format(module_type)
)
self.board = self.query("board",timeout=2).rstrip()
self.board = self.query("board", timeout=2).rstrip()
except Exception as e:
# If an error occurred while setting up (e.g. because the board isn't connected or something)
# make sure we close the serial port cleanly (otherwise it hangs open).
self.close()
logging.error(e)
logging.error("You may need to update the firmware running on the Sangaboard.")
logging.error(
"You may need to update the firmware running on the Sangaboard."
)
raise e
def test_communications(self):
@ -124,16 +151,22 @@ class Sangaboard(ExtensibleSerialInstrument):
logging.debug("Running firmware checks")
# Request firmware version from the board
self.firmware = self.query("version",timeout=2).rstrip()
self.firmware = self.query("version", timeout=2).rstrip()
logging.info("Firmware response: {}".format(self.firmware))
# Check for valid firmware string
if self.firmware:
match = re.match(r"Sangaboard Firmware v(([\d]+)(?:\.([\d]+))+)", self.firmware)
match = re.match(
r"Sangaboard Firmware v(([\d]+)(?:\.([\d]+))+)", self.firmware
)
if not match:
# Try old firmware format
match = re.match(r"OpenFlexure Motor Board v(([\d]+)(?:\.([\d]+))+)", self.firmware)
match = re.match(
r"OpenFlexure Motor Board v(([\d]+)(?:\.([\d]+))+)", self.firmware
)
if not match:
logging.error("Version string \"{}\" not recognised.".format(self.firmware))
logging.error(
'Version string "{}" not recognised.'.format(self.firmware)
)
return False
else:
logging.error("No firmware version string was returned.")
@ -146,15 +179,17 @@ class Sangaboard(ExtensibleSerialInstrument):
self.firmware_version = match.group(1)
if version_tuple not in Sangaboard.valid_firmwares:
logging.error("This version of the Python module requires firmware v0.5 (with legacy support for v0.4)")
logging.error(
"This version of the Python module requires firmware v0.5 (with legacy support for v0.4)"
)
return False
if version_tuple in Sangaboard.deprecated_firmwares:
logging.warning(
"Warning this Sangaboard is using v0.4 of the firmware, this will soon be depreciated! "
"Please consider updating the Sangaboard firmware"
)
return True
def move_rel(self, displacement, axis=None):
@ -164,10 +199,12 @@ class Sangaboard(ExtensibleSerialInstrument):
axis: None (for 3-axis moves) or one of 'x','y','z'
"""
if axis is not None:
assert axis in self.axis_names, "axis must be one of {}".format(self.axis_names)
assert axis in self.axis_names, "axis must be one of {}".format(
self.axis_names
)
self.query("mr{} {}".format(axis, int(displacement)))
else:
#TODO: assert displacement is 3 integers
# TODO: assert displacement is 3 integers
self.query("mr {} {} {}".format(*list(displacement)))
def release_motors(self):
@ -181,12 +218,12 @@ class Sangaboard(ExtensibleSerialInstrument):
queries the board for its position, then instructs it to make about
relative move.
"""
rel_mov = [f_pos-i_pos for f_pos,i_pos in zip(final,self.position)]
rel_mov = [f_pos - i_pos for f_pos, i_pos in zip(final, self.position)]
return self.move_rel(rel_mov, **kwargs)
def query(self, message, *args, **kwargs):
"""Send a message and read the response. See ExtensibleSerialInstrument.query()"""
time.sleep(0.001) # This is to protect the stage from us talking too fast!
time.sleep(0.001) # This is to protect the stage from us talking too fast!
return ExtensibleSerialInstrument.query(self, message, *args, **kwargs)
def list_modules(self):
@ -194,12 +231,15 @@ class Sangaboard(ExtensibleSerialInstrument):
Each module will correspond to a string of the form ``Module Name: Model``
"""
modules = self.query("list_modules",multiline=True,termination_line="--END--\r\n").split('\r\n')[:-2]
modules = self.query(
"list_modules", multiline=True, termination_line="--END--\r\n"
).split("\r\n")[:-2]
return [str(module) for module in modules]
def print_help(self):
"""Print the stage's built-in help message."""
print(self.query("help",multiline=True,termination_line="--END--\r\n"))
print(self.query("help", multiline=True, termination_line="--END--\r\n"))
class LightSensor(OptionalModule):
"""An optional module giving access to the light sensor.
@ -209,19 +249,25 @@ class LightSensor(OptionalModule):
module which is an instance of this class. It can be used to access
the light sensor (usually via the I2C bus).
"""
valid_gains = None
_valid_gains_int = None
integration_time = QueriedProperty(
get_cmd="light_sensor_integration_time?",
set_cmd="light_sensor_integration_time %d",
response_string="light sensor integration time %d ms",
doc="Get or set the integration time of the light sensor in milliseconds."
doc="Get or set the integration time of the light sensor in milliseconds.",
)
intensity = QueriedProperty(
get_cmd="light_sensor_intensity?",
response_string="%d",
doc="Read the current intensity measured by the light sensor (arbitrary units).",
)
intensity = QueriedProperty(get_cmd="light_sensor_intensity?", response_string="%d",
doc="Read the current intensity measured by the light sensor (arbitrary units).")
def __init__(self,available,parent=None,model="Generic"):
super(LightSensor, self).__init__(available,parent=parent,module_type="LightSensor",model=model)
def __init__(self, available, parent=None, model="Generic"):
super(LightSensor, self).__init__(
available, parent=parent, module_type="LightSensor", model=model
)
if available:
self.valid_gains = self.__get_gain_values()
self._valid_gains_int = [int(g) for g in self.valid_gains]
@ -232,21 +278,25 @@ class LightSensor(OptionalModule):
Valid gain values are defined in the `valid_gains` property, and should be floating-point numbers."""
self.confirm_available()
gain = self._parent.query('light_sensor_gain?')
M = re.search('[0-9\.]+(?=x)',gain)
assert M is not None, "Cannot read gain string: \"{}\"".format(gain)
#gain is a float as non integer gains exist but are set with floor of value
gain = self._parent.query("light_sensor_gain?")
M = re.search("[0-9\.]+(?=x)", gain)
assert M is not None, 'Cannot read gain string: "{}"'.format(gain)
# gain is a float as non integer gains exist but are set with floor of value
return float(M.group())
@gain.setter
def gain(self,val):
def gain(self, val):
self.confirm_available()
assert int(val) in self._valid_gains_int, "Gain {} not valid must be one of: {}".format(val,self.valid_gains)
gain = self._parent.query('light_sensor_gain %d'%(int(val)))
M = re.search('[0-9\.]+(?=x)',gain)
assert M is not None, "Cannot read gain string: \"{}\"".format(gain)
#gain is a float as non integer gains exist but are set with floor of value
assert int(val) == int(float(M.group())), 'Gain of {} set, \"{}\" returned'.format(val,gain)
assert (
int(val) in self._valid_gains_int
), "Gain {} not valid must be one of: {}".format(val, self.valid_gains)
gain = self._parent.query("light_sensor_gain %d" % (int(val)))
M = re.search("[0-9\.]+(?=x)", gain)
assert M is not None, 'Cannot read gain string: "{}"'.format(gain)
# gain is a float as non integer gains exist but are set with floor of value
assert int(val) == int(
float(M.group())
), 'Gain of {} set, "{}" returned'.format(val, gain)
def __get_gain_values(self):
"""Read the allowable values for the light sensor's gain.
@ -256,15 +306,16 @@ class LightSensor(OptionalModule):
values, the list will be of strings.
"""
self.confirm_available()
gains = self._parent.query('light_sensor_gain_values?')
gains = self._parent.query("light_sensor_gain_values?")
try:
M = re.findall('[0-9\.]+(?=x)',gains)
M = re.findall("[0-9\.]+(?=x)", gains)
return [float(gain) for gain in M]
except:
# Fall back to strings if we don't get floats (unlikely)
gain_strings = gains[20:].split(", ")
return gain_strings
class Endstops(OptionalModule):
"""An optional module for use with endstops.
@ -273,35 +324,41 @@ class Endstops(OptionalModule):
the type, state of the endstops, read and write maximum positions, and home.
"""
installed=[]
installed = []
"""List of installed endstop types (min, max, soft)"""
def __init__(self,available,parent=None,model="min"):
super(Endstops, self).__init__(available,parent=parent,model="Endstops")
self.installed=model.split(' ')
def __init__(self, available, parent=None, model="min"):
super(Endstops, self).__init__(available, parent=parent, model="Endstops")
self.installed = model.split(" ")
status = QueriedProperty(get_cmd="endstops?", response_string=r"%d %d %d",
doc="Get endstops status as {-1,0,1} for {min,no,max} endstop triggered for each axis")
maxima = QueriedProperty(get_cmd="max_p?", set_cmd="max %d %d %d", response_string="%d %d %d",
doc="Vector of maximum positions, homing to max endstops will measure this, "+
"can be set to a known value for use with max only and min+soft endstops")
status = QueriedProperty(
get_cmd="endstops?",
response_string=r"%d %d %d",
doc="Get endstops status as {-1,0,1} for {min,no,max} endstop triggered for each axis",
)
maxima = QueriedProperty(
get_cmd="max_p?",
set_cmd="max %d %d %d",
response_string="%d %d %d",
doc="Vector of maximum positions, homing to max endstops will measure this, "
+ "can be set to a known value for use with max only and min+soft endstops",
)
def home(self, direction="min", axes=['x','y','z']):
def home(self, direction="min", axes=["x", "y", "z"]):
""" Home given/all axes in the given direction (min/max/both)
:param direction: one of {min,max,both}
:param axes: list of axes e.g. ['x','y']
"""
ax=0
if 'x' in axes:
ax+=1
if 'y' in axes:
ax+=2
if 'z' in axes:
ax+=3
ax = 0
if "x" in axes:
ax += 1
if "y" in axes:
ax += 2
if "z" in axes:
ax += 3
if direction == "min" or direction == "both":
self._parent.query('home_min {}'.format(ax))
self._parent.query("home_min {}".format(ax))
if direction == "max" or direction == "both":
self._parent.query('home_max {}'.format(ax))
self._parent.query("home_max {}".format(ax))

View file

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

View file

@ -17,7 +17,11 @@ def set_properties(obj, **kwargs):
try:
saved_properties[k] = getattr(obj, k)
except AttributeError:
print("Warning: could not get {} on {}. This property will not be restored!".format(k, obj))
print(
"Warning: could not get {} on {}. This property will not be restored!".format(
k, obj
)
)
for k, v in kwargs.items():
setattr(obj, k, v)
try:
@ -27,12 +31,14 @@ def set_properties(obj, **kwargs):
setattr(obj, k, v)
def axes_to_array(coordinate_dictionary, axis_keys=('x', 'y', 'z'), base_array=None, asint=True):
def axes_to_array(
coordinate_dictionary, axis_keys=("x", "y", "z"), base_array=None, asint=True
):
"""Takes key-value pairs of a JSON value, and maps onto an array"""
# If no base array is given
if not base_array:
# Create an array of zeros
base_array = [0]*len(axis_keys)
base_array = [0] * len(axis_keys)
else:
# Create a copy of the passed base_array
base_array = copy.copy(base_array)
@ -40,7 +46,9 @@ def axes_to_array(coordinate_dictionary, axis_keys=('x', 'y', 'z'), base_array=N
# Do the mapping
for axis, key in enumerate(axis_keys):
if key in coordinate_dictionary:
base_array[axis] = int(coordinate_dictionary[key]) if asint else coordinate_dictionary[key]
base_array[axis] = (
int(coordinate_dictionary[key]) if asint else coordinate_dictionary[key]
)
return base_array
@ -84,4 +92,4 @@ def recursively_apply(data, func):
return [recursively_apply(x, func) for x in data]
# if the object is neither a map nor iterable
else:
return func(data)
return func(data)