Blackened everything
This commit is contained in:
parent
e213647217
commit
5966ce29be
57 changed files with 1938 additions and 1414 deletions
|
|
@ -1,4 +1,4 @@
|
|||
__all__ = ['Microscope', 'config', 'task', 'lock', 'utilities']
|
||||
__all__ = ["Microscope", "config", "task", "lock", "utilities"]
|
||||
__version__ = "0.1.0"
|
||||
|
||||
from .microscope import Microscope
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
__all__ = ['utilities']
|
||||
__all__ = ["utilities"]
|
||||
|
||||
from . import utilities
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -188,7 +181,7 @@ def err_log():
|
|||
return send_file(
|
||||
DEFAULT_LOGFILE,
|
||||
as_attachment=True,
|
||||
attachment_filename='openflexure_microscope_{}.log'.format(timestamp)
|
||||
attachment_filename="openflexure_microscope_{}.log".format(timestamp),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -219,4 +212,4 @@ def cleanup():
|
|||
atexit.register(cleanup)
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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',
|
||||
return redirect(
|
||||
url_for(
|
||||
".capture_download",
|
||||
capture_id=capture_id,
|
||||
filename=capture_obj.filename,
|
||||
thumbnail=thumbnail
|
||||
), code=307)
|
||||
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',
|
||||
return redirect(
|
||||
url_for(
|
||||
"capture_download",
|
||||
capture_id=capture_id,
|
||||
filename=capture_obj.filename,
|
||||
thumbnail=thumbnail
|
||||
), code=307)
|
||||
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',
|
||||
return redirect(
|
||||
url_for(
|
||||
".metadata_download",
|
||||
capture_id=capture_id,
|
||||
filename=capture_obj.metadataname
|
||||
), code=307)
|
||||
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',
|
||||
return redirect(
|
||||
url_for(
|
||||
"capture_download",
|
||||
capture_id=capture_id,
|
||||
filename=capture_obj.metadataname
|
||||
), code=307)
|
||||
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,10 +443,10 @@ 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)
|
||||
|
||||
|
|
@ -460,7 +473,7 @@ 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,29 +79,36 @@ 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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,6 +113,7 @@ 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
|
||||
|
|
@ -128,10 +130,10 @@ 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
|
||||
|
|
@ -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.")
|
||||
|
|
@ -254,7 +256,8 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
temporary: bool = True,
|
||||
filename: str = None,
|
||||
folder: str = "",
|
||||
fmt: str = 'jpeg'):
|
||||
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)
|
||||
|
|
@ -299,7 +301,8 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
temporary: bool = False,
|
||||
filename: str = None,
|
||||
folder: str = "",
|
||||
fmt: str = 'h264'):
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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.id = exif_dict["id"]
|
||||
|
||||
capture.timestring = exif_dict['time']
|
||||
capture.format = exif_dict['format']
|
||||
capture.timestring = exif_dict["time"]
|
||||
capture.format = exif_dict["format"]
|
||||
|
||||
capture._metadata = exif_dict['custom']
|
||||
capture.tags = exif_dict['tags']
|
||||
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
|
||||
|
||||
|
|
@ -208,7 +208,9 @@ class CaptureObject(object):
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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 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',
|
||||
fmt: str = "jpeg",
|
||||
use_video_port: bool = False,
|
||||
resize: Tuple[int, int] = None,
|
||||
bayer: bool = True):
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -7,5 +7,4 @@ from ._exif import *
|
|||
from ._exceptions import *
|
||||
|
||||
|
||||
|
||||
VERSION = '1.1.2'
|
||||
VERSION = "1.1.2"
|
||||
|
|
|
|||
|
|
@ -22,10 +22,11 @@ def split_into_segments(data):
|
|||
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.
|
||||
"""
|
||||
|
|
@ -43,7 +44,7 @@ def read_exif_from_file(filename):
|
|||
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -31,8 +31,12 @@ 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"]
|
||||
|
|
@ -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:
|
||||
|
|
@ -275,16 +297,14 @@ def _value_to_bytes(raw_value, value_type, offset):
|
|||
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
|
||||
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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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_dict = {
|
||||
"0th": {},
|
||||
"Exif": {},
|
||||
"GPS": {},
|
||||
"Interop": {},
|
||||
"1st": {},
|
||||
"thumbnail":None}
|
||||
"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:
|
||||
|
|
@ -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:
|
||||
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,12 +122,14 @@ 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]
|
||||
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))
|
||||
|
|
@ -146,8 +155,9 @@ class _ExifReader(object):
|
|||
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
|
||||
|
|
@ -159,42 +169,54 @@ class _ExifReader(object):
|
|||
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])
|
||||
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)
|
||||
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])
|
||||
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
|
||||
|
|
@ -206,51 +228,72 @@ class _ExifReader(object):
|
|||
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])
|
||||
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)
|
||||
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])
|
||||
(
|
||||
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])
|
||||
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)
|
||||
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()},
|
||||
"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()},
|
||||
"Interop": {
|
||||
TAGS["Interop"][n]["name"]: value
|
||||
for n, value in exif_dict["Interop"].items()
|
||||
},
|
||||
"thumbnail": exif_dict["thumbnail"],
|
||||
}
|
||||
return new_dict
|
||||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import struct
|
||||
|
||||
|
||||
|
|
@ -25,19 +24,26 @@ def split(data):
|
|||
pointer += LENGTH_BYTES_LENGTH
|
||||
|
||||
chunk_data = data[pointer : pointer + chunk_length]
|
||||
chunks.append({"fourcc":fourcc, "length_bytes":chunk_length_bytes, "data":chunk_data})
|
||||
chunks.append(
|
||||
{"fourcc": fourcc, "length_bytes": chunk_length_bytes, "data": chunk_data}
|
||||
)
|
||||
|
||||
padding = 1 if chunk_length % 2 else 0
|
||||
|
||||
pointer += chunk_length + padding
|
||||
return chunks
|
||||
|
||||
|
||||
def merge_chunks(chunks):
|
||||
merged = b"".join([chunk["fourcc"]
|
||||
merged = b"".join(
|
||||
[
|
||||
chunk["fourcc"]
|
||||
+ chunk["length_bytes"]
|
||||
+ chunk["data"]
|
||||
+ (len(chunk["data"]) % 2) * b"\x00"
|
||||
for chunk in chunks])
|
||||
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)
|
||||
|
|
@ -61,15 +68,19 @@ def _get_size_from_vp8(chunk):
|
|||
length_start = begin_index + BEGIN_CODE_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")
|
||||
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]
|
||||
|
|
@ -93,11 +107,21 @@ def _get_size_from_anmf(chunk):
|
|||
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")
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -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,7 +54,9 @@ 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)
|
||||
|
|
@ -70,7 +70,11 @@ if __name__ == "__main__":
|
|||
|
||||
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...")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
__all__ = ['AutofocusPlugin']
|
||||
__all__ = ["AutofocusPlugin"]
|
||||
from .plugin import AutofocusPlugin
|
||||
|
|
@ -1,12 +1,18 @@
|
|||
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):
|
||||
|
|
@ -22,6 +28,7 @@ class AutofocusAPI(MicroscopeViewPlugin):
|
|||
# return a handle on the autofocus task
|
||||
return jsonify(task.state), 202
|
||||
|
||||
|
||||
class FastAutofocusAPI(MicroscopeViewPlugin):
|
||||
def post(self):
|
||||
payload = JsonResponse(request)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -26,10 +27,12 @@ class JPEGSharpnessMonitor():
|
|||
|
||||
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):
|
||||
|
|
@ -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())
|
||||
|
|
@ -96,13 +98,16 @@ class JPEGSharpnessMonitor():
|
|||
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)))
|
||||
decimation = np.max(
|
||||
np.ceil(np.array(image.shape, dtype=np.float)[: len(shape)] / np.array(shape))
|
||||
)
|
||||
return image[:: int(decimation), :: int(decimation), ...]
|
||||
|
||||
|
||||
|
|
@ -119,4 +124,6 @@ def sharpness_edge(image):
|
|||
gray = np.mean(image.astype(float), 2)
|
||||
n = 20
|
||||
edge = np.array([[-1] * n + [1] * n])
|
||||
return np.sum([np.sum(ndimage.filters.convolve(gray, W)**2) for W in [edge, edge.T]])
|
||||
return np.sum(
|
||||
[np.sum(ndimage.filters.convolve(gray, W) ** 2) for W in [edge, edge.T]]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,7 +71,6 @@ 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:
|
||||
|
|
@ -78,13 +78,17 @@ class AutofocusPlugin(MicroscopePlugin):
|
|||
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.
|
||||
|
|
@ -133,23 +137,32 @@ class AutofocusPlugin(MicroscopePlugin):
|
|||
# (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
|
||||
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()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
__all__ = ['Plugin']
|
||||
__all__ = ["Plugin"]
|
||||
from .plugin import Plugin
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
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
|
||||
|
||||
|
|
@ -88,14 +104,14 @@ def lst_from_channels(channels):
|
|||
# half the size of the full image - remember the Bayer pattern... This
|
||||
# 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.
|
||||
|
|
@ -103,7 +119,9 @@ def lst_from_channels(channels):
|
|||
# horizontally, but not vertically.
|
||||
for dx in np.arange(box) - box // 2:
|
||||
for dy in np.arange(box) - box // 2:
|
||||
ls_channel[:, :] += padded_image_channel[16+dx::32, 16+dy::32] - 64
|
||||
ls_channel[:, :] += (
|
||||
padded_image_channel[16 + dx :: 32, 16 + dy :: 32] - 64
|
||||
)
|
||||
ls_channel /= box ** 2
|
||||
# The original C code written by 6by9 normalises to the central 64 pixels in each channel.
|
||||
# ls_channel /= np.mean(image_channel[iw//2-4:iw//2+4, ih//2-4:ih//2+4])
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
__all__ = ['ScanPlugin']
|
||||
__all__ = ["ScanPlugin"]
|
||||
from .plugin import ScanPlugin
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ 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.
|
||||
|
|
@ -27,13 +28,14 @@ def construct_grid(initial, step_sizes, n_steps, style='raster'):
|
|||
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,16 +45,16 @@ 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,
|
||||
def capture(
|
||||
self,
|
||||
basename,
|
||||
scan_id,
|
||||
temporary: bool = False,
|
||||
|
|
@ -60,7 +62,8 @@ class ScanPlugin(MicroscopePlugin):
|
|||
resize: Tuple[int, int] = None,
|
||||
bayer: bool = False,
|
||||
metadata: dict = {},
|
||||
tags: list = []):
|
||||
tags: list = [],
|
||||
):
|
||||
|
||||
# Construct a tile filename
|
||||
filename = "{}_{}_{}_{}".format(basename, *self.microscope.stage.position)
|
||||
|
|
@ -68,29 +71,27 @@ 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)
|
||||
|
|
@ -101,14 +102,15 @@ class ScanPlugin(MicroscopePlugin):
|
|||
temporary: bool = False,
|
||||
step_size: int = [2000, 1500, 100],
|
||||
grid: list = [3, 3, 5],
|
||||
style='raster',
|
||||
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 = []):
|
||||
tags: list = [],
|
||||
):
|
||||
|
||||
# Generate a basename if none given
|
||||
if not basename:
|
||||
|
|
@ -121,26 +123,29 @@ 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
|
||||
|
|
@ -153,10 +158,12 @@ class ScanPlugin(MicroscopePlugin):
|
|||
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
|
||||
|
|
@ -174,12 +181,13 @@ class ScanPlugin(MicroscopePlugin):
|
|||
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")
|
||||
|
|
@ -204,14 +212,18 @@ class ScanPlugin(MicroscopePlugin):
|
|||
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)
|
||||
|
|
@ -229,7 +241,8 @@ class ScanPlugin(MicroscopePlugin):
|
|||
resize: Tuple[int, int] = None,
|
||||
bayer: bool = False,
|
||||
metadata: dict = {},
|
||||
tags: list = []):
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
__all__ = ['Plugin', 'HelloWorldAPI', 'IdentifyAPI']
|
||||
__all__ = ["Plugin", "HelloWorldAPI", "IdentifyAPI"]
|
||||
|
||||
from .plugin import Plugin
|
||||
from .api import HelloWorldAPI, IdentifyAPI
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
__all__ = ['base', 'mock', 'sanga']
|
||||
__all__ = ["base", "mock", "sanga"]
|
||||
|
||||
from . import base, mock, sanga
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -86,21 +89,15 @@ class SangaStage(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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
||||
|
|
@ -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?"
|
||||
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?"
|
||||
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:
|
||||
|
|
@ -115,7 +127,9 @@ class ExtensibleSerialInstrument(object):
|
|||
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?"
|
||||
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
|
||||
|
|
@ -128,13 +142,20 @@ class ExtensibleSerialInstrument(object):
|
|||
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"""
|
||||
|
|
@ -152,10 +173,14 @@ 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
|
||||
|
|
@ -175,7 +200,7 @@ class ExtensibleSerialInstrument(object):
|
|||
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:
|
||||
|
|
@ -183,9 +208,18 @@ class ExtensibleSerialInstrument(object):
|
|||
if multiline:
|
||||
return self.read_multiline(termination_line)
|
||||
else:
|
||||
return self.readline(timeout).strip() #question: should we strip the final newline?
|
||||
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):
|
||||
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.
|
||||
|
||||
|
|
@ -207,22 +241,43 @@ class ExtensibleSerialInstrument(object):
|
|||
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"%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 = [
|
||||
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
|
||||
|
|
@ -232,18 +287,32 @@ class ExtensibleSerialInstrument(object):
|
|||
res = re.search(response_regex, reply, flags=re_flags)
|
||||
while res is None:
|
||||
if not waited:
|
||||
time.sleep(.1)
|
||||
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)
|
||||
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)
|
||||
|
|
@ -293,6 +373,7 @@ class ExtensibleSerialInstrument(object):
|
|||
else:
|
||||
return None
|
||||
|
||||
|
||||
class OptionalModule(object):
|
||||
"""This allows a `ExtensibleSerialInstrument` to have optional features.
|
||||
|
||||
|
|
@ -301,14 +382,24 @@ class OptionalModule(object):
|
|||
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))
|
||||
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))
|
||||
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))
|
||||
assert (
|
||||
type(model) is str
|
||||
), "Option module type should be a string not a {}".format(type(model))
|
||||
self.model = model
|
||||
else:
|
||||
self.model = None
|
||||
|
|
@ -319,7 +410,7 @@ class OptionalModule(object):
|
|||
|
||||
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"""
|
||||
|
|
@ -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
|
||||
|
|
@ -370,7 +471,6 @@ class QueriedProperty(object):
|
|||
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):
|
||||
|
|
@ -382,13 +482,12 @@ class QueriedProperty(object):
|
|||
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)
|
||||
|
|
@ -403,14 +502,22 @@ class QueriedProperty(object):
|
|||
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":
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
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",
|
||||
"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 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
|
||||
|
|
@ -92,17 +111,23 @@ class Sangaboard(ExtensibleSerialInstrument):
|
|||
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()
|
||||
|
||||
|
|
@ -111,7 +136,9 @@ class Sangaboard(ExtensibleSerialInstrument):
|
|||
# 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):
|
||||
|
|
@ -128,12 +155,18 @@ class Sangaboard(ExtensibleSerialInstrument):
|
|||
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,7 +179,9 @@ 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:
|
||||
|
|
@ -164,7 +199,9 @@ 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
|
||||
|
|
@ -194,13 +231,16 @@ 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"))
|
||||
|
||||
|
||||
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)
|
||||
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 = 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):
|
||||
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)
|
||||
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) == 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.
|
||||
|
||||
|
|
@ -278,30 +329,36 @@ class Endstops(OptionalModule):
|
|||
|
||||
def __init__(self, available, parent=None, model="min"):
|
||||
super(Endstops, self).__init__(available, parent=parent, model="Endstops")
|
||||
self.installed=model.split(' ')
|
||||
self.installed = model.split(" ")
|
||||
|
||||
status = QueriedProperty(get_cmd="endstops?", response_string=r"%d %d %d",
|
||||
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:
|
||||
if "x" in axes:
|
||||
ax += 1
|
||||
if 'y' in axes:
|
||||
if "y" in axes:
|
||||
ax += 2
|
||||
if 'z' in axes:
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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,7 +31,9 @@ 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:
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue